Spaces:
Running
Running
| #!/usr/bin/env python3 | |
| """C19: Adaptive refined labeling. | |
| Hypothesis: In low-vol regimes (20d vol < median), standard 1.0σ barriers are | |
| too wide relative to signal, generating noisy UP labels. Tightening to 0.7σ | |
| filters true signals from noise, improving up_precision. | |
| Label scheme: | |
| low-vol (vol < median): pt_sl = LOW_RATIO (0.7 — tighter) | |
| high-vol (vol ≥ median): pt_sl = HIGH_RATIO (1.0 — unchanged baseline) | |
| Same CURRENT_FEATURES, only labels change. | |
| """ | |
| import sys | |
| from pathlib import Path | |
| ROOT = Path(__file__).resolve().parent.parent | |
| sys.path.insert(0, str(ROOT)) | |
| import warnings; warnings.filterwarnings("ignore") | |
| import json | |
| import argparse | |
| import numpy as np | |
| from scripts.improvement_harness import ( | |
| fetch_df, walk_forward, compute_metrics, | |
| CURRENT_FEATURES, DEFAULT_STOCKS, EXTENDED_STOCKS, | |
| PASS_DIR_ACC, PASS_UP_PREC, | |
| LABEL_HORIZON, VOL_LOOKBACK, | |
| ) | |
| from models.predictor import _build_features | |
| # Regime ratios to test: (low_ratio, high_ratio) | |
| VARIANTS = [ | |
| (0.7, 1.0), # primary hypothesis: tighten low-vol only | |
| (0.7, 1.3), # tighten low, widen high | |
| (0.6, 1.0), # more aggressive tightening | |
| ] | |
| def build_adaptive_labels( | |
| close: np.ndarray, | |
| low_ratio: float = 0.7, | |
| high_ratio: float = 1.0, | |
| ) -> np.ndarray: | |
| n = len(close) | |
| log_ret = np.diff(np.log(close + 1e-9)) | |
| vols = [] | |
| for i in range(n): | |
| 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 | |
| vols.append(max(vol, 0.001)) | |
| vol_threshold = float(np.median(vols)) | |
| labels = np.full(n, np.nan) | |
| for i in range(n - LABEL_HORIZON): | |
| vol = vols[i] | |
| pt_sl = low_ratio if vol < vol_threshold else high_ratio | |
| upper = close[i] * (1.0 + vol * pt_sl) | |
| lower = close[i] * (1.0 - vol * pt_sl) | |
| label = 0 | |
| for j in range(1, LABEL_HORIZON + 1): | |
| c = close[i + j] | |
| if c >= upper: | |
| label = 1; break | |
| if c <= lower: | |
| label = -1; break | |
| labels[i] = label | |
| return labels | |
| def build_baseline_labels(close: np.ndarray) -> np.ndarray: | |
| """Replicate harness build_triple_barrier_labels for fair comparison.""" | |
| n = len(close) | |
| log_ret = np.diff(np.log(close + 1e-9)) | |
| labels = np.full(n, np.nan) | |
| for i in range(n - LABEL_HORIZON): | |
| 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 * 1.0) | |
| lower = close[i] * (1.0 - vol * 1.0) | |
| label = 0 | |
| for j in range(1, LABEL_HORIZON + 1): | |
| c = close[i + j] | |
| if c >= upper: label = 1; break | |
| if c <= lower: label = -1; break | |
| labels[i] = label | |
| return labels | |
| def label_stats(labels: np.ndarray) -> str: | |
| valid = labels[~np.isnan(labels)].astype(int) | |
| n = len(valid) | |
| if n == 0: | |
| return "empty" | |
| up = (valid == 1).sum() | |
| dn = (valid == -1).sum() | |
| ho = (valid == 0).sum() | |
| return f"UP={up/n:.1%} DN={dn/n:.1%} HOLD={ho/n:.1%}" | |
| def main(): | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--extended", action="store_true") | |
| args = parser.parse_args() | |
| stocks = EXTENDED_STOCKS if args.extended else DEFAULT_STOCKS | |
| tag = "12-stock" if args.extended else "5-stock" | |
| suffix = "_12stock" if args.extended else "" | |
| print(f"\n=== C19: Adaptive refined labeling [{tag}] ===") | |
| print(f" Variants: {VARIANTS}\n") | |
| def _avg(results, key): | |
| vals = [m[key] for m in results if m and not np.isnan(m.get(key, float("nan")))] | |
| return round(float(np.mean(vals)), 1) if vals else float("nan") | |
| # Run all variants | |
| variant_results = {v: [] for v in VARIANTS} | |
| base_results = [] | |
| per_stock = {} | |
| for stock_no in stocks: | |
| print(f" {stock_no}...", end=" ", flush=True) | |
| df = fetch_df(stock_no) | |
| if df is None or df.empty: | |
| print("no data"); continue | |
| feat = _build_features(df) | |
| close = (df.set_index("date")["close"] if "date" in df.columns else df["close"]).values | |
| base_labels = build_baseline_labels(close) | |
| m_base = walk_forward(feat, base_labels, CURRENT_FEATURES) | |
| base_results.append(m_base) | |
| per_stock[stock_no] = {"baseline": m_base, "variants": {}} | |
| variant_lines = [] | |
| for (lr, hr) in VARIANTS: | |
| adap_labels = build_adaptive_labels(close, lr, hr) | |
| m = walk_forward(feat, adap_labels, CURRENT_FEATURES) | |
| variant_results[(lr, hr)].append(m) | |
| per_stock[stock_no]["variants"][f"{lr},{hr}"] = m | |
| d = m.get("dir_accuracy", float("nan")) | |
| u = m.get("up_precision", float("nan")) | |
| variant_lines.append(f"({lr},{hr}) dir={d}% ↑prec={u}%") | |
| b_dir = m_base.get("dir_accuracy", float("nan")) | |
| b_up = m_base.get("up_precision", float("nan")) | |
| print(f"base dir={b_dir}% ↑prec={b_up}% → " + " | ".join(variant_lines)) | |
| print() | |
| base_agg = { | |
| "dir_accuracy": _avg(base_results, "dir_accuracy"), | |
| "up_precision": _avg(base_results, "up_precision"), | |
| } | |
| print(f" Baseline avg: dir={base_agg['dir_accuracy']}% ↑prec={base_agg['up_precision']}%") | |
| best_variant = None | |
| best_agg = None | |
| best_score = -999 | |
| for (lr, hr), results in variant_results.items(): | |
| agg = { | |
| "dir_accuracy": _avg(results, "dir_accuracy"), | |
| "up_precision": _avg(results, "up_precision"), | |
| } | |
| passed = agg["dir_accuracy"] >= PASS_DIR_ACC and agg["up_precision"] >= PASS_UP_PREC | |
| flag = "PASS ✓" if passed else "FAIL ✗" | |
| print(f" ({lr},{hr}) avg: dir={agg['dir_accuracy']}% ↑prec={agg['up_precision']}% {flag}") | |
| # Pick best by sum of metrics, only if dir passes gate | |
| score = agg["up_precision"] if agg["dir_accuracy"] >= PASS_DIR_ACC else agg["up_precision"] - 10 | |
| if score > best_score: | |
| best_score = score | |
| best_variant = (lr, hr) | |
| best_agg = agg | |
| passed = best_agg["dir_accuracy"] >= PASS_DIR_ACC and best_agg["up_precision"] >= PASS_UP_PREC | |
| print(f"\n Best variant: low_ratio={best_variant[0]}, high_ratio={best_variant[1]}") | |
| print(f" Gate (dir≥{PASS_DIR_ACC}% AND ↑prec≥{PASS_UP_PREC}%): {'PASS ✓' if passed else 'FAIL ✗'}") | |
| result = { | |
| "experiment": "C19", | |
| "description": "Adaptive triple-barrier labeling (regime-conditional pt_sl)", | |
| "variants": [{"low_ratio": lr, "high_ratio": hr} for (lr, hr) in VARIANTS], | |
| "best_variant": {"low_ratio": best_variant[0], "high_ratio": best_variant[1]}, | |
| "stocks": stocks, | |
| "aggregate": {"baseline": base_agg, "best_c19": best_agg}, | |
| "all_variants": { | |
| f"{lr},{hr}": { | |
| "dir_accuracy": _avg(variant_results[(lr, hr)], "dir_accuracy"), | |
| "up_precision": _avg(variant_results[(lr, hr)], "up_precision"), | |
| } | |
| for (lr, hr) in VARIANTS | |
| }, | |
| "passed": passed, | |
| "pass_gate": {"dir_accuracy": PASS_DIR_ACC, "up_precision": PASS_UP_PREC}, | |
| "per_stock": per_stock, | |
| } | |
| out = ROOT / f"docs/c19_result{suffix}.json" | |
| out.parent.mkdir(exist_ok=True) | |
| out.write_text(json.dumps(result, indent=2)) | |
| print(f"\n Saved: {out}") | |
| return 0 if passed else 1 | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |