Tune EUR/USD trade setup: quality-scaled TP and tighter SL

EUR/USD A-grade signals had inverted quality curve (-1.65R) because all
trades got RR≈0.50 and mature_move logic penalized high-score entries.

Fix: add per-pair trade setup params (sl_atr_floor=1.2, min_rr=1.0,
mature_move_threshold=35) and apply quality_scale to both tp_mult and
min_rr so A-grade gets wider TP targets. Also wire EUR config into
backtest via PAIR_PRODUCERS registry.

Result: A-grade now +1.24R (was -1.65R), total EUR/USD +10.06R (was +4.20R).
XAU/USD unchanged at +13.01R.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
harwinder
2026-05-18 12:56:34 -04:00
parent 3146a632d4
commit 74b723ba06
4 changed files with 77 additions and 18 deletions
+30 -11
View File
@@ -53,10 +53,12 @@ from config import ( # noqa: E402
SignalStrength, SignalStrength,
Timeframe, Timeframe,
XAUUSD, XAUUSD,
EURUSD,
) )
from data_provider import ForexDataProvider # noqa: E402 from data_provider import ForexDataProvider # noqa: E402
from engine import IndicatorSignal, TechnicalAnalysis, TechnicalEngine # noqa: E402 from engine import IndicatorSignal, TechnicalAnalysis, TechnicalEngine # noqa: E402
from signal_aggregator import SignalAggregator, SignalReport, TradeSetup # 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. # GPU shim — CuPy if available AND CUDA runtime works, else NumPy.
@@ -96,6 +98,8 @@ PAIR_ALIASES = {
"XAUUSD": XAUUSD, "XAUUSD": XAUUSD,
"XAU/USD": XAUUSD, "XAU/USD": XAUUSD,
"GOLD": XAUUSD, "GOLD": XAUUSD,
"EURUSD": EURUSD,
"EUR/USD": EURUSD,
} }
# Bar duration in seconds — used to compute "fully-closed" cutoffs so # 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 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( def run_backtest(
bt_cfg: BacktestConfig, bt_cfg: BacktestConfig,
platform_cfg: Optional[PlatformConfig] = None, platform_cfg: Optional[PlatformConfig] = None,
@@ -1387,12 +1401,14 @@ def run_backtest(
# Decision time = close of the current primary bar. # Decision time = close of the current primary bar.
decision_time = ts + primary_dur 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": if PAIR_INFO[bt_cfg.pair]["display"] == "XAU/USD":
block = _is_xau_blocked_by_calendar(ts, platform_cfg) block = _is_xau_blocked_by_calendar(ts, platform_cfg)
if block: else:
skipped_reasons[block] = skipped_reasons.get(block, 0) + 1 block = _is_pair_blocked_by_calendar(ts, platform_cfg)
continue if block:
skipped_reasons[block] = skipped_reasons.get(block, 0) + 1
continue
# Cooldown. # Cooldown.
if pos - last_entry_pos < bt_cfg.cooldown_bars: 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()}" f"Pair: {PAIR_INFO[pair]['display']} | Window: {start.date()} → {end.date()}"
) )
platform_cfg = PlatformConfig( if pair in PAIR_PRODUCERS:
symbol=pair, platform_cfg = PAIR_PRODUCERS[pair](primary_tf, confirm_tfs)
display_name=PAIR_INFO[pair]["display"], else:
pip_value=PAIR_INFO[pair]["pip_value"], platform_cfg = PlatformConfig(
primary_timeframe=primary_tf, symbol=pair,
confirmation_timeframes=confirm_tfs, 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
+7
View File
@@ -172,6 +172,13 @@ class EurUsdStrategyConfig:
sl_atr_multiplier: float = 1.5 sl_atr_multiplier: float = 1.5
tp_atr_multiplier: float = 1.0 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 @dataclass
class FilterConfig: class FilterConfig:
+17 -7
View File
@@ -859,15 +859,25 @@ class SignalAggregator:
else: else:
is_buy = report.final_score >= 0 is_buy = report.final_score >= 0
levels = report.primary_analysis.key_levels levels = report.primary_analysis.key_levels
min_rr = self.config.signals.min_rr_ratio
mature_move = abs(report.final_score) > 30 _is_eur = report.display_name == "EUR/USD" and self.config.eur_strategy.enabled
tp_mult = 1.0 if mature_move else 1.5
tp_swing_cap = 2.0 if mature_move else 2.5
# Fix 3: Wider SL multiplier for XAU/USD (1.8x vs default 1.5x) if _is_eur:
sl_atr_floor = (self.config.xau_strategy.v2_sl_atr_multiplier eur = self.config.eur_strategy
if report.display_name == "XAU/USD" else 1.5) 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: if is_buy:
swing_lows = sorted( swing_lows = sorted(
+23
View File
@@ -1676,6 +1676,12 @@ def _():
assert 5 not in cfg.allowed_weekdays # No Saturday assert 5 not in cfg.allowed_weekdays # No Saturday
assert 6 not in cfg.allowed_weekdays # No Sunday assert 6 not in cfg.allowed_weekdays # No Sunday
assert cfg.min_atr_threshold == 0.0003 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") @test("PlatformConfig has eur_strategy field")
@@ -1758,6 +1764,23 @@ def _():
assert "EUR/USD" not in NewsEngine._get_affected_pairs("GBP") 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 # SUMMARY
# ══════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════