mirror of
https://github.com/lucasmos/GoldRegime_X.git
synced 2026-11-02 06:26:15 +00:00
feat: enhance LSTM health checks and handle collapsed model scenarios
This commit is contained in:
Binary file not shown.
+52
-15
@@ -243,11 +243,13 @@ class LSTMRegimeClassifier:
|
||||
|
||||
# ── Inference ─────────────────────────────────────────────────────────────
|
||||
|
||||
def predict_proba(self, df_recent: pd.DataFrame) -> Dict[int, float]:
|
||||
def predict_proba(self, df_recent: pd.DataFrame) -> Optional[Dict[int, float]]:
|
||||
"""Return state probability dict from recent bars.
|
||||
|
||||
Needs at least ``sequence_length`` rows; pads with zeros if fewer.
|
||||
Returns uniform distribution on error (graceful degradation).
|
||||
Returns None when the model output is collapsed (range < 0.05) —
|
||||
callers should treat None as "LSTM unreliable, use HMM only".
|
||||
Returns uniform distribution on unexpected error.
|
||||
"""
|
||||
if self._model is None:
|
||||
return {s: 1.0 / self.n_states for s in range(self.n_states)}
|
||||
@@ -268,14 +270,26 @@ class LSTMRegimeClassifier:
|
||||
feat = feat[-self.sequence_length:].astype(np.float32)
|
||||
|
||||
probs = self._model.predict(feat[np.newaxis], verbose=0)[0]
|
||||
|
||||
# Detect collapsed model — output range near zero means all classes
|
||||
# are equally likely, which is indistinguishable from random.
|
||||
if float(probs.max() - probs.min()) < 0.05:
|
||||
logger.warning(
|
||||
"[LSTM COLLAPSED] probs=%s — near-uniform output, model unreliable. "
|
||||
"Retrain with --mode train_lstm.", probs,
|
||||
)
|
||||
return None
|
||||
|
||||
return {int(i): float(probs[i]) for i in range(self.n_states)}
|
||||
|
||||
except Exception as exc:
|
||||
logger.debug("LSTM predict_proba error: %s", exc)
|
||||
return {s: 1.0 / self.n_states for s in range(self.n_states)}
|
||||
|
||||
def predict_state(self, df_recent: pd.DataFrame) -> int:
|
||||
def predict_state(self, df_recent: pd.DataFrame) -> Optional[int]:
|
||||
probs = self.predict_proba(df_recent)
|
||||
if probs is None:
|
||||
return None
|
||||
return max(probs, key=probs.get)
|
||||
|
||||
def ensemble_predict(
|
||||
@@ -296,18 +310,39 @@ class LSTMRegimeClassifier:
|
||||
hmm_state int
|
||||
ensemble_state int
|
||||
lstm_probs dict
|
||||
lstm_status str — 'OK' | 'COLLAPSED'
|
||||
"""
|
||||
lstm_probs = self.predict_proba(df_recent)
|
||||
lstm_probs = self.predict_proba(df_recent)
|
||||
|
||||
# Collapsed LSTM — return HMM-only fallback with zero influence
|
||||
if lstm_probs is None:
|
||||
return hmm_state, {
|
||||
"agreement": True, # don't penalise HMM on broken LSTM
|
||||
"lstm_confidence": 0.0,
|
||||
"transition_risk": 0.0, # never tighten Z on a broken model
|
||||
"lstm_state": hmm_state,
|
||||
"hmm_state": hmm_state,
|
||||
"ensemble_state": hmm_state,
|
||||
"lstm_probs": {},
|
||||
"ensemble_scores": {s: 1.0 if s == hmm_state else 0.0 for s in range(self.n_states)},
|
||||
"lstm_status": "COLLAPSED",
|
||||
}
|
||||
|
||||
lstm_state = max(lstm_probs, key=lstm_probs.get)
|
||||
lstm_confidence = lstm_probs[lstm_state]
|
||||
agreement = (lstm_state == hmm_state)
|
||||
|
||||
if not agreement and lstm_confidence > 0.6:
|
||||
# transition_risk: only meaningful when LSTM is CONFIDENT about a
|
||||
# DIFFERENT state than HMM. Agreement (or low confidence) → no risk.
|
||||
if lstm_state != hmm_state and lstm_confidence > 0.50:
|
||||
# LSTM disagrees AND is confident — genuine transition signal
|
||||
transition_risk = lstm_confidence
|
||||
elif not agreement:
|
||||
transition_risk = 0.3
|
||||
elif lstm_state != hmm_state and lstm_confidence > 0.35:
|
||||
# LSTM disagrees but uncertain — mild risk, won't reach 0.5 gate
|
||||
transition_risk = 0.30
|
||||
else:
|
||||
transition_risk = max(0.0, 1.0 - lstm_confidence)
|
||||
# Agreement, or LSTM not confident enough to matter
|
||||
transition_risk = 0.0
|
||||
|
||||
ensemble_scores = {}
|
||||
for s in range(self.n_states):
|
||||
@@ -319,13 +354,15 @@ class LSTMRegimeClassifier:
|
||||
ensemble_state = max(ensemble_scores, key=ensemble_scores.get)
|
||||
|
||||
info = {
|
||||
"agreement": agreement,
|
||||
"lstm_confidence": lstm_confidence,
|
||||
"transition_risk": transition_risk,
|
||||
"lstm_state": lstm_state,
|
||||
"hmm_state": hmm_state,
|
||||
"ensemble_state": ensemble_state,
|
||||
"lstm_probs": lstm_probs,
|
||||
"agreement": agreement,
|
||||
"lstm_confidence": lstm_confidence,
|
||||
"transition_risk": transition_risk,
|
||||
"lstm_state": lstm_state,
|
||||
"hmm_state": hmm_state,
|
||||
"ensemble_state": ensemble_state,
|
||||
"lstm_probs": lstm_probs,
|
||||
"ensemble_scores": ensemble_scores,
|
||||
"lstm_status": "OK",
|
||||
}
|
||||
return ensemble_state, info
|
||||
|
||||
|
||||
+36
-1
@@ -1003,6 +1003,29 @@ def _run_loop_inner(tf: str, broker: str, account_size: float, mt5,
|
||||
obs_cov = obs_cov if obs_cov is not None else cfg_tf["obs_cov_default"]
|
||||
trans_cov = trans_cov if trans_cov is not None else cfg_tf["trans_cov_default"]
|
||||
|
||||
# ── LSTM startup health check ──────────────────────────────────────────
|
||||
if lstm_classifier is not None:
|
||||
try:
|
||||
_hc_df = _build_live_df(DEFAULT_SYMBOL, tf_mt5, 150, obs_cov, trans_cov)
|
||||
_hc_probs = lstm_classifier.predict_proba(_hc_df)
|
||||
if _hc_probs is None:
|
||||
logger.warning(
|
||||
"[LSTM HEALTH] FAILED — collapsed output on startup warmup data. "
|
||||
"Running HMM-only. Retrain with --mode train_lstm."
|
||||
)
|
||||
lstm_classifier = None
|
||||
else:
|
||||
_hc_max = max(_hc_probs.values())
|
||||
logger.info("[LSTM HEALTH] OK — max confidence: %.2f", _hc_max)
|
||||
if _hc_max < 0.30:
|
||||
logger.warning(
|
||||
"[LSTM HEALTH] LOW CONFIDENCE (%.2f) — LSTM may not add reliable signal. "
|
||||
"Consider retraining.", _hc_max,
|
||||
)
|
||||
except Exception as _hc_exc:
|
||||
logger.warning("[LSTM HEALTH] ERROR — %s — running HMM-only.", _hc_exc)
|
||||
lstm_classifier = None
|
||||
|
||||
# Session state (persists across bars within a day)
|
||||
last_bar_time = None
|
||||
current_day = None
|
||||
@@ -1243,7 +1266,13 @@ def _run_loop_inner(tf: str, broker: str, account_size: float, mt5,
|
||||
lstm_weight=0.3,
|
||||
)
|
||||
_lstm_ensemble_info = _lstm_info
|
||||
if not _lstm_info["agreement"]:
|
||||
if _lstm_info.get("lstm_status") == "COLLAPSED":
|
||||
logger.warning(
|
||||
"[LSTM] Collapsed output detected mid-session — "
|
||||
"disabling LSTM for the rest of this session. Retrain recommended."
|
||||
)
|
||||
lstm_classifier = None
|
||||
elif not _lstm_info["agreement"]:
|
||||
logger.info(
|
||||
"[LSTM] HMM=%d vs LSTM=%d (conf=%.2f, risk=%.2f) — %s",
|
||||
hmm_state,
|
||||
@@ -1252,6 +1281,12 @@ def _run_loop_inner(tf: str, broker: str, account_size: float, mt5,
|
||||
_lstm_info["transition_risk"],
|
||||
"TIGHTENING Z" if _lstm_info["transition_risk"] > 0.5 else "watching",
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
"[LSTM] HMM=%d LSTM=%d agree (conf=%.2f, risk=%.2f)",
|
||||
hmm_state, _lstm_info["lstm_state"],
|
||||
_lstm_info["lstm_confidence"], _lstm_info["transition_risk"],
|
||||
)
|
||||
if _lstm_info["transition_risk"] > 0.5:
|
||||
# TIGHTEN — regime instability detected
|
||||
from src.signal_evaluator import SignalEvaluator as _SE
|
||||
|
||||
Reference in New Issue
Block a user