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>
Two fixes:
1. _rebuild_daily_from_intraday_cache now filters to regular market hours
(9:30–16:00 ET) before computing OHLCV. Previously used bars[-1] which
included after-hours data, distorting prev_close for gap calculations.
Root cause of V23 regression: HIMS Aug-4 after-hours drop to $54.81
made it appear as a +0.89% gap on Aug 5 instead of the correct -12.85%
gap (from $63.45 market close), causing it to fail min_abs_gap_pct filter.
V23 with fix: +109.32%, WR 58.1%, Sharpe 3.01, DD -12.91%
2. Remove temporary debug instrumentation (HIMS/2025-08-05 trace blocks)
that was left in orb_simulator.py during regression investigation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_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>
12 tests covering all screener filters: min_price, min_atr_14,
min_avg_dollar_volume, date selection (latest-before, future excluded),
and the V23 quality filters min_atr_pct / max_atr_pct added last session
but previously untested.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tests the stop evaluation loop that mirrors orb_simulator.py:477-580:
- Long/short stop_loss hit
- Breakeven promotion (stop moves to entry at 1R)
- trailing_tighten_at_r: verifies tight ATR multiplier fires at 2R vs
normal multiplier, exercising the tighten logic added in V23 port
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Tests the three structural guards that were previously untested:
- Rolling loss filter: skip logic, boundary, window slicing, date exclusion
- Circuit breaker: 25% drawdown halts session
- max_simultaneous_entries: blocks new entries when at cap
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>