mirror of
https://github.com/lucasmos/GoldRegime_X.git
synced 2026-11-02 06:26:15 +00:00
feat: Enhance signal engine with exit confirmation logic and improve performance metrics calculation
This commit is contained in:
Binary file not shown.
+32
-4
@@ -156,7 +156,16 @@ def compute_purged_ts_score(
|
||||
rf = min(fold_result.get("recovery_factor", 0.0), 5.0)
|
||||
pf = min(fold_result.get("profit_factor", 1.0), 3.0)
|
||||
sharpe = fold_result.get("sharpe_ratio", 0.0)
|
||||
fold_scores.append(float(rf * 0.4 + pf * 0.3 + sharpe * 0.3))
|
||||
wr = fold_result.get("win_rate", 0.0)
|
||||
logger.info(
|
||||
" Fold %s→%s: n=%d WR=%.1f%% Sharpe=%.3f RF=%.2f PF=%.2f",
|
||||
is_end, oos_end or "latest", fold_n, wr * 100, sharpe, rf, pf,
|
||||
)
|
||||
# Cap at 2.0 — prevents a single anomalous period (e.g. COVID bull run)
|
||||
# from dominating the cross-fold average. Raw formula can hit 2.2+
|
||||
# when one period has exceptional RF + PF + Sharpe simultaneously.
|
||||
fold_score = min(float(rf * 0.4 + pf * 0.3 + sharpe * 0.3), 2.0)
|
||||
fold_scores.append(fold_score)
|
||||
|
||||
except Exception as exc:
|
||||
logger.warning("Purged TS fold (%s→%s) failed: %s", is_end, oos_end or "latest", exc)
|
||||
@@ -166,7 +175,24 @@ def compute_purged_ts_score(
|
||||
if len(valid_scores) < 2:
|
||||
return -50.0
|
||||
|
||||
mean_score = float(sum(valid_scores) / len(valid_scores))
|
||||
# Hard reject if any evaluated fold is negative — a robust strategy must not
|
||||
# lose money in any of the three tested macro-regime periods.
|
||||
if any(s < 0.0 for s in valid_scores):
|
||||
logger.warning(
|
||||
"Purged TS: %d fold(s) negative — rejecting (folds: %s)",
|
||||
sum(1 for s in valid_scores if s < 0.0),
|
||||
[f"{s:.3f}" for s in valid_scores],
|
||||
)
|
||||
return -50.0
|
||||
|
||||
# Variance-penalised mean: subtract half the population std from the mean.
|
||||
# A strategy that aces one anomalous period (e.g. COVID bull run) but is
|
||||
# mediocre in others scores lower than one that performs consistently across
|
||||
# all three folds. This prevents single-period outliers from winning the study.
|
||||
_n_v = len(valid_scores)
|
||||
_mean_v = sum(valid_scores) / _n_v
|
||||
_std_v = (sum((s - _mean_v) ** 2 for s in valid_scores) / _n_v) ** 0.5
|
||||
mean_score = float(_mean_v - 0.5 * _std_v)
|
||||
|
||||
# Walk-Forward Efficiency: recent fold performance relative to first fold.
|
||||
# Rewards models that hold up in the most recent macro-regime.
|
||||
@@ -186,8 +212,10 @@ def compute_purged_ts_score(
|
||||
else:
|
||||
return -50.0
|
||||
|
||||
# Consistency bonus: all valid folds positive
|
||||
if all(s > 0 for s in valid_scores):
|
||||
# Consistency bonus: all folds show meaningful positive performance (> 0.30).
|
||||
# Threshold raised from > 0.0 — ensures a single barely-passing fold doesn't
|
||||
# silently collect the bonus when the other folds are carrying the strategy.
|
||||
if all(s > 0.30 for s in valid_scores):
|
||||
mean_score *= 1.05
|
||||
|
||||
logger.info(
|
||||
|
||||
+24
-2
@@ -12,6 +12,13 @@ import numpy as np
|
||||
MIN_CONFIRMATION_BARS = {"H1": 2, "M15": 2, "M5": 2}
|
||||
MIN_CHOP_CONFIRM_BARS = {"H1": 2, "M15": 2, "M5": 2}
|
||||
|
||||
# Minimum consecutive bars in a NEW (different) regime before regime-reversal
|
||||
# exit fires. Mirrors the entry confirmation logic so single-bar regime blips
|
||||
# (e.g. one Chop bar interrupting a Bull trend) don't cause premature exits.
|
||||
# Without this, the engine enters after 2 confirmation bars, then exits on the
|
||||
# FIRST bar of any regime change → 1-bar trades → spread dominates → WR ~25%.
|
||||
MIN_EXIT_CONFIRM_BARS = {"H1": 2, "M15": 2, "M5": 3}
|
||||
|
||||
# XGBoost probability threshold for trend and MR entries
|
||||
ENTRY_PROB = {"H1": 0.58, "M15": 0.55, "M5": 0.52}
|
||||
MR_ENTRY_PROB = {"H1": 0.55, "M15": 0.52, "M5": 0.50}
|
||||
@@ -57,6 +64,7 @@ class SignalEngine:
|
||||
self.in_trade: bool = False
|
||||
self.entry_regime: int | None = None
|
||||
self.bars_in_trade: int = 0
|
||||
self._reversal_bars: int = 0 # consecutive bars in non-entry regime
|
||||
|
||||
# ── Core API ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -79,6 +87,14 @@ class SignalEngine:
|
||||
else:
|
||||
stability = 0.70
|
||||
|
||||
# Track consecutive bars in a non-entry regime for exit confirmation.
|
||||
# Reset to zero when we return to the entry regime (blip resolved).
|
||||
if self.in_trade and self.entry_regime is not None:
|
||||
if new_state != self.entry_regime:
|
||||
self._reversal_bars += 1
|
||||
else:
|
||||
self._reversal_bars = 0
|
||||
|
||||
return {
|
||||
"state": new_state,
|
||||
"bars_in_regime": self.bars_in_regime,
|
||||
@@ -148,9 +164,12 @@ class SignalEngine:
|
||||
"""
|
||||
state = regime_info["state"]
|
||||
|
||||
# Regime reversal — regime has shifted away from the entry regime
|
||||
# Regime reversal — require MIN_EXIT_CONFIRM_BARS consecutive bars in the
|
||||
# new regime before exiting. Single-bar blips (e.g. one Chop bar in a Bull
|
||||
# trend) reset _reversal_bars and keep the trade alive.
|
||||
if self.entry_regime is not None and state != self.entry_regime:
|
||||
return True, "regime_reversal"
|
||||
if self._reversal_bars >= MIN_EXIT_CONFIRM_BARS.get(self.tf, 2):
|
||||
return True, "regime_reversal"
|
||||
|
||||
# Persistence collapse — regime is no longer stable
|
||||
if regime_info["stability"] < PERSISTENCE_MIN.get(self.tf, 0.55):
|
||||
@@ -173,12 +192,14 @@ class SignalEngine:
|
||||
self.in_trade = True
|
||||
self.entry_regime = regime_state
|
||||
self.bars_in_trade = 0
|
||||
self._reversal_bars = 0
|
||||
|
||||
def on_trade_closed(self) -> None:
|
||||
"""Call after a trade is fully closed."""
|
||||
self.in_trade = False
|
||||
self.entry_regime = None
|
||||
self.bars_in_trade = 0
|
||||
self._reversal_bars = 0
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Full state reset — use between independent backtest runs."""
|
||||
@@ -187,3 +208,4 @@ class SignalEngine:
|
||||
self.in_trade = False
|
||||
self.entry_regime = None
|
||||
self.bars_in_trade = 0
|
||||
self._reversal_bars = 0
|
||||
|
||||
Reference in New Issue
Block a user