Fix live/backtest parity, SELL-side losses, and indicator repairs (+42% total R)
Live pipeline: - Drop still-forming candle before analysis (signals no longer repaint) - Direction-aware MTF agreement + opposition gate (no trade against 2-of-3 TF consensus) - SELL volatility gate: block shorts in low/normal vol regimes (config sell_vol_regimes) - Divergence: 40-bar lookback, price/indicator swings must align within ±2 bars (fired on 0.5% of trades before, 29% after), trending weight 2.0→1.0 - RSI regime_aware by default; transitioning regime only signals at extremes - ADX ±DM tie no longer nudges bearish; TP2 clamped beyond TP1 Backtester realism: - Charge $0.30 round-trip spread (--spread), gap-through SL/TP fills at bar open - Resolve trades on RAW price path (cache stores raw bars, cleaning runs on load) - Weekend/session filters use bar-close decision time to match live wall clock Validated per-step on M15 2025-03-30→2026-05-31 under identical pricing: +46.9R → +66.8R, win rate 69.9% → 73.0%, SELL side -2.3R → +9.6R. Tested-and-reverted: ROC weight removal (-13R), BB fade suppression (-11.6R), candlestick de-weight (-2R) — composite thresholds are co-tuned with weight mass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+62
-10
@@ -269,10 +269,10 @@ class SignalAggregator:
|
||||
(high - close.shift()).abs(),
|
||||
(low - close.shift()).abs(),
|
||||
], axis=1).max(axis=1)
|
||||
plus_dm = high.diff()
|
||||
minus_dm = -low.diff()
|
||||
plus_dm = plus_dm.where((plus_dm > minus_dm) & (plus_dm > 0), 0.0)
|
||||
minus_dm = minus_dm.where((minus_dm > plus_dm) & (minus_dm > 0), 0.0)
|
||||
plus_dm_raw = high.diff()
|
||||
minus_dm_raw = -low.diff()
|
||||
plus_dm = plus_dm_raw.where((plus_dm_raw > minus_dm_raw) & (plus_dm_raw > 0), 0.0)
|
||||
minus_dm = minus_dm_raw.where((minus_dm_raw > plus_dm_raw) & (minus_dm_raw > 0), 0.0)
|
||||
atr_s = tr.ewm(alpha=1/period, min_periods=period, adjust=False).mean()
|
||||
plus_di = 100 * (plus_dm.ewm(alpha=1/period, min_periods=period, adjust=False).mean() / atr_s)
|
||||
minus_di = 100 * (minus_dm.ewm(alpha=1/period, min_periods=period, adjust=False).mean() / atr_s)
|
||||
@@ -411,7 +411,17 @@ class SignalAggregator:
|
||||
report.signal_quality = "B"
|
||||
report.action_recommendation = f"{report.final_signal.name.replace('_', ' ')} — Trend forming, watch closely"
|
||||
|
||||
# ── 6d. SELL quality gate ─────────────────────────────────
|
||||
# ── 6d. Exhausted trend quality cap ───────────────────────
|
||||
if (report.market_regime.startswith("trending")
|
||||
and report.exhaustion_count >= 2
|
||||
and report.signal_quality in ("C", "D")):
|
||||
report.warnings.append(
|
||||
"EXHAUSTED TREND FILTER: C-grade blocked in exhausted trend"
|
||||
)
|
||||
report.signal_quality = "D"
|
||||
report.trade_setup = None
|
||||
|
||||
# ── 6e. SELL quality gate ─────────────────────────────────
|
||||
xau_is_sell_dir = (
|
||||
report.final_signal in (SignalStrength.SELL, SignalStrength.STRONG_SELL)
|
||||
or (report.final_signal == SignalStrength.NEUTRAL and report.final_score < 0)
|
||||
@@ -425,6 +435,38 @@ class SignalAggregator:
|
||||
report.signal_quality = "D"
|
||||
report.trade_setup = None
|
||||
|
||||
# ── 6f. MTF opposition gate ───────────────────────────────
|
||||
# Never trade against a multi-timeframe consensus: if the
|
||||
# threshold number of TFs point opposite to the trade direction,
|
||||
# block the setup (counter-trend entries in gold bleed).
|
||||
if report.signal_quality in ("A+", "A", "B", "C") and report.mtf_signals:
|
||||
sigs = list(report.mtf_signals.values())
|
||||
bull_n = sum(1 for s in sigs if s in (SignalStrength.BUY, SignalStrength.STRONG_BUY))
|
||||
bear_n = sum(1 for s in sigs if s in (SignalStrength.SELL, SignalStrength.STRONG_SELL))
|
||||
thr = self.config.signals.mtf_agreement_threshold
|
||||
opposed = (bear_n >= thr and report.final_score > 0) or \
|
||||
(bull_n >= thr and report.final_score < 0)
|
||||
if opposed:
|
||||
report.warnings.append(
|
||||
"MTF OPPOSITION: majority of timeframes point against signal — no trade"
|
||||
)
|
||||
report.signal_quality = "D"
|
||||
report.trade_setup = None
|
||||
|
||||
# ── 6g. SELL volatility gate ──────────────────────────────
|
||||
# Shorts in quiet markets fade against gold's macro uptrend;
|
||||
# only take sells when volatility regime confirms real selling.
|
||||
if (self.config.xau_strategy.enabled
|
||||
and xau_is_sell_dir
|
||||
and report.signal_quality in ("A+", "A", "B", "C")
|
||||
and report.volatility_regime
|
||||
and report.volatility_regime not in self.config.xau_strategy.sell_vol_regimes):
|
||||
report.warnings.append(
|
||||
f"SELL FILTER: volatility '{report.volatility_regime}' too quiet for shorts"
|
||||
)
|
||||
report.signal_quality = "D"
|
||||
report.trade_setup = None
|
||||
|
||||
# ── 7. Trade setup for actionable signals ──────────────────
|
||||
if report.trade_setup is None and report.signal_quality in ("A+", "A"):
|
||||
atr = (report.primary_analysis.key_levels.get("atr", 0)
|
||||
@@ -615,6 +657,12 @@ class SignalAggregator:
|
||||
bearish = sum(1 for s in signals if s in (SignalStrength.SELL, SignalStrength.STRONG_SELL))
|
||||
|
||||
threshold = self.config.signals.mtf_agreement_threshold
|
||||
# Agreement must point the same way as the primary timeframe —
|
||||
# two bullish confirmation TFs must not "confirm" a bearish primary.
|
||||
if report.primary_score > 0:
|
||||
return bullish >= threshold
|
||||
if report.primary_score < 0:
|
||||
return bearish >= threshold
|
||||
return bullish >= threshold or bearish >= threshold
|
||||
|
||||
def _compute_final_signal(self, report: SignalReport):
|
||||
@@ -776,7 +824,6 @@ class SignalAggregator:
|
||||
momentum_fading = roc_fading or macd_fading
|
||||
exhaustion_blocked = exhaustion_count >= 2 and momentum_fading
|
||||
|
||||
# Apply penalties with diminishing returns (cap at 30%)
|
||||
MAX_PENALTY = 30.0
|
||||
penalties.sort(key=lambda x: x[1], reverse=True)
|
||||
total_penalty = 0.0
|
||||
@@ -870,11 +917,9 @@ class SignalAggregator:
|
||||
tf = self.config.signals.tp_tightening_factor
|
||||
pre_tight_min_rr = min_rr / tf if tf > 0 else min_rr
|
||||
|
||||
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
|
||||
tp_mult = 1.5
|
||||
tp_swing_cap = 2.5
|
||||
|
||||
# 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)
|
||||
|
||||
@@ -945,6 +990,13 @@ class SignalAggregator:
|
||||
tp1 = entry + (tp1 - entry) * tf
|
||||
tp2 = entry + (tp2 - entry) * tf
|
||||
|
||||
# TP2 must sit beyond TP1 — swing picks can invert when TP1 falls
|
||||
# back to the ATR target while TP2 takes a nearer second swing.
|
||||
if is_buy:
|
||||
tp2 = max(tp2, tp1 + atr)
|
||||
else:
|
||||
tp2 = min(tp2, tp1 - atr)
|
||||
|
||||
tp1_distance = abs(tp1 - entry)
|
||||
rr = tp1_distance / sl_distance if sl_distance > 0 else 0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user