chore: keep only what the system needs — drop research tooling, study scripts, diagram, plan
Owner directive 2026-09-06: Dukascopy and similar research material is irrelevant to the running system; keep only code and files with a role in system functioning. Deleted (all in git history before this commit): - backtest/research/ tooling and data: book_report, book_diff, burst_map, burst_precursors, burst_indicator_map, open_trade_bursts, calendar_impact, watch_rule_eval, sizing_model, commit_ladder, llm_news_eval + llm_news_golden.csv, ff_calendar_scraper, duka_spread + duka_spread_by_hour.csv (the Dukascopy spread study), feed_divergence_oanda_vs_tiingo.csv, the panel dry-test JSONL, __init__.py - scripts/feed_divergence_study.py, scripts/signal_parity_study.py (completed studies; their reports keep the results) - docs/architecture.png + architecture_diagram.py (developer illustration) - docs/data_source_change_test_plan.md (Phase 4 shadow scan stays noted in CLAUDE.md) Kept: the campaign .md reports in backtest/research/ as the decision ledger CLAUDE.md cites (rule 1 evidence); scripts/shadow_scan.py (pending operational step); backtest/diagnose_filters.py (test-locked backtester diagnostic). Local, untracked material removed on this box: .superpowers/, walkforward/, baseline/, the commit-ladder and LLM-eval result folders, the Dukascopy boundary CSV; the sizing_2026-09 folder is pruned to the four 3-year baseline books CLAUDE.md names. .gitignore no longer lists the removed research outputs; the backtest instructions and CLAUDE.md no longer point at deleted tools. Suite: 461 passed, 0 failed (nothing imported the removed files). Claude-Session: https://claude.ai/code/session_0156YhogLytzj9P2iH1skGbL
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 578 KiB |
@@ -1,191 +0,0 @@
|
||||
"""ForexBot2 architecture diagram → docs/architecture.png.
|
||||
|
||||
Regenerate after architecture changes: venv/bin/python docs/architecture_diagram.py
|
||||
(matplotlib only — no graphviz on the dev box)."""
|
||||
import os
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.patches import FancyBboxPatch, FancyArrowPatch
|
||||
|
||||
W, H = 100.0, 100.0
|
||||
fig, ax = plt.subplots(figsize=(16, 11.5))
|
||||
ax.set_xlim(0, W); ax.set_ylim(0, H); ax.axis("off")
|
||||
|
||||
INK = "#263238"; MUT = "#607D8B"; ARR = "#455A64"; HAUL = "#7B8FA1"
|
||||
|
||||
def box(x, y, w, h, title, lines, fc, ec, title_fs=8.2, body_fs=6.6, lw=1.3):
|
||||
ax.add_patch(FancyBboxPatch((x, y), w, h,
|
||||
boxstyle="round,pad=0.35,rounding_size=0.9",
|
||||
facecolor=fc, edgecolor=ec, linewidth=lw, zorder=2))
|
||||
cy = y + h - 1.9
|
||||
ax.text(x + w / 2, cy, title, ha="center", va="center", fontsize=title_fs,
|
||||
fontweight="bold", color=INK, zorder=3)
|
||||
cy -= 2.3
|
||||
for ln in lines:
|
||||
ax.text(x + w / 2, cy, ln, ha="center", va="center", fontsize=body_fs,
|
||||
color=INK, zorder=3)
|
||||
cy -= 1.85
|
||||
|
||||
def band(x, y, w, h, label, color):
|
||||
ax.add_patch(FancyBboxPatch((x, y), w, h,
|
||||
boxstyle="round,pad=0.3,rounding_size=1.2",
|
||||
facecolor="none", edgecolor=color, linewidth=1.0,
|
||||
linestyle=(0, (4, 3)), zorder=1))
|
||||
ax.text(x + 0.9, y + h - 1.4, label, ha="left", va="center",
|
||||
fontsize=7.2, fontweight="bold", color=color, zorder=3)
|
||||
|
||||
def arrow(p1, p2, color=ARR, lw=1.5, style="-|>", conn="arc3,rad=0.0",
|
||||
label=None, lx=0, ly=0, ls="solid", fs=6.4, rot=0):
|
||||
ax.add_patch(FancyArrowPatch(p1, p2, arrowstyle=style, mutation_scale=11,
|
||||
color=color, linewidth=lw, linestyle=ls,
|
||||
connectionstyle=conn, zorder=4))
|
||||
if label:
|
||||
mx, my = (p1[0] + p2[0]) / 2 + lx, (p1[1] + p2[1]) / 2 + ly
|
||||
ax.text(mx, my, label, fontsize=fs, color=MUT, ha="center",
|
||||
va="center", zorder=5, rotation=rot,
|
||||
bbox=dict(boxstyle="round,pad=0.15", fc="white", ec="none", alpha=0.9))
|
||||
|
||||
# ── title ────────────────────────────────────────────────────────────
|
||||
ax.text(2, 98.4, "ForexBot2 — XAU/USD Signal-Intelligence Platform",
|
||||
fontsize=15, fontweight="bold", color=INK, ha="left")
|
||||
ax.text(2, 96.0, "one pipeline, four delivery surfaces · closed bars only · live == backtest parity",
|
||||
fontsize=8.5, color=MUT, ha="left")
|
||||
|
||||
EXT_FC, EXT_EC = "#ECEFF1", "#90A4AE"
|
||||
DAT_FC, DAT_EC = "#E3F2FD", "#5C9BD1"
|
||||
NWS_FC, NWS_EC = "#E0F2F1", "#3D9C8F"
|
||||
COR_FC, COR_EC = "#FFFDF5", "#C9A227"
|
||||
REP_FC, REP_EC = "#FFE9A8", "#B8860B"
|
||||
DEL_FC, DEL_EC = "#E8F5E9", "#5FAF63"
|
||||
BTS_FC, BTS_EC = "#EDE7F6", "#8E6BC6"
|
||||
|
||||
# ── external services (top band) ─────────────────────────────────────
|
||||
band(1, 85.0, 98, 9.4, "EXTERNAL SERVICES", "#78909C")
|
||||
box(2.5, 86.2, 15.5, 6.4, "Oanda v20 API",
|
||||
["candles (live feed)", "practice orders"], EXT_FC, EXT_EC)
|
||||
box(19.5, 86.2, 15.5, 6.4, "Twelve Data · Tiingo",
|
||||
["registered fallbacks", "(Tiingo: tick stream)"], EXT_FC, EXT_EC)
|
||||
box(36.5, 86.2, 15.5, 6.4, "Forex Factory",
|
||||
["economic calendar", "MyFXBook RSS fallback"], EXT_FC, EXT_EC)
|
||||
box(53.5, 86.2, 12.5, 6.4, "News RSS ×4",
|
||||
["gold / macro", "headlines"], EXT_FC, EXT_EC)
|
||||
box(67.5, 86.2, 13.5, 6.4, "DeepSeek API",
|
||||
["V4-Flash LLM", "thinking: disabled"], EXT_FC, EXT_EC)
|
||||
box(82.5, 86.2, 15, 6.4, "Telegram API",
|
||||
["owner bot +", "subscriber channel"], EXT_FC, EXT_EC)
|
||||
|
||||
# ── data & context layer ─────────────────────────────────────────────
|
||||
box(2.5, 69.8, 33, 11.0, "providers/ (BaseDataProvider)",
|
||||
["single-flight fetch · TTL+boundary cache",
|
||||
"forming-candle DROP · validate/clean",
|
||||
"paginated fetch_range · optional tick stream",
|
||||
"active feed knob: data_providers.active = oanda"],
|
||||
DAT_FC, DAT_EC)
|
||||
box(38.5, 69.8, 33, 11.0, "news_engine.py + llm_client.py",
|
||||
["calendar events + keyword sentiment (gold-inverse)",
|
||||
"DeepSeek enrichment: batch headline scores,",
|
||||
"event gold-direction, market blurb — display-only,",
|
||||
"keyword fallback, daily request cap"],
|
||||
NWS_FC, NWS_EC)
|
||||
box(74.5, 69.8, 23, 11.0, "app_config.py",
|
||||
["credentials & deployment:",
|
||||
"env → config.json → refuse",
|
||||
"(no hardcoded fallbacks)",
|
||||
".env loaded at import"],
|
||||
"#FAFAFA", "#9E9E9E")
|
||||
|
||||
# ── signal core container ────────────────────────────────────────────
|
||||
ax.add_patch(FancyBboxPatch((2.5, 40.5), 95, 26.5,
|
||||
boxstyle="round,pad=0.35,rounding_size=1.2",
|
||||
facecolor=COR_FC, edgecolor=COR_EC, linewidth=1.6, zorder=1))
|
||||
ax.text(50, 65.2, "SIGNAL CORE — runs identically in live scans and backtests (closed bars only)",
|
||||
ha="center", fontsize=8.4, fontweight="bold", color="#8a6d1a", zorder=3)
|
||||
|
||||
box(4.5, 51.8, 28, 12.0, "engine.py — TechnicalEngine",
|
||||
["9 scored indicators + ATR context:",
|
||||
"EMA · RSI · MACD · BB · ADX · ROC ·",
|
||||
"Keltner · divergence · candlestick",
|
||||
"regime → adaptive weights → composite"],
|
||||
"white", COR_EC)
|
||||
box(4.5, 42.0, 28, 8.4, "pair_signals/xauusd.py — factory",
|
||||
["ALWAYS used: config.json overlay +",
|
||||
"validated per-TF deltas (hour blocks, trail…)"],
|
||||
"white", COR_EC)
|
||||
box(35.5, 42.0, 60, 20.7, "signal_aggregator.py — analyze_pair → analyze_from_dataframes",
|
||||
["multi-TF agreement (M5→[M15,H1] · M15→[M5,H1] · H1→[M15,H4] · H4→[H1,D1])",
|
||||
"→ news context (warn-only by default) → diminishing-decay penalties",
|
||||
"→ exhaustion hard block → quality grade A+…F",
|
||||
"→ ordered post-grade gate chain (session · XAU SELL vol/hour · regime · M5 noise)",
|
||||
"→ setup generators: XAU ATR (A+/A) · generic swing · XAU_PB · XAU_CP",
|
||||
"→ entry · SL · TP1/TP2 · trailing plan"],
|
||||
"white", COR_EC, body_fs=6.9)
|
||||
|
||||
# ── SignalReport hub ─────────────────────────────────────────────────
|
||||
box(31, 33.2, 38, 5.2, "SignalReport",
|
||||
["signal (−2…+2) · grade · trade setup · reasons · news summary"],
|
||||
REP_FC, REP_EC, title_fs=8.8)
|
||||
|
||||
# ── delivery surfaces ────────────────────────────────────────────────
|
||||
band(1, 13.2, 98, 17.2, "DELIVERY SURFACES", "#4C8F50")
|
||||
box(2.5, 15.0, 12, 12.6, "main.py",
|
||||
["CLI report", "--detail", "--json"], DEL_FC, DEL_EC)
|
||||
box(42.5, 15.0, 31, 12.6, "dashboard_publisher.py → Apache",
|
||||
["independent daemon · scans at candle closes",
|
||||
"atomic publish to /var/www/trade:",
|
||||
"dashboard.html · snapshot.json · news.json",
|
||||
"+ news_summary.php — on-demand AI",
|
||||
"gold-impact summary (whitelist · cache · cap)"],
|
||||
DEL_FC, DEL_EC)
|
||||
box(76, 15.0, 21.5, 12.6, "telegram_bot.py",
|
||||
["candle-boundary auto-scans", "quality threshold + dedup",
|
||||
"alerts + LLM context line", "WS level-breach monitor"],
|
||||
DEL_FC, DEL_EC)
|
||||
|
||||
# ── backtest / QA ────────────────────────────────────────────────────
|
||||
box(2.5, 3.2, 71.8, 8.0, "backtest/gpu_backtest.py — bar-by-bar replay through the SAME analyze_from_dataframes",
|
||||
["realism (results-critical): next-open entries · raw-path resolution · spread charged · SL-first ties · close-only trail",
|
||||
"CuPy GPU / NumPy fallback · parquet cache · fed by providers.fetch_range (news off)"],
|
||||
BTS_FC, BTS_EC)
|
||||
box(77.5, 3.2, 20, 8.0, "test_all.py",
|
||||
["461 tests · 31 modules", "no network (mocked)"], "#FAFAFA", "#9E9E9E")
|
||||
|
||||
# ── arrows: short structural hops (solid dark) ───────────────────────
|
||||
arrow((10, 86.2), (14, 81.2), label="OHLCV", lx=-3.4, ly=0.2) # oanda → providers
|
||||
arrow((27, 86.2), (24, 81.2), ls=(0, (3, 2)), lw=1.1) # fallbacks → providers
|
||||
arrow((44, 86.2), (48, 81.2)) # FF → news
|
||||
arrow((59.5, 86.2), (56, 81.2)) # RSS → news
|
||||
arrow((72, 86.2), (62, 81.2), label="LLM", lx=3.4, ly=0.6) # deepseek → news
|
||||
arrow((19, 69.8), (19, 64.2), label="DataFrames (UTC, closed bars)", lx=0.2, ly=0.9)
|
||||
arrow((33.5, 57.5), (35.5, 55.5)) # engine → aggregator
|
||||
arrow((32.5, 46.0), (35.5, 48.0)) # factory → aggregator
|
||||
arrow((55, 69.8), (55, 63.1), label="warn-only context", lx=0, ly=1.0)
|
||||
arrow((86, 69.8), (86, 67.3), ls=(0, (3, 2)), lw=1.1) # config → core
|
||||
arrow((50, 40.5), (50, 38.8)) # core → report
|
||||
arrow((36, 34.5), (8.5, 27.95), conn="arc3,rad=0.12") # report → cli
|
||||
arrow((60, 33.2), (58, 27.95)) # report → dashboard
|
||||
arrow((66, 34.5), (86.5, 27.95), conn="arc3,rad=-0.12") # report → telegram
|
||||
|
||||
# ── long-haul edges (thin dashed, routed through gutters) ────────────
|
||||
# telegram bot ↔ Telegram API — right-margin arc, clear of app_config
|
||||
arrow((95, 86.2), (95, 27.95), conn="arc3,rad=0.15", color=HAUL,
|
||||
lw=1.2, ls=(0, (4, 2)))
|
||||
ax.text(99.2, 57, "alerts / commands", fontsize=6.4, color=MUT,
|
||||
rotation=90, ha="center", va="center")
|
||||
# browser click → news_summary.php → DeepSeek
|
||||
arrow((66, 27.95), (73, 86.2), conn="arc3,rad=-0.18", color=HAUL,
|
||||
lw=1.2, ls=(0, (4, 2)),
|
||||
label="on-demand AI summary\n(browser click → PHP)", lx=2.5, ly=11.6)
|
||||
# providers.fetch_range → backtester — far-left margin channel
|
||||
arrow((1.5, 69.8), (1.5, 11.0), color=BTS_EC, lw=1.1, ls=(0, (3, 2)),
|
||||
label="fetch_range\n(history)", lx=3.6, ly=-1.2)
|
||||
# backtester replays the signal core — right gutter between boxes
|
||||
arrow((74.0, 11.6), (74.9, 40.3), color=HAUL, lw=1.3)
|
||||
ax.text(81.8, 38.9, "replays the exact signal code", fontsize=6.4,
|
||||
color=MUT, ha="center", va="center", zorder=5,
|
||||
bbox=dict(boxstyle="round,pad=0.15", fc="white", ec="none", alpha=0.9))
|
||||
|
||||
if __name__ == "__main__": # importing the module must never rewrite the PNG
|
||||
out = os.path.join(os.path.dirname(os.path.abspath(__file__)), "architecture.png")
|
||||
fig.savefig(out, dpi=200, bbox_inches="tight", facecolor="white")
|
||||
print(f"saved {out}")
|
||||
@@ -1,92 +0,0 @@
|
||||
# Test Plan — Data Source Change Validation
|
||||
|
||||
**Purpose:** determine whether switching `data_providers.active` (e.g. Twelve Data → Oanda, or any future provider swap) changes signal calculations materially, and gate the switch on evidence.
|
||||
|
||||
**Status:** in progress, 2026-08-14
|
||||
|
||||
> **Progress (2026-08-14):** Phase 0 ✅, Phase 1 ✅ (`backtest/research/feed_divergence_2026-08-14.md`), Phase 2 ✅ (`backtest/research/signal_parity_2026-08-14.md`), Phase 3 ✅ (`backtest/research/repin_verification_2026-08-14.md` — re-pin already existed), Phase 4 🔨 tool ready (`scripts/shadow_scan.py`, needs a 2-week run — launch manually per owner decision), Phase 5 owner decisions recorded (dual cost reporting; Tiingo streaming-only; see bottom).
|
||||
**Context:** Oanda has been the live feed since 2026-07-28. The backtest books were **re-pinned on Oanda bars on 2026-08-04** (`backtest/oanda_*.csv` — 27 books, now the baseline; all TD-era books superseded). Cost reporting is dual: $0.30 retained for cross-book comparability, $0.65 is the realistic headline (measured Oanda round trip $0.65–0.72). Twelve Data was proven signal-incompatible in Phase 1 (~19% of M5 bars disagree on direction; systematic ~$1 bias) — it is NOT a valid candle fallback.
|
||||
|
||||
**Why signals can differ between feeds** (mechanisms to test):
|
||||
|
||||
- Feeds publish different mid prices → tiny OHLC diffs shift every indicator; near a threshold (RSI band, `FB2_EMA_LO` EMA200-proximity band, composite/grade cutoffs, M5 min-score gate) one bar's discrete score flips.
|
||||
- Gate edge cases turn a tiny price diff into "setup generated" vs "no signal".
|
||||
- ATR-derived SL/TP/trail levels differ even when the signal is identical.
|
||||
- The shared 10σ cleaning pass can drop different bars per feed.
|
||||
- Volume present (Oanda) vs absent (Tiingo FX); real spread (Oanda MBA) vs pair default (Tiingo).
|
||||
- Known reference point: H4 seeding divergence — same provider, different fetch depth flipped ~2% of indicator signals (composite Δ up to 12.3). Cross-feed diffs act the same way.
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 — Recent-bar parity smoke test ✅ DONE
|
||||
|
||||
- **Tool:** `scripts/compare_oanda_tiingo.py` (removed 2026-09-06 once Phases 1-2 superseded it; the result is recorded here)
|
||||
- **Result 2026-08-14:** 10/10 timestamp alignment on M5/M15/H1, Close max |Δ| ≤ $0.175 (≤ 0.004% of price), no directional bias.
|
||||
- **Verdict:** recent-tail parity confirmed. Does NOT cover historical depth — proceed to Phase 1.
|
||||
|
||||
## Phase 1 — Historical candle divergence study
|
||||
|
||||
**Goal:** quantify raw-data divergence over the full window used for validation, not just the recent tail.
|
||||
|
||||
- [x] Extend the comparison script to use `fetch_range` (paginated, raw bars) for both providers over **≥ 12 months** for M5, M15, H1 (and H4 if in scope).
|
||||
- [x] Record per TF:
|
||||
- % of bar timestamps present in both feeds; list of missing/extra bars (gaps, holidays, weekend boundary handling).
|
||||
- OHLC |Δ| distribution: p50 / p95 / p99 / max, in $ and % of price.
|
||||
- Count of bars dropped by `validate_and_clean` on each feed (10σ outliers, malformed).
|
||||
- Bar-close timestamp conventions (alignment, DST behavior around US transitions).
|
||||
- [x] **Pass criteria (measured on the weekend-filtered bar grid — the aggregator's NY-anchored `_is_forex_weekend` decides which bars exist for signal purposes, so raw timestamp overlap includes coverage-only diffs like Tiingo's Sunday bars and is NOT the criterion):** ≥ 99.5% overlap of *signal-eligible* bar timestamps; Close p95 |Δ| ≤ 0.01% of price; no systematic bias (mean Δ ≈ 0); no unexplained multi-hour gaps unique to one feed. (Raw-overlap result 2026-08-14: 94.6–96.6% vs Tiingo — entirely explained by Tiingo's ~22h/week of flat Sunday bars, which the weekend filter gates out.)
|
||||
- [x] Write findings to `backtest/research/feed_divergence_<date>.md` with tables and the exact commands used.
|
||||
|
||||
## Phase 2 — Signal-level replay parity
|
||||
|
||||
**Goal:** measure how often candle divergence actually changes a *decision*.
|
||||
|
||||
- [x] Replay bar-by-bar through the real `analyze_from_dataframes` (news off) for both feeds over the same ≥ 12-month window, per TF (M5, M15, H1). Reuse the gpu_backtest replay harness; disable the parquet cache (`--no-cache`) or use separate cache keys per provider.
|
||||
- [x] Compare per bar: signal direction, composite score, quality grade, gate outcomes (which gate fired), setup presence, strategy label (XAU ATR / XAU_PB / XAU_CP), entry/SL/TP prices.
|
||||
- [x] Metrics:
|
||||
- % bars with identical direction+grade.
|
||||
- Direction flips, grade changes, gate-only changes (setup appears/disappears).
|
||||
- Distribution of composite Δ on mismatched bars.
|
||||
- For matching signals: mean/max |Δ| of entry, SL, TP1/TP2 in $ and in ATR fractions.
|
||||
- [x] **Result (Oanda↔Tiingo):** 98.32% / 98.16% same direction (M15/M5), zero opposite-sign flips; 93.41% / 94.21% same direction+grade+setup. The original ≥97% direction+grade proposal is too strict for benign grade-letter changes and was not met; fallback acceptance is direction ≥98%, zero sign flips, and marginal/boundary-hour-only flips. See `signal_parity_2026-08-14.md`.
|
||||
|
||||
## Phase 3 — Backtest re-pin on the new feed
|
||||
|
||||
**Goal:** a new pinned book on the new provider's bars, evaluated under rule 1 acceptance criteria.
|
||||
|
||||
- [x] **Provider selection:** `gpu_backtest.py` has **no `--provider` flag** — it builds the provider from `data_providers.active` in config.json (`gpu_backtest.py:1753`). A cross-provider run requires flipping that config line. The parquet cache is already keyed per provider (`gpu_backtest.py:984` — `oanda_`-prefixed files vs legacy unprefixed TD files), so TD and Oanda data cannot mix in cache; `--no-cache` is only needed to force a refetch.
|
||||
- [x] Run the full backtest on new-feed bars, both cost assumptions:
|
||||
```bash
|
||||
python backtest/gpu_backtest.py --pair XAUUSD --days 365 --spread 0.30 --export-csv backtest/repin_<provider>_m15_030.csv
|
||||
python backtest/gpu_backtest.py --pair XAUUSD --days 365 --spread 0.65 --export-csv backtest/repin_<provider>_m15_065.csv
|
||||
```
|
||||
(repeat for each TF under test; note the `Backend:` log line and provider in the report per `backtest/instructions_for_backtest.txt`).
|
||||
- [x] Split into train/validation halves; require **positive total R in both halves and PF ≥ 1.3 at realistic cost** (rule 1), with trailing exits and next-open fills untouched.
|
||||
- [x] Marginal configs additionally pass the **2024-06 → 2025-03 OOS window** (within-year splits alone have approved configs that later failed OOS).
|
||||
- [x] ~~Compare against the pinned Twelve-Data book trade-by-trade~~ **AMENDED 2026-08-14:** skipped with justification — Phase 1 showed TD bars disagree on direction 19% of the time on M5 with a systematic ~$1 bias, so a TD-pinned book is a different instrument's book and a trade-level diff carries no decision value. Phantom-trade classes remain documented (e.g. the weekend-calendar fix already dropped exactly one live-impossible M5 trade).
|
||||
- [x] **Deliverable:** re-pinned book + report in `backtest/research/`; update the reference numbers in CLAUDE.md if the book is adopted. Current-HEAD M5 canonical book: `backtest/oanda_m5_12mo_065_nopb_20260814.csv`, +106.82R PF 1.51.
|
||||
|
||||
## Phase 4 — Live shadow comparison
|
||||
|
||||
**Goal:** confirm parity under real fetch depths, caching, and boundary timing — not just history.
|
||||
|
||||
- [ ] Run a shadow scan loop (no orders, no Telegram) for **≥ 2 weeks**: each candle close, scan with both providers, log direction/grade/gates/setup to a side-by-side file or SQLite table.
|
||||
- [ ] Include at least one weekend transition and one news-heavy day in the window.
|
||||
- [ ] Track: scan-to-scan agreement rate, feed-lag incidents ("feed lagging — just-closed bar missing"), staleness warnings, spread readings (Oanda real vs Tiingo default).
|
||||
- [ ] **Acceptance thresholds (baseline = Phase 2 replay parity):** ≥ 98% direction agreement; **zero** opposite-sign (BUY↔SELL) flips; direction flips confined to actionable↔NEUTRAL at marginal composites, clustered only at Friday-close/Sunday-reopen (UTC 22–23) and Asian (03–04) hours; setup-level drift (SL/TP) within the Phase 2 envelope (mean |Δ| ≲ $3, p95 ≲ $17 on M15); every feed-lag/staleness incident explained.
|
||||
- [ ] H4 caution: live H4 fetches only 380 bars vs unbounded backtest history (known ~2% indicator-signal flip from seeding alone) — keep H4 labeled "unproven" until a dedicated campaign closes this.
|
||||
|
||||
## Phase 5 — Cutover checklist
|
||||
|
||||
- [x] `config.json` `data_providers.active: "oanda"` and `data_providers.streaming: "tiingo"` set deliberately (verified 2026-08-14 — Oanda cannot stream; Tiingo streams).
|
||||
- [x] Cost model: **dual reporting adopted (owner decision 2026-08-14)** — $0.65 (measured Oanda round trip) is the realistic headline, $0.30 retained for cross-book comparability with TD-era books. ⚠️ **Cost-gate coupling:** `config.py round_trip_cost` (0.30) is NOT just a backtest knob — it feeds the live TP1 spread gate (`min_tp = round_trip_cost × min_tp_cost_multiple`, three sites in `signal_aggregator.py`). Raising it to 0.65 moves the TP1 floor from ~$9 to ~$19.50 and is a rule-1 revalidation event, not a config tweak. `round_trip_cost` stays 0.30.
|
||||
- [x] QA suite green: `python test_all.py` (381 tests; verified 2026-08-14).
|
||||
- [ ] Week 1 post-cutover: daily review of `logs/signals.db` rows vs shadow log; alert on grade/direction disagreement beyond the Phase 2 baseline rate.
|
||||
- [ ] Rollback: revert `data_providers.active`; pinned books of the previous feed remain valid for comparison. ⚠️ Twelve Data is a permanent *registered* fallback but is **signal-incompatible** (Phase 1) — on an Oanda candle outage, stop scanning rather than switching `active` to `twelve_data`; a TD-fed book cannot be compared to the Oanda baseline.
|
||||
- [x] Docs sync (CLAUDE.md rule): `docs/user_guide.html` backtest numbers reflect the Oanda-pinned books, not TD-era numbers.
|
||||
|
||||
## Open questions — RESOLVED 2026-08-14 (owner decisions)
|
||||
|
||||
- ~~Adopt which spread as canonical~~ → **dual reporting**: $0.65 realistic headline + $0.30 cross-book comparability; `round_trip_cost` stays 0.30 (gate coupling above).
|
||||
- ~~Tiingo role~~ → **streaming-only**. Tiingo remains registered for the WS tick stream and comparison research, but is not a designated candle fallback; candle fallback is a manual halt-and-decide, never an automatic TD failover.
|
||||
- H4 seeding campaign (parked since 2026-08-04) → **still needs an owner**; H4 stays "unproven" until then.
|
||||
Reference in New Issue
Block a user