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.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Harwinder Sekhon
2026-07-10 21:02:44 -04:00
parent 3b5c0b27ed
commit e0789457dc
+25 -8
View File
@@ -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,14 +2117,23 @@ 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}"
)
# 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"],