Compare commits

...

2 Commits

Author SHA1 Message Date
harwinder b7ec5341cf Tune 5M signals, fix XAU regression, add signal dedup cooldown
EUR/USD 5M: raise min_entry_score 28→35, block hours 4/19/21
  → 83.4% WR, 145 trades, +9.68R, PF 1.42 (120-day)

XAU/USD 5M: fix B-grade exclusion regression in setup_grades,
  add breakeven trail (trigger 0.15R, buffer 0.05R)
  → 84.9% WR, 4810 trades, +164.54R, PF 1.23 (730-day)

EUR/USD M15: tighten defaults (SL 1.5x ATR, TP 1.0x, min_rr 0.5,
  block ranging+transitioning, BE trigger 0.2R)

Backtest: multiprocess fork parallelism, vectorized pre-filter,
  CUDA preload fix, per-pair BE trigger routing

Telegram bot: add signal deduplication — suppress repeated
  same-direction messages within 5-bar cooldown per pair+TF.
  Resend on direction flip, quality upgrade, or cooldown expiry.
  Manual scans always ungated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-19 20:19:11 -04:00
harwinder 9e5410d617 GPU parallel EMA + fix backtest/live parity bugs
- Add CUDA parallel prefix scan (Blelloch) for EMA: 16 EMAs in 3 batched kernel launches
- Eliminate CPU↔GPU array bouncing; keep arrays on-device until final cache build
- Auto-detect pip-installed CUDA libs for CuPy kernel compilation
- Add --gpu-only and --quick (30-day) CLI flags
- Fix missing _adx_slope in GPUTechnicalEngine (was suppressing transitioning regime)
- Fix ROC early-entry: guard was pos < n+1, should be pos < n+4 to match live engine
- Fix weekend filter: use proper forex weekend (Fri 22:00–Sun 22:00 UTC) instead of weekday >= 5

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-19 13:26:12 -04:00
7 changed files with 803 additions and 331 deletions
+729 -309
View File
File diff suppressed because it is too large Load Diff
+10 -8
View File
@@ -159,6 +159,8 @@ class XauStrategyConfig:
rsi_zone: tuple = (40.0, 65.0)
allowed_weekdays: List[int] = field(default_factory=lambda: [0, 1, 2, 3, 4, 6]) # Mon-Fri + Sun (Sydney open)
sell_min_quality: List[str] = field(default_factory=lambda: ["A+", "A", "B"])
be_trigger_r: float = 0.0
be_buffer_r: float = 0.0
@dataclass
@@ -169,24 +171,24 @@ class EurUsdStrategyConfig:
weekend_filter: bool = True
allowed_weekdays: List[int] = field(default_factory=lambda: [0, 1, 2, 3, 4])
min_atr_threshold: float = 0.0003
sl_atr_multiplier: float = 0.5
tp_atr_multiplier: float = 1.5
sl_atr_multiplier: float = 1.5
tp_atr_multiplier: float = 1.0
sl_atr_floor: float = 1.2
min_rr_ratio: float = 1.0
min_rr_ratio: float = 0.5
mature_move_threshold: float = 35.0
tp_quality_scale: Dict[str, float] = field(default_factory=lambda: {
"A+": 1.0, "A": 1.0, "B": 1.0, "C": 1.0,
})
blocked_regimes: List[str] = field(default_factory=list)
min_quality_grades: List[str] = field(default_factory=lambda: ["A+", "A"])
blocked_regimes: List[str] = field(default_factory=lambda: ["ranging", "transitioning"])
min_quality_grades: List[str] = field(default_factory=lambda: ["A+", "A", "B"])
max_exhaustion_tags: int = 99
block_near_ema200: bool = False
session_hours: List[int] = field(default_factory=list)
min_entry_score: float = 30.0
min_entry_confidence: float = 50.0
min_entry_score: float = 38.0
min_entry_confidence: float = 60.0
block_rsi_extended: bool = True
be_trigger_r: float = 0.3
be_trigger_r: float = 0.2
be_buffer_r: float = 0.05
+2 -1
View File
@@ -80,8 +80,9 @@ def make_config(
config.eur_strategy.sl_atr_floor = 0.8
config.eur_strategy.sl_atr_multiplier = 0.3
config.eur_strategy.min_quality_grades = ["A+", "A", "B"]
config.eur_strategy.min_entry_score = 28.0
config.eur_strategy.min_entry_score = 35.0
config.eur_strategy.min_entry_confidence = 50.0
config.backtest.blocked_hours_utc = [22, 23, 0, 1, 2, 3, 4, 19, 21]
else:
config.technical.regime_weights_override = EUR_REGIME_WEIGHTS
+5
View File
@@ -20,4 +20,9 @@ def make_config(
config.confirmation_timeframes = (
confirmation_tfs or _CONFIRMATIONS.get(primary_tf, [Timeframe.M15, Timeframe.H4])
)
if primary_tf == Timeframe.M5:
config.xau_strategy.be_trigger_r = 0.15
config.xau_strategy.be_buffer_r = 0.05
return config
+2 -1
View File
@@ -498,7 +498,8 @@ class SignalAggregator:
if report.primary_analysis else 0)
if atr > 0:
report.trade_setup = self._generate_xau_atr_trade_setup(report, atr)
if report.trade_setup is None and report.signal_quality in ("A+", "A", "B", "C"):
setup_grades = ("A+", "A", "B", "C")
if report.trade_setup is None and report.signal_quality in setup_grades:
report.trade_setup = self._generate_trade_setup(report, pip_value)
# ── 8. Reasons ─────────────────────────────────────────────
+4 -5
View File
@@ -1656,15 +1656,14 @@ def _():
assert 6 not in cfg.allowed_weekdays # No Sunday
assert cfg.min_atr_threshold == 0.0003
assert cfg.sl_atr_floor == 1.2
assert cfg.min_rr_ratio == 1.0
assert cfg.min_rr_ratio == 0.5
assert cfg.mature_move_threshold == 35.0
assert cfg.tp_quality_scale["A+"] == 1.0
assert cfg.tp_quality_scale["A"] == 1.0
assert cfg.tp_quality_scale["C"] == 1.0
assert cfg.blocked_regimes == []
assert cfg.blocked_regimes == ["ranging", "transitioning"]
assert "A" in cfg.min_quality_grades
assert "A" in cfg.min_quality_grades
assert "B" not in cfg.min_quality_grades
assert "B" in cfg.min_quality_grades
@test("PlatformConfig has eur_strategy field")
@@ -1756,7 +1755,7 @@ def _():
config.eur_strategy.blocked_regimes = []
config.eur_strategy.min_quality_grades = []
assert config.eur_strategy.sl_atr_floor == 1.2
assert config.eur_strategy.min_rr_ratio == 1.0
assert config.eur_strategy.min_rr_ratio == 0.5
agg = SignalAggregator(config)
df = make_ohlcv(n=300, start_price=1.0800, trend=0.0003, seed=77)
dfs = {Timeframe.H1: df, Timeframe.M15: df, Timeframe.H4: df}
+51 -7
View File
@@ -86,6 +86,8 @@ _LEVEL_KEYS = (
_signal_stats: Dict[str, Dict[str, SignalReport]] = {}
_stats_updated: Optional[float] = None
_last_sent: Dict[str, Dict] = {}
SCAN_BUTTONS = [
{"cb": "scan_xauusd_m5", "symbol": XAUUSD, "tf": Timeframe.M5, "label": "XAU 5m"},
{"cb": "scan_xauusd_m15", "symbol": XAUUSD, "tf": Timeframe.M15, "label": "XAU 15m"},
@@ -468,6 +470,37 @@ def _seconds_until_boundary(interval_minutes: int, buffer: int = 10) -> float:
return remaining
_QUALITY_RANK = {"A+": 4, "A": 3, "B": 2, "C": 1}
def _should_send(report: SignalReport, tf: Timeframe) -> bool:
if not report.trade_setup:
return False
key = f"{report.symbol}:{tf.value}"
prev = _last_sent.get(key)
if prev is None:
return True
direction = report.trade_setup.direction
quality = report.signal_quality
if direction != prev["direction"]:
return True
if _QUALITY_RANK.get(quality, 0) > _QUALITY_RANK.get(prev["quality"], 0):
return True
cooldown = 5 * _TF_SECONDS.get(tf, 300)
if time.time() - prev["sent_at"] >= cooldown:
return True
return False
def _record_sent(report: SignalReport, tf: Timeframe) -> None:
key = f"{report.symbol}:{tf.value}"
_last_sent[key] = {
"direction": report.trade_setup.direction,
"quality": report.signal_quality,
"sent_at": time.time(),
}
async def _scan_timeframe(
bot,
tf: Timeframe,
@@ -492,13 +525,19 @@ async def _scan_timeframe(
await _refresh_dashboard(bot)
actionable = [r for r in reports if r.signal_quality in ("A+", "A", "B", "C")]
for chat_id in list(_autoscan_chats):
for report in actionable:
detail = format_signal_message(report)
try:
await bot.send_message(chat_id, detail, parse_mode=ParseMode.MARKDOWN)
except Exception as exc:
logger.warning(f"Failed to send signal for chat {chat_id}: {exc}")
sendable = [r for r in actionable if _should_send(r, tf)]
for report in sendable:
_record_sent(report, tf)
if sendable:
for chat_id in list(_autoscan_chats):
for report in sendable:
detail = format_signal_message(report)
try:
await bot.send_message(chat_id, detail, parse_mode=ParseMode.MARKDOWN)
except Exception as exc:
logger.warning(f"Failed to send signal for chat {chat_id}: {exc}")
elif actionable:
logger.info(f"Auto-scan {tf.value}: {len(actionable)} signal(s) suppressed by cooldown")
# ── WebSocket level-breach monitor ──────────────────────────────────
@@ -563,6 +602,11 @@ _AUTOSCAN_LOOPS = [
(Timeframe.H4, 240),
]
_TF_SECONDS = {
Timeframe.M5: 300, Timeframe.M15: 900,
Timeframe.H1: 3600, Timeframe.H4: 14400,
}
def _start_scheduler(bot) -> None:
global _dashboard_task