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 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -20,3 +20,6 @@ Thumbs.db
|
|||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
# Backtesting (scripts, data, results)
|
||||||
|
backtest/
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ class SignalConfig:
|
|||||||
default_risk_pct: float = 1.0
|
default_risk_pct: float = 1.0
|
||||||
default_rr_ratio: float = 2.0
|
default_rr_ratio: float = 2.0
|
||||||
atr_sl_multiplier: float = 1.5
|
atr_sl_multiplier: float = 1.5
|
||||||
min_rr_ratio: float = 1.5
|
min_rr_ratio: float = 1.2
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
+46
-22
@@ -309,7 +309,7 @@ class SignalAggregator:
|
|||||||
self._compute_final_signal(report)
|
self._compute_final_signal(report)
|
||||||
|
|
||||||
# ── 7. Trade setup for actionable signals ──────────────────
|
# ── 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)
|
report.trade_setup = self._generate_trade_setup(report, pip_value)
|
||||||
|
|
||||||
# ── 8. Reasons ─────────────────────────────────────────────
|
# ── 8. Reasons ─────────────────────────────────────────────
|
||||||
@@ -366,6 +366,14 @@ class SignalAggregator:
|
|||||||
report.warnings.append(f"Upcoming event: {report.event_description}")
|
report.warnings.append(f"Upcoming event: {report.event_description}")
|
||||||
confidence -= 15
|
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":
|
if report.volatility_regime == "extreme":
|
||||||
report.warnings.append("EXTREME volatility — wider stops needed, reduced position size")
|
report.warnings.append("EXTREME volatility — wider stops needed, reduced position size")
|
||||||
confidence -= 10
|
confidence -= 10
|
||||||
@@ -383,23 +391,23 @@ class SignalAggregator:
|
|||||||
report.warnings.append(
|
report.warnings.append(
|
||||||
f"NEAR EMA200 ({ema_dist_pct:.2f}%) — chop risk"
|
f"NEAR EMA200 ({ema_dist_pct:.2f}%) — chop risk"
|
||||||
)
|
)
|
||||||
confidence -= 20
|
confidence -= 10
|
||||||
elif ema_dist_pct > _hi:
|
elif ema_dist_pct > _hi:
|
||||||
report.warnings.append(
|
report.warnings.append(
|
||||||
f"FAR FROM EMA200 ({ema_dist_pct:.2f}%) — over-extended"
|
f"FAR FROM EMA200 ({ema_dist_pct:.2f}%) — over-extended"
|
||||||
)
|
)
|
||||||
confidence -= 20
|
confidence -= 10
|
||||||
|
|
||||||
confidence = max(0.0, min(95.0, confidence))
|
confidence = max(0.0, min(95.0, confidence))
|
||||||
|
|
||||||
# Final signal
|
# Final signal
|
||||||
if score >= 40 and confidence >= 65:
|
if score >= 35 and confidence >= 60:
|
||||||
report.final_signal = SignalStrength.STRONG_BUY
|
report.final_signal = SignalStrength.STRONG_BUY
|
||||||
elif score >= 15 and confidence >= 50:
|
elif score >= 12 and confidence >= 45:
|
||||||
report.final_signal = SignalStrength.BUY
|
report.final_signal = SignalStrength.BUY
|
||||||
elif score <= -40 and confidence >= 65:
|
elif score <= -35 and confidence >= 60:
|
||||||
report.final_signal = SignalStrength.STRONG_SELL
|
report.final_signal = SignalStrength.STRONG_SELL
|
||||||
elif score <= -15 and confidence >= 50:
|
elif score <= -12 and confidence >= 45:
|
||||||
report.final_signal = SignalStrength.SELL
|
report.final_signal = SignalStrength.SELL
|
||||||
else:
|
else:
|
||||||
report.final_signal = SignalStrength.NEUTRAL
|
report.final_signal = SignalStrength.NEUTRAL
|
||||||
@@ -409,15 +417,15 @@ class SignalAggregator:
|
|||||||
|
|
||||||
# Quality grade
|
# Quality grade
|
||||||
abs_score = abs(score)
|
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+"
|
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"
|
report.signal_quality = "A"
|
||||||
elif abs_score >= 25 and confidence >= 55:
|
elif abs_score >= 20 and confidence >= 50:
|
||||||
report.signal_quality = "B"
|
report.signal_quality = "B"
|
||||||
elif abs_score >= 15 and confidence >= 45:
|
elif abs_score >= 12 and confidence >= 40:
|
||||||
report.signal_quality = "C"
|
report.signal_quality = "C"
|
||||||
elif abs_score >= 10:
|
elif abs_score >= 8:
|
||||||
report.signal_quality = "D"
|
report.signal_quality = "D"
|
||||||
else:
|
else:
|
||||||
report.signal_quality = "F"
|
report.signal_quality = "F"
|
||||||
@@ -446,23 +454,33 @@ class SignalAggregator:
|
|||||||
levels = report.primary_analysis.key_levels
|
levels = report.primary_analysis.key_levels
|
||||||
min_rr = self.config.signals.min_rr_ratio
|
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:
|
if is_buy:
|
||||||
swing_lows = sorted(
|
swing_lows = sorted(
|
||||||
[v for k, v in levels.items() if k.startswith("swing_low_") and v < price],
|
[v for k, v in levels.items() if k.startswith("swing_low_") and v < price],
|
||||||
reverse=True,
|
reverse=True,
|
||||||
)
|
)
|
||||||
if swing_lows:
|
if swing_lows:
|
||||||
sl_price = swing_lows[0] - atr * 0.5
|
sl_price = swing_lows[0] - atr * 1.0
|
||||||
else:
|
else:
|
||||||
|
sl_price = price - atr * 2.0
|
||||||
|
if price - sl_price < atr * 1.5:
|
||||||
sl_price = 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(
|
swing_highs = sorted(
|
||||||
v for k, v in levels.items() if k.startswith("swing_high_") and v > price + atr * 0.3
|
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
|
if swing_highs and swing_highs[0] - price <= atr * tp_swing_cap:
|
||||||
tp2 = swing_highs[1] if len(swing_highs) >= 2 else price + atr * 3.0
|
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
|
sl_distance = price - sl_price
|
||||||
direction = "BUY"
|
direction = "BUY"
|
||||||
@@ -472,18 +490,24 @@ class SignalAggregator:
|
|||||||
v for k, v in levels.items() if k.startswith("swing_high_") and v > price
|
v for k, v in levels.items() if k.startswith("swing_high_") and v > price
|
||||||
)
|
)
|
||||||
if swing_highs:
|
if swing_highs:
|
||||||
sl_price = swing_highs[0] + atr * 0.5
|
sl_price = swing_highs[0] + atr * 1.0
|
||||||
else:
|
else:
|
||||||
|
sl_price = price + atr * 2.0
|
||||||
|
if sl_price - price < atr * 1.5:
|
||||||
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(
|
swing_lows = sorted(
|
||||||
[v for k, v in levels.items() if k.startswith("swing_low_") and v < price - atr * 0.3],
|
[v for k, v in levels.items() if k.startswith("swing_low_") and v < price - atr * 0.3],
|
||||||
reverse=True,
|
reverse=True,
|
||||||
)
|
)
|
||||||
tp1 = swing_lows[0] if len(swing_lows) >= 1 else price - atr * 2.0
|
if swing_lows and price - swing_lows[0] <= atr * tp_swing_cap:
|
||||||
tp2 = swing_lows[1] if len(swing_lows) >= 2 else price - atr * 3.0
|
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
|
sl_distance = sl_price - price
|
||||||
direction = "SELL"
|
direction = "SELL"
|
||||||
|
|||||||
+2
-2
@@ -296,7 +296,7 @@ def _format_scan_summary(reports: List[SignalReport], tf: Timeframe) -> str:
|
|||||||
)
|
)
|
||||||
lines.append("━━━━━━━━━━━━━━━━━━━━")
|
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:
|
if actionable:
|
||||||
names = ", ".join(r.display_name for r in actionable)
|
names = ", ".join(r.display_name for r in actionable)
|
||||||
lines.append(f"🔔 Actionable: {names}")
|
lines.append(f"🔔 Actionable: {names}")
|
||||||
@@ -390,7 +390,7 @@ async def _scan_timeframe(
|
|||||||
_update_level_cache(reports)
|
_update_level_cache(reports)
|
||||||
|
|
||||||
summary = _format_scan_summary(reports, tf)
|
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):
|
for chat_id in list(_autoscan_chats):
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user