2 Commits (867d70afaef16ea762f78663e29d2c24a7a78010)

Author SHA1 Message Date
I Luk Kim 867d70afae fix: correct simulation loop and equity curve calculation after real-data testing
- BacktestRunner.run() now iterates all NYSE trading days (not just candidate
  days) via SnapshotStore.all_trading_days() so stop/target/time exits are
  checked every day, not only on days with new candidates
- Record initial DailyPortfolioState before simulation loop starts so
  total_return_pct is computed relative to the true initial equity (100k),
  not the first post-entry equity snapshot
- SnapshotStore._fetch_event_metadata() now synthesises event_timestamp from
  event_date + 21:00 UTC when filed_at_utc is NULL (transparent enrichment at
  loader boundary, not silent substitution in selector)
- SnapshotStore._async_load() maps event_close → entry_price_est when the
  column is absent, and derives score from abs(reaction_day_return) when the
  Parquet snapshot has no score column
- Add --snapshot-dir CLI flag to BacktestRunner to override the default
  parquet_dir base path (needed for non-standard snapshot locations)
- Fix integration test assertion: total_trading_days >= 2 (was == 2)
- Add configs/experiments/realdata_test_v1.json for real Phase 3 snapshot runs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
I Luk Kim 2f4d9f61f7 feat: implement Phase 4 -- event-driven backtester
Full backtesting engine that reads Parquet snapshots and simulates a
swing-trading strategy with no look-ahead bias.

## New modules (libs/backtest/)
- domain.py: All Pydantic v2 models (Candidate, PlannedOrder, FilledTrade,
  OpenPosition, DailyPortfolioState, MetricsBundle, BacktestConfig, etc.)
- calendar.py: Thin wrappers over time_utils + reaction_date
- manifests.py: Config load/deep-merge/validate, run-ID generation
- metrics.py: 21 pure-function metrics (no pandas, stdlib statistics only)
- selector.py: build_candidate(), rank_candidates() (score↓ ADV↓ symbol↑)
- allocator.py: 7-gate run_entry_gates(), ATR stop, floor() shares
- execution.py: simulate_entry/exit(), update_trailing_stop() (ratchet-up only)
- splits.py: Walk-forward windows, year/regime split utilities
- snapshot_store.py: Sync load() → asyncio.run(_async_load()), no look-ahead
- artifacts.py: Full run-dir writer (Parquet, CSV, JSON)

## App modules (apps/backtester/)
- run.py: BacktestRunner (exit-first→entry simulation, 25% kill switch) + CLI
- replay.py: Double-run determinism checker

## Config files
- configs/backtest/defaults.json: Base strategy defaults
- configs/experiments/baseline_v1.json: First experiment manifest

## Tests: 142 new tests, all passing
- 132 unit tests (no DB/HTTP required)
- 8 integration tests (synthetic SnapshotStore)
- 3 backtest determinism/replay tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago