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.
This commit is contained in:
harwinder
2026-07-08 21:26:30 -04:00
parent baeee4997a
commit 1f076c0b54
7 changed files with 273 additions and 60 deletions
+62 -10
View File
@@ -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