diff --git a/.gitignore b/.gitignore index 984c108..3ee7566 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ backtest/__pycache__/ logs/ backtest/research/sizing_2026-09/ backtest/research/replay_2026-09/ +backtest/research/cycle_2026-09-10/ diff --git a/CLAUDE.md b/CLAUDE.md index 8a21bf1..ac1a957 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,7 +12,7 @@ Every numeric threshold in `config.py` and `pair_signals/xauusd.py` was backtest ```bash python test_all.py # QA suite, no network (mocked); prints its own count - # (499 tests / 33 modules as of 2026-09-09) + # (501 tests / 33 modules as of 2026-09-10) python main.py --pair XAUUSD --timeframe H1 --detail # CLI signal report (--json for full dump, # --panel for the advisory LLM agent panel, --outlook for the # hourly AI market brief; the violent-move watch line appears @@ -65,7 +65,7 @@ Signal currency everywhere is `SignalStrength` (−2..+2). DataFrames are UTC tz ### Layers -- **`providers/`** — one class per provider subclassing `BaseDataProvider` (`providers/base.py`), which owns everything the pipeline depends on: TTL+boundary cache, forming-candle drop, freshness check, validate/clean, paginated `fetch_range` (feeds the backtester from any provider), optional tick streaming. Subclasses implement only `_fetch_raw` + symbol/interval maps. **Cleaning rule (2026-09-07):** the signal-side outlier filter is a bad-print filter, not a volatility filter — a >10σ close-to-close bar is dropped only when the NEXT close snaps back ≥80% of the way (`OUTLIER_SIGMA`/`OUTLIER_REVERT_FRACTION` on the base class); persisting moves (NFP/CPI/Sunday-gap bars) and the newest bar are always kept. The previous unconditional 10σ rule deleted ~36 real M5 bars/yr in the backtester and a *different* ~24/yr live (window-dependent σ), hiding e.g. the 2026-09-04 12:30 UTC NFP bar from ATR/exhaustion math. Oanda fetches retry 401/5xx/network faults along `RETRY_DELAYS_S` (2/5/10 s) under a 45 s `RETRY_BUDGET_S`; a scan that still loses its primary frame is logged with `scan_kind='failed'`. Registry + `make_data_provider(config, name=None)` in `providers/__init__.py`; config.json `data_providers.active` selects the live candle feed and the optional `data_providers.streaming` scalar selects a different provider for the tick stream. A test enforces that subclasses don't override shared base methods (`supports_streaming`/`iter_ticks` are the sanctioned streaming override points). Tiingo streams mid-price ticks (`wss://api.tiingo.com/fx`) — do not re-add spread display from its free FX feed (two-tier indicative quotes, removed 2026-07-23); the no-spread rule extends to the WS stream. +- **`providers/`** — one class per provider subclassing `BaseDataProvider` (`providers/base.py`), which owns everything the pipeline depends on: TTL+boundary cache, forming-candle drop, freshness check, validate/clean, paginated `fetch_range` (feeds the backtester from any provider), optional tick streaming. Subclasses implement only `_fetch_raw` + symbol/interval maps. **Cleaning rule (2026-09-07):** the signal-side outlier filter is a bad-print filter, not a volatility filter — a >10σ close-to-close bar is dropped only when the NEXT close snaps back ≥80% of the way (`OUTLIER_SIGMA`/`OUTLIER_REVERT_FRACTION` on the base class); persisting moves (NFP/CPI/Sunday-gap bars) and the newest bar are always kept. The previous unconditional 10σ rule deleted ~36 real M5 bars/yr in the backtester and a *different* ~24/yr live (window-dependent σ), hiding e.g. the 2026-09-04 12:30 UTC NFP bar from ATR/exhaustion math. Oanda fetches retry 401/5xx/network faults along `RETRY_DELAYS_S` (2/5/10 s) under a 45 s `RETRY_BUDGET_S`; a scan that still loses its primary frame is logged with `scan_kind='failed'`. **Live fetch depth (2026-09-10):** every live fetch requests `max(MIN_LIVE_BARS=1000, min(60 days × bars/day + 20, 5000))` bars (`_live_outputsize`) — M5/M15 5000, H1 1460, H4 1000 (was 380), D1 1000 (was 300). The engine's EMAs use `ewm(adjust=False)`, so the first bar of the frame keeps weight (1−2/201)^n in the 200-bar trend EMA; at 380/300 bars the live H4/D1 trend reading flipped on ~0.3–0.75% of bars versus the backtester's converged history (and on the live D1 bar of 2026-09-10 itself), at ≥ 600 bars on none (`backtest/research/cycle_2026-09-10.md`). Registry + `make_data_provider(config, name=None)` in `providers/__init__.py`; config.json `data_providers.active` selects the live candle feed and the optional `data_providers.streaming` scalar selects a different provider for the tick stream. A test enforces that subclasses don't override shared base methods (`supports_streaming`/`iter_ticks` are the sanctioned streaming override points). Tiingo streams mid-price ticks (`wss://api.tiingo.com/fx`) — do not re-add spread display from its free FX feed (two-tier indicative quotes, removed 2026-07-23); the no-spread rule extends to the WS stream. - **`engine.py`** — `TechnicalEngine`: 9 scored indicators (ema_trend, rsi, macd, bollinger, adx, roc, keltner, divergence, candlestick) + ATR as context. Regime classification (trending_up/down, `trending` = mixed/direction-UNKNOWN, transitioning, ranging) drives per-indicator weight multipliers and regime-aware RSI reinterpretation. Compute order matters (ATR first; MACD always computed even when not a pillar — divergence needs the histogram). Private series ride on the result as `_underscore` attrs; scorers/formatters skip anything `_`-prefixed or weight-0. - **`signal_aggregator.py`** — MTF agreement, diminishing-decay penalty system, exhaustion hard block, quality grading, then an ordered chain of post-grade gates (session, XAU SELL quality/vol/hour, regime blocks, M5 noise gates), then three setup generators: XAU ATR (A+/A), generic swing-based, plus the M5 pullback (`XAU_PB`) and chart-pattern (`XAU_CP`, `chart_patterns.py`) entry paths. Step 7 lives in `_attach_core_setup`; a final pass after the PB/CP archetypes (`_drop_stale_spread_gate_vetoes`, 2026-09-08) moves a core generator's `SPREAD GATE:` veto to `cleaned_warnings` whenever a later generator shipped a setup (metadata only — books unchanged; the exported `warnings` text column can differ; the prefix is the `_SPREAD_GATE_PREFIX` constant shared by the emitters and both cleanups). The PB/CP generators run AFTER the gate chain and force quality to "B"; the CP path deliberately accepts grade-F composites and re-checks only session/day/big-candle (via warning prefixes) plus its own SELL/vol/RSI gates — it does NOT re-check exhaustion or the M5 min-score/MTF gates. **This is validated behavior, not a bug** (2026-08-04 audit: 226/586 trades in the shipped M5 12mo book are XAU_CP, +38.9R, 104 of them exhaustion-flagged and net-positive) — changing it is a rule-1 revalidation event. `TradeSetup.strategy` selects trailing params; trail params are stamped on the setup from the *producing* config so per-TF overrides reach formatters. - **`pair_signals/`** — per-pair config factories. **Always use the factory (`produce_signal` / `make_config`), never bare `PlatformConfig()`** — the factory applies the config.json overlay first, then the validated per-TF deltas (M5 hour-9 block, M15 trail arm 0.70, M15 SELL-hour block, M5 SELL-in-transitioning block, H1 all-shorts block (2026-09-03), M15 RSI regime-multiplier × 0.5 via `technical.regime_weights_override` (2026-09-09), etc.; evidence in the docstring). A test enforces factory ≡ inline-config equivalence. Confirmation map: M5→[M15,H1], M15→[M5,H1], H1→[M15,H4], H4→[H1,D1]. @@ -77,7 +77,7 @@ Signal currency everywhere is `SignalStrength` (−2..+2). DataFrames are UTC tz - **`violent_watch.py`** — **display-only** violent-move watch, dark by default (`violent_watch` config block: `enabled`, `alert_line`, `headsup`, `max_headsup_per_day`, `headsup_min_level`; env `VIOLENT_WATCH_ENABLED`). Flags when a "violent bar" (range ≥ 3× prior ATR14) is unusually likely and stamps `SignalReport.watch_line` **post-hoc** via `attach_watch` (telegram alert path, `main.py`, `dashboard_publisher`) — same contract as the agent panel: never imported by `signal_aggregator`/`engine`/`backtest/`, never raises, never read by a gate (test-locked, Module 30). Triggers: a high-impact **USD** calendar release within ±30 min (hot), hot clock slots (weekday 12–14 UTC, weekday 01 UTC, Sunday reopen 22–23 UTC), and **price-extreme** indicator readings (outside the Bollinger bands, RSI <30 / ≥70, outside the Keltner channel). Level = hot when the calendar fires or a clock slot plus ≥1 price extreme; watch on a clock slot alone or ≥2 price extremes. Context readings (ADX slope >3, volatility "extreme", regime "transitioning", MACD STRONG_BUY/STRONG_SELL) are printed but **never raise the level** — the first draft that let them count flagged 27% of bars "hot" at only ~2× odds; the shipped rule flags 8.6% at 3–5× (evidence: `backtest/research/sizing_campaign_2026-09.md` addenda). Direction is called DOWN on bearish readings (70–78% on M5 and M15) and UP only on M5. Telegram also sends a standalone heads-up on the fastest scanned TF (`_HEADSUP_TF`), rate-limited per UTC hour+trigger-set and per day; it explicitly tells the owner **not** to touch open trades — every protection tested lost money, the stop is the protection. - **`telegram_bot.py`** — owner-locked bot + subscriber channel; candle-boundary auto-scan loops (M5/M15/H1/H4, boundary+10 s), alert quality threshold + fingerprint dedup (committed only after a successful send; the fingerprint is signal+direction — grade is NOT part of it, a grade UPGRADE re-alerts, an A↔B flicker does not), 📰 LLM market-context line on alerts when available, WebSocket level-breach monitor fed by the streaming provider (`data_providers.streaming`, falling back to the active provider; auto-disabled when the resolved provider can't stream or has no API key — Oanda does not stream, Tiingo does). The breach handler reuses this bar's M15 report (`_cached_or_scan`, keyed by closed-bar epoch) instead of rescanning — it used to log 3.5 M15 rows per candle. **Alerts require a trade setup (2026-09-08):** `_is_alert_candidate` (shared by the scan and breach paths) needs non-neutral + grade ≥ threshold + `trade_setup`; a signal every generator declined used to go out anyway (a B-grade no-setup SELL under a "costs would eat the edge" warning). **Sends are logged:** every delivered alert / breach alert / violent-move heads-up writes an `… sent:` INFO line carrying the signal-log row id, and a no-setup candidate writes `alert skipped: … (gate reason)` — before this only failures were logged and a review had to reconstruct sends from panel rows. Rotating log: `logs/telegram.log`, one file per server-local day × 30 backups (was 3-h × 240 until 2026-09-08; the old `_HH`-suffixed files on the live box are not matched by the daily cleanup and need one manual delete). - **`dashboard_publisher.py` / `static/dashboard.html`** — web dashboard as a **static publisher** (live LAMP server): standalone daemon scans at candle closes via `produce_signal` and atomically writes `snapshot.json`/`news.json`/`dashboard.html` to `$DASHBOARD_OUT_DIR` (Apache serves them; page is public by owner decision). **Hard independence rule: never imports, and is never imported by, `telegram_bot`** — confirmation map and boundary math are deliberate local copies, so a web-side failure can only mean a stale page. Due-TF classification uses the RAW wake time (flooring wake−buffer skips every H1/H4 scan — 2026-08-02 bug). News panel reads `NewsEngine.get_fundamental_context()` plus the additive `get_week_events()` accessor (display-only; no signal path calls it); as a separate process it has its own news caches and DeepSeek daily cap. News/calendar rows are clickable (detail dialog); headlines carry `id` = sha1(headline) in news.json, and exactly one on-demand endpoint exists: `static/news_summary.php` (installed by `install_static`), which serves a DeepSeek gold-impact summary per headline — whitelist-gated against news.json ids, cache-forever in `ai/`, own fail-closed daily cap (150/day, file-based, separate from the publisher's), key at `/etc/gold-dashboard/deepseek.key`, never reads repo config. Deployment checklist: `deploy/README-dashboard.md`. -- **`backtest/gpu_backtest.py`** — bar-by-bar replay through the real `analyze_from_dataframes` (news off). Realism model (do not regress): next-bar-open entries re-anchored to the raw price path, round-trip `--spread` charged per trade, trade resolution on RAW bars (cleaning applies only to signal-side data), same-bar SL+TP tie → SL first, gap fills at bar open, trail ratchet at bar close only. Indicators are precomputed once per TF (CuPy GPU or NumPy fallback — check the `Backend:` log line); replay fans out over worker processes and the 5-bar cooldown is applied in an ordered pass afterwards, so parallel and sequential books are identical. Parquet cache in `backtest/cache/` keyed by provider/symbol/TF/window. `backtest/*.csv` are gitignored pinned trade books from past campaigns — used for manual before/after comparison. `backtest/instructions_for_backtest.txt` lists what every backtest report must record. +- **`backtest/gpu_backtest.py`** — bar-by-bar replay through the real `analyze_from_dataframes` (news off). Realism model (do not regress): next-bar-open entries re-anchored to the raw price path, round-trip `--spread` charged per trade, trade resolution on RAW bars (cleaning applies only to signal-side data), same-bar SL+TP tie → SL first, gap fills at bar open, trail ratchet at bar close only. Indicators are precomputed once per TF over the whole cached history (CuPy GPU or NumPy fallback — check the `Backend:` log line); the per-TF warm-up pad before `--start` is `_history_pad_days` (62 days for M5/M15/H1, 234 for H4, 1400 for D1 — the same 1000-bar seed the live floor guarantees; 2026-09-10, no trade in any book moved); replay fans out over worker processes and the 5-bar cooldown is applied in an ordered pass afterwards, so parallel and sequential books are identical. Parquet cache in `backtest/cache/` keyed by provider/symbol/TF/window. `backtest/*.csv` are gitignored pinned trade books from past campaigns — used for manual before/after comparison. `backtest/instructions_for_backtest.txt` lists what every backtest report must record. ## Rules for making changes (read before editing signal logic) @@ -104,7 +104,7 @@ Signal currency everywhere is `SignalStrength` (−2..+2). DataFrames are UTC tz - Cost reporting is dual: $0.65 is the measured-Oanda realistic headline ($0.65-$0.72 round trip), while $0.30 remains for cross-book comparability and as the live TP spread-gate input. Raising `round_trip_cost` from $0.30 would move the TP floor from about $9 to $19.50 and requires full rule-1 revalidation. The $0.65 figure comes from the 2026-07-24 Dukascopy tick study (9M ticks over 12 months; its tooling was removed 2026-09-06 — the conclusion lives here and in memory). - Twelve Data is signal-incompatible with Oanda (Phase 1: about 19% M5 bar-direction disagreement and a systematic ~$1 high bias; see `backtest/research/feed_divergence_2026-08-14.md`). On an Oanda candle outage, stop scanning rather than switching to TD candles. - Tiingo is streaming-only (`data_providers.streaming: "tiingo"`), not a designated candle fallback. Oanda-Tiingo signal parity is documented in `backtest/research/signal_parity_2026-08-14.md`; Phase 4's two-week live shadow scan (`scripts/shadow_scan.py`) remains a manual operational step. -- Human-facing docs in `docs/` (`user_guide.html`, `beginner_guide.html` + PDF builds) must be kept in sync when signal behavior changes. As of 2026-09-06 the PDF builds are BEHIND the HTML (violent-move watch entry added without a PDF rebuild). +- Human-facing docs in `docs/` (`user_guide.html`, `beginner_guide.html`) must be kept in sync when signal behavior changes. There are NO PDF builds in the tree (checked 2026-09-10; earlier notes about PDFs lagging the HTML are moot). The backtester and the replay recorder read/write Parquet through `pyarrow`, which is installed in the dev `venv/` but deliberately absent from `requirements.txt` (live code never touches Parquet). - 2026-09-03 signal-log campaign shipped two new per-TF gates (`backtest/research/signal_log_campaign_2026-09.md`): M5 `sell_blocked_regimes=["transitioning"]` and H1 `sell_min_quality=[]` (all H1 shorts blocked). Current-HEAD M5 canonical book is now `backtest/oanda_m5_12mo_065_nopb_20260903.csv` (557 / +112.21R / PF 1.56, halves +55.9/+56.4); H1 reference `backtest/oanda_h1_12mo_065_20260903.csv` (91 / +22.2R / PF 1.79; 5yr +49.1R PF 1.46; OOS +16.7R PF 1.74). - 2026-09-06 stake-sizing campaign (`backtest/research/sizing_campaign_2026-09.md`): a walk-forward "second opinion" stake multiplier (meta-labelling on 65 decision-time features, 3-year Oanda books at $0.65) FAILED the pre-registered rule on M5 (clean null) and M15 (lift inside the label-shuffle null, explained by BUY-vs-SELL). Nothing that changes numbers shipped. Behaviour-neutral tooling remains: `position_sizing.build_features` + `--export-features` (default export stays 63 columns), `SignalReport.penalty_tags/penalty_total/momentum_fading`, `TradeSetup.stake_multiplier` (None; reserved) (the campaign's research scripts were removed 2026-09-06; git history has them). Stage-B facts: M5 stretch measures are POSITIVE in both halves (do not re-propose exhaustion sizing on M5); M15 exhaustion tag is weakly negative (≈ +3R/yr, noise); bursts cluster at 12–15 UTC and 01 UTC bar-close and do NOT hurt entries. - Auto-execution (oanda_daemon.py / oanda_executor.py) was removed 2026-09-03 — the owner is not auto-trading; last commit containing it is 31002cb. Book-parity rules that lived in its docstring (no broker TP, 5-bar cooldown, once-per-bar trail) remain the backtester's model. @@ -114,3 +114,4 @@ Signal currency everywhere is `SignalStrength` (−2..+2). DataFrames are UTC tz - 2026-09-08 news archive shipped (`news_archive.py`, 483 tests; nothing in the signal core changed, both news gating flags stay `False`, books untouched). Motivation: headlines only ever survived as a side effect of signal logging and `signals.db` was rotated around 2026-09-04, destroying the 3-week / 4,054-headline corpus the news study was written on — with `logs/` gitignored, git could not recover it and nobody noticed for four days. Day-one corpus after backfilling both DBs on disk: **1,016 distinct headlines, 846 of them DeepSeek-labelled**, growing ~200/day (~1,400/week). The point of collecting it: distil DeepSeek's headline scoring (78/78 on the 2026-08-06 golden set) into a numpy-only local model so the fallback stops being the keyword scorer (**24/78 = 31%**, which is what silently runs on no key / disabled / daily cap / timeout / HTTP or parse failure / batch overflow). **Do not train yet** — 800-odd labels gives a holdout too small to grade (SE ±0.032); the pre-registered plan is ~4,000 deduplicated headlines over ≥3 weeks, time-split with near-duplicates clustered first (the same story is re-titled by several outlets, so a random split scores memorisation), headline text ONLY (the teacher never sees `description` — `_llm_headline_scores` sends titles), graded against the golden set recoverable with `git show 1cebebf^:backtest/research/llm_news_golden.csv`. When it ships it slots in as DeepSeek → model → keyword and stays display-only. Training happens offline in `venv/`; a Module 32 test now AST-walks the root, `providers/` and `pair_signals/` asserting no `sklearn`/`scipy`/`lightgbm`/`torch` import and that none of them reach `requirements.txt` — that guard did not exist before. - 2026-09-09 replay recording shipped (`backtest/replay_record.py`, `backtest/replay_scorecard.py`, Module 33, 498 tests; nothing in the signal core changed, books byte-identical — `backtest/research/replay_recording_2026-09-09.md`). `--record DIR` stores every ACCEPTED trade's decision evidence in Parquet: each indicator's vote / value / confidence / BASE weight / regime multiplier / EFFECTIVE weight / contribution on the primary AND both confirmation TFs (`indicators.parquet`, long; absence = the indicator did not fire, never a zero row), per-TF regime/composite, penalty tags, setup, outcome, and the Forex Factory calendar events from 4 h before entry to exit (`events.parquet`, all-day rows matched on the New-York date; scalar `ev_*`/`us_holiday` columns on `trades.parquet`); `--record-bars` adds one float32 row per ANALYZED bar with the same votes plus 1 h/4 h/24 h forward moves in ATRs (`bars.parquet`). The manifest carries the engine + gate fingerprints, the regime weight table in force and the book MD5. Calendar source is the gitignored `backtest/cache/calendar.parquet` (15,124 FF events 2023-09-01→2026-09-10, unix-UTC `dateline`; `timeLabel` is display-only); the scraper that built it is back at `scripts/ff_calendar_scraper.py` (resume + idempotent merge, ~1.6 s/request). The 2-year books (2024-09-05→2026-09-05, $0.65) live in `backtest/research/replay_2026-09/{m5,m15}/`: M5 781 / +146.07R / PF 1.52 (halves +28.0/+118.1), M15 793 / +117.62R / PF 1.41 (halves +38.2/+79.4); control-vs-recorded MD5 equal on both. First-look facts are descriptive only (Bollinger opposes 94% of M5 entries; divergence-opposed M5 trades did best again; every indicator's SELL votes were followed by moves against them on both TFs; the bar-level baseline drifts +1.9 ATR/24 h on M5 so BUY votes must be drift-adjusted first) — every candidate change stays a rule-1 rerun with a control book. - 2026-09-09 weight-change test (`backtest/research/weight_test_2026-09-09.md`, first use of the recorded replay files): a drift-adjusted per-bar screen ranked 108 regime-multiplier candidates per TF; the top 3 per TF (24 h-sign filter) went through the full 2-year replay against same-harness controls plus the 2024-06→2025-03 OOS window under a rule pre-registered before any run. M5: all three MACD candidates FAIL (−10 to −46R; the screen ranked them first — never trust the per-bar screen on M5 without a replay). M15: ema_trend × 2 FAILS (PF 1.32, DD 26R, +55% trades), divergence → 0 in trending FAILS (H1 −3R, DD); **RSI multiplier × 0.5 in every regime PASSES a–f** (2-yr 793/+117.6R/PF 1.41 → 931/+146.1R/PF 1.43, halves +27.9/+0.6, OOS +40.6 → +60.7R PF 1.49, 3-yr +151.9 → +186.6R PF 1.39 but max DD 15.5 → 19.3R and 2026 −4R; added trades are mostly B-grade BUYs). SHIPPED the same day on the owner's decision as an M15-only `technical.regime_weights_override` in `pair_signals/xauusd.py` (M15 override 5: the shipped table with RSI halved in every regime — 0.5 trending family / 0.75 ranging / 0.5 transitioning; derived from the default table, pinned literally by the Module 9 equivalence test) — the factory run without `--set` reproduces the validated candidate book byte-for-byte (parity MD5 in `weight_test_2026-09-09.md`); M15 canonical book re-pinned to `backtest/oanda_m15_2yr_065_20260909.csv`. Live M15 `engine_fingerprint` changes from that deploy; signal-log comparisons across it must split at the deploy time. Suite 499 tests. +- 2026-09-10 improvement cycle 1 (`backtest/research/cycle_2026-09-10.md`; 501 tests; no signal-core change): HEAD baseline re-established on the 3-yr window at $0.65 (M5 870/+153.6R/PF 1.50, M15 1273/+186.6/1.39, H1 253/+70.0/1.98, H4 114/+21.7/1.59) and the three pinned MD5s (M5 2-yr, M15 2-yr, M15 OOS) reproduced exactly. Robustness: the cost ladder trips no red flag (every TF keeps PF ≥ 1.3 with positive halves at $0.80; at $1.30 M5 PF 1.36, M15 1.29, H1 1.89, H4 1.53); the intraday edge is a fat right tail (top 5% of trades carry 94% of M5 R and 96% of M15 R, versus 48% on H1; M5 2024 was flat at +1.0R over 102 trades). Pre-registered bucket screen: exactly one qualifying bucket — M15 grade-A SELLs (230 / −14.4R, negative in all three years and OOS; the 2026-09-03 5-yr run said the opposite) — parked as the only cycle-2 candidate. **C1 REJECTED:** H1 divergence multiplier → 0 in every regime (the parked 2026-09 candidate on its now-sanctioned vehicle): 3-yr ΔR +1.0, second half −1.5R, PF 1.98 → 1.89 (its OOS book 88/+21.9/PF 1.92 is trade-identical to the old `disabled_indicators` run, so a 0.0 regime multiplier ≡ disabling for the composite). **C2:** the M15 RSI × 0.5 ship sits on a stable plateau (k = 0.25 / 0.75 keep 78% / 71% of its 2-yr gain, both OOS-positive; R is monotone in k across 0…1.5) — no other k proposed. **Seeding parity SHIPPED:** live fetches floor at `MIN_LIVE_BARS = 1000` (H4 380 → 1000, D1 300 → 1000; measured ema_trend flips 2/267 H4 and 1/313 D1 bars at the old depths, none ≥ 600; the live D1 bar of 2026-09-10 itself read SELL at 300 bars and NEUTRAL at 1000/5000) and the backtester pad targets the same depth (`_history_pad_days`: 62 / 234 / 1400 days) — M5/M15 3-yr books byte-identical, H1/H4 trade-identical with only window-start float noise in the exported columns, so no re-pin. Also fixed in these notes: no PDF docs exist; `pyarrow` is a dev-venv-only dependency of the backtester/recorder. diff --git a/backtest/research/cycle_2026-09-10.md b/backtest/research/cycle_2026-09-10.md new file mode 100644 index 0000000..1848f9f --- /dev/null +++ b/backtest/research/cycle_2026-09-10.md @@ -0,0 +1,287 @@ +# Improvement cycle 1 — 2026-09-10 (HEAD baseline, robustness challenge, seeding parity) + +## Purpose + +First cycle under the owner's standing brief (evidence-driven incremental improvement of +the existing XAU/USD system). No new strategy idea is tested. The cycle (1) re-establishes +the baseline at HEAD and proves the harness reproduces the pinned books, (2) challenges the +current edge — cost sensitivity, concentration, and the parameter neighbourhood of the M15 +RSI × 0.5 change shipped 2026-09-09, (3) reruns the one parked candidate with prior rule-1 +evidence (H1 divergence off) through the production vehicle it now has, and (4) measures +and fixes a live-vs-backtest indicator-seeding gap (H4 fetches 380 bars, D1 300, while the +backtester seeds from its whole cached history). Everything under "Pre-registered" was +written before the corresponding run. + +## Harness + +`venv/bin/python backtest/gpu_backtest.py --pair XAUUSD --spread 0.65 --workers 12 +--primary-tf {5min|15min|1h|4h}`, pair-factory config (no `--quality/--strategies/ +--confirm-tfs`). Windows: **W3** 2023-09-05→2026-09-05 (halves at 2025-03-05; last-12-months +slice from 2025-09-05 reported descriptively), **W2** 2024-09-05→2026-09-05 (halves at +2025-09-05), **OOS** 2024-06-01→2025-03-30. Code: git `2ac7301` (branch `sql-integration`). +Backend: NumPy CPU (CuPy imports but has no CUDA toolkit on this box). Bars: cached +`backtest/cache/oanda_XAU_USD_{5min,15min,1h,4h}_20230705_20260905.parquet`, +`_1d_20221110_20260905`, `_{5min,15min,1h}_20240705_20260905`, `_{…}_20240331_20250330`. +Outputs: `backtest/research/cycle_2026-09-10/` (gitignored). Every candidate is compared +with a CONTROL produced on this harness in this session. `--set +technical.regime_weights_override=` replaces the WHOLE table, so each candidate +passes the complete table with one indicator's multipliers changed. + +## Pre-registered rules (written 2026-09-10 before any run) + +### Step 0 — reproducibility gate +The HEAD factory run must reproduce the ledger's MD5s: M5 W2 +`29120a1ee32979431d062f210d41a9d1`, M15 W2 `d185f099482a07f29502cb5a371a9bbf`, M15 OOS +`6f8f24b49597ced28f50a19e08c3472f`. A mismatch stops the cycle until explained. + +### Step 1 — diagnostics (descriptive; flags fixed in advance) +- Cost ladder $0.65 / 0.80 / 1.00 / 1.30 on W3 per TF. Red flag for a TF: at $0.80 PF < 1.3 + or either W3 half ≤ 0. +- Concentration/stability: share of total R in the top 5% of trades, % of calendar months + positive, longest losing streak, max DD and R per calendar year. No action threshold. +- Bucket screen over the recorded W3 trades (direction, quality, regime, strategy, + entry bar-close hour, weekday, `us_holiday`, `ev_entry_within_30m_hi_usd`, + `ev_hi_usd_during_hold`, year/quarter; plus M15/H4 SELL-in-transitioning and holiday + trades). A bucket becomes a cycle-2 candidate ONLY if n ≥ 30, R < 0 in each of the three + 12-month slices AND in OOS, and a mechanism can be stated. No gate is built this cycle. + +### C1 — H1 divergence multiplier → 0 in every regime (H1-only) +Vehicle: `--set technical.regime_weights_override=`, primary 1h, W3 + OOS, control = the Step-0 HEAD run. +PASS only if ALL: (a) ΔR ≥ +5R on W3; (b) ΔR > 0 in both W3 halves; (c) PF ≥ 1.3 and not +more than 0.05 below control; (d) max DD ≤ control + 2R; (e) trade count within ±25%; +(f) OOS ΔR ≥ 0. Last-12-months slice reported, not a clause. No rescue, no regime-family +fine-tuning after seeing the result. A pass is a shipping PROPOSAL for the owner. + +### C2 — M15 RSI multiplier neighbourhood (stability of the live change) +Ladder k ∈ {0, 0.25, 0.5 (HEAD), 0.75, 1.0 (pre-2026-09-09), 1.5}, full table with +rsi × k, on W2, OOS and W3. Reading, with G(k) = R(k) − R(1.0) on W2: **stable plateau** if +G(0.25) ≥ 0.5·G(0.5) and G(0.75) ≥ 0.5·G(0.5) and the OOS deltas at 0.25 and 0.75 are ≥ 0; +**spike** if either neighbour retains < 50% of G(0.5). Outcome is a report to the owner; +no different k is proposed whatever the result. + +### Step 3 — seeding-depth parity +3a: on the deepest cached H4/D1 frames (H1 as control), sampled bars over the last two +years, compare `TechnicalEngine.analyze()` on the trailing N bars with the full-history +result; `MIN_SEED_BARS` = smallest N with zero indicator-signal flips on both H4 and D1. +3b: live per-TF fetch floor raised to that depth (no book changes; the backtester is the +reference). 3c: backtester slow-TF pad raised to the same depth, shipped only if the M5 +and M15 W3 books stay byte-identical and the H1/H4 differences are confined to the +window-start seeding zone; the H1/H4 re-pin is the owner's decision. + +## Results + +(filled in as runs complete) + +### Step 0 — HEAD baseline (W3, $0.65, factory config; recorded with `--record`) + +| TF | trades | total R | WR | PF | max DD | half 1 (→2025-03-05) | half 2 | last 12 mo | SELL bucket | book MD5 | +|---|---|---|---|---|---|---|---|---|---|---| +| M5 | 870 | +153.64 | 53.3% | 1.50 | 10.1 | 136 / +16.73 / PF 1.36 | 734 / +136.91 / 1.52 | 593 / +119.65 / 1.57 | 100 / +8.95 / 1.25 | `c2722f9830528658b53854f6bae61b8c` | +| M15 | 1273 | +186.61 | 59.4% | 1.39 | 19.3 | 556 / +75.87 / 1.37 | 717 / +110.74 / 1.41 | 474 / +81.04 / 1.47 | 404 / −15.18 / 0.91 | `b2cfa9e2e6cd3997f4d5ca9b49cab90e` | +| H1 | 253 | +70.02 | 64.4% | 1.98 | 4.4 | 115 / +26.26 / 1.79 | 138 / +43.76 / 2.15 | 89 / +24.62 / 1.96 | 0 (all shorts blocked) | `b99111132d9ecb80c2998ed5aee2f3ea` | +| H4 | 114 | +21.68 | 57.9% | 1.59 | 5.4 | 52 / +4.91 / 1.26 | 62 / +16.77 / 1.94 | 46 / +12.34 / 1.79 | 20 / −0.97 / 0.89 | `4508e7a3402e008ba1d93805094cd981` | + +These equal the numbers CLAUDE.md records for HEAD (M5 870/+153.6/1.50, M15 1273/+186.61/1.39, +H1 253/+70.0/1.98, H4 114/+21.7/1.59). Engine / gate fingerprints from the manifests: +M5 `f28b1a8c409d`/`bc0991492340`, M15 `536f84340ea0`/`b2920991cdb8` (new since the RSI +override), H1 `6efb6d365f01`/`0b67d738dc15`, H4 `ed96f258b10e`/`0c3b51f063f9`. +`replay_scorecard.py --check` on the four recorded runs: see `step0.out`. +Per calendar year (R / PF): M5 2023 +2.4/1.47, 2024 **+1.0/1.02** (102 trades — a flat +year), 2025 +40.5/1.34, 2026 +109.7/1.77; M15 +13.2/1.40, +52.9/1.36, +77.3/1.43, ++43.2/1.37; H1 +5.5/2.60, +14.7/1.58, +33.7/2.11, +16.1/2.35; H4 +1.2/1.28, +4.6/1.39, ++7.7/1.74, +8.2/1.78. + +### Step 3a — seeding-depth measurement (`seed_depth.out`) + +Deepest Oanda caches (H4 12,419 bars, D1 2,617, H1 45,914; 2018→2026-08-06), bars sampled +every 12th (H4, 267 bars) / 2nd (D1, 313) / 48th (H1, 247) over the last two years; +`TechnicalEngine.analyze()` on the trailing N bars vs on the full history; identical +results with raw frames and with frames cleaned by `validate_and_clean` (the frame-wide +σ of the outlier filter did not add a single flip). + +| TF | N = live depth today | bars with ≥1 indicator flip | which | max composite Δ | N = 600 … 5000 | +|---|---|---|---|---|---| +| H4 | 380 | 2 / 267 | ema_trend | 4.66 | 0 flips at every depth | +| D1 | 300 | 1 / 313 | ema_trend | 9.47 | 0 flips at every depth | +| H1 | 1460 (control) | 0 / 247 | — | 0.00 | 0 | + +Only the 200-bar EMA trend reading is affected, as the `adjust=False` seeding predicts; +composite-signal and regime never flipped in the sample. Pre-registered pick = smallest +zero-flip depth = 600; shipped floor = **1000** (seed weight e^-10, a ×40 margin on a +sample of a few hundred bars; costs nothing — one request either way). The 2026-08-04 +audit's "~2% of H4 bars" was measured on a different sample; this sample says 0.75% +(H4) and 0.3% (D1) of bars, with composite errors up to 9.5 points on the ones that flip. + +### Step 1 — concentration, stability, bucket screen (W3 recorded books, `diag_w3_head.txt`) + +| TF | top 5% of trades carry | months positive | longest losing streak | +|---|---|---|---| +| M5 | 94% of total R (44 trades, +144R of +154R) | 62% (23/37) | 8 | +| M15 | 96% (64 trades, +179R of +187R) | 68% (25/37) | 9 | +| H1 | 48% (13 trades) | 71% (25/35) | 4 | +| H4 | 80% (6 trades) | 50% (15/30) | 6 | + +The intraday edge is a fat right tail: trailing exits let a few dozen runners pay for +everything else (M5 2024 was flat over 102 trades). H1 is the only timeframe whose edge is +spread across its book. + +Bucket screen (pre-registered criterion: n ≥ 30 and R < 0 in each 12-month slice y1/y2/y3 +AND in the OOS slice of the same book). Exactly one bucket qualifies (besides the trivial +`exit_reason = SL`): +- **M15 SELL grade A**: 230 trades, −14.4R, PF 0.86, y1 −7.4 (67) / y2 −0.4 (80) / + y3 −6.5 (83) / OOS −6.9 (62). Context: M15 SELL overall 404 / −15.2R / PF 0.91 (SELL + max DD 36R on its own curve); SELL|B 160 / −5.5R (y3 +4.6 — not qualifying); + SELL|A+ 14 / +4.7R; SELL|trending_down 301 / −10.9R (y2 +0.9 — not qualifying). + This is the same bucket the 2026-09-03 campaign tested as `sell_min_quality=["B"]` + (allow only B-grade M15 shorts) and REJECTED: 12mo +0.7R, 5-yr −8.9R with max DD 35.8 → + 41.4R, OOS +8.3R — A-grade M15 shorts paid in 2021–2023. Under the owner's ≤3-year + window they lose in every slice. Recorded as the ONE cycle-2 candidate from this screen + (M15 `sell_min_quality` excluding "A" via the factory), to be run against a control with + the 3-yr DD clause; the 5-yr verdict is the fact on the other side of the scale. Not + built this cycle (rule: no gate from the screen). +- Not qualifying / no evidence: holiday-day trades are POSITIVE on M15 (27 / +10.6R) and + small everywhere; entries within 30 min after a high-impact USD release are positive + (M15 114 / +9.3R); M15 SELL-in-transitioning (the gate-6c bucket) 75 / −2.2R with y2 + and y3 positive; M15 `vol_regime = high` 291 / +4.1R / PF 1.03 (y3 +12.6); hour 7 UTC + −4.0R (OOS +2.4). M5/H1/H4 produced no qualifying bucket. + +### Step 0 — reproducibility gate: PASS + +| book | MD5 at HEAD | ledger MD5 | result | +|---|---|---|---| +| M5 W2 | `29120a1ee32979431d062f210d41a9d1` | same | equal | +| M15 W2 | `d185f099482a07f29502cb5a371a9bbf` | same | equal | +| M15 OOS | `6f8f24b49597ced28f50a19e08c3472f` | same | equal | +| H1 OOS (new control, no prior MD5) | `c02eebc9066916d7637bbf5796993f87` | — | recorded | + +`replay_scorecard.py --check`: all eight invariants hold on every recorded W3 run. + +### Step 3b — live floor shipped (commit `9942736`) and verified on the live feed + +`BaseDataProvider.MIN_LIVE_BARS = 1000`, applied in a new pure helper +`_live_outputsize(timeframe, lookback_days)`; requested depths now M5 5000, M15 5000, +H1 1460 (unchanged), **H4 1000 (was 380), D1 1000 (was 300)**. The backtester never calls +`fetch_ohlcv`, so no book changes. Tests: new Module 16 depth test, Module 21 cache test +updated from 300 to 1000; suite **500 passed**. +Live verification 2026-09-10 (real Oanda fetch, same latest closed bar, engine on the +frame cleaned exactly as live does): +- H4 (bar 2026-09-10 04:00 UTC): 380 / 1000 / 5000 bars → identical readings + (composite +15.03, ranging) — no flip on today's bar. +- D1 (bar 2026-09-09): **300 bars → ema_trend SELL, composite −9.53; 1000 and 5000 bars → + ema_trend NEUTRAL, composite −8.87.** The old depth was mis-reading the daily trend + today; the new floor agrees with the converged value. + +### Step 1 — cost ladder (W3, same trade lists; `--spread` only re-prices; mechanics check: the H1 $1.00 total predicted from the $0.65 book (−0.35 $/trade ÷ risk) equals the rerun to the cent) + +| TF | $0.65 R / PF / DD | $0.80 R / PF / DD (halves) | $1.00 R / PF / DD | $1.30 R / PF / DD | red flag ($0.80: PF < 1.3 or a half ≤ 0)? | +|---|---|---|---|---|---| +| M5 | +153.6 / 1.50 / 10.1 | +145.6 / 1.46 / 11.0 (+14.9 / +130.7) | +134.9 / 1.42 / 12.2 | +118.8 / 1.36 / 14.1 | no | +| M15 | +186.6 / 1.39 / 19.3 | +176.4 / 1.37 / 19.9 (+70.0 / +106.4) | +162.8 / 1.34 / 20.8 | +142.4 / 1.29 / 22.6 | no (PF dips under 1.3 only at $1.30) | +| H1 | +70.0 / 1.98 / 4.4 | +68.9 / 1.96 / 4.5 (+25.6 / +43.2) | +67.3 / 1.93 / 4.5 | +65.0 / 1.89 / 4.7 | no | +| H4 | +21.7 / 1.59 / 5.4 | +21.3 / 1.57 / 5.5 (+4.7 / +16.6) | +20.8 / 1.56 / 5.7 | +20.1 / 1.53 / 6.0 | no | + +Doubling the assumed round-trip cost ($0.65 → $1.30) costs M5 23% and M15 24% of total R +and H1/H4 about 7%; the H1 book is the least cost-sensitive because its stops are the +widest in dollars. The M5 first half is thin at every price (+8.7R at $1.30 over 136 +trades), which is the 2024 flat year again, not a cost effect. + +### C1 — H1 divergence multiplier → 0 in every regime: **FAIL (a, b, c) — closed** + +| book | trades | total R | ΔR | half 1 | half 2 | PF | max DD | last 12 mo | OOS | +|---|---|---|---|---|---|---|---|---|---| +| H1 control (HEAD, W3) | 253 | +70.02 | | +26.26 | +43.76 | 1.98 | 4.4 | +24.62 (89) | 78 / +16.68 / PF 1.74 | +| H1 divergence 0 (W3) | 280 | +71.01 | **+0.99** | +28.70 (+2.4) | +42.30 (**−1.5**) | **1.89** | 4.8 | +23.09 (102) | 88 / +21.91 / PF 1.92 (+5.2) | + +Clauses: (a) ΔR +1.0 < +5 FAIL; (b) second half negative FAIL; (c) PF 1.98 → 1.89, drop +0.09 > 0.05 FAIL; (d) DD +0.4 pass; (e) trades +10.7% pass; (f) OOS +5.2 pass. Calendar +years: 2024 +14.7 → +19.1, 2025 +33.7 → +34.1, 2026 +16.1 → +12.4 — the gain sits in +2024, the most recent year gives it back. The OOS candidate book (88 / +21.91 / PF 1.92) +is exactly the 2026-09-03 campaign's `b12_h1_nosell_nodiv_oos` under +`disabled_indicators` — so a 0.0 regime multiplier is mechanically the same as disabling +the indicator for the composite (engine skips only base weight 0, but an effective weight +of 0 adds nothing to score, weight sum or confidence); the only residual difference is +display (reasons text still lists the vote). The parked candidate's earlier evidence +(5-yr +9.6R over no-shorts) was carried by 2021–2023; on the owner's 3-year window it is +noise with a worse profit factor. Not shipped; do not re-propose without new years. + +### C2 — M15 RSI multiplier neighbourhood: **STABLE PLATEAU** (the 2026-09-09 ship stands) + +Full regime table with the RSI multiplier × k (k = 1.0 is the pre-2026-09-09 table, +k = 0.5 is HEAD; every other point is a `--set` run on the same harness). + +| k | W2 trades / R / PF / DD (halves) | G(k) = R − R(1.0) on W2 | OOS trades / R / PF / DD | ΔOOS | W3 trades / R / PF / DD (halves) | +|---|---|---|---|---|---| +| 0 (RSI off) | 1091 / +145.46 / 1.35 / 18.5 (+74.1 / +71.3) | +27.8 | 418 / +51.89 / 1.34 / 19.5 | +11.3 | 1490 / +188.14 / 1.33 / 22.7 (+69.3 / +118.8) | +| 0.25 | 1008 / +139.93 / 1.37 / 20.1 (+66.1 / +73.9) | +22.3 (78% of G(0.5)) | 390 / +59.55 / 1.44 / 17.3 | +19.0 | 1374 / +187.58 / 1.36 / 21.4 (+77.7 / +109.9) | +| **0.5 (HEAD)** | 931 / +146.14 / 1.43 / 16.1 (+66.1 / +80.0) | **+28.5** | 355 / +60.67 / 1.49 / 17.1 | +20.1 | 1273 / +186.61 / 1.39 / 19.3 (+75.9 / +110.7) | +| 0.75 | 854 / +137.83 / 1.45 / 14.9 (+47.6 / +90.3) | +20.2 (71%) | 326 / +55.56 / 1.49 / 15.6 | +15.0 | 1176 / +178.81 / 1.42 / 17.2 (+66.9 / +111.9) | +| 1.0 (old) | 793 / +117.62 / 1.41 / 14.8 (+38.2 / +79.4) | 0 | 300 / +40.59 / 1.38 / 15.5 | 0 | 1092 / +151.90 / 1.38 / 15.5 (+55.7 / +96.2) | +| 1.5 | 676 / +110.17 / 1.46 / 14.2 (+39.4 / +70.7) | −7.5 | 266 / +37.27 / 1.39 / 18.8 | −3.3 | 949 / +136.20 / 1.39 / 18.8 (+42.6 / +93.6) | + +Reading (rule fixed in advance): G(0.25) = 78% and G(0.75) = 71% of G(0.5), both ≥ 50%; +OOS deltas +19.0 and +15.0, both ≥ 0 → **stable plateau, not a spike**. The response is +monotone in k on every window (R falls as RSI's weight rises; trade count falls ~90 per +0.25 step), PF peaks in the 0.5–0.75 band, and drawdown grows as k → 0 (RSI off has the +same R as HEAD with PF 1.35 and DD 22.7 on W3). The shipped 0.5 sits inside a broad +region, on its PF/DD-favourable side. Under this rule no other k is proposed. + +### Step 3c — backtester history pad targets the same 1000-bar seed (`_history_pad_days`) + +Change: the per-TF warm-up pad now targets `BaseDataProvider.MIN_LIVE_BARS` (1000 bars, +converted to calendar days with 7/5) instead of 210 bars, so a window's first bars are +seeded like the rest of the window and like live. Intraday pads are unchanged (62 days +already hold ≥ 1488 bars), so M5/M15/H1 keep their cache windows; H4 now pads 234 days +(was 62 → 372 bars) and D1 1400 days (was 299 → ~210 bars for a 200-bar EMA). New caches +`oanda_XAU_USD_4h_20230114_20260905.parquet` (5,826 bars) and +`_1d_20191105_20260905.parquet` (2,124 bars); the first H4 chunk (2023-01-14, a Saturday) +is legitimately empty. The raw-path fetch for trade resolution now reuses the primary +timeframe's own padded start (one cache file per TF, as before). Test: Module 21 +"history pad seeds slow TFs like live"; suite **501 passed**. + +W3 books before → after, trade-by-trade (`bookdiff.py` with the recorded runs): + +| TF | trades / R before | trades / R after | trades added / removed / R changed | what actually differs | +|---|---|---|---|---| +| H1 | 253 / +70.02 | 253 / +70.02 | 0 / 0 / 0 | ONE confirmation reading: 2023-09-20 18:00 (15 days into the window), H4 ema_trend STRONG_BUY → BUY (value 1.0 → 0.5, confidence 90 → 65); that trade's blended `score` moves 0.21; nothing else in 5,761 recorded indicator rows | +| H4 | 114 / +21.68 | 114 / +21.68 | 0 / 0 / 0 | no signal flips; 20 D1 rows (ADX/RSI/MACD Wilder recursions) differ at the 1e-6…1e-9 level, all Oct–Dec 2023; exported floats differ by ≤ 1.7e-10 (score ≤ 1.4e-9, R ≤ 1e-15) | + +Both differences sit in the window-start seeding zone, exactly where the deeper seed was +meant to act, and neither moves a trade. The canonical H1 book therefore needs NO re-pin +(trade content identical); only the CSV bytes differ (H1 `3b45d12bade823178b267c13bc698458` +vs `b99111132d9ecb80c2998ed5aee2f3ea`, H4 `e75adae13c839b15736f69c0114e6aa5` vs +`4508e7a3402e008ba1d93805094cd981`), which the parity recipe must state. + +M5 and M15 on the patched harness: **byte-identical** to Step 0 (M5 +`c2722f9830528658b53854f6bae61b8c`, M15 `b2cfa9e2e6cd3997f4d5ca9b49cab90e`) — the pad +change did not leak into the intraday books. Shipped (commit below); no re-pin needed. + +## Verdicts + +| item | decision | one-line reason | +|---|---|---| +| Step 0 reproducibility | PASS | three ledger MD5s reproduced at HEAD; scorecard invariants hold | +| Cost ladder | no red flag | every TF keeps PF ≥ 1.3 with positive halves at $0.80; edge survives $1.00 on all four | +| Concentration | fact recorded | M5/M15: top 5% of trades carry 94–96% of R; H1 48% | +| Bucket screen | one cycle-2 candidate | M15 grade-A SELLs (230 / −14.4R, negative in y1, y2, y3 and OOS); 5-yr history says the opposite — run it against a control with the DD clause before believing it | +| C1 H1 divergence → 0 | **REJECT** | ΔR +1.0 (< +5), second half −1.5R, PF 1.98 → 1.89; OOS-only gain | +| C2 M15 RSI ladder | stable plateau | neighbours keep 78% / 71% of the shipped gain, OOS positive at both; ship stands | +| 3a/3b live seeding floor | **ACCEPT, shipped** (`9942736`) | measured flips at 380/300 bars, none at ≥ 600; live D1 flipped on 2026-09-10 at the old depth; books untouched | +| 3c backtester pad | **ACCEPT, shipped** | no trade moved in any book; M5/M15 byte-identical; H1/H4 differ only in window-start numerics | + +## What NOT to re-propose (added to the ledger's closed list) +- H1 divergence weight 0 (any vehicle) without new years of data. +- M15 RSI multiplier other than 0.5 on the strength of a single-point rerun; the ladder is on record. +- Cost-driven gates: the books are not cost-fragile at $0.80–$1.00. + +## Shipped + +- `9942736` providers: `MIN_LIVE_BARS = 1000` live floor + `_live_outputsize` (Module 16 + test; Module 21 expectation 300 → 1000). Suite 500. +- backtester: `_history_pad_days` (62 / 234 / 1400 days for intraday / H4 / D1), raw-path + fetch reuses the primary's start (Module 21 test). Suite 501. Live H1/H4 scans and the + books now share the same seeding depth; pinned H1/H4 CSVs are trade-identical but no + longer byte-reproducible (float noise in exported indicator columns — same status the + M5/M15 pins have had since 2026-09-07). +- No signal-core change, no docs change (nothing user-visible moved), both news gating + flags untouched.