Tune engine from backtesting: wider SL, capped TP, counter-trend filter

Backtest-driven fixes (0% win → 33% win, PF 0.00 → 1.05 over 7 days):
- SL: swing buffer 0.5→1.0 ATR, min distance 1.0→1.5 ATR (more room to breathe)
- TP: cap swing targets at ATR×3.0, fallback ATR×2.0 (was targeting unreachable levels)
- Mature-move TP: when score>30 (A/B grade), tighten TP to ATR×1.5 (move already extended)
- Counter-trend penalty: -15 confidence when signal fights regime direction
- EMA200 penalties halved: -20→-10 (was killing EUR/USD signals)
- Score thresholds lowered: BUY/SELL ±15→±12, STRONG ±40→±35
- Quality grades relaxed, C-grade now gets trade setups and Telegram alerts
- min_rr_ratio: 1.5→1.2
- Gitignore backtest/ directory
This commit is contained in:
harwinder
2026-05-11 17:00:12 -04:00
parent 90b7e0c54e
commit 8ead05432e
4 changed files with 52 additions and 25 deletions
+3
View File
@@ -20,3 +20,6 @@ Thumbs.db
# Logs
*.log
# Backtesting (scripts, data, results)
backtest/
+1 -1
View File
@@ -116,7 +116,7 @@ class SignalConfig:
default_risk_pct: float = 1.0
default_rr_ratio: float = 2.0
atr_sl_multiplier: float = 1.5
min_rr_ratio: float = 1.5
min_rr_ratio: float = 1.2
@dataclass
+46 -22
View File
@@ -309,7 +309,7 @@ class SignalAggregator:
self._compute_final_signal(report)
# ── 7. Trade setup for actionable signals ──────────────────
if report.signal_quality in ("A+", "A", "B"):
if report.signal_quality in ("A+", "A", "B", "C"):
report.trade_setup = self._generate_trade_setup(report, pip_value)
# ── 8. Reasons ─────────────────────────────────────────────
@@ -366,6 +366,14 @@ class SignalAggregator:
report.warnings.append(f"Upcoming event: {report.event_description}")
confidence -= 15
is_counter_trend = (
(report.market_regime == "trending_up" and score < 0) or
(report.market_regime == "trending_down" and score > 0)
)
if is_counter_trend:
confidence -= 15
report.warnings.append("Counter-trend signal — reduced confidence")
if report.volatility_regime == "extreme":
report.warnings.append("EXTREME volatility — wider stops needed, reduced position size")
confidence -= 10
@@ -383,23 +391,23 @@ class SignalAggregator:
report.warnings.append(
f"NEAR EMA200 ({ema_dist_pct:.2f}%) — chop risk"
)
confidence -= 20
confidence -= 10
elif ema_dist_pct > _hi:
report.warnings.append(
f"FAR FROM EMA200 ({ema_dist_pct:.2f}%) — over-extended"
)
confidence -= 20
confidence -= 10
confidence = max(0.0, min(95.0, confidence))
# Final signal
if score >= 40 and confidence >= 65:
if score >= 35 and confidence >= 60:
report.final_signal = SignalStrength.STRONG_BUY
elif score >= 15 and confidence >= 50:
elif score >= 12 and confidence >= 45:
report.final_signal = SignalStrength.BUY
elif score <= -40 and confidence >= 65:
elif score <= -35 and confidence >= 60:
report.final_signal = SignalStrength.STRONG_SELL
elif score <= -15 and confidence >= 50:
elif score <= -12 and confidence >= 45:
report.final_signal = SignalStrength.SELL
else:
report.final_signal = SignalStrength.NEUTRAL
@@ -409,15 +417,15 @@ class SignalAggregator:
# Quality grade
abs_score = abs(score)
if abs_score >= 50 and confidence >= 75 and report.mtf_agreement:
if abs_score >= 45 and confidence >= 70 and report.mtf_agreement:
report.signal_quality = "A+"
elif abs_score >= 35 and confidence >= 65 and report.mtf_agreement:
elif abs_score >= 30 and confidence >= 60 and report.mtf_agreement:
report.signal_quality = "A"
elif abs_score >= 25 and confidence >= 55:
elif abs_score >= 20 and confidence >= 50:
report.signal_quality = "B"
elif abs_score >= 15 and confidence >= 45:
elif abs_score >= 12 and confidence >= 40:
report.signal_quality = "C"
elif abs_score >= 10:
elif abs_score >= 8:
report.signal_quality = "D"
else:
report.signal_quality = "F"
@@ -446,23 +454,33 @@ class SignalAggregator:
levels = report.primary_analysis.key_levels
min_rr = self.config.signals.min_rr_ratio
mature_move = abs(report.final_score) > 30
tp_mult = 1.5 if mature_move else 2.0
tp_swing_cap = 2.5 if mature_move else 3.0
if is_buy:
swing_lows = sorted(
[v for k, v in levels.items() if k.startswith("swing_low_") and v < price],
reverse=True,
)
if swing_lows:
sl_price = swing_lows[0] - atr * 0.5
sl_price = swing_lows[0] - atr * 1.0
else:
sl_price = price - atr * 2.0
if price - sl_price < atr * 1.5:
sl_price = price - atr * 1.5
if price - sl_price < atr * 1.0:
sl_price = price - atr * 1.0
swing_highs = sorted(
v for k, v in levels.items() if k.startswith("swing_high_") and v > price + atr * 0.3
)
tp1 = swing_highs[0] if len(swing_highs) >= 1 else price + atr * 2.0
tp2 = swing_highs[1] if len(swing_highs) >= 2 else price + atr * 3.0
if swing_highs and swing_highs[0] - price <= atr * tp_swing_cap:
tp1 = swing_highs[0]
else:
tp1 = price + atr * tp_mult
if len(swing_highs) >= 2 and swing_highs[1] - price <= atr * 5.0:
tp2 = swing_highs[1]
else:
tp2 = price + atr * (tp_mult + 1.5)
sl_distance = price - sl_price
direction = "BUY"
@@ -472,18 +490,24 @@ class SignalAggregator:
v for k, v in levels.items() if k.startswith("swing_high_") and v > price
)
if swing_highs:
sl_price = swing_highs[0] + atr * 0.5
sl_price = swing_highs[0] + atr * 1.0
else:
sl_price = price + atr * 2.0
if sl_price - price < atr * 1.5:
sl_price = price + atr * 1.5
if sl_price - price < atr * 1.0:
sl_price = price + atr * 1.0
swing_lows = sorted(
[v for k, v in levels.items() if k.startswith("swing_low_") and v < price - atr * 0.3],
reverse=True,
)
tp1 = swing_lows[0] if len(swing_lows) >= 1 else price - atr * 2.0
tp2 = swing_lows[1] if len(swing_lows) >= 2 else price - atr * 3.0
if swing_lows and price - swing_lows[0] <= atr * tp_swing_cap:
tp1 = swing_lows[0]
else:
tp1 = price - atr * tp_mult
if len(swing_lows) >= 2 and price - swing_lows[1] <= atr * 5.0:
tp2 = swing_lows[1]
else:
tp2 = price - atr * (tp_mult + 1.5)
sl_distance = sl_price - price
direction = "SELL"
+2 -2
View File
@@ -296,7 +296,7 @@ def _format_scan_summary(reports: List[SignalReport], tf: Timeframe) -> str:
)
lines.append("━━━━━━━━━━━━━━━━━━━━")
actionable = [r for r in reports if r.signal_quality in ("A+", "A", "B")]
actionable = [r for r in reports if r.signal_quality in ("A+", "A", "B", "C")]
if actionable:
names = ", ".join(r.display_name for r in actionable)
lines.append(f"🔔 Actionable: {names}")
@@ -390,7 +390,7 @@ async def _scan_timeframe(
_update_level_cache(reports)
summary = _format_scan_summary(reports, tf)
actionable = [r for r in reports if r.signal_quality in ("A+", "A", "B")]
actionable = [r for r in reports if r.signal_quality in ("A+", "A", "B", "C")]
for chat_id in list(_autoscan_chats):
try: