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:
+23
@@ -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),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user