docs: record fundamentals research decision and clean up

This commit is contained in:
2026-07-23 17:50:33 +02:00
parent dba7ea739b
commit 361cfd7883
21 changed files with 102 additions and 209972 deletions
+10 -44
View File
@@ -1,9 +1,9 @@
"""Create a portable local SQLite snapshot for offline backtest research.
"""Create a minimal local SQLite snapshot for offline backtest research.
Copies the data required by the production backtest and fundamentals research:
Copies only the data required by app.services.backtest_service.run_backtest:
tickers, OHLCV bars, SPY benchmark closes, and the activation / recommendation /
paper-exit settings the run reads, immutable SEC snapshots, and Dolt earnings
events. Other system settings are skipped to avoid copying secrets locally.
paper-exit settings the run reads. Other system settings are intentionally
skipped to avoid copying secrets into local snapshot files.
"""
from __future__ import annotations
@@ -54,9 +54,7 @@ def _parse_args() -> argparse.Namespace:
help="SQLite snapshot path to create.",
)
parser.add_argument("--batch-size", type=int, default=5000)
parser.add_argument(
"--force", action="store_true", help="Overwrite an existing snapshot file."
)
parser.add_argument("--force", action="store_true", help="Overwrite an existing snapshot file.")
return parser.parse_args()
@@ -67,7 +65,6 @@ async def _copy_table(
*,
batch_size: int,
where=None,
row_transform=None,
) -> int:
table = model.__table__
columns = list(table.columns)
@@ -90,8 +87,6 @@ async def _copy_table(
stream = await source.stream(stmt.execution_options(yield_per=batch_size))
async for partition in stream.partitions(batch_size):
rows = [dict(row._mapping) for row in partition]
if row_transform is not None:
rows = [row_transform(row) for row in rows]
if not rows:
continue
await dest.execute(insert(table), rows)
@@ -110,8 +105,6 @@ async def _main() -> None:
from app.database import Base
import app.models # noqa: F401 - registers all metadata tables
from app.models.benchmark_price import BenchmarkPrice
from app.models.earnings_event import EarningsEvent
from app.models.fundamental_snapshot import FundamentalSnapshot
from app.models.ohlcv import OHLCVRecord
from app.models.settings import SystemSetting
from app.models.ticker import Ticker
@@ -130,12 +123,8 @@ async def _main() -> None:
connect_args={"server_settings": {"default_transaction_read_only": "on"}},
)
dest_engine = create_async_engine(_sqlite_url(output))
SourceSession = async_sessionmaker(
source_engine, class_=AsyncSession, expire_on_commit=False
)
DestSession = async_sessionmaker(
dest_engine, class_=AsyncSession, expire_on_commit=False
)
SourceSession = async_sessionmaker(source_engine, class_=AsyncSession, expire_on_commit=False)
DestSession = async_sessionmaker(dest_engine, class_=AsyncSession, expire_on_commit=False)
print(f"Source: {_hide_password(source_url)}")
print(f"Snapshot: {output}")
@@ -146,9 +135,7 @@ async def _main() -> None:
async with SourceSession() as source, DestSession() as dest:
counts = {
"tickers": await _copy_table(
source, dest, Ticker, batch_size=args.batch_size
),
"tickers": await _copy_table(source, dest, Ticker, batch_size=args.batch_size),
"system_settings": await _copy_table(
source,
dest,
@@ -165,30 +152,9 @@ async def _main() -> None:
SystemSetting.key.like("paper_%"),
),
),
"benchmark_prices": await _copy_table(
source, dest, BenchmarkPrice, batch_size=args.batch_size
),
"ohlcv_records": await _copy_table(
source, dest, OHLCVRecord, batch_size=args.batch_size
),
"benchmark_prices": await _copy_table(source, dest, BenchmarkPrice, batch_size=args.batch_size),
"ohlcv_records": await _copy_table(source, dest, OHLCVRecord, batch_size=args.batch_size),
}
# Import-run provenance is operational metadata, not a research input.
# Null it so the portable snapshot needs no data_import_runs rows.
async with SourceSession() as source, DestSession() as dest:
counts["fundamental_snapshots"] = await _copy_table(
source,
dest,
FundamentalSnapshot,
batch_size=args.batch_size,
row_transform=lambda row: {**row, "import_run_id": None},
)
counts["earnings_events"] = await _copy_table(
source,
dest,
EarningsEvent,
batch_size=args.batch_size,
row_transform=lambda row: {**row, "import_run_id": None},
)
finally:
await source_engine.dispose()
await dest_engine.dispose()
-56
View File
@@ -1,56 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(git rev-parse --show-toplevel)"
cd "$ROOT"
SNAPSHOT="${1:-backtest_snapshots/fundamentals-backtest.sqlite}"
PROTOCOL="${PROTOCOL:-split-safe}"
WORKERS="${WORKERS:-$(sysctl -n hw.logicalcpu 2>/dev/null || echo 8)}"
if [[ "$WORKERS" -gt 1 ]]; then
WORKERS=$((WORKERS - 1))
fi
if [[ -x .venv/bin/python ]]; then
PYTHON=.venv/bin/python
else
PYTHON="${PYTHON:-python3}"
fi
if [[ ! -f "$SNAPSHOT" ]]; then
echo "Snapshot not found: $SNAPSHOT" >&2
exit 1
fi
case "$PROTOCOL" in
split-safe)
PREFIX="fundamentals-splitsafe"
SCORE_CACHE="reports/.cache/fundamentals-splitsafe-scores.pkl"
;;
original)
PREFIX="fundamentals-overlay"
SCORE_CACHE="reports/.cache/fundamentals-scores.pkl"
;;
*)
echo "Unsupported PROTOCOL: $PROTOCOL (use split-safe or original)" >&2
exit 1
;;
esac
STAMP="$(date -u +%Y%m%d-%H%M%S)"
OUT="reports/${PREFIX}-${STAMP}.json"
echo "Snapshot: $SNAPSHOT"
echo "Workers: $WORKERS"
echo "Protocol: $PROTOCOL"
echo "Output: $OUT"
"$PYTHON" scripts/run_fundamentals_research.py "$SNAPSHOT" \
--protocol "$PROTOCOL" \
--workers "$WORKERS" \
--candidate-cache reports/.cache/fundamentals-candidates.pkl \
--fundamentals-cache "$SCORE_CACHE" \
--out "$OUT"
echo
echo "Bring this file back for review: ${OUT%.json}.zip"
File diff suppressed because it is too large Load Diff