Backtester: EUR/USD support with per-pair configs and default spread

- 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.
This commit is contained in:
Harwinder Sekhon
2026-07-10 21:02:44 -04:00
parent 0da5ed03af
commit 9ed07d4b65
+32 -15
View File
@@ -48,6 +48,7 @@ if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT)) sys.path.insert(0, str(PROJECT_ROOT))
from config import ( # noqa: E402 from config import ( # noqa: E402
EURUSD,
PAIR_INFO, PAIR_INFO,
PlatformConfig, PlatformConfig,
SignalStrength, SignalStrength,
@@ -96,6 +97,9 @@ PAIR_ALIASES = {
"XAUUSD": XAUUSD, "XAUUSD": XAUUSD,
"XAU/USD": XAUUSD, "XAU/USD": XAUUSD,
"GOLD": XAUUSD, "GOLD": XAUUSD,
"EURUSD": EURUSD,
"EUR/USD": EURUSD,
"EUR": EURUSD,
} }
# Bar duration in seconds — used to compute "fully-closed" cutoffs so # Bar duration in seconds — used to compute "fully-closed" cutoffs so
@@ -1632,8 +1636,8 @@ def run_backtest(
tag = "XAU_PB" tag = "XAU_PB"
elif "XAU STRATEGY" in first_bull: elif "XAU STRATEGY" in first_bull:
tag = "XAU" tag = "XAU"
elif "EUR STRATEGY" in first_bull: elif "EUR STRATEGY" in first_bull or "EUR STRATEGY" in first_bear:
tag = "EUR_PB" tag = "EUR"
elif "GBP STRATEGY" in first_bull: elif "GBP STRATEGY" in first_bull:
tag = "GBP_PB" tag = "GBP_PB"
@@ -2034,7 +2038,7 @@ def _parse_args(argv: Optional[List[str]] = None) -> argparse.Namespace:
description="GPU-accelerated backtester for ForexBot2", description="GPU-accelerated backtester for ForexBot2",
formatter_class=argparse.ArgumentDefaultsHelpFormatter, 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("--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("--start", help="ISO date YYYY-MM-DD (overrides --days)")
p.add_argument("--end", help="ISO date YYYY-MM-DD (defaults to today)") 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)") 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("--quality", default="A+,A,B,C", help="Comma-separated allowed grades")
p.add_argument("--strategies", default="", 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("--no-cache", action="store_true", help="Skip parquet cache (force API fetch)")
p.add_argument("--spread", type=float, default=0.30, p.add_argument("--spread", type=float, default=None,
help="Round-trip cost in price units per trade (XAU ~$0.30; 0 disables)") 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("--export-csv", help="Optional path to dump per-trade CSV (incl. indicator breakdown)")
p.add_argument("--detail", action="store_true", p.add_argument("--detail", action="store_true",
help="Print full per-trade reasoning (indicators, signals, agreement, why entered)") help="Print full per-trade reasoning (indicators, signals, agreement, why entered)")
@@ -2097,6 +2102,9 @@ def main(argv: Optional[List[str]] = None) -> int:
else: else:
start = end - timedelta(days=args.days) 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( bt_cfg = BacktestConfig(
pair=pair, pair=pair,
start=start, start=start,
@@ -2109,21 +2117,30 @@ def main(argv: Optional[List[str]] = None) -> int:
use_cache=not args.no_cache, use_cache=not args.no_cache,
quality_filter=[q.strip() for q in args.quality.split(",") if q.strip()], 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()], strategies_only=[s.strip() for s in args.strategies.split(",") if s.strip()],
spread=args.spread, spread=spread,
) )
logger.info( logger.info(
f"Backend: {'CuPy GPU' if _GPU_AVAILABLE else 'NumPy CPU fallback'} | " 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( # Per-pair tuned configs come from the pair_signals factories (regime
symbol=pair, # weights, strategy gates, sessions). Unregistered pairs fall back to
display_name=PAIR_INFO[pair]["display"], # the inline construction.
pip_value=PAIR_INFO[pair]["pip_value"], from pair_signals import PAIR_PRODUCERS # noqa: E402 (after sys.path fix)
primary_timeframe=primary_tf, factory = PAIR_PRODUCERS.get(pair)
confirmation_timeframes=confirm_tfs, 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: if args.swing_sr_lookback > 0:
platform_cfg.technical.swing_sr_lookback = args.swing_sr_lookback platform_cfg.technical.swing_sr_lookback = args.swing_sr_lookback