36 Commits (27de44c8d80900afc28efe8c77519d71f9bb574d)

Author SHA1 Message Date
I Luk Kim fcf379c759 Fix snapshot refresh: enable auto-rebuild, fix date-range edge cases, log incremental failures
- registry.json: change _ftb_fix_v2 from manual_only to auto_full_rebuild so
  backtest auto-refreshes when snapshot doesn't cover the requested period
- run.py: return [] (not all_trading_days fallback) when parking cap pushes
  requested_end before requested_start, preventing silent wrong-date-range runs
- run.py: allow 1-trading-day lag tolerance in parking cap so a single lagging
  symbol (e.g. QQQM shortly after close) doesn't cap the whole simulation
- backtest_sim.py: log incremental_update_failed_falling_back warning so
  silent fallback to full rebuild is visible in direct-mode logs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 004f1a5bce Fix parking entry/exit showing same price on last simulation day
On the last simulation day, parking was entered at EOD close price
(when _had_event_activity_today=True) and immediately liquidated at
close by end-of-backtest cleanup → entry == exit → PnL = 0.

Fix: force all six parking entry code paths to use "open" price when
date == last_simulation_date, so entry and cleanup-close are always
different prices.

Also adds _parking_cap logic in _extend_store_to_requested_window to
cap _requested_end_date at the last date where QQQM/TQQQ/SGOV all
have Oracle close-price data, preventing the simulation from including
days where macro is incomplete and the exit fallback would fire.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 9e622c6614 Investigate compound mode: V23 is absolute champion in all modes
- V23 pure compound (live-equivalent): 200d +148.13% DD-14.23%, 400d +209.41% DD-17.35%
- Hybrid V2 compound tested: 200d +175.41% looks promising but 400d +181.71% DD-23.72%
  loses to V23 by -27.7pp return AND -6.4pp worse DD → rejected
- Safe v9 compound 400d: +128.49% DD-14.43% — better DD but -81pp return vs V23 → rejected
- V23 tight governor compound 400d: +192.54% DD-17.32% — marginal gain, not worth config
- Live paper trader uses compound mode (engine.py:1816 session_equity = initial_equity + P&L)
- All improvement axes exhausted; V23 daily_reset declared TERMINAL

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 0cae86aa87 Revert: remove daily_budget_reset from PEAD backtest
Feature was added to wrong system (PEAD backtester). Fully reverted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 189aa58343 Add daily_budget_reset mode for research backtesting
New mode (risk.daily_budget_reset=True) where cash_available and sizing
equity reset to initial_equity at the start of each day, regardless of
how many open positions or realized P&L exist. Unlike fixed_capital_sizing
(단리, sizing only), this also treats buying power as if no positions are
held — useful for evaluating signal quality independent of capital constraints.

- domain.py: daily_budget_reset field on RiskConfig
- run.py: _daily_budget_reset flag; _sizing_equity / _sleeve_equity_est /
  _build_portfolio_state all honor the new flag
- backtest_sim.py: daily_budget_reset param threaded through
- direct_runner.py: --daily-budget-reset CLI flag
- routers/backtest.py: BacktestRequest field + cmd arg
- client.ts: BacktestParams / BacktestTask types updated
- Backtest.tsx: checkbox in form + DBR badge in task list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 426de9038d Fix lookback entry bugs: current-price sizing + MHD expiration filter
Bug #2 (paper trader): lookback entries sized using historical entry_price_est
but filled at current market price, causing cash overdraft. Fix: override
entry_price_est with get_latest_bars() close before entering _process_entries.

Bug #3 (paper trader + backtester): paper trader was missing the per-candidate
MHD expiration check that the backtester already had. Also adds
lookback_min_remaining_days (default 3) to reject candidates with too little
holding time remaining — prevents entering a position the day before forced exit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 236148de2e Remove momentum breakout sleeve (overfitting, valid -31%) and revert related code
- Delete v7.360-v7.363 experiment configs (rotation/momentum tests)
- Remove _schedule_momentum_breakout_candidates() from backtester run.py
- Remove MomentumBreakoutConfig from domain.py
- Delete momentum_calendar.py, momentum_screener.py, build_momentum_calendar.py
- Delete data/momentum_calendar/ parquet data

Valid period performance was -31.36% vs +152.4% baseline — sleeve is not viable
without walk-forward validation. Abandoning for now.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim b3ba914a8d Optimize v7 strategy: v7.356 achieves CW 2159% + SQS 90.7 (Pareto improvement over v7.314)
Key changes from v7.314 baseline (CW 2012%, SQS 90.0):
- max_position_value_pct 15→25, non_a_tier_target_1_fraction 0.2→0
- max_daily_new_risk_pct 30→50 (via v7.330, CW champion 2148%)
- bullish_raised_recovery per_trade_risk_pct 0.71→0.55 (DD improvement)
- bullish_raised_recovery max_holding_days 12→10 (sweet spot, +98pp CW)

Result: v7.356 CW 2159% (+147pp), SQS 90.7 (+0.7), risk 66.2 (+2.9), robustness 94.3 (+0.5)
All metrics improved simultaneously — return increase AND DD reduction achieved.

Also includes: web UI updates, pipeline scripts, v16/v17/v18/v19 experiment pruning,
Form4 preset additions, snapshot registry updates, domain.py enhancements.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 5496059b6c Add v7.120 composed GLD experiment updates 4 months ago
I Luk Kim 5cb2b9fcb8 Add non-core allocator v2 and Form4 freshness presets 4 months ago
I Luk Kim e38c314a09 Add ownership/risk-off sleeves, v17-v19 experiments, and web app restructure
New features:
- Ownership 13D/13G residual-cash sleeve with PIT calendar and quality filters
- Risk-off alpha sleeve (GLD/DBC rotation on crisis regime signals)
- Crisis relay target in parking: evaluates before defensive relay
- Bearish symbol allocation split (bearish_alloc_pct + sgov remainder)
- Alternative defensive ETF candidate (cash_parking_defensive_alt_symbol)
- Composite eval and engine ablation tools
- experiment and overfit CLI apps

New experiments:
- v17.x series (v17.1 champion SQS 78.4; v17.5–v17.129 exhausted)
- v18.x and v19.x families from v12.8 OOT defense branch
- v7.119 composed variants (idle alpha + ownership + risk-off sleeves)
- parking_only configs: bufb, jepq, merix, regime_tiered
- empty_strategy baseline config

Web app:
- Restructured into routers/services modules (experiments, leaderboard, runs, sqs, docs)
- Ownership sleeve and risk-off sleeve controls in backtest UI
- Frontend: ComposeStrategy page, tradeSleeves lib, idle decomposition display

Research tools:
- Ownership 13D/13G probe and PIT cache builder
- Dividend capture probe and cache builder
- Insider Form4 idle alpha probe
- Alternative ETF parking probe, put-spread overlay probe
- Wikimedia low-attention and peer-relative idle alpha probes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 5056295cb6 Add lookback entry feature for bounded backtests
When a backtest starts mid-stream (via --start), events that fired
before the start date but are still within their max_holding_days
window can now be entered on the first simulation day.

- Add `lookback_entry_enabled: bool = False` to ExecutionConfig
- On first sim day, _collect_lookback_candidates() gathers pre-start
  events, runs them through the same select_candidates() pipeline,
  and injects them before normal candidates
- Entry fills at the first day's open price; gap-cap check is skipped
  since the event is multi-days old
- days_held is initialized to the elapsed trading days so TIME exits
  fire at the correct time relative to the original event date
- Store slice is extended backward by max_mhd calendar buffer so
  pre-start rows survive slice_by_date_range when feature is enabled
- Enabled in return_max_long_v7.119 for testing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim eb20c32a94 Fix QQQM parking signal prefix regression 4 months ago
I Luk Kim ea9f156eeb Tune Form4 sleeve quality filters and reserve sizing 4 months ago
I Luk Kim 72681e69e5 Add Form4 residual-cash sleeve and UI support 4 months ago
I Luk Kim f2113b7e06 Fix cash parking phantom-money bug + live engine parking liquidation for events
Backtester (run.py):
- cash_available = (self._cash + parking_value) * multiplier caused trades to be
  approved even when self._cash ≈ 0 (all money in SGOV/QQQ).  Trades executed
  by deducting from self._cash → negative cash (phantom money).
- Fix: after simulate_entry, if self._cash < actual trade cost and parking exists,
  call _liquidate_parking_for_cash(shortfall) before deducting from cash.
- Verified: 2022-2026 backtest with qqqm_low_dd shows 0 cash_negative events.

Live engine (engine.py):
- Add _parking_liquidate_for_event(): frees parking cash to fund event entries.
  SGOV (virtual) reduces entry_value in DB; QQQM/QQQ sells real shares via broker.
- Both entry loops (engines mode + flat/reaction_close mode) now attempt parking
  liquidation when plan.skip_reason == "insufficient_cash" before giving up.

Also includes prior session work (accumulated since last commit):
- 6 novel parking gate signals: VRP, Market Temperature, Hurst exponent, Rolling
  Kurtosis, Return Autocorrelation, SPY-QQQ Correlation (composite risk score v2)
- QQQM parking symbol support (lower expense ratio vs QQQ)
- Snapshot auto-refresh + bar extension cache (pickle) to avoid 10-min re-fetches
- Bar extension clamps to last market-closed date (ET 4PM check)
- fithia2 refresh command; --no-refresh flag for paper backtest
- Paper backtest macro extension beyond last event date (parking-only periods)
- parking_state DB schema: 7 new columns (peak_price, gate_in_sgov,
  committed_target, pending_target, pending_days, sgov_entry_value, sold_today)
- Live engine: target confirmation (2-day), top-up drawdown gate, trailing stop,
  SGOV interest accrual, full 6-signal gate evaluation
- New PARKING_PRESETS: qqqm_low_dd, composite_v2, vv_24_vrp8, vt_24_t13, etc.
- Web GUI / CLI result parity fix (Oracle URL via get_settings().stock_oracle_url)
- Force-close uses last_exec_date (has bar data); parking liquidates at last_date

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
I Luk Kim 784c581f19 Enhance backtest engine: v11 scoring, selector expansion, snapshot store improvements
Extends selector with new scoring model support, adds execution
enhancements, and improves snapshot store loading and split handling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
5 months ago
I Luk Kim e2229646e6 Add oversold bounce engine experiments (v6new.259-264) — blocked by architecture
Bounce engine (buy negative reaction, bet on mean reversion) could not
execute: system architecture ties scoring to single model per backtest,
and selector/store indexes are optimized for positive-reaction PEAD.
Negative-reaction candidates get score=0 from PEAD scoring, blocking
engine selection regardless of engine-level threshold overrides.

Implementing bounce trades requires: dual scoring model support,
selector changes for negative-reaction candidate routing, and
store indexing changes. Deferred to future refactor.

Current best CW return: 293.2% (v6new.255)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
5 months ago
I Luk Kim 090bfa8e36 Add contrarian feature analysis + v15 scoring (v6new.174-188)
Data analysis revealed OBV Q1 (distribution) has 56.4% WR vs Q5 51.2% —
contrarian signal confirmed. Previous OBV bonus was applied in wrong
direction. Corrected with v15 scoring models.

Best result: v6new.185 (entropy + risk 0.058) CW 274.4% but SQS 72.2,
still below v6new.122 (72.4). WFV/robustness offsets CW gains.

v6new.122 confirmed as optimal under current SQS v4 formula.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
5 months ago
I Luk Kim 1e66b67c7a Add technical/scientific feature experiments (v6new.106-173) and v6new.122 SQS 72.4
Tier 1: Vol/RSI/BB/OBV features — sizing scalers hurt public SQS, scoring
adjustments ineffective on 28-30 trades. Only doc_quality gate lowering
(0.66→0.55) improved results (+2 trades, +0.8 SQS).

Tier 2: Hurst exponent, Shannon entropy, sector momentum — entropy bonus
CW +10.7pp but SQS equivalent (72.3 vs 72.4). Sector momentum hurt badly.

Tier 3: OU theta, gravitational pull, market temperature — all caused
large CW return drops (-80 to -103pp). Physics-based indicators don't
fit event-driven PEAD.

Best result: v6new.122 (SQS 72.4, #3 leaderboard) = v6new.29 + doc quality
gate 0.66→0.55. Single parameter change outperformed all feature engineering.

New code:
- libs/features/market_features.py: 9 new features (vol, RSI, BB, OBV,
  Hurst, entropy, OU theta, gravitational pull, market temperature)
- libs/backtest/scoring.py: v12-v14 scoring models with technical gates
- libs/backtest/allocator.py: volatility + conviction size scalers
- libs/backtest/domain.py: volatility_size_scaler + conviction_boost config
- scripts/enrich_*.py: snapshot enrichment scripts
- 68 experiment configs (v6new.106-173)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
5 months ago
I Luk Kim fecdc12007 Add earnings surprise feature pipeline and v11 scoring
New data source integration:
- EarningsSurpriseService: GET /api/v1/earnings/surprise/{symbol}
  Returns actual vs estimated EPS with surprise_percentage
- Feature builder: creates earnings_surprise_v1 snapshots for earnings events
- Backfill script runs for existing 1,273 tickers (Alpha Vantage rate limited)

New scoring (v11):
- Small beat (0-3% surprise): +10% bonus (82.4% WR in sample)
- Medium beat (3-8%): +5% bonus
- Big beat (>8%): no bonus (already priced in)
- Miss (<=0%): -5% penalty

Signal validation (n=66 sample):
  Small beat: 82.4% WR, +1.79% mean 5d return
  Big beat: 54.8% WR, +0.47%
  Miss: 55.6% WR, -0.10%

Backfill running (~4 hours). Experiment pending data completion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
5 months ago
I Luk Kim fb4fec7dac Unify BacktestRunner and PaperTradingEngine trade decision logic
Phase 1-4 of engine unification to eliminate research/live divergence.

Phase 1 — Scoring (event_detector.py):
  EventDetector now uses config's scoring_model (v5/v9 etc.) when
  event_v1 features are present (parse_confidence_overall not null).
  Falls back to compute_entry_score only for incomplete events.

Phase 2 — Execution config (execution.py):
  Extracted build_effective_execution_config() as shared function.
  BacktestRunner delegates to it. PaperTradingEngine can now use
  identical per-engine overrides, adaptive exit, tiered targets.

Phase 3 — Attention filtering (attention.py):
  New AttentionFilterService class extracted from BacktestRunner.
  Provides: engine_requires_attention, apply_filters, rescoring.
  BacktestRunner now delegates to this service.
  PaperTradingEngine can import and use the same service.

Phase 4 — Gap cap (execution.py):
  check_next_open_gap_cap() shared function for next-open gap rejection.

All 450 unit tests pass. Paper backtest verified working.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
5 months ago
I Luk Kim 8dacdaab4e Add v10 macro regime scoring (VIX+HY) and FRED macro features — SQS 59.1 (rejected)
New features:
- _enrich_macro_features() in snapshot_export: adds macro_vix, macro_hy_spread from FRED
- compute_return_max_long_score_v10: +12% bonus in favorable regime (VIX>18+HY>3.25)
- _macro_regime_score(): regime-aware scoring component

Findings:
- VIX signal is statistically strong: 62.3% WR (VIX>18+HY>3.25) vs 50.8% (other)
- But scoring bonus promotes marginal trades, diluting OOS quality
- Same pattern as eps_growth, drift bonus: raw signal ≠ scoring improvement
- v6new.17 SQS 59.1 < v6new.9 SQS 63.3

v6new.9 remains best at SQS 63.3 after 17 experiments.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
5 months ago
I Luk Kim 85c4d98987 Add v6new experiment suite: scoring v8/v9/v9g, cross-event drift, coverage engines
Scoring additions (libs/backtest/scoring.py):
- v8: conditional financial bonus (eps_growth_qoq/revenue_growth_qoq)
- v9: cross-event drift momentum (+/-10% from prior same-ticker 5d return)
- v9g: gated variant — reject events with negative prior drift

Snapshot export (libs/export/snapshot_export.py):
- _enrich_prior_event_drift: computes prior_event_fwd5d for all snapshots
- smallcap-liquid-long-v1 universe profile ($500M-$2B)
- market_cap_max support in screener and filtering

8 experiment configs (v6new.1-v6new.8):
- v6new.1: unknown event reclassification (neutral)
- v6new.2: financial features (neutral, EPS growth is noise)
- v6new.3: small-cap (blocked, survivorship bias)
- v6new.4-6: cross-event drift variants (rejected)
- v6new.7: engine pruning (quality up, count down)
- v6new.8: coverage expansion with 2 new post-market engines (best result)

Best result: v6new.8 SQS 41.5 vs v6.29 control 32.3 on same conditions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
5 months ago
I Luk Kim 02542248b7 Add attention-aware PEAD caps and promote step75 5 months ago
I Luk Kim b507fbf499 Add attention client and continue PEAD research 5 months ago
I Luk Kim c646303423 Recalibrate public SQS and exposure-aware tracking 5 months ago
I Luk Kim 9ec0b26e10 Implement multi-engine PEAD strategy research workflow 5 months ago
I Luk Kim cb19afa87b feat: add strategy improvement tracking system (SQS + journal + leaderboard)
Track experiment cycles with SQS scoring (0-100), JSONL journal, and
auto-generated leaderboard to prevent duplicate experiments and enable
data-driven strategy decisions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5 months ago
I Luk Kim 191653394d feat: scoring cleanup — alpha-only composite, default-deny unknown events, new exit/risk features
Remove 5 non-alpha features (earnings surprise, risk penalty, parse confidence,
direction clarity, LM sentiment) from composite score to eliminate double-counting
with hard gates and noise sources. Redistribute weights to 5 alpha features.

Add default-deny for unknown event types, no-follow-through early exit (D+1),
kill switch log-only mode, macro regime size scaler. Remove SUE gate (Gate 8).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5 months ago
I Luk Kim 3779be78e3 fix: kill switch research mode reset + adjust veto threshold
- Fix kill switch reset: remove unreachable drawdown recovery condition
  (equity can't change while trading is halted), reset peak_equity and
  drawdown_pct to 0 on cooldown expiry
- Raise veto_oneoff_penalty threshold 0.5 → 0.7 (was blocking 67% of
  candidates due to high median oneoff_penalty in dataset)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5 months ago
I Luk Kim 4d0e773ba0 feat: overhaul strategy — document quality > price momentum
Flip scoring weights so event/document quality is primary signal (55%)
and market confirmation is secondary (35%). Add research mode with
kill-switch cooldown/reset, veto gates for bad events, reduced portfolio
risk, and 4 diagnostic analysis scripts.

Phase A: Research mode kill-switch reset, risk reduction (0.5%/trade,
max 4 positions), bullish-only direction for all event types.

Phase B: 2 new sub-scorers (parse_confidence, direction_clarity),
4 veto gates (oneoff risk, parse confidence, unknown/bearish direction).

Phase C: signal_quality, event_type_decomposition, kill_switch_impact,
concurrent_position analysis scripts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5 months ago
I Luk Kim cdf6ae3493 feat: Phase 5 fundamental strategy improvements
Fix core strategy flaws identified from academic research and Phase 4
backtest results (23% win rate, 0% target hits, 77% stop exits).

5A — Exit mechanics: ATR-based targets (reachable ~4.5% vs unreachable ~6-8%),
     partial profit-taking at target with breakeven stop on remainder,
     wider catastrophic stop (3.0 ATR), trailing stop enabled by default.
5B — Event-type-specific logic: EventTypeProfile with per-type overrides
     for holding days, ATR multipliers, score thresholds, direction filter.
     Disabled management_change and other_material_event (low evidence).
5C-1 — Expanded universe from 15 to 97 symbols across sectors including
       mid-cap growth where PEAD is stronger.
5C-3 — Bootstrap 95% confidence intervals for key trade metrics.
5D — SUE integration: earnings surprise scoring (eps_growth_qoq) at 10%
     weight, entry gate blocks negative EPS surprise for earnings events.
5F — Extended label horizons to 10D/20D with Alembic migration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5 months ago
I Luk Kim d387d567c5 fix: correct equity calculation — use market value, not unrealized PnL
equity was computed as cash + unrealized_pnl where unrealized_pnl =
(close - entry) × shares. Since cash already had entry cost subtracted,
this double-counted the cost basis:

  buggy:   equity = (initial - entry×shares) + (close - entry)×shares
                  = initial + close×shares − 2×entry×shares  ← WRONG

  correct: equity = cash + market_value
                  = (initial - entry×shares) + close×shares
                  = initial + (close − entry)×shares          ← RIGHT

This caused drawdown to spike to ~73% the instant a position opened
(e.g. TSLA $330 × 222 shares → equity appeared to drop from 100k to
27k), falsely triggering the kill switch at 25% and blocking all
subsequent entries.

Before fix: 3 trades, +0.08% return, 39.2% max drawdown (fake)
After fix:  10 trades, -2.63% return, 4.24% max drawdown (real)

Also: when bar data is missing, positions now use entry_price as
fallback market value instead of treating the position as worthless.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5 months ago
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