- New: libs/backtest/xsmom_cache.py — disk cache for xsmom ranked-universe
per rebalance date; keyed by snapshot fingerprint + param hash; top_n is
NOT in key so v9.3.1 (top20) and v9.3.2c (top10) share one cache file
- libs/backtest/cross_sectional_momentum.py — add cache= param to
build_candidates(); hit path skips 900-symbol scan; miss path writes ranked
rows post-quality-gate to cache buffer
- libs/backtest/snapshot_store.py — expose snapshot_dir attribute; propagate
through slice_by_date_range() so runner always has the path
- apps/backtester/run.py — wire up XsmomRankCache per engine (lazy init,
flush after simulation loop); cache is no-op when snapshot_dir is None
- configs: add return_max_long_v9.3.2b_fc.json (SQS 93.5 champion,
ER days[2,8]); remove all other v9 sweep variants from .index.json
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New engines (configurable but not promoted by default):
- libs/backtest/earnings_runup.py: pre-earnings drift entry (T-7 to T-3, attention+volume z-scores)
- libs/backtest/peer_sympathy.py: peer reaction trade after leader earnings, with reaction_close variant
- libs/backtest/vol_breakout_52w.py: 52-week high volume breakout
- libs/backtest/cross_sectional_momentum.py: 12-1 momentum with VIX/SPY-50dma regime filter
Pipeline additions:
- libs/parser/event_type_normalizer.py: normalize Oracle fallback raw vocab to strategy
vocabulary (earnings_result→earnings_release, regulation_fd→guidance_update, etc.).
Wired into apps/pipeline/event_parser/main.py oracle-fallback path.
- libs/labeler/label_generator.py: preserve future entry_dates as label_status='pending'
instead of dropping as 'unavailable'.
- libs/export/snapshot_export.py: include 'pending' labels in snapshot export.
- libs/parser/rule_parser.py: harden 8-K item-code classifier against dirty input strings.
Infrastructure:
- libs/backtest/snapshot_store.py: _PRICE_FEATURE_WARMUP_DAYS 120→400 (needed for
xsmom 12-1 lookback of 273 trading days).
- apps/backtester/run.py + apps/paper_trader/backtest_sim.py: disable auto-refresh
of snapshots (user request — auto-refresh was silently rebuilding snapshots with
current code, making historical backtests irreproducible across DB mutations).
Engine config support:
- libs/backtest/domain.py: add fields for ER/PS/VolBO/xsmom engine configs.
- libs/backtest/execution.py: wire pct-trailing for EarningsRunup.
- libs/backtest/scoring.py: synthetic candidate scoring for new engines.
Configs (POC + sweeps, none promoted as active strategy):
- Phase A-E PEAD baseline comparisons (no PEAD / +ER / +xsmom / +sleeves variants)
- Phase F1-F8 silo allocation sweep (v7.356 + ER/xsmom silos 15-60%)
- xsmom_poc_v1, xsmom_v2_regime_mdd
- v7.356_plus_er_silo_05/10/15/20/25/30 ranges
- earnings_runup_* / peer_sympathy_* / vol_breakout_52w_* POCs
Tests: 16-tests-each for each new engine + event_type_normalizer tests.
Note: backtest reproducibility infrastructure remains broken — see
_backup_2026-05-10/HANDOFF.md for details on the +12,592% reference run that
cannot be reproduced after rebuilds. Followup work needed before trusting any
specific backtest number.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug: synthetic Candidate builders in earnings_runup.py / peer_sympathy.py /
vol_breakout_52w.py did not propagate engine_capital_bucket_id /
engine_capital_bucket_allocation_pct from StrategyEngineConfig onto the
emitted Candidate. The selector path (libs/backtest/selector.py:371-383)
correctly sets these for selector-built candidates, but the synthetic-
candidate paths used by the new engine classes silently dropped them.
Cascade: apps/backtester/run.py:_active_capital_bucket_ids_for_candidates
scans candidate.engine_capital_bucket_id; when empty, the silo allocator
(run.py:790-791) short-circuits and the engine sizes against the full
equity pool — making capital_bucket fields a no-op for the new engines.
Fix: 3-line addition to each builder mirroring selector.py convention.
108 unit tests pass across the 3 engines.
ER silo sweep result (4y midlarge-liquid-long-v1, --split all):
config trades er_tr return% mdd% sharpe er_pnl$
v7.356_baseline 390 0 +2356.28 23.55 2.69 0
phase6_shared_0.20 464 77 +2746.21 25.21 2.63 -7589
silo_05 519 89 +2343.02 22.56 2.69 215
silo_10 508 86 +2450.62 23.51 2.73 321
silo_15 516 88 +2489.91 23.48 2.74 389
silo_20 508 84 +2538.27 23.48 2.76 511
silo_25 503 77 +2566.34 23.46 2.77 466
silo_30 513 88 +2589.80 23.45 2.77 560
Phase 6 shared budget's headline +390pp gain was portfolio-luck distributed
(ER's 30 cancelled trades freed cash for other sleeves to make ~+30k pnl).
With proper silo, ER fires those trades within its dedicated bucket, sizing
correctly relative to the 5-30% pool — flipping ER engine PnL from
-$7,589 (Phase 6) to +$215..+$560 (silos).
silo_30 is the recommended variant:
- Return +2589.80% vs baseline +2356.28% (+233pp, structural not luck)
- MDD 23.45% vs baseline 23.55% (slightly better)
- Sharpe 2.77 vs baseline 2.69 (+0.08, real improvement)
- ER PnL +$560 (engine actually contributing)
- Higher silos (40%+) likely starve PEAD; 30% appears near optimal.
Per-split validation of silo_30 NOT yet run — flagged for follow-up
before any live deployment.
Files:
- libs/backtest/earnings_runup.py:494-499 (3-line fix)
- libs/backtest/peer_sympathy.py:771-776 (3-line fix)
- libs/backtest/vol_breakout_52w.py:654-659 (3-line fix)
- configs/experiments/return_max_long_v7.356_plus_er_silo_{05,10,15,20,25,30}.json
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Apply libs.parser.event_type_normalizer (added in commit 722e5cf) to
existing events whose parser_version LIKE 'oracle-fallback%' so historical
rows match the forward-going normalization wired into the parser.
Implementation:
- New renormalize_oracle_fallback_events() in apps/pipeline/event_parser/main.py
- SELECT filter Event.parser_version.like("oracle-fallback%") — broader
than a hardcoded IN list, so already-aligned values skip naturally and
future additions to _ORACLE_TO_STRATEGY get picked up automatically
- New --renormalize-oracle-fallback CLI flag, chainable with --reparse
- JobRun row written (job_name=event_parser_renormalize_oracle)
- Per-row renormalize_event_updated INFO log + final renormalize_done
summary with transition counters
Live DB run: seen=2937 / updated=1069 / skipped=1868 / errors=0.
Wall ~2 sec (pure DB UPDATEs, no Oracle calls).
Transitions:
earnings_result -> earnings_release : 412
shareholder_vote -> other_material_event : 409
regulation_fd -> guidance_update : 237
acquisition_disposition -> other_material_event : 7
other -> other_material_event : 4
Unmapped Oracle values (financial_obligation 225, articles_amendment 81,
contract_termination 48, etc.) preserved verbatim — honest filter-drop.
Stale-by-design (mirrors existing reparse_events convention):
- Event.event_id PK still embeds old raw event_type substring
- EventParse.output_json["event_type"] still carries raw Oracle value
Strategies read Event.event_type, not those fields. Avoids cascading
PK rewrites across event_parses/feature_snapshots/event_labels tables.
Integration test: tests/integration/test_renormalize_oracle_fallback.py
inserts 4 fixtures, drives _apply_oracle_renormalization() against the
rolled-back db_session, asserts updated/skipped/error counts and final
row state.
Snapshot rebuild not run — nightly auto-rebuild picks up normalized
values incrementally.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When the rule parser can't classify an 8-K and falls back to Stock Oracle's
filing-events API, Oracle's vocabulary (e.g. earnings_result, shareholder_vote,
regulation_fd) was being written verbatim into events.event_type. The DB has
no CHECK constraint (libs/db/models.py:189), so 22 distinct Oracle values
silently leaked into a column the strategy's engine filters expect to be in
its 4-event vocabulary. Result: ~1,069 live rows silently dropped from
strategy candidate pool.
Files:
- NEW libs/parser/event_type_normalizer.py: normalize_oracle_event_type()
with conservative synonym map; normalize_oracle_event() additionally
uses _classify_event_type from rule_parser when an item_number is
present (item-code path is more reliable than Oracle's event taxonomy)
- MOD apps/pipeline/event_parser/main.py: oracle-fallback branch (~line
140) now calls normalize_oracle_event before writing to DB; emits
oracle_event_type_normalized log event when value changes
- NEW tests/unit/test_event_type_normalizer.py: 60 tests covering
identity, synonyms, case/separator insensitivity, None/empty,
non-string, item_number-precedence
Mapping highlights (justifications in test docstrings):
earnings_result/earnings_announcement/earnings -> earnings_release
guidance_revision/guidance_change/regulation_fd -> guidance_update
material_definitive_agreement/definitive_agreement -> material_contract
shareholder_vote/acquisition_disposition/bankruptcy/other -> other_material_event
Reg FD -> guidance_update mirrors rule_parser's Item 7.01 mapping for
internal consistency. Debatable but auditable.
Conservative pass-through for ambiguous values (financial_obligation,
articles_amendment, contract_termination, etc., 14 distinct values).
Visible filter-drop > silent re-tag.
Live DB counts that would reclassify on a future --reparse pass:
412 earnings_result -> earnings_release
409 shareholder_vote -> other_material_event
237 regulation_fd -> guidance_update
7 acquisition_disposition -> other_material_event
4 other -> other_material_event
TOTAL 1,069 rows currently in oracle-fallback dead-zone.
60/60 normalizer tests pass; combined parser+schema validator suite 86/86.
Follow-up flagged: run --reparse on historical oracle-fallback rows after
extending reparse_events() to also re-normalize known oracle-fallback
values (currently only re-parses event_type='unknown').
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
On 2026-05-08 19:45 UTC a transient Oracle FRED-proxy 5xx storm caused
paper_engine_vix_fred_unavailable to fire (3 sequential 500s; the 4th
attempt returned 200 OK with VIX=17.08). The pre-fix engine just left
VIXCLS missing from the macro dict, which the selector at
libs/backtest/selector.py:1171-1173 already treats strict-conservatively
(None → veto). So the 5/8 incident vetoed v7.356 PEAD candidates for
~1 minute with no money-loss exposure. But:
- Log severity was thin (info-level "unavailable", no escalation).
- No tolerance for short outages — every 500 cost the gate's signal.
- EventDetector PostgreSQL rows do not pre-populate macro_vix per
engine.py:2810-2812 comment, so live trading depends entirely on
the FRED fetch path.
Fix: in-memory session-scoped cache + 3-tier fallback in
PaperTradingEngine._fetch_macro:
Tier 1 fetch ok → cache (value, now_utc), log ..._ok (info)
Tier 2 fail, cache <24h → return cached value, log ..._stale_fallback
(warning) with staleness_sec
Tier 3 fail, cache stale → None, log ..._unavailable_blocking (error)
with reason={no_cache,cache_too_stale}
The None-veto path through the selector is preserved exactly, so no
silent-pass on unknown VIX. Empty/0 observations now treated as outage
to defend against an upstream regression flipping "missing→veto" into
"0→always-pass".
The thin libs/oracle_client/fred.py is intentionally untouched — fallback
policy belongs in the engine, not the generic client.
6 new tests in tests/unit/paper_trader/test_vix_fred_fallback.py
(success/cache-write, 500+stale-<24h, 500+stale->24h-blocks,
no-cache+500-blocks, empty-observations-blocks, recovery-refresh).
All 18 tests in -k "vix or fred" pass; full paper_trader suite 45/45.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add peer_sympathy_entry_timing_policy ("next_open"|"reaction_close") and
peer_sympathy_leader_filing_time_buckets to StrategyEngineConfig. The
reaction_close variant enters peers at peer's T 16:00 ET close on the
SAME trading day as the leader's print, addressing the v1 hypothesis
failure where T+1 gap had already absorbed the news overnight.
Lookahead defenses tightened for the new branch: cutoff is T 16:00 ET
(_bar_close_timestamp(decision_date)) instead of T+1 09:30 ET; bucket
allow-list excludes AMC filings (which under PEAD's reaction_date=T+1
convention pass the timestamp check but defeat same-session sympathy).
LeaderPrint now carries filing_time_bucket from the runner.
Runner: split _schedule_peer_sympathy_candidates into two phases.
reaction_close fires BEFORE _select_candidates_for_date(date) and emits
into _scheduled_add_ons[date]; next_open keeps the existing tail-of-loop
position emitting into _scheduled_delayed_entries[next_date].
v2 backtest (1052 trading days, midlarge-liquid-long-v1 snapshot):
trades 256→120, return -52.9%→-2.4%, MDD 61.6%→24.5%, SQS 19.6→30.2.
Sample sympathy plays: GOOGL on META +7.7%, AVGO on COHR +6.3%,
SLB on HAL +5.5%, GE on HWM +5.1%. Profit factor 0.977 (one tweak
from breakeven). Verdict: VIABLE BUT WEAK — salvage hypothesis
empirically validated, near breakeven, not promoted yet.
35/35 peer_sympathy unit tests pass (29 pre-existing + 6 new for
reaction_close path).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Falsifies the "EarningsRunup as PEAD sleeve adjunct" hypothesis on
v7.356 base. Backtest results:
v7.356 baseline: +2356% / MDD 23.55% / Sharpe 2.69
ER standalone: +254% / MDD 23.97% / Sharpe 1.10
v7.356 + ER comp: +2746% / MDD 25.21% / Sharpe 2.63
Headline composite gain (+390pp) is portfolio-luck distributed across
other sleeves (bullish_raised +$19.8k, parking +$11.2k, idle_form4
+$8.2k). EarningsRunup engine itself contributed -$7.6k in composite.
Symbol orthogonality CONFIRMED (0 ticker overlap between ER/PEAD
within ±14 days). Capital orthogonality FALSIFIED — composite missed
30 of 121 standalone ER entries (25%) due to insufficient_cash;
v7.356 base is already capital-saturated (47% idle baseline → 41%
with ER, indicating cash pressure).
Sharpe and profit_factor BOTH worse in composite, MDD slightly higher.
Verdict: do not integrate at engine_risk_budget=0.20. Engine kept
in-tree as falsification evidence + scaffolding for alternate
integration paths (lower budget, v7.364 base, or fully separate
capital bucket).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds three new synthetic-Candidate emitter engines parallel to the
existing leader_follower scheduler hook, plus look-ahead defenses
(LookaheadViolationError + per-engine assertions). Each engine is
covered by a standalone PoC config (no PEAD/parking/idle alpha) for
isolation backtests against the midlarge or broad snapshot.
Engines:
EarningsRunup (libs/backtest/earnings_runup.py)
- Trigger: days_to_earnings ∈ [3,7] AND attention_zscore_20d ≥ 1.5
AND dollar_volume_20d_zscore ≥ 1.0 (all evaluated at T-1 close)
- Entry: T+1 next_open. Exit: -4% / +8% / max_holding_days =
days_to_earnings - buffer (forced flat by close before announcement)
- PIT calendar: PointInTimeEarningsCalendar adapter for backtest;
oracle_surprise_prefetch fallback when parquet calendar absent
- PoC verdict (configs/experiments/earnings_runup_poc_v1.json):
119 trades over 1051 days, +37.27% total return, 44.46% MDD,
SQS 45.2 (profitability=55.5, risk=23.5, robustness=50.1).
VIABLE BUT NEEDS WORK — signal exists; standalone risk profile
too aggressive for v7.356 baseline (8.8% MDD on v7.364). Path
forward: per_trade_risk reduction, VIX gate, position cap, or
integrate as PEAD sleeve adjunct (not as standalone replacement).
PeerSympathy (libs/backtest/peer_sympathy.py)
- Trigger: leader passes PEAD filter (earnings_release / guidance_update
/ material_contract) AND leader reaction_close ≥ +5% AND peer 60d
correlation ≥ 0.55 over [T-65, T-5]. Top-2 peers by correlation
from leader_follower_extra_peer_symbols_by_sector + sector ETF
holdings.
- Entry: T+1 next_open on peer. Exit: -3.5% / +6% / max_holding=3 /
peer-earnings blackout
- PoC verdict (configs/experiments/peer_sympathy_poc_v1.json):
256 trades over 1051 days, -52.92% total return, 54.47% MDD,
SQS 19.6 (profitability=0.0, risk=5.4, robustness=100.0).
DEAD. The leader's catalyst is already absorbed by T+1 next_open
— peers gap up overnight before entry. robustness=100 confirms
the negative result is not noise. Salvage paths (not implemented):
reaction_close entry, raised-guidance-only restriction.
- Note: initial run_id was 0 trades due to a select_candidates
filter mismatch (engine.event_types=['peer_sympathy'] dropping
real event_type='earnings_release' rows). The runner adapter
was patched to bypass strategy_engine filtering for leader
selection; the manual peer_sympathy_leader_event_types filter
does the gating.
VolBreakout52w (libs/backtest/vol_breakout_52w.py)
- Trigger: close_T-1 > max(high[T-252:T-2]) AND volume_T-1 ≥
2 × median_volume_20d_T-2 AND ATR_14_T-1/close ∈ [0.015, 0.06].
Entry T next_open, exit -3% / +5% / max_holding=2 / MOC.
- Honest, look-ahead-safe descendant of the retired topgainer v1-v54
family. Five layers of strict-before assertions guard the bar
provider, candidate construction, trigger evaluation, and feature
timestamps. A leaky-provider proof-by-contradiction test
demonstrates the categorical catch.
- PoC verdict (configs/experiments/vol_breakout_52w_poc_v1.json,
broad-liquid universe): 1,332 trades, -87.28% total return,
88.74% MDD, SQS 24.4 (profitability=0.0, robustness=100.0).
DEAD AND HONEST. This is the most important finding of the three
PoCs: the topgainer v1-v54 lineage's headline returns (+267%
Sharpe 13.73 in best variants) were 100% lookahead bug. With
the bug removed, the 52w-high + volume + ATR signal has no real
alpha — the lookahead-corrected -4.3% from prior memory is
confirmed and amplified to -87% on a fuller universe and longer
horizon. Future "revive topgainer" proposals can cite this run
(bt_return_max_long_v1_broad-liquid_20260509042903892342_3bb473d9)
as definitive falsification.
- Pre-open gap guard inactive (no premarket data in broad snapshot).
skip_if_no_gap_data=true; the +4% gap-fade guard would not move
the result given the magnitude.
Shared infrastructure additions:
- libs/backtest/domain.py: LookaheadViolationError class +
StrategyEngineConfig fields (11 EarningsRunup + 11 PeerSympathy
+ 13 VolBreakout52w = 35 new fields)
- apps/backtester/run.py: _BacktestAttentionZscoreAdapter,
_RunnerPeerResolver, _schedule_earnings_runup_candidates,
_schedule_peer_sympathy_candidates,
_schedule_vol_breakout_52w_candidates wired into the daily
scheduler block. PeerSympathy adapter bypasses strategy_engine
filtering on leader selection (manual filter handles gating).
Tests: 21 (EarningsRunup) + 27 (PeerSympathy) + 38 (VolBreakout52w)
= 86 new unit tests, all passing. Broader unit suite: 1392 passed,
2 pre-existing failures unrelated.
Net engine state: EarningsRunup is the only viable new engine class.
PeerSympathy and VolBreakout52w are kept in-tree as falsification
evidence, not as production engines.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The _classify_event_type mapping (2.02 → earnings_release, 7.01 →
guidance_update, 1.01 → material_contract, 1.03 → other_material_event,
8.01 → other_material_event, 5.02 → management_change) was already in
place but used naive 'in items' string matching. Upstream extractors
sometimes deliver items as 'Item 2.02' or '2.02 - Results of Operations'
(full-description form), which silently slipped through to event_type
'unknown' and were rejected by all 12 v7.356 PEAD engines.
A reparse using the patched classifier touched 9,779 historical 'unknown'
rows; only 16 actually flipped (the rest are genuinely off-vocab 8-Ks
like 9.01-only, 3.01, 5.07). The fix is therefore small in retroactive
impact, but defends against future ingestion drift.
Changes:
- libs/parser/rule_parser.py: rewrote _classify_event_type with
_normalize_item_codes (regex \\b(\\d+\\.\\d+)\\b token extractor) and
tuple-of-pairs _ITEM_TO_EVENT_TYPE mapping. Earnings_release wins
priority over management_change when 2.02 + 5.02 co-occur, consistent
with the strategy's vocabulary intent.
- tests/unit/test_rule_parser.py: 7 new regression tests covering
AMD/MNST-style 2.02+9.01, dirty 'Item 2.02' / '2.02 - Results...'
forms, and negative cases (9.01-only, 2.03, 3.01 remain unknown).
Note: a follow-up vocabulary normalizer is still needed for the
Oracle-fallback path in apps/pipeline/event_parser/main.py:140, which
writes raw oracle_event.event_type values like 'earnings_result',
'shareholder_vote', 'regulation_fd' that don't match the strategy
vocabulary. Flagged for separate ticket.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pre-market label_generator runs request future-dated price windows from
Stock Oracle, which correctly returns 404 because the data does not yet
exist. The labeler was swallowing this as label_status='unavailable' with
entry_date=None. Snapshot export then filtered these rows out, so live
PEAD trading silently lost candidates whose entry_dates fell on
later trading days (e.g., post-market 8-K filings late Friday → Monday
open entry). This explains today's missed RKLB/SNDK/AKAM/MNST/AMD/MRNA
even though their 8-Ks parsed correctly.
Changes:
- libs/labeler/label_generator.py: in 404/empty-bars path, when
entry_date >= today, preserve entry_date and mark label_status='pending'.
New log event label_price_pending_future_window distinguishes from real
data-unavailable failures (past dates still log label_price_unavailable).
- libs/export/snapshot_export.py: include 'pending' in the
label_status filter so today's not-yet-labeled events flow into the
live snapshot.
- apps/pipeline/label_generator/main.py: regeneration logic also
retries existing 'unavailable' rows whose entry_date is null or future
to recover events already mis-labeled in the DB.
- tests/unit/test_labeler.py: regression test reproducing the
RKLB/SNDK/AKAM failure mode and asserting label_status='pending' with
entry_date preserved.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
룩백 진입 시 NO_PROGRESS 체크 기간(lookback_days_elapsed >= np_days)이 이미
지난 후보를 _process_entries 호출 전에 필터링. 현재 종가가 원래 진입가 기준
progress threshold 미달이면 진입 거부.
백테스트와 live paper trader 간 행동 불일치 원인: 세션 생성일이 이벤트 발생일
이후인 경우 룩백 기능이 NO_PROGRESS 조건을 무시하고 진입, 백테스트가
청산했을 포지션을 그대로 보유하는 문제 수정.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- List all orb_gainers_*.yaml configs via GET /orb-scanner/strategies (reads _meta.name)
- _get_params() now accepts strategy_id with per-strategy dict cache
- exit_check() replaces hardcoded stop constants (0.75/0.6/0.2/1.0/1.5) with params fields
- check() and gainers_scan() accept optional strategy param
- Frontend: strategy dropdown in page header (localStorage persisted)
- Position.strategy locked at add-time so exit checks always use entry-strategy params
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
event_volume (and potentially other columns) can arrive as int64 from the
pipeline while the stored snapshot uses double, causing pa.concat_tables to
fail with "incompatible types" every run and silently fall back to a full
rebuild. _coerce_schema() casts new rows to the existing snapshot's types
before concatenation so incremental works without a full rebuild.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
_process_entries silently let cash-capped plans through — e.g. a $1k
session with 0.55 risk sized NOW at 1 share, triggering parking
liquidation of only 2 TQQQ instead of the full balance needed for
the risk-based target. Broaden the _cash_limited trigger to include
plans where plan.shares < risk-based target, and size the liquidation
needed against that target instead of plan.shares. Mirrors the
engine_batches loop fix from the prior commit. cmd_run was also
constructing the engine without snapshot_store, so run daily's
lookback entry path never fired — route it through _make_engine.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Previously close_position(..., fill_price=X) was silently swallowed by
**kwargs while the DB wrote the simulated exit_price — broker and ledger
drifted on every exit.
- AlpacaBroker.close_position drops the **kwargs sink; docstring documents
that callers must poll get_order(order.id) for the actual filled_avg_price
- _poll_exit_fill(order_id, symbol, timeout=30s) added; 1s interval, returns
actual Alpaca fill price, logs timeout/terminal status
- 3 exit sites now poll and write the actual fill + re-derive net_pnl
with direction-aware sign (short-safe for future short configs):
run_daily main exit path (simulate_exit)
_monitor_close (intraday stop/target hits)
_process_exits (scheduled & forced exits incl. partial T1)
- WARN-level "paper_engine_exit_fill_drift" when actual vs simulated
diverges >0.5% so drift is visible in logs even when non-pathological
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- _is_market_open() now queries Alpaca's get_clock() so holidays, early
closes, and halted markets no longer skip orders; falls back to weekday
9:30–16:00 ET only on broker error (1.B.3)
- AlpacaBroker.get_clock() + MarketClock dataclass wrapping alpaca-py's
TradingClient.get_clock()
- PaperTradingEngine logs scoring_model at session startup (WARNING level)
so multi-session daemon makes the live-path scorer dispatch explicit
in every boot log — verifies v7.356 config's return_max_long_v13e
actually reaches _compute_score (1.C.1)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- v7.356 config: swap dataset_snapshot_id from manual_only ftb_fix_v2 to
auto_full_rebuild base canonical so paper trader can refresh snapshot
(root cause of processed_events=0 for 30 days)
- Multi-session order isolation (1.A.2/1.A.3): tag client_order_id with
pt-{session_id[:8]}-{uuid} prefix on all entry orders; _cancel_stale_orders
filters by own session prefix so one session no longer ghost-cancels another's
orders on shared Alpaca account
- Pipeline halt on failure (1.B.1): _run_pipeline returns bool and stops on
first subprocess failure instead of silently progressing with stale data
- Daemon restart window skip (2.2): run_open/run_close only marked completed
if processed_phases DB confirms prior execution — no more trading-less days
after mid-day restart
- event_parser: periodic batch commits every 500 docs (hypothesis fix for
3h hangs; unverified — may just be slow serial Oracle calls)
- Tests updated for _verify_order_fill tuple return + new cross-session
isolation test; all 23 paper_trader unit tests green
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Parking positions now use parking_state.avg_price instead of Alpaca's
blended avg_entry_price, which gets polluted when multiple sessions
share one broker account.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tests momentum_20d, grav_pull_20_50, range_pos_52w, obv_slope_20 on V46's
284-trade set (400d). All signals collapse vs V24 base: best G2=0.118R
(momentum_20d) vs 0.291R on V24 and pre-committed gate of 0.30R.
Composite gate (2+ features ≥ 0.20R) also fails. Axis permanently closed.
V46's PEAD event catalyst selection fundamentally changes the ticker/entry
universe vs V24's pure momentum base — momentum signals non-transferable.
V47 requires new data sources (options flow, 13F institutional ownership).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- scripts/backfill_finra_short_volume_cdn.py: bulk backfill FINRA short
sale CDN files (400d) into short_sale_daily table (382K rows inserted)
- apps/intraday_bt/scripts/diag_orb_short_volume_v46.py: Phase 1 diagnostic
on V46 400d trade set (180 trades, 99% coverage)
- scripts/audit_short_volume_coverage.py: fix asyncpg date param types
Result: all three short-ratio features fail G2 (max +0.168R vs gate 0.30R).
Direction is INVERTED from Boehmer (short squeeze dominates over informed-bear
signal in gap-up ORB). V25 axis permanently closed. V46 terminal.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
V31 research findings (2026-04-22):
- Hard gate (max_gap_zscore_20d=1.0): 45.2% vs V24 95.3% — catastrophically bad.
All three terciles are profitable; hard rejection removes positive-EV trades.
- Negative weight (weight_gap_zscore=-0.05): 90.6% DD-12.33% Sh=2.649.
Signal too weak (G2 failed at 0.181R < 0.30R threshold). G2 ≥ 0.30R
validated as reliable promotion gate: OBV-slope (G2=0.394R) passed; all
signals below 0.30R failed in backtest.
All 7 signal axes exhausted — V24 is the peak for current feature library.
domain.py: add max_gap_zscore_20d param (no-op at None default)
orb_simulator.py: add gainers_leader hard-gate (no-op at None default)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Was using non-existent gap_pct key in enrichment dict; now computes from
today_open and prev_close fields. V30 results: hurst INVERTED+G2 FAIL,
ou_theta G1 FAIL (near-zero Pearson). gap_vs_market re-running as V30b.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 1-min microstructure features tested on V24 200d trade set (n=101).
range_coil_orb shows directional signal (Pearson=-0.128, WR gap +9.9pp)
but fails G1 (n=101 < 120) and G2 (avg_R gap 0.195R < 0.30R threshold).
p-value ~0.10 — insufficient for promotion. V24 remains champion.
Script fetches 1-min bars from Oracle and caches to data/cache/intraday_1min/.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 1 diagnostic (diag_orb_short_volume.py) tests 3 short-ratio features
(prior-day, 20d avg, 20d z-score) on V24 200d trade set via FINRA CDN data.
All features fail all gates (|Pearson| ≤ 0.062, avg_R gap < 0.30R, WR gap < 5pp).
Direction inverts Boehmer 2020 hypothesis: high short ratio correlates positively
with ORB r_multiple (short-squeeze dynamics dominate at intraday ORB timescale).
V25 short-volume overlay not wired. V24 remains live champion.
Also adds scripts/audit_short_volume_coverage.py (Phase 0 DB coverage audit).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
V23 400d TRUE (correct pipeline, 2026-04-21): +146.09%, DD -13.66%, Sharpe 2.33.
Prior result (+120.87%, DD -23.97%) was from buggy AH-close pipeline; data fix
improved DD by 10.31pp. V23 now strictly dominates Safe v9 on ALL 400d metrics
(+45pp return, +3.5pp DD, +0.34 Sharpe). Updated Safe v9 status to
validated_200d_only and clarified Korean framing to remove the misleading
"최종 검증 전략" without qualification.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Should be actual_sym (defined in _parking_evaluate_gate scope),
not sym (which is only defined in _parking_buy).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
If Oracle is unreachable at 09:20 ET, without this check the pre-screen silently
waits through 5 chunks × 15s timeout (75s) before logging the empty-bars warning.
The new 3s probe logs a CRITICAL warning immediately so the operator can restart
Oracle before the market opens.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
os.kill(pid, 0) returns success for zombie (defunct) processes.
Added ps stat check — if process is in Z state, treat as dead and
clean up PID file so subsequent start() calls work correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Paper trader: Alpaca broker fixes, catchup-thread state improvements
- Web GUI: intraday backtest duplicate run button, paper trading fixes
- Experiment registry: cleanup old v15/v16 experiments, update index
- Tests: Oracle client test additions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- orb_simulator.py: min_abs_gap_pct filter, premarket dollar vol filter,
rolling_loss circuit breaker, drawdown_governor, streak_sizing,
trailing_tighten_at_r, allow_doji/red_to_green breakout, abs_gap scoring
for gainers_leader, ORBSimulationState, run_orb_simulation_with_state API
- metrics.py: loss_containment_score and related metrics
- features.py: enrich_daily_bars with gap_zscore, ATR ratio, range compression
- domain.py: extended ORBStrategyParams with new fields
- cache.py: DailyBarCache with merged parquet storage and coverage metadata
- simulator.py: base simulator updates for new entry/exit mechanics
- configs/intraday: updated orb_gainers_v23.yaml with canonical params
- Added BLD to midlarge symbol snapshot
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>