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:
+30
-11
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
+17
-7
@@ -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(
|
||||
|
||||
+23
@@ -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
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user