Spaces:
Running
Running
| #!/usr/bin/env python3 | |
| """C12: 1-day vs 5-day label horizon comparison.""" | |
| 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 json | |
| import numpy as np | |
| import pandas as pd | |
| from sklearn.ensemble import RandomForestClassifier | |
| from scripts.improvement_harness import ( | |
| fetch_df, build_triple_barrier_labels, walk_forward, | |
| compute_metrics, BASELINE_FEATURES, DEFAULT_STOCKS, | |
| RF_PARAMS, MIN_TRAIN, STEP, LABEL_HORIZON, | |
| ) | |
| from models.predictor import _build_features | |
| PASS_DIR = 44.0 | |
| PASS_UP = 54.0 | |
| def build_1day_labels(close: np.ndarray, vol_lookback: int = 20, pt_sl_ratio: float = 1.0) -> np.ndarray: | |
| """Triple barrier with 1-day vertical barrier instead of 5-day.""" | |
| n = len(close) | |
| log_ret = np.diff(np.log(close + 1e-9)) | |
| labels = np.full(n, np.nan) | |
| for i in range(n - 1): # horizon=1 | |
| start_v = max(0, i - vol_lookback) | |
| window = log_ret[start_v:i] | |
| vol = float(np.std(window)) if len(window) >= 5 else 0.015 | |
| vol = max(vol, 0.001) | |
| upper = close[i] * (1.0 + vol * pt_sl_ratio) | |
| lower = close[i] * (1.0 - vol * pt_sl_ratio) | |
| c = close[i + 1] | |
| if c >= upper: | |
| labels[i] = 1 | |
| elif c <= lower: | |
| labels[i] = -1 | |
| else: | |
| labels[i] = 0 | |
| return labels | |
| def walk_forward_1d(feat_df: pd.DataFrame, label_arr: np.ndarray, cols: list) -> dict: | |
| LABEL_HORIZON_1D = 1 | |
| avail = [c for c in cols if c in feat_df.columns] | |
| X_all = feat_df[avail].fillna(0).values | |
| n = len(feat_df) | |
| y_true_all, y_pred_all = [], [] | |
| cutoff = MIN_TRAIN | |
| while cutoff + STEP + LABEL_HORIZON_1D <= n: | |
| y_tr = label_arr[:cutoff] | |
| valid = ~np.isnan(y_tr) | |
| y_v = y_tr[valid] | |
| if len(y_v) < 10 or len(np.unique(y_v)) < 2: | |
| cutoff += STEP; continue | |
| clf = RandomForestClassifier(**RF_PARAMS) | |
| clf.fit(X_all[:cutoff][valid], y_v.astype(int)) | |
| test_end = min(cutoff + STEP, n - LABEL_HORIZON_1D) | |
| y_te = label_arr[cutoff:test_end] | |
| valid_te = ~np.isnan(y_te) | |
| if valid_te.sum() == 0: | |
| cutoff += STEP; continue | |
| y_pred = clf.predict(X_all[cutoff:test_end][valid_te]) | |
| y_true_all.extend(y_te[valid_te].tolist()) | |
| y_pred_all.extend(y_pred.tolist()) | |
| cutoff += STEP | |
| if not y_true_all: | |
| return {} | |
| return compute_metrics(np.array(y_true_all), np.array(y_pred_all)) | |
| def main(): | |
| hdr = f"{'Stock':>6} {'5day_dir%':>9} {'1day_dir%':>9} {'Δdir':>5} {'5day_↑prec%':>11} {'1day_↑prec%':>11} {'Δprec':>6} {'1day_sigs':>9}" | |
| print(f"\n{hdr}\n{'-'*len(hdr)}") | |
| per_stock = {} | |
| rows_5d, rows_1d = [], [] | |
| for stock_no in DEFAULT_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 | |
| feat = _build_features(df) | |
| close = (df.set_index("date")["close"] if "date" in df.columns else df["close"]).values | |
| labels_5d = build_triple_barrier_labels(close) | |
| r5 = walk_forward(feat, labels_5d, BASELINE_FEATURES) | |
| labels_1d = build_1day_labels(close) | |
| r1 = walk_forward_1d(feat, labels_1d, BASELINE_FEATURES) | |
| per_stock[stock_no] = {"5day": r5, "1day": r1} | |
| if r5: | |
| rows_5d.append(r5) | |
| if r1: | |
| rows_1d.append(r1) | |
| d5 = r5.get("dir_accuracy", float("nan")) | |
| d1 = r1.get("dir_accuracy", float("nan")) | |
| p5 = r5.get("up_precision", float("nan")) | |
| p1 = r1.get("up_precision", float("nan")) | |
| dd = d1 - d5 if not (np.isnan(d1) or np.isnan(d5)) else float("nan") | |
| dp = p1 - p5 if not (np.isnan(p1) or np.isnan(p5)) else float("nan") | |
| sigs = r1.get("n_signals", 0) | |
| print(f"{stock_no:>6} {d5:>9.1f} {d1:>9.1f} {dd:>+5.1f} {p5:>11.1f} {p1:>11.1f} {dp:>+6.1f} {sigs:>9}") | |
| def _mean(rows, k): | |
| vals = [r[k] for r in rows if isinstance(r.get(k), (int, float)) and not np.isnan(r.get(k, float("nan")))] | |
| return round(sum(vals) / len(vals), 1) if vals else float("nan") | |
| agg5 = {k: _mean(rows_5d, k) for k in ("accuracy", "dir_accuracy", "up_precision", "dn_precision", "n_signals")} | |
| agg1 = {k: _mean(rows_1d, k) for k in ("accuracy", "dir_accuracy", "up_precision", "dn_precision", "n_signals")} | |
| dd_agg = agg1["dir_accuracy"] - agg5["dir_accuracy"] | |
| dp_agg = agg1["up_precision"] - agg5["up_precision"] | |
| print(f"\n{'=== AGGREGATE ==='}") | |
| print(f" 5day: dir={agg5['dir_accuracy']}% ↑prec={agg5['up_precision']}% signals={agg5['n_signals']}") | |
| print(f" 1day: dir={agg1['dir_accuracy']}% ↑prec={agg1['up_precision']}% signals={agg1['n_signals']}") | |
| print(f" Δ : dir={dd_agg:+.1f}pp ↑prec={dp_agg:+.1f}pp") | |
| passed = bool((agg1["dir_accuracy"] >= PASS_DIR) and (agg1["up_precision"] >= PASS_UP)) | |
| status = "PASSED" if passed else "FAILED" | |
| print(f"\n Pass criterion: 1day dir ≥ {PASS_DIR} AND ↑prec ≥ {PASS_UP}") | |
| print(f" C12 {status}") | |
| result = { | |
| "results": per_stock, | |
| "aggregate": {"5day": agg5, "1day": agg1}, | |
| "passed": passed, | |
| "pass_criterion": f"1day dir_accuracy >= {PASS_DIR} AND up_precision >= {PASS_UP}", | |
| } | |
| out = ROOT / "docs" / "c12_1day_result.json" | |
| out.parent.mkdir(exist_ok=True) | |
| with open(out, "w") as f: | |
| json.dump(result, f, indent=2) | |
| print(f"\n Written: {out}") | |
| print(f"\nC12 {status} — 1day dir: {agg1['dir_accuracy']}%, ↑prec: {agg1['up_precision']}%, signals/stock: {agg1['n_signals']}") | |
| if __name__ == "__main__": | |
| main() | |