127 Commits (465e248cb4b9653b351c5a71df0c13bde084744a)

Author SHA1 Message Date
I Luk Kim 465e248cb4 Wire pct-trailing end-to-end for EarningsRunup; tune v2 risk profile
Fix EarningsRunup trailing config that was previously captured in
Candidate.features only. Add activation-gated pct trailing to
update_trailing_stop with entry-relative giveback semantics
("lock in peak − giveback% of entry"). Plumb through
Candidate → ExecutionConfig → simulator. Default None preserves
legacy pct_X behavior — no impact on engines that don't opt in.

Add earnings_runup_poc_v2_tuned config: per_trade_risk_pct 0.65→0.30,
max_positions 30→8, max_positions_per_sector 30→4, macro_vix_max=30,
trailing_warmup_days 7→0 (activation gate replaces warmup).

v2 backtest (4y, 121 trades) vs v1 baseline (119 trades):
  Total return:  +37.27%  →  +254.69%
  Max drawdown:  53.23%   →  23.97%
  SQS:           45.2     →  69.7
  Robustness:    50.1     →  100.0
  Risk score:    23.5     →  35.0
  STOP r-mult:   −0.41    →  +0.65 (trailing-locked winners)

Promotion thresholds met: MDD < 25%, return preserved, SQS > 55.
Recommend integration as PEAD sleeve adjunct (orthogonal entry timing:
pre-print attention runup vs post-print drift) rather than standalone.

7 new pct-trailing tests: activation gate, ratchet, no-ratchet-down,
reversal-stop, legacy compat, end-to-end engine wiring. 28 passed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
3 months ago
I Luk Kim 956cc78f1b Add 3 candidate engine classes beyond PEAD: EarningsRunup, PeerSympathy, VolBreakout52w
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>
3 months ago
I Luk Kim 27de44c8d8 Fix label_price_unavailable: preserve future entry_dates as pending
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>
3 months ago
I Luk Kim 4067dab700 Fix lookback entry NO_PROGRESS bypass: screen candidates before entry
룩백 진입 시 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>
3 months ago
I Luk Kim 8771667e61 Add dynamic strategy selection to ORB Scanner
- 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>
3 months ago
I Luk Kim 1d24893326 Add centralized event logging + Logs/Health UI with PEAD/ORB tabs
- New EventsStore (SQLite WAL) captures all structlog + stdlib events
- ORB engine: 13 _emit() calls for orders, errors, kill-switch, circuit breaker
- ORB daemon: configures structlog sink so engine emits reach events.db
- ORB scheduler: phase lifecycle events (phase_started/completed) with job_run_id
- PEAD scheduler: same lifecycle pattern, pipeline stdout capture improved
- New /api/events + /api/health endpoints
- Logs/Health page: All / PEAD / ORB tabs, health cards, event table, detail panel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
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 24e88cb74f Fix cash-capped entries + wire snapshot store into run daily
_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>
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 99ad043f14 Paper trader Phase 1.A.1: poll actual fill price on exits, kill silent drop
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>
4 months ago
I Luk Kim d83a666f30 Paper trader Phase 1.B.3 + 1.C.1: market clock via Alpaca + scoring dispatch log
- _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>
4 months ago
I Luk Kim 2b6cea57b2 Paper trader Phase 1 fixes: multi-session isolation, pipeline halt, snapshot refresh unblock
- 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>
4 months ago
I Luk Kim df10d1291f Fix cross-session avg_entry_price contamination in positions view
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>
4 months ago
I Luk Kim bba1ce6789 Add V47 near-miss composite diagnostic on V46 400d base
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>
4 months ago
I Luk Kim 9095b376d9 Clean up superseded configs and commit accumulated R&D infrastructure
Key changes:
- Delete superseded strategy configs: orb_gainers safe_v2-v9, orb_pullback, vwap_reclaim, hypergap, leader_safe
- Add V46 prior_event_types param to domain.py + run.py event type wiring
- Major simulator.py enhancements: sector thrust sleeve, sector proxy mapping, helper functions
- Improve screener.py with better scoring/filtering
- Add new test coverage: test_simulator.py (776 lines) + test_screener.py (313 lines)
- Add V24.1 research candidate configs (w002/w003/w004/entrycap/losscap010 variants)
- Add leader momentum research configs and sweep files
- Update configs/snapshots/registry.json with new strategy registrations
- Add docs/leader_intraday_momentum_workflow.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 92840b857a Close V25 FINRA short-volume axis: backfill CDN data + Phase 1 diagnostic
- 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>
4 months ago
I Luk Kim 08e41831bc Promote ORB Gainers V46: PEAD prior-event signal (D-7 lookback, w=0.12)
V24 → V46 via PEAD (Post-Earnings Announcement Drift) signal. Stocks with
earnings_release or guidance_update in prior 7 calendar days show +14.5pp
win rate improvement and +0.348R advantage on ORB breakouts.

Phase 1 diagnostic (291 V24 200d trades):
  Pearson=+0.135, Δ=+0.348R, WR gap=+14.5pp — all gates pass.

Phase 2 validation (w=0.12, Pareto-optimal from sweep):
  200d: V46 +114.60% / -11.83% / 3.21  vs  V24 +94.78% / -11.29% / 2.83
  400d: V46 +173.78% / -14.11% / 2.60  vs  V24 +162.1% / -13.70% / 2.471

Code changes:
- libs/intraday/domain.py: add prior_event_lookback_days: int = 0 param
- libs/intraday/orb_simulator.py: fix bug — weight_event_catalyst now wired
  for gainers_leader engine (was restricted to stocks_in_play_dual_regime only)
- apps/intraday_bt/run.py: _prefetch_prior_event_features_db() helper +
  DB routing in both catalyst trigger blocks when prior_event_lookback_days>0

V24 → status: superseded. V46 → status: live_champion.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 4e2d2c0d13 Add V45 sector ETF diagnostic script; document exhausted signal axes
diag_orb_sector_etf.py tests sector ETF gap/momentum as ORB quality signals.
Finding: sector ETF signals null (best G2=0.122R vs threshold 0.30R); QQQ regime
filter already captures all market-level information.

Also documents exhausted axes in lineage memory:
- Momentum curve 60-252d (best G2=0.259R, 252d)
- Calendar effects (period-specific artifact)
- Event catalyst (n=39, below G1 n≥120 threshold)
- CMF 20d (G2=0.046R)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 16f49411cb Add V34-V44 ORB diagnostic scripts; wire obv_slope_5 + min_obv_slope_20d infra
Signal axes tested (V34-V44, all failed G2 ≥ 0.30R gate):
- V34 obv_slope_5 (5d): G2=0.083R (null)
- V35 obv_slope composite (5d+20d): regime artifact (200d +18pp, 400d -12pp)
- V36 RSI-14: G2=0.148R, G5a=0.742 (redundant with OBV)
- V37 BB %B / BB width: G2=0.186R
- V38 dollar_vol_trend / sleep_streak / prior_day / vol_trend: all fail G2
- V39 premarket acceleration + hold ratio: G2=0.013R (null)
- V40 prior-day market breadth: G2=0.008R (null)
- V41 min_obv_slope_20d=0.0 hard filter: -15pp (OBV as gate too aggressive)
- V42 52w-high proximity + range position: G2=0.200R (best near-miss, fails)
- V43 30-min ORB window: -14.79% (catastrophic)
- V44 trailing multiplier sweep 0.6-1.0: 0.80 confirmed global optimum

Infrastructure added (backward-compatible, V24 parity preserved):
- features.py: obv_slope_5 enrichment key
- domain.py: weight_obv_slope_5=0.0, min_obv_slope_20d=None
- orb_simulator.py: 5 wiring sites for obv_slope_5; min_obv_slope_20d gate

V24 remains live champion. 20 signal axes exhausted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 8bd4dba89c V32 exit sweep FAILED; V33 QQQ ORB quality FAILED — V24 absolute terminal
V32 parametric sweep on V24 base (4 variants — all failed):
- risk6 (0.05→0.06): -1.93pp 200d FAIL
- trail_loose (0.8→1.0): +7.69pp 200d but -6.72pp 400d FAIL (regime-specific)
- breakeven_early (1.0→0.75R): -7.39pp 200d FAIL
- trail_09 (0.8→0.9): -14.83pp 400d FAIL (non-monotonic; 0.8 is global optimum)

V33 QQQ ORB candle quality diagnostic (new script):
- qqq_orb_body_pct/return/close_loc: Pearson ~0.001–0.022, day-level ρ ≈ 0
- QQQ daily gap filter already captures all market-level regime signal
- All 3 features fail G1 and G2 completely

All improvement axes now exhausted: 7 signal axes + exit sweep + market intraday.
V24 (OBV-slope w=0.05, 200d +95% / 400d +162%) is the absolute final champion.
Next R&D cycle requires new data source: sector ETF ORB / options flow / architecture.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 5c6950af9d Fix gap_vs_market computation in diag_orb_trending_features.py
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>
4 months ago
I Luk Kim 733d8dabbd Add V29/V30 ORB diagnostic scripts; document V29 structural axis failure
V29 (diag_orb_structural_features.py): grav_pull G2 FAIL (+0.203R), market_temp G1 FAIL,
momentum_20d near-miss G2 FAIL (+0.291R). All 3 pass G1+G3 but miss G2 threshold.

V30 (diag_orb_trending_features.py): hurst_60d, ou_theta_60d, gap_vs_market (idiosyncratic
gap above QQQ). Running.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 5b7e17d50a Add V28/V29 ORB diagnostic scripts; V27/V28 axis verdicts documented
V28 (diag_orb_volatility_features.py): tests gap_zscore_20d, range_compression_10_60,
atr_ratio_10_60. At 400d: gap_zscore passes G1(n=176)+G3(+10.6pp WR)+G4+G5 but
fails G2 (avg_R gap 0.181R < 0.30R threshold). ABORT — V24 champion.

V29 (diag_orb_structural_features.py): tests grav_pull_20_50, market_temp_5_20,
momentum_20d — structural/regime axis orthogonal to OBV-slope.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim ab34c3ef1b Diagnose RSI-14/BB%B axis for V27: ABORT — redundant with OBV-slope
RSI-14: Pearson=+0.157, G2 passes (+0.316R gap, direction confirmed).
BUT ρ(RSI-14, obv_slope_20)=+0.726 — G5a FAIL. RSI captures same momentum
information as V24's OBV-slope. BB %B is ρ=0.889 with RSI — identical axis.
Both fail G1 (n=98 < 120 threshold). V24 remains champion.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 137b413084 Diagnose ORB tape ignition (V26): NULL result, V24 remains champion
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>
4 months ago
I Luk Kim 1e7fcde14d Diagnose FINRA short-volume axis for V25: NULL result, V24 remains champion
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>
4 months ago
I Luk Kim 4b8a157a67 Promote V24 ORB Gainers: add OBV-slope(20d) accumulation quality weight
Phase 1 diagnostic (diag_orb_quality_features.py) on V23 200d trade set found
obv_slope_20 passes all edge gates: Pearson=+0.2349 with r_multiple, top-tercile
WR 75% vs bottom 59.4% (+15.6pp), avg_R gap +0.394R. Hurst_60 and OU-θ_60 failed.

Weight sweep: 0.05 is Pareto-dominant (0.10/0.15 blow DD).

200d (same window): V24 +94.8% DD-11.3% Sharpe 2.83 vs V23 +85.0% DD-11.6% Sharpe 2.66
400d (same window): V24 +162.1% DD-13.7% Sharpe 2.47 vs V23 +149.4% DD-13.7% Sharpe 2.36
V24 Pareto-dominates V23 on both windows. V23 marked superseded.

Code changes:
- libs/intraday/features.py: add compute_obv_slope_approx() + enrich_daily_bars field
- libs/intraday/domain.py: add weight_obv_slope field to ORBStrategyParams
- libs/intraday/orb_simulator.py: wire obv_slope_20 read/store/score in gainers_leader branch
- configs: orb_gainers_v24_quality_overlay.yaml (new champion, live_readiness: experimental)
- configs: orb_gainers_v23.yaml status → superseded

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 2560556c72 Fix: NameError 'sym' undefined in parking overlay gate check
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>
4 months ago
I Luk Kim 04bb11f9bb Add Oracle health pre-check in run_pre_screen to warn before bar-fetch timeout
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>
4 months ago
I Luk Kim ee2f1f6f84 Fix zombie daemon blocking ORB restart in ORBDaemonController
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>
4 months ago
I Luk Kim b98442b28a Add paper trader improvements, web GUI updates, and experiment registry cleanup
- 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>
4 months ago
I Luk Kim 6489b22c89 Fix: paper trader missing TQQQ overlay evaluation in parking gate
_parking_evaluate_gate() was returning only QQQM or SGOV, never TQQQ,
because the low-vol overlay logic (_evaluate_low_vol_overlay_target in
backtester) was not implemented in the live engine. Add overlay check
at the end of the gate: vol threshold, temperature, entropy, and Hurst
R/S analysis — matching the backtester's conditions exactly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim cbeb463685 Fix stale hardcoded schedule references in run_breakout_check
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 2d6da31bd5 Fix module-level docstring: run_stop_check schedule description
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim ba491b2a0d Fix EOD DB sweep bug and add regression test
Bug: pending candidate DB records were only swept inside the "has open
positions" branch of run_eod_exit, so a server restart mid-day (ORB
detection ran, no breakouts, no positions) left candidates as "pending"
forever.

Fix: move the in-memory and DB pending sweep to run unconditionally before
the positions check.

Test: TestEodDbSweep verifies both code paths (no-position + in-memory).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim ecbff65ea4 Update run_stop_check docstring to reflect dynamic sim_bar_minutes schedule
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 1f1fd4299f Move function-scope library imports to module top in orb engine
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 713d04deef Fix EOD exit: sweep DB for stale pending candidates after restart; remove misleading compound_returns override
- run_eod_exit now queries DB for any pending candidates not in-memory
  (guards against server restart mid-day leaving zombie pending records)
- Remove compound_returns=True force-override; live engine doesn't use it
  and V23 config has compound_returns=false

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 6ae4588a62 Move inline stdlib imports (copy, time) to module top in orb engine
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 190f7a47fa Fix V23 live engine: regime filter, sizing fidelity, trailing tighten
- screener.py: add min_atr_pct/max_atr_pct filters to live_pre_screen
- engine.py: prepend regime ticker (QQQ) to bar fetch so regime filter works
- engine.py: add rolling loss + account circuit breaker at run_orb_detection
- engine.py: patch today_open from first 1-min bar so regime gap is real
- engine.py: explicit regime + breadth filter before compute_orb_candidates
- engine.py: max_simultaneous_entries guard in run_breakout_check
- engine.py: _compute_sizing_capital with daily_budget_reset (fixed $10k base),
  drawdown governor, and streak sizing (win bonus / loss penalty)
- engine.py: trailing_tighten_at_r in run_stop_check (tight multiplier at 2R)

Bug fixes in _compute_sizing_capital:
- streak direction: remove reversed() so outcomes[0] = newest trade
- daily reset: use initial_equity as base (not growing equity), matching V23

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 41462c9e2c Fix: parking buy fill confirmation blocks event loop with 5s hard timeout
Make _parking_buy async (asyncio.sleep instead of time.sleep) and extend
confirmation from 5s to 30s per attempt with up to 3 retries on timeout.
On timeout, cancels the stale order before retrying with fresh price data.

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 27b8a49d77 Fix: paper trader ignores per-engine overrides in build_planned_order
All five build_planned_order() calls in apps/paper_trader/engine.py were
missing execution_config, causing build_planned_order to fall back to
base config.execution and silently ignore per-engine target_1_r_override,
target_1_fraction_override, max_holding_days, tiered-target settings, etc.

The backtester has always passed execution_config=_build_effective_execution_config()
(run.py:2210). This divergence caused paper trading to compute wrong target
prices and partial-exit fractions — e.g. AVGO entered with target_r=1.5
(base) instead of 3.0 (engine override), triggering a premature partial
exit on 4/15 that the backtest never produced.

Fix: add execution_config=build_effective_execution_config(candidate, self._config)
to all five call sites and hoist the function to the module-level import.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim fdd56ff683 Add duplicate run button and improve RunConfigPopup UX in Intraday ORB Backtest
- Add Copy button per run row in Run History table; opens pre-filled RunConfigPopup
  with original run's period/universe/compound_returns settings
- Make RunConfigPopup larger (500px wide) and more spacious with proper header/X button
- Prevent dialog from closing on backdrop click; only Cancel or X button closes it
- Range date inputs laid out side-by-side in 2-column grid

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim ebfada24cb Fix: don't permanently reject events when parking sell fails transiently
- When _parking_liquidate_for_event returns False but parking had shares
  (sell failed due to transient Alpaca error), skip without recording so
  event retries on next run_next_open instead of being permanently blocked

- Improve parking_sell_for_event_failed log: include exc_type, symbol, qty
  so we can see the actual exception (was silently swallowed with empty msg)

- Also deleted AVGO from processed_events so it retries tomorrow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 5ea8850ac2 Fix paper trader startup: run missed run_open if market still open; improve poll_error logging
- AutoScheduler._run_catchup: if server starts after 9:35 AM ET but before
  market close (16:00 ET), and run_open hasn't already run today
  (checked via processed_phases), run it immediately instead of silently
  skipping it — prevents AVGO/event entries being missed on late starts

- filing_poller: log exc_type alongside error so empty-string exceptions
  (e.g. HTTPError()) are still identifiable by their type

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 86419beeb0 Fix ORB intraday data pipeline and consolidate strategy configs
- screener: switch from non-existent single-ticker endpoint to multi-ticker
  /alpaca/intraday batch calls (grouped by date, chunk ≤ 75); fixes 0-trades
- cache: bump version 2→3 to invalidate stale IEX Parquet files
- oracle_client: add get_multi_intraday_bars_today() for IEX real-time feed
- paper_trader: use /alpaca/intraday/today for live sessions, /alpaca/intraday
  for historical (SIP)
- intraday.py: define _BUILTIN_STRATEGIES={} to fix /api/orb/strategies import
- delete orb_p1–p10_winner + variant configs; add strategies/orb_default.yaml
  (Phase 10 params) as the single registered web strategy

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 564bcba27c Add ORB pre-market screening and fix Oracle/Alpaca reliability bugs
- Add run_pre_screen() at 9:20 ET: fetch daily bars + enrichment + quality filter
  before market open, narrowing universe for faster orb_detect intraday fetch
- run_orb_detection() uses cached pre-screen data when available; falls back to
  full pipeline if pre_screen missed (late start, failure)
- Add _last_trading_day() helper to skip weekends/holidays for bars_end,
  preventing Alpaca 502 on Mondays (today-1 = Sunday was causing failures)
- Fix Oracle client chunk_size 300→75: Alpaca rejects 100+ ticker URL requests
- Add pre_screen event to build_schedule() at 9:20 ET and dispatch in _run_trading()
- run_session_now() runs pre_screen before orb_detect for efficiency
- Add ORB daemon, engine, models, state, screener, and intraday strategy configs
- Add intraday library (libs/intraday/) and web routes for ORB/intraday trading

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