Spaces:
Sleeping
Sleeping
File size: 5,736 Bytes
72ae528 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | #!/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()
|