Add EMA200 distance gate — strongest single signal-quality filter

Across 2024 trend / 2023 chop / 2026 hostile windows, distance from the
H1 EMA200 was empirically the most discriminative variable. Sweet spot
0.5-1.5% from EMA200; closer = false-breakout chop, farther = chase.

Outside the band, confidence is multiplied by 0.55 so the trade falls
below the B-grade threshold and is rejected.

Cumulative return across the 3 windows: +13.88% baseline -> +35.50%.
Win rate improved in every regime; 2023 chop turned positive (-2.56% to
+14.94%).

Also extends the trade journal with per-timeframe MTF scores, RSI at
entry, and EMA200 distance for future filter mining; surfaces ema_trend
in key_levels so the gate can read it.
This commit is contained in:
Harwinder
2026-05-07 19:28:19 -04:00
parent be42736223
commit 5a392a578d
4 changed files with 54 additions and 0 deletions
+23
View File
@@ -358,11 +358,24 @@ class WalkForwardBacktester:
analysis = report.primary_analysis
adx_val = 0.0
atr_pct = 0.0
rsi_val = 50.0
ema_dist = 0.0
if analysis is not None:
adx_sig = analysis.indicators.get("adx")
if adx_sig is not None:
adx_val = float(adx_sig.value)
atr_pct = float(analysis.key_levels.get("atr_pct", 0.0))
rsi_sig = analysis.indicators.get("rsi")
if rsi_sig is not None:
rsi_val = float(rsi_sig.value)
ema_trend = analysis.key_levels.get("ema_trend")
if ema_trend and ema_trend > 0:
ema_dist = (raw_entry - ema_trend) / ema_trend * 100.0
# Per-timeframe scores from MTF map (keys are tf.value strings)
m15_s = float(report.mtf_scores.get("15min", 0.0))
h1_s = float(report.mtf_scores.get("1h", 0.0))
h4_s = float(report.mtf_scores.get("4h", 0.0))
entry_ts = next_bar.name
entry_hour = int(entry_ts.hour) if hasattr(entry_ts, "hour") else -1
@@ -389,6 +402,11 @@ class WalkForwardBacktester:
"market_regime": report.market_regime or "",
"volatility_regime": report.volatility_regime or "",
"mtf_agreement": bool(report.mtf_agreement),
"m15_score": m15_s,
"h1_score": h1_s,
"h4_score": h4_s,
"rsi_at_entry": rsi_val,
"ema_distance_pct": ema_dist,
}
def _check_exit(
@@ -477,6 +495,11 @@ class WalkForwardBacktester:
market_regime=position.get("market_regime", ""),
volatility_regime=position.get("volatility_regime", ""),
mtf_agreement=position.get("mtf_agreement", False),
m15_score=position.get("m15_score", 0.0),
h1_score=position.get("h1_score", 0.0),
h4_score=position.get("h4_score", 0.0),
rsi_at_entry=position.get("rsi_at_entry", 50.0),
ema_distance_pct=position.get("ema_distance_pct", 0.0),
)
+5
View File
@@ -173,6 +173,11 @@ class TechnicalEngine:
signal=signal, confidence=conf,
description=desc, weight=1.2,
)
if t is not None:
result.key_levels["ema_trend"] = float(t)
result.key_levels["ema_fast"] = float(f)
result.key_levels["ema_medium"] = float(m)
result.key_levels["ema_slow"] = float(s)
def _compute_rsi(self, df: pd.DataFrame, result: TechnicalAnalysis):
"""RSI with trend-adjusted interpretation."""
+5
View File
@@ -39,6 +39,11 @@ class Trade:
market_regime: str = "" # trending_up / trending_down / ranging
volatility_regime: str = "" # low / medium / high
mtf_agreement: bool = False
m15_score: float = 0.0
h1_score: float = 0.0
h4_score: float = 0.0
rsi_at_entry: float = 50.0
ema_distance_pct: float = 0.0 # (price - ema_trend) / ema_trend * 100
@dataclass
+21
View File
@@ -383,6 +383,27 @@ class SignalAggregator:
elif report.volatility_regime == "low":
report.warnings.append("Low volatility — potential for breakout, tight ranges")
# EMA200 distance gate — empirically the strongest single discriminator
# across 2024 trend / 2023 chop / 2026 hostile windows. Sweet spot is
# 0.5–1.5% from EMA200: closer = false-breakout chop, farther = chase.
# Within band keeps confidence; outside band cuts it sharply so the
# trade falls below the B-grade threshold and is rejected.
primary = report.primary_analysis
if primary is not None:
ema200 = primary.key_levels.get("ema_trend")
if ema200 and ema200 > 0 and report.current_price > 0:
ema_dist_pct = abs(report.current_price - ema200) / ema200 * 100.0
if ema_dist_pct < 0.5:
report.warnings.append(
f"NEAR EMA200 ({ema_dist_pct:.2f}%) — chop risk, signal damped"
)
confidence *= 0.55
elif ema_dist_pct > 1.5:
report.warnings.append(
f"FAR FROM EMA200 ({ema_dist_pct:.2f}%) — over-extended, signal damped"
)
confidence *= 0.55
# Final signal
if score >= 40 and confidence >= 65:
report.final_signal = SignalStrength.STRONG_BUY