diff --git a/backtest/gpu_backtest.py b/backtest/gpu_backtest.py index 259647e..1b88100 100644 --- a/backtest/gpu_backtest.py +++ b/backtest/gpu_backtest.py @@ -53,10 +53,12 @@ from config import ( # noqa: E402 SignalStrength, Timeframe, XAUUSD, + EURUSD, ) from data_provider import ForexDataProvider # noqa: E402 from engine import IndicatorSignal, TechnicalAnalysis, TechnicalEngine # noqa: E402 from signal_aggregator import SignalAggregator, SignalReport, TradeSetup # noqa: E402 +from pair_signals import PAIR_PRODUCERS # noqa: E402 # ───────────────────────────────────────────────────────────────────── # GPU shim — CuPy if available AND CUDA runtime works, else NumPy. @@ -96,6 +98,8 @@ PAIR_ALIASES = { "XAUUSD": XAUUSD, "XAU/USD": XAUUSD, "GOLD": XAUUSD, + "EURUSD": EURUSD, + "EUR/USD": EURUSD, } # Bar duration in seconds — used to compute "fully-closed" cutoffs so @@ -1310,6 +1314,16 @@ def _is_xau_blocked_by_calendar(ts: pd.Timestamp, cfg: PlatformConfig) -> Option return None +def _is_pair_blocked_by_calendar(ts: pd.Timestamp, cfg: PlatformConfig) -> Optional[str]: + """Generic weekend + weekday filter using FilterConfig (backtest field).""" + bt = cfg.backtest + if ts.weekday() >= 5: + return "WEEKEND_FILTER" + if bt.allowed_weekdays and ts.weekday() not in bt.allowed_weekdays: + return "DAY_FILTER" + return None + + def run_backtest( bt_cfg: BacktestConfig, platform_cfg: Optional[PlatformConfig] = None, @@ -1387,12 +1401,14 @@ def run_backtest( # Decision time = close of the current primary bar. decision_time = ts + primary_dur - # XAU calendar filter (matches live analyze_pair behavior). + # Calendar filter (matches live analyze_pair behavior). if PAIR_INFO[bt_cfg.pair]["display"] == "XAU/USD": block = _is_xau_blocked_by_calendar(ts, platform_cfg) - if block: - skipped_reasons[block] = skipped_reasons.get(block, 0) + 1 - continue + else: + block = _is_pair_blocked_by_calendar(ts, platform_cfg) + if block: + skipped_reasons[block] = skipped_reasons.get(block, 0) + 1 + continue # Cooldown. if pos - last_entry_pos < bt_cfg.cooldown_bars: @@ -1890,13 +1906,16 @@ def main(argv: Optional[List[str]] = None) -> int: f"Pair: {PAIR_INFO[pair]['display']} | Window: {start.date()} → {end.date()}" ) - 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 pair in PAIR_PRODUCERS: + platform_cfg = PAIR_PRODUCERS[pair](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 diff --git a/config.py b/config.py index cc3cfaa..6679bb5 100755 --- a/config.py +++ b/config.py @@ -172,6 +172,13 @@ class EurUsdStrategyConfig: sl_atr_multiplier: float = 1.5 tp_atr_multiplier: float = 1.0 + sl_atr_floor: float = 1.2 + min_rr_ratio: float = 1.0 + mature_move_threshold: float = 35.0 + tp_quality_scale: Dict[str, float] = field(default_factory=lambda: { + "A+": 1.8, "A": 1.2, "B": 1.1, "C": 1.0, + }) + @dataclass class FilterConfig: diff --git a/signal_aggregator.py b/signal_aggregator.py index ec43398..4d471b4 100755 --- a/signal_aggregator.py +++ b/signal_aggregator.py @@ -859,15 +859,25 @@ class SignalAggregator: else: is_buy = report.final_score >= 0 levels = report.primary_analysis.key_levels - min_rr = self.config.signals.min_rr_ratio - mature_move = abs(report.final_score) > 30 - tp_mult = 1.0 if mature_move else 1.5 - tp_swing_cap = 2.0 if mature_move else 2.5 + _is_eur = report.display_name == "EUR/USD" and self.config.eur_strategy.enabled - # Fix 3: Wider SL multiplier for XAU/USD (1.8x vs default 1.5x) - sl_atr_floor = (self.config.xau_strategy.v2_sl_atr_multiplier - if report.display_name == "XAU/USD" else 1.5) + if _is_eur: + eur = self.config.eur_strategy + q_scale = eur.tp_quality_scale.get(report.signal_quality, 1.0) + min_rr = eur.min_rr_ratio * q_scale + mature_move = abs(report.final_score) > eur.mature_move_threshold + base_tp = eur.tp_atr_multiplier if mature_move else eur.tp_atr_multiplier * 1.5 + tp_mult = base_tp * q_scale + tp_swing_cap = (2.0 if mature_move else 2.5) * q_scale + sl_atr_floor = eur.sl_atr_floor + else: + min_rr = self.config.signals.min_rr_ratio + mature_move = abs(report.final_score) > 30 + tp_mult = 1.0 if mature_move else 1.5 + tp_swing_cap = 2.0 if mature_move else 2.5 + sl_atr_floor = (self.config.xau_strategy.v2_sl_atr_multiplier + if report.display_name == "XAU/USD" else 1.5) if is_buy: swing_lows = sorted( diff --git a/test_all.py b/test_all.py index acd412e..add0e10 100644 --- a/test_all.py +++ b/test_all.py @@ -1676,6 +1676,12 @@ def _(): assert 5 not in cfg.allowed_weekdays # No Saturday assert 6 not in cfg.allowed_weekdays # No Sunday assert cfg.min_atr_threshold == 0.0003 + assert cfg.sl_atr_floor == 1.2 + assert cfg.min_rr_ratio == 1.0 + assert cfg.mature_move_threshold == 35.0 + assert cfg.tp_quality_scale["A+"] == 1.8 + assert cfg.tp_quality_scale["A"] == 1.2 + assert cfg.tp_quality_scale["C"] == 1.0 @test("PlatformConfig has eur_strategy field") @@ -1758,6 +1764,23 @@ def _(): assert "EUR/USD" not in NewsEngine._get_affected_pairs("GBP") +@test("EUR/USD trade setup uses quality-scaled TP and tighter SL") +def _(): + from pair_signals.eurusd import make_config + from signal_aggregator import SignalAggregator + config = make_config(Timeframe.H1) + config.backtest.session_filter = False + assert config.eur_strategy.sl_atr_floor == 1.2 + assert config.eur_strategy.min_rr_ratio == 1.0 + assert config.eur_strategy.tp_quality_scale["A"] == 1.2 + agg = SignalAggregator(config) + df = make_ohlcv(n=300, start_price=1.0800, trend=0.0003, seed=77) + dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df} + report = agg.analyze_from_dataframes(symbol=EURUSD, dfs=dfs, use_news=False) + if report.trade_setup: + assert report.trade_setup.risk_reward >= 1.0 - 0.01 + + # ══════════════════════════════════════════════════════════════════════ # SUMMARY # ══════════════════════════════════════════════════════════════════════