Spaces:
Running
Running
| #!/usr/bin/env python3 | |
| """C6 validation: HMM regime detection as feature + conditional confidence.""" | |
| import sys | |
| from pathlib import Path | |
| ROOT = Path(__file__).resolve().parent.parent | |
| sys.path.insert(0, str(ROOT)) | |
| try: | |
| from dotenv import load_dotenv; load_dotenv(ROOT / ".env") | |
| except ImportError: pass | |
| import warnings; warnings.filterwarnings("ignore") | |
| import numpy as np | |
| import pandas as pd | |
| import json | |
| sys.path.insert(0, str(ROOT / "scripts")) | |
| from improvement_harness import ( | |
| run_comparison, BASELINE_FEATURES, fetch_df, build_triple_barrier_labels, | |
| walk_forward, RF_PARAMS, MIN_TRAIN, STEP, LABEL_HORIZON, compute_metrics, | |
| ) | |
| from models.predictor import _build_features, FEATURE_COLUMNS | |
| STOCKS = ["2330", "0050", "2317", "2454", "2881"] | |
| def add_hmm_regime(df: pd.DataFrame) -> pd.DataFrame: | |
| """ | |
| Fit a 3-state GaussianHMM on TAIEX 5d return + 20d vol proxy. | |
| Add columns: hmm_regime (0/1/2), hmm_regime_bull (binary), hmm_regime_bear (binary). | |
| If TAIEX columns are missing or hmmlearn fails, add zeros (graceful fallback). | |
| """ | |
| try: | |
| from hmmlearn import hmm | |
| obs_cols = [] | |
| if "taiex_return_5d" in df.columns: | |
| obs_cols.append("taiex_return_5d") | |
| if "volatility_20d" in df.columns: | |
| obs_cols.append("volatility_20d") | |
| if "taiex_ma20_ratio" in df.columns: | |
| obs_cols.append("taiex_ma20_ratio") | |
| if len(obs_cols) < 2: | |
| raise ValueError("not enough observation columns") | |
| X_obs = df[obs_cols].fillna(0).values.astype(float) | |
| model = hmm.GaussianHMM( | |
| n_components=3, covariance_type="diag", | |
| n_iter=100, random_state=42, | |
| ) | |
| model.fit(X_obs) | |
| regimes = model.predict(X_obs) | |
| # Label states by mean taiex_return_5d: highest = bull (2), lowest = bear (0) | |
| means = [X_obs[regimes == s, 0].mean() if (regimes == s).sum() > 0 else 0.0 | |
| for s in range(3)] | |
| order = np.argsort(means) | |
| remap = {order[0]: 0, order[1]: 1, order[2]: 2} | |
| labeled = np.array([remap[r] for r in regimes]) | |
| df["hmm_regime"] = labeled.astype(float) | |
| df["hmm_regime_bull"] = (labeled == 2).astype(float) | |
| df["hmm_regime_bear"] = (labeled == 0).astype(float) | |
| except Exception as e: | |
| df["hmm_regime"] = 0.0 | |
| df["hmm_regime_bull"] = 0.0 | |
| df["hmm_regime_bear"] = 0.0 | |
| return df | |
| def main(): | |
| BASELINE = BASELINE_FEATURES | |
| HMM_FEATS = BASELINE + ["hmm_regime", "hmm_regime_bull", "hmm_regime_bear"] | |
| per_stock = {} | |
| agg = {"baseline": [], "hmm": []} | |
| hdr = f"{'Stock':>6} {'Set':>10} {'Acc%':>5} {'Dir%':>5} {'↑Prec%':>7} {'Signals':>7}" | |
| print(f"\n{hdr}\n{'-'*len(hdr)}") | |
| for stock_no in STOCKS: | |
| print(f" computing {stock_no}...", end="\r", flush=True) | |
| df = fetch_df(stock_no) | |
| if df is None or df.empty: | |
| print(f"{stock_no:>6} no data"); continue | |
| df = add_hmm_regime(df) | |
| feat = _build_features(df) | |
| for col in ["hmm_regime", "hmm_regime_bull", "hmm_regime_bear"]: | |
| if col in df.columns: | |
| if len(df) == len(feat): | |
| feat[col] = df[col].values | |
| else: | |
| feat[col] = 0.0 | |
| close = (df.set_index("date")["close"] if "date" in df.columns else df["close"]).values | |
| labels = build_triple_barrier_labels(close) | |
| per_stock[stock_no] = {} | |
| for name, cols in [("baseline", BASELINE), ("hmm", HMM_FEATS)]: | |
| r = walk_forward(feat, labels, cols) | |
| per_stock[stock_no][name] = r | |
| if r: | |
| agg[name].append(r) | |
| prefix = f"{stock_no:>6}" if name == "baseline" else f"{'':>6}" | |
| print(f"{prefix} {name:>10} {r['accuracy']:>5.1f} {r['dir_accuracy']:>5.1f} " | |
| f"{r['up_precision']:>7.1f} {r.get('n_signals',0):>7}") | |
| if per_stock[stock_no].get("baseline") and per_stock[stock_no].get("hmm"): | |
| rb, rh = per_stock[stock_no]["baseline"], per_stock[stock_no]["hmm"] | |
| print(f"{'':>6} {'Δ':>10} {'':>5} {rh['dir_accuracy']-rb['dir_accuracy']:>+5.1f} " | |
| f"{rh['up_precision']-rb['up_precision']:>+7.1f}") | |
| print() | |
| def mf(rows, f): | |
| vals = [r[f] for r in rows if isinstance(r.get(f), (int, float)) and not np.isnan(r.get(f, float("nan")))] | |
| return round(sum(vals)/len(vals), 1) if vals else float("nan") | |
| print("=== AGGREGATE ===") | |
| agg_summary = {} | |
| for name in ("baseline", "hmm"): | |
| rows = agg[name] | |
| s = {k: mf(rows, k) for k in ("accuracy", "dir_accuracy", "up_precision", "dn_precision", "n_signals")} | |
| agg_summary[name] = s | |
| print(f" {name:>10}: acc={s['accuracy']}% dir={s['dir_accuracy']}% ↑prec={s['up_precision']}%") | |
| h = agg_summary.get("hmm", {}) | |
| b = agg_summary.get("baseline", {}) | |
| no_regress = h.get("dir_accuracy", 0) >= b.get("dir_accuracy", 0) - 0.5 and \ | |
| h.get("up_precision", 0) >= b.get("up_precision", 0) - 0.5 | |
| improvement = (h.get("dir_accuracy", 0) - b.get("dir_accuracy", 0) >= 1.5 or | |
| h.get("up_precision", 0) - b.get("up_precision", 0) >= 1.5) | |
| passed = no_regress and improvement | |
| print(f"\n Pass: {'YES' if passed else 'NO'} (no_regress={no_regress}, improvement={improvement})") | |
| Path("docs").mkdir(exist_ok=True) | |
| with open("docs/c6_hmm_result.json", "w") as f: | |
| json.dump({"results": per_stock, "aggregate": agg_summary, | |
| "passed": passed, "pass_criterion": "no_regress AND >=1.5pp improvement"}, f, indent=2) | |
| if __name__ == "__main__": | |
| main() | |