Spaces:
Running
Running
| #!/usr/bin/env python3 | |
| """C7 validation: BASELINE vs BASELINE + calendar features + 36-month lookback.""" | |
| import sys | |
| import json | |
| 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 | |
| from scripts.improvement_harness import ( | |
| BASELINE_FEATURES, | |
| build_triple_barrier_labels, | |
| compute_metrics, | |
| walk_forward, | |
| RF_PARAMS, | |
| DEFAULT_STOCKS, | |
| ) | |
| CALENDAR_FEATURES = [ | |
| "dow_sin", "dow_cos", | |
| "month_sin", "month_cos", | |
| "is_options_expiry_week", | |
| "is_earnings_season", | |
| ] | |
| C7_FEATURES = BASELINE_FEATURES + CALENDAR_FEATURES | |
| def fetch_df_36(stock_no: str): | |
| from services.predictor_service import _fetch_with_cache | |
| from indicators.technical import add_all_indicators, add_cross_asset_tw | |
| from data.institutional_flow import add_institutional_flow | |
| from data.margin_flow import add_margin_flow | |
| from data.fetcher import fetch_cross_asset_tw | |
| df = _fetch_with_cache(stock_no, months=36) | |
| if df is None or df.empty: | |
| return None | |
| df = add_all_indicators(df) | |
| df = add_institutional_flow(df, stock_no) | |
| df = add_margin_flow(df, stock_no) | |
| start, end = str(df["date"].min()), str(df["date"].max()) | |
| result = fetch_cross_asset_tw(start, end) | |
| taiex, usdtwd = result[0], result[1] | |
| sox = result[2] if len(result) > 2 else None | |
| tnx = result[3] if len(result) > 3 else None | |
| df = add_cross_asset_tw(df, taiex, usdtwd, sox_close=sox, tnx_close=tnx) | |
| return df | |
| def _mean(rows, field): | |
| vals = [r[field] for r in rows | |
| if isinstance(r.get(field), (int, float)) and not np.isnan(r.get(field, float("nan")))] | |
| return round(sum(vals) / len(vals), 1) if vals else float("nan") | |
| def main(): | |
| from models.predictor import _build_features | |
| feature_sets = [("baseline", BASELINE_FEATURES), ("calendar", C7_FEATURES)] | |
| per_stock = {} | |
| agg = {"baseline": [], "calendar": []} | |
| hdr = f"{'Stock':>6} {'Set':>12} {'Acc%':>5} {'Dir%':>5} {'↑Prec%':>7} {'Signals':>7}" | |
| print(f"\n{hdr}\n{'-' * len(hdr)}") | |
| for stock_no in DEFAULT_STOCKS: | |
| print(f" computing {stock_no}...", end="\r", flush=True) | |
| df = fetch_df_36(stock_no) | |
| if df is None or df.empty: | |
| print(f"{stock_no:>6} no data") | |
| continue | |
| feat = _build_features(df) | |
| # Merge calendar columns from df into feat so walk_forward can use them | |
| for col in CALENDAR_FEATURES: | |
| if col in df.columns: | |
| feat[col] = df[col].values | |
| 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 feature_sets: | |
| 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:>12} {r['accuracy']:>5.1f} {r['dir_accuracy']:>5.1f} " | |
| f"{r['up_precision']:>7.1f} {r.get('n_signals', 0):>7}") | |
| r0 = per_stock[stock_no].get("baseline", {}) | |
| r1 = per_stock[stock_no].get("calendar", {}) | |
| if r0 and r1: | |
| dd = r1["dir_accuracy"] - r0["dir_accuracy"] | |
| dp = r1["up_precision"] - r0["up_precision"] | |
| print(f"{'':>6} {'Δ':>12} {'':>5} {dd:>+5.1f} {dp:>+7.1f}") | |
| print() | |
| print("=== AGGREGATE ===") | |
| agg_summary = {} | |
| for name, rows in agg.items(): | |
| s = {k: _mean(rows, k) for k in ("accuracy", "dir_accuracy", "up_precision", "dn_precision", "n_signals")} | |
| agg_summary[name] = s | |
| print(f" {name:>12}: acc={s['accuracy']}% dir={s['dir_accuracy']}% " | |
| f"↑prec={s['up_precision']}% signals={s['n_signals']}") | |
| PASS_DIR_ACC = 43.0 | |
| PASS_UP_PREC = 54.0 | |
| cal = agg_summary.get("calendar", {}) | |
| bas = agg_summary.get("baseline", {}) | |
| cal_dir = cal.get("dir_accuracy", 0) | |
| cal_prec = cal.get("up_precision", 0) | |
| bas_dir = bas.get("dir_accuracy", 0) | |
| bas_prec = bas.get("up_precision", 0) | |
| meets_threshold = (cal_dir >= PASS_DIR_ACC) or (cal_prec >= PASS_UP_PREC) | |
| no_regression = ( | |
| (cal_dir >= bas_dir - 1.0) and | |
| (cal_prec >= bas_prec - 1.0) | |
| ) | |
| passed = meets_threshold and no_regression | |
| print(f"\n calendar dir={cal_dir}% ↑prec={cal_prec}%") | |
| print(f" Pass (dir≥{PASS_DIR_ACC} OR ↑prec≥{PASS_UP_PREC}, no regression>1pp): {'YES' if passed else 'NO'}") | |
| result = { | |
| "results": per_stock, | |
| "aggregate": agg_summary, | |
| "passed": passed, | |
| "pass_criterion": f"dir_accuracy >= {PASS_DIR_ACC} OR up_precision >= {PASS_UP_PREC}", | |
| } | |
| def _jsonify(obj): | |
| if isinstance(obj, dict): | |
| return {k: _jsonify(v) for k, v in obj.items()} | |
| if isinstance(obj, list): | |
| return [_jsonify(v) for v in obj] | |
| if isinstance(obj, np.integer): | |
| return int(obj) | |
| if isinstance(obj, np.floating): | |
| return float(obj) | |
| if isinstance(obj, np.bool_): | |
| return bool(obj) | |
| return obj | |
| out = ROOT / "docs" / "c7_calendar_result.json" | |
| out.parent.mkdir(exist_ok=True) | |
| with open(out, "w") as f: | |
| json.dump(_jsonify(result), f, indent=2) | |
| print(f"\n Written: {out}") | |
| if __name__ == "__main__": | |
| main() | |