chore: drop unreachable branches outside the signal core (no behaviour change)

- providers: _to_oanda_instrument / _to_tiingo_ticker / _to_td_symbol no longer
  guess a wire symbol for pairs that are not registered — the registries are
  the only source, so an unknown symbol raises the provider's DataIntegrityError
  instead of silently fetching something else. Every scanned symbol is validated
  against PAIR_INFO at import, so nothing reachable changes.
- main / telegram_bot _fmt_price: the sub-$10 five-decimal branch could never
  fire for gold.
- news_engine._parse_pair: BASE/QUOTE only; the Yahoo "=X" fallback was
  unreachable (its test goes with it).
- dashboard_publisher.event_to_dict: drop minutes_until (frozen at publish
  time, misleading) and is_high_impact — dashboard.js never read either.
- static/dashboard.html: unused id="cal-table".

Tried and reverted: trimming the LogRow fields nothing in analyze_signal_log
reads — the tests read them to verify the loader round-trips those DB columns,
so they stay.

Suite: 460 passed, 0 failed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0156YhogLytzj9P2iH1skGbL
This commit is contained in:
Harwinder Sekhon
2026-09-06 17:47:45 -04:00
parent da5c220997
commit f7c244c6b5
10 changed files with 30 additions and 33 deletions
+2 -2
View File
@@ -12,7 +12,7 @@ Every numeric threshold in `config.py` and `pair_signals/xauusd.py` was backtest
```bash ```bash
python test_all.py # QA suite, no network (mocked); prints its own count python test_all.py # QA suite, no network (mocked); prints its own count
# (461 tests / 31 modules as of 2026-09-06) # (460 tests / 31 modules as of 2026-09-06)
python main.py --pair XAUUSD --timeframe H1 --detail # CLI signal report (--json for full dump, 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 # --panel for the advisory LLM agent panel, --outlook for the
# hourly AI market brief; the violent-move watch line appears # hourly AI market brief; the violent-move watch line appears
@@ -103,4 +103,4 @@ Signal currency everywhere is `SignalStrength` (−2..+2). DataFrames are UTC tz
- 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-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. - 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. - 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.
- 2026-09-06 stale-code cleanup (behaviour-neutral; suite green, M5/H1 12-month books byte-identical after every signal-side commit): `main.py --timeframe` now accepts M5/M15/H1/H4 and takes its confirmation TFs from the pair factory (the CLI's private table had paired M5 with M1+M15); `PlatformConfig.pip_value` is gone (`PAIR_INFO` is the source); removed as dead or superseded: `_get_affected_pairs`/`event_impact_on_pairs`, `SignalLogger.advance_with_bars`/`path`/`news_for`, `reset_llm_client`, the Telegram scan-summary/keyboard helpers and duplicate confirmation map, the 2026-07 Oanda-vs-TD research pager+study, `burst_profile.py`, `hour_analysis.py`, `scripts/compare_oanda_tiingo.py`, the never-implemented LLM-provider plan and the merged config.json spec. Second pass the same day (owner directive: keep only what the system needs): every research script and data file under `backtest/research/` (book_report, book_diff, the burst tools, sizing_model, commit_ladder, llm_news_eval + golden CSV, ff_calendar_scraper, calendar_impact, watch_rule_eval, the Dukascopy spread study + CSVs, the panel dry-test JSONL), the two completed study scripts in `scripts/`, the architecture diagram + generator and the data-source test plan were deleted; the campaign `.md` reports stay as the decision ledger. Git history before `953c5ad` has all of it. - 2026-09-06 stale-code cleanup (behaviour-neutral; suite green, M5/H1 12-month books byte-identical after every signal-side commit): `main.py --timeframe` now accepts M5/M15/H1/H4 and takes its confirmation TFs from the pair factory (the CLI's private table had paired M5 with M1+M15); `PlatformConfig.pip_value` is gone (`PAIR_INFO` is the source); removed as dead or superseded: `_get_affected_pairs`/`event_impact_on_pairs`, `SignalLogger.advance_with_bars`/`path`/`news_for`, `reset_llm_client`, the Telegram scan-summary/keyboard helpers and duplicate confirmation map, the 2026-07 Oanda-vs-TD research pager+study, `burst_profile.py`, `hour_analysis.py`, `scripts/compare_oanda_tiingo.py`, the never-implemented LLM-provider plan and the merged config.json spec. Second pass the same day (owner directive: keep only what the system needs): every research script and data file under `backtest/research/` (book_report, book_diff, the burst tools, sizing_model, commit_ladder, llm_news_eval + golden CSV, ff_calendar_scraper, calendar_impact, watch_rule_eval, the Dukascopy spread study + CSVs, the panel dry-test JSONL), the two completed study scripts in `scripts/`, the architecture diagram + generator and the data-source test plan were deleted; the campaign `.md` reports stay as the decision ledger. Git history before `953c5ad` has all of it. Third pass (owner: "remove dead functions as well"): the write-only pb/cp rejection counters, the unreachable `_TP_QUALITY_SCALE` B/C entries, the engine's unused DataFrame argument and M1 arm, the providers' generic-symbol guesses (unknown symbols now raise), the sub-$10 price format, the `=X` pair parsing and the unread `minutes_until`/`is_high_impact` news.json keys are gone.
+2 -4
View File
@@ -172,8 +172,8 @@ def report_to_webapp_dict(report) -> dict:
def event_to_dict(ev) -> dict: def event_to_dict(ev) -> dict:
"""EconomicEvent → dict. surprise/minutes_until/is_high_impact are """EconomicEvent → dict. surprise is a property, so asdict() would
properties, so asdict() would skip them — added explicitly. skip it — added explicitly.
actual/forecast/previous stay strings as published ('228K', '0.3%').""" actual/forecast/previous stay strings as published ('228K', '0.3%')."""
return { return {
"time": ev.time.astimezone(timezone.utc).isoformat(), "time": ev.time.astimezone(timezone.utc).isoformat(),
@@ -184,8 +184,6 @@ def event_to_dict(ev) -> dict:
"previous": ev.previous, "previous": ev.previous,
"actual": ev.actual, "actual": ev.actual,
"surprise": ev.surprise, "surprise": ev.surprise,
"minutes_until": _safe_num(ev.minutes_until),
"is_high_impact": ev.is_high_impact,
} }
+2 -2
View File
@@ -103,8 +103,8 @@ def _compact_reason(reason: str) -> str:
def _fmt_price(price: float) -> str: def _fmt_price(price: float) -> str:
"""2dp for gold-scale prices, 5dp for sub-10 FX-scale prices.""" """Gold-scale price, 2 dp."""
return f"{price:.2f}" if price >= 10 else f"{price:.5f}" return f"{price:.2f}"
def print_signal_report(report: SignalReport, detail: bool = False): def print_signal_report(report: SignalReport, detail: bool = False):
+4 -5
View File
@@ -457,11 +457,10 @@ class NewsEngine:
@staticmethod @staticmethod
def _parse_pair(pair_symbol: str) -> Tuple[str, str]: def _parse_pair(pair_symbol: str) -> Tuple[str, str]:
if "/" in pair_symbol: base, sep, quote = pair_symbol.partition("/")
parts = pair_symbol.split("/") if not sep:
return parts[0], parts[1] raise ValueError(f"pair symbol must be BASE/QUOTE, got {pair_symbol!r}")
clean = pair_symbol.replace("=X", "") return base, quote
return clean[:3], clean[3:]
def get_event_risk_level(self, pair_symbol: str, context: FundamentalContext) -> Tuple[str, str]: def get_event_risk_level(self, pair_symbol: str, context: FundamentalContext) -> Tuple[str, str]:
""" """
+4 -2
View File
@@ -93,9 +93,11 @@ class OandaDataProvider(BaseDataProvider):
return self._fetch_oanda_candles(symbol, timeframe, outputsize) return self._fetch_oanda_candles(symbol, timeframe, outputsize)
def _to_oanda_instrument(self, symbol: str) -> str: def _to_oanda_instrument(self, symbol: str) -> str:
if symbol in self.OANDA_INSTRUMENT: try:
return self.OANDA_INSTRUMENT[symbol] return self.OANDA_INSTRUMENT[symbol]
return symbol.replace("/", "_") except KeyError:
raise OandaDataError(
f"Unsupported symbol {symbol!r} for Oanda") from None
def _fetch_oanda_candles( def _fetch_oanda_candles(
self, self,
+4 -2
View File
@@ -91,9 +91,11 @@ class TiingoDataProvider(BaseDataProvider):
}) })
def _to_tiingo_ticker(self, symbol: str) -> str: def _to_tiingo_ticker(self, symbol: str) -> str:
if symbol in self.TIINGO_TICKER: try:
return self.TIINGO_TICKER[symbol] return self.TIINGO_TICKER[symbol]
return symbol.replace("/", "").replace("=X", "").lower() except KeyError:
raise TiingoDataError(
f"Unsupported symbol {symbol!r} for Tiingo") from None
# ── Raw fetch ────────────────────────────────────────────────── # ── Raw fetch ──────────────────────────────────────────────────
+3 -3
View File
@@ -58,10 +58,10 @@ class TwelveDataProvider(BaseDataProvider):
@staticmethod @staticmethod
def _to_td_symbol(symbol: str) -> str: def _to_td_symbol(symbol: str) -> str:
if "/" in symbol: if "/" not in symbol:
raise DataIntegrityError(
f"Unsupported symbol {symbol!r} for Twelve Data")
return symbol return symbol
clean = symbol.replace("=X", "")
return f"{clean[:3]}/{clean[3:]}"
# ── Raw fetch ────────────────────────────────────────────────── # ── Raw fetch ──────────────────────────────────────────────────
+1 -1
View File
@@ -71,7 +71,7 @@
</div> </div>
</div> </div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-sm" id="cal-table"> <table class="table table-sm">
<thead> <thead>
<tr> <tr>
<th>Time</th> <th>Time</th>
+1 -1
View File
@@ -199,7 +199,7 @@ def _escape_md(text: str) -> str:
def _fmt_price(price: float) -> str: def _fmt_price(price: float) -> str:
return f"{price:.2f}" if price >= 10 else f"{price:.5f}" return f"{price:.2f}"
# ── Shared data provider (single cache across all scans) ───────────── # ── Shared data provider (single cache across all scans) ─────────────
+6 -10
View File
@@ -755,13 +755,6 @@ def _():
assert quote == "USD" assert quote == "USD"
@test("NewsEngine._parse_pair with =X suffix")
def _():
base, quote = NewsEngine._parse_pair("EURUSD=X")
assert base == "EUR"
assert quote == "USD"
@test("NewsEngine._parse_pair for XAU pair") @test("NewsEngine._parse_pair for XAU pair")
def _(): def _():
base, quote = NewsEngine._parse_pair("XAU/USD") base, quote = NewsEngine._parse_pair("XAU/USD")
@@ -3597,7 +3590,11 @@ def _():
provider = providers.oanda.OandaDataProvider.__new__( provider = providers.oanda.OandaDataProvider.__new__(
providers.oanda.OandaDataProvider) providers.oanda.OandaDataProvider)
assert provider._to_oanda_instrument("XAU/USD") == "XAU_USD" assert provider._to_oanda_instrument("XAU/USD") == "XAU_USD"
assert provider._to_oanda_instrument("EUR/USD") == "EUR_USD" try:
provider._to_oanda_instrument("EUR/USD")
assert False, "unregistered symbol must raise, not be guessed"
except DataIntegrityError:
pass
@test("providers.oanda: granularity mapping covers all scan timeframes") @test("providers.oanda: granularity mapping covers all scan timeframes")
@@ -5151,8 +5148,7 @@ def _():
assert d["event"] == "Non-Farm Payrolls" assert d["event"] == "Non-Farm Payrolls"
assert d["actual"] == "228K" # strings stay strings assert d["actual"] == "228K" # strings stay strings
assert d["surprise"] == "positive" # property, added explicitly assert d["surprise"] == "positive" # property, added explicitly
assert d["is_high_impact"] is True assert "minutes_until" not in d # frozen-at-publish value, dropped
assert isinstance(d["minutes_until"], float)
assert d["time"] == "2026-08-07T12:30:00+00:00" assert d["time"] == "2026-08-07T12:30:00+00:00"
json.dumps(d, allow_nan=False) json.dumps(d, allow_nan=False)