From e0789457dc0dfafd2848c624c04c1647997719cc Mon Sep 17 00:00:00 2001 From: Harwinder Sekhon Date: Fri, 10 Jul 2026 21:02:44 -0400 Subject: [PATCH] Backtester: EUR/USD support with per-pair configs and default spread MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EURUSD aliases in PAIR_ALIASES - main() builds PlatformConfig via pair_signals.PAIR_PRODUCERS factories (per-pair regime weights/gates/sessions); unregistered pairs keep the inline construction. XAU factory is asdict-identical to inline (enforced by test) so XAU runs are unchanged. - --spread default now resolves from PAIR_INFO default_spread (XAU $0.30, EUR 0.0001) instead of a hardcoded gold value - EUR strategy tag: renamed EUR_PB→EUR, detected in both bullish and bearish reasons[0] (SELL setups mark the bearish list) Smoke-validated: 30-day EUR M5 run produces EUR-tagged trades with 1-pip round-trip cost charged. Co-Authored-By: Claude Fable 5 --- backtest/gpu_backtest.py | 47 +++++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/backtest/gpu_backtest.py b/backtest/gpu_backtest.py index 732bc78..6a8df43 100644 --- a/backtest/gpu_backtest.py +++ b/backtest/gpu_backtest.py @@ -48,6 +48,7 @@ if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) from config import ( # noqa: E402 + EURUSD, PAIR_INFO, PlatformConfig, SignalStrength, @@ -96,6 +97,9 @@ PAIR_ALIASES = { "XAUUSD": XAUUSD, "XAU/USD": XAUUSD, "GOLD": XAUUSD, + "EURUSD": EURUSD, + "EUR/USD": EURUSD, + "EUR": EURUSD, } # Bar duration in seconds — used to compute "fully-closed" cutoffs so @@ -1632,8 +1636,8 @@ def run_backtest( tag = "XAU_PB" elif "XAU STRATEGY" in first_bull: tag = "XAU" - elif "EUR STRATEGY" in first_bull: - tag = "EUR_PB" + elif "EUR STRATEGY" in first_bull or "EUR STRATEGY" in first_bear: + tag = "EUR" elif "GBP STRATEGY" in first_bull: tag = "GBP_PB" @@ -2034,7 +2038,7 @@ def _parse_args(argv: Optional[List[str]] = None) -> argparse.Namespace: description="GPU-accelerated backtester for ForexBot2", formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) - p.add_argument("--pair", required=True, help="XAUUSD | XAU/USD | GOLD") + p.add_argument("--pair", required=True, help="XAUUSD | GOLD | EURUSD") p.add_argument("--days", type=int, default=180, help="Days of history (ignored if --start/--end set)") p.add_argument("--start", help="ISO date YYYY-MM-DD (overrides --days)") p.add_argument("--end", help="ISO date YYYY-MM-DD (defaults to today)") @@ -2047,10 +2051,11 @@ def _parse_args(argv: Optional[List[str]] = None) -> argparse.Namespace: help="Override TechnicalConfig.swing_sr_lookback (0 = use config default)") p.add_argument("--quality", default="A+,A,B,C", help="Comma-separated allowed grades") p.add_argument("--strategies", default="", - help="Comma-separated strategy tags to include (XAU,EUR_PB,GBP_PB,GENERIC). Empty = all.") + help="Comma-separated strategy tags to include (XAU,XAU_PB,EUR,GENERIC). Empty = all.") p.add_argument("--no-cache", action="store_true", help="Skip parquet cache (force API fetch)") - p.add_argument("--spread", type=float, default=0.30, - help="Round-trip cost in price units per trade (XAU ~$0.30; 0 disables)") + p.add_argument("--spread", type=float, default=None, + help="Round-trip cost in price units per trade (default: pair's " + "default_spread from PAIR_INFO — XAU $0.30, EUR 0.0001; 0 disables)") p.add_argument("--export-csv", help="Optional path to dump per-trade CSV (incl. indicator breakdown)") p.add_argument("--detail", action="store_true", help="Print full per-trade reasoning (indicators, signals, agreement, why entered)") @@ -2097,6 +2102,9 @@ def main(argv: Optional[List[str]] = None) -> int: else: start = end - timedelta(days=args.days) + spread = (args.spread if args.spread is not None + else PAIR_INFO[pair].get("default_spread", 0.30)) + bt_cfg = BacktestConfig( pair=pair, start=start, @@ -2109,21 +2117,30 @@ def main(argv: Optional[List[str]] = None) -> int: use_cache=not args.no_cache, quality_filter=[q.strip() for q in args.quality.split(",") if q.strip()], strategies_only=[s.strip() for s in args.strategies.split(",") if s.strip()], - spread=args.spread, + spread=spread, ) logger.info( f"Backend: {'CuPy GPU' if _GPU_AVAILABLE else 'NumPy CPU fallback'} | " - f"Pair: {PAIR_INFO[pair]['display']} | Window: {start.date()} → {end.date()}" + f"Pair: {PAIR_INFO[pair]['display']} | Window: {start.date()} → {end.date()} | " + f"Spread: {spread}" ) - platform_cfg = PlatformConfig( - symbol=pair, - display_name=PAIR_INFO[pair]["display"], - pip_value=PAIR_INFO[pair]["pip_value"], - primary_timeframe=primary_tf, - confirmation_timeframes=confirm_tfs, - ) + # Per-pair tuned configs come from the pair_signals factories (regime + # weights, strategy gates, sessions). Unregistered pairs fall back to + # the inline construction. + from pair_signals import PAIR_PRODUCERS # noqa: E402 (after sys.path fix) + factory = PAIR_PRODUCERS.get(pair) + if factory is not None: + platform_cfg = factory(primary_tf, confirm_tfs) + else: + platform_cfg = PlatformConfig( + symbol=pair, + display_name=PAIR_INFO[pair]["display"], + pip_value=PAIR_INFO[pair]["pip_value"], + primary_timeframe=primary_tf, + confirmation_timeframes=confirm_tfs, + ) if args.swing_sr_lookback > 0: platform_cfg.technical.swing_sr_lookback = args.swing_sr_lookback