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,
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
# ══════════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
Reference in New Issue
Block a user