From 5885021dda58c6a3c874fe086c1661eaa7973ea3 Mon Sep 17 00:00:00 2001 From: Harwinder Sekhon Date: Sun, 6 Sep 2026 16:51:18 -0400 Subject: [PATCH] main: build_config uses the pair factory's confirmation map (fixes an M5 divergence) main.py kept a private confirmation table and passed it into the pair factory, which honours any non-None list. For M5 that table said [M1, M15] while the factory, telegram_bot.TF_CONFIRMATIONS and dashboard_publisher.CONFIRMATIONS all use the validated [M15, H1] - so `main.py --timeframe M5` scanned with an unvalidated pairing (and an extra M1 fetch) that no other surface used. - build_config now returns PAIR_PRODUCERS[pair](tf, None): the factory map is the single source of truth. The unreachable bare-PlatformConfig fallback is gone here and in telegram_bot._run_signal_sync (every symbol that reaches either is registered). - --timeframe offers the four validated primaries (M5, M15, H1, H4). M30 and D1 were never validated and silently fell through to the generic [M15, H4] map; they are removed from the CLI. - The "all valid timeframes" test now asserts each CLI timeframe's confirmation list equals pair_signals.xauusd._CONFIRMATIONS - the check that would have caught this. CLI behaviour change: M5 confirmations [M1, M15] -> [M15, H1]; M30/D1 rejected. Bot and dashboard scans are unchanged. Suite: 461 passed, 0 failed. Claude-Session: https://claude.ai/code/session_0156YhogLytzj9P2iH1skGbL --- main.py | 46 +++++++++++++++++----------------------------- telegram_bot.py | 12 +++--------- test_all.py | 11 ++++++++--- 3 files changed, 28 insertions(+), 41 deletions(-) diff --git a/main.py b/main.py index 51b98f2..aa6b253 100755 --- a/main.py +++ b/main.py @@ -308,7 +308,7 @@ Examples: parser.add_argument("--pair", "-p", default="XAUUSD", help="Pair to analyze: XAUUSD (default: XAUUSD)") parser.add_argument("--timeframe", "-tf", default="H1", - choices=["M5", "M15", "M30", "H1", "H4", "D1"], + choices=list(_TF_BY_NAME), help="Primary analysis timeframe (default: H1)") parser.add_argument("--detail", action="store_true", help="Show full indicator breakdown") @@ -326,6 +326,14 @@ Examples: return parser.parse_args() +# The four validated primary timeframes (the same set the bot and the +# dashboard scan). M30/D1 were never validated as primaries. +_TF_BY_NAME = { + "M5": Timeframe.M5, "M15": Timeframe.M15, + "H1": Timeframe.H1, "H4": Timeframe.H4, +} + + def _resolve_pair(s: str) -> str: aliases = { "XAUUSD": XAUUSD, "XAU/USD": XAUUSD, "GOLD": XAUUSD, @@ -337,35 +345,15 @@ def _resolve_pair(s: str) -> str: def build_config(args) -> PlatformConfig: + """Per-pair tuned config from the pair_signals factory. + + The factory owns the validated confirmation map (M5->[M15,H1], + M15->[M5,H1], H1->[M15,H4], H4->[H1,D1]); passing None here keeps + the CLI on the same map as the bot and the dashboard. A CLI-local + table once paired M5 with the unvalidated [M1, M15]. + """ pair = _resolve_pair(args.pair) - - tf_map = { - "M5": Timeframe.M5, "M15": Timeframe.M15, "M30": Timeframe.M30, - "H1": Timeframe.H1, "H4": Timeframe.H4, "D1": Timeframe.D1, - } - primary_tf = tf_map[args.timeframe] - - tf_confirmations = { - "M5": [Timeframe.M1, Timeframe.M15], - "M15": [Timeframe.M5, Timeframe.H1], - "M30": [Timeframe.M15, Timeframe.H1], - "H1": [Timeframe.M15, Timeframe.H4], - "H4": [Timeframe.H1, Timeframe.D1], - "D1": [Timeframe.H4, Timeframe.W1], - } - confirmation_tfs = tf_confirmations.get( - args.timeframe, [Timeframe.M15, Timeframe.H4] - ) - - # Per-pair tuned configs from the pair_signals factories. - factory = PAIR_PRODUCERS.get(pair) - if factory is not None: - return factory(primary_tf, confirmation_tfs) - - config = PlatformConfig() - config.primary_timeframe = primary_tf - config.confirmation_timeframes = confirmation_tfs - return config + return PAIR_PRODUCERS[pair](_TF_BY_NAME[args.timeframe], None) def run_live(args, config: PlatformConfig): diff --git a/telegram_bot.py b/telegram_bot.py index b1104c6..6d36589 100755 --- a/telegram_bot.py +++ b/telegram_bot.py @@ -497,15 +497,9 @@ def _run_signal_sync( timeframe, [Timeframe.M15, Timeframe.H4] ) # Per-pair tuned configs (regime weights, gates, sessions) come from - # the pair_signals factories; a plain PlatformConfig would silently - # run every pair with XAU defaults. - factory = PAIR_PRODUCERS.get(symbol) - if factory is not None: - config = factory(timeframe, confirmation_tfs) - else: - config = PlatformConfig() - config.primary_timeframe = timeframe - config.confirmation_timeframes = confirmation_tfs + # the pair_signals factories; every scanned symbol is registered there + # (SCAN_PAIRS is validated against PAIR_INFO at import). + config = PAIR_PRODUCERS[symbol](timeframe, confirmation_tfs) aggregator = SignalAggregator(config) aggregator.data_provider = _get_shared_dp() return aggregator.analyze_pair(symbol=symbol, log_source="telegram") diff --git a/test_all.py b/test_all.py index f946668..713c03f 100644 --- a/test_all.py +++ b/test_all.py @@ -1594,14 +1594,19 @@ def _(): assert Timeframe.D1 in config.confirmation_timeframes -@test("build_config all valid timeframes") +@test("build_config: every CLI timeframe uses the pair factory's confirmation map") def _(): - for tf_str in ["M5", "M15", "M30", "H1", "H4", "D1"]: + from pair_signals import xauusd as _xau + for tf_str, tf in (("M5", Timeframe.M5), ("M15", Timeframe.M15), + ("H1", Timeframe.H1), ("H4", Timeframe.H4)): class FakeArgs: pair = "XAUUSD" timeframe = tf_str config = build_config(FakeArgs()) - assert config.primary_timeframe is not None + assert config.primary_timeframe == tf + # The CLI must not carry its own confirmation table: it once gave + # M5 an unvalidated [M1, M15] pairing that no other surface used. + assert config.confirmation_timeframes == _xau._CONFIRMATIONS[tf], tf_str # ══════════════════════════════════════════════════════════════════════