Spaces:
Running
Running
| #!/usr/bin/env python3 | |
| """C10: grid-search asymmetric triple barrier pt_ratio to maximize UP precision.""" | |
| 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 scripts.improvement_harness import ( | |
| DEFAULT_STOCKS, BASELINE_FEATURES, LABEL_HORIZON, VOL_LOOKBACK, | |
| RF_PARAMS, fetch_df, walk_forward, | |
| ) | |
| from models.predictor import _build_features | |
| PT_RATIOS = [1.0, 1.5, 2.0, 2.5, 3.0] | |
| SL_RATIO = 1.0 | |
| def build_asymmetric_labels(close, pt_ratio=1.0, sl_ratio=1.0, | |
| horizon=5, vol_lookback=20): | |
| """pt_ratio: multiplier for take-profit barrier (UP label) | |
| sl_ratio: multiplier for stop-loss barrier (DOWN label) | |
| """ | |
| n = len(close) | |
| log_ret = np.diff(np.log(close + 1e-9)) | |
| labels = np.full(n, np.nan) | |
| for i in range(n - 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 * pt_ratio) | |
| lower = close[i] * (1.0 - vol * sl_ratio) | |
| label = 0 | |
| for j in range(1, horizon + 1): | |
| c = close[i + j] | |
| if c >= upper: label = 1; break | |
| if c <= lower: label = -1; break | |
| labels[i] = label | |
| return labels | |
| 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(): | |
| # Pre-fetch all stock data once | |
| print("Fetching stock data...") | |
| stock_data = {} | |
| for stock_no in DEFAULT_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 | |
| stock_data[stock_no] = (feat, close) | |
| print("ok") | |
| grid_results = {} | |
| hdr = f"{'pt_ratio':>8} {'stock':>6} {'dir%':>5} {'↑prec%':>7} {'signals':>7} {'n_pred':>7}" | |
| print(f"\n{hdr}\n{'-'*len(hdr)}") | |
| for pt_ratio in PT_RATIOS: | |
| ratio_key = str(pt_ratio) | |
| per_stock_results = {} | |
| rows = [] | |
| for stock_no, (feat, close) in stock_data.items(): | |
| labels = build_asymmetric_labels( | |
| close, pt_ratio=pt_ratio, sl_ratio=SL_RATIO, | |
| horizon=LABEL_HORIZON, vol_lookback=VOL_LOOKBACK, | |
| ) | |
| r = walk_forward(feat, labels, BASELINE_FEATURES) | |
| per_stock_results[stock_no] = r | |
| if r: | |
| rows.append(r) | |
| print(f"{pt_ratio:>8.1f} {stock_no:>6} {r['dir_accuracy']:>5.1f} " | |
| f"{r['up_precision']:>7.1f} {r.get('n_signals',0):>7} " | |
| f"{r.get('n_predictions',0):>7}") | |
| agg = {k: _mean(rows, k) for k in ("dir_accuracy", "up_precision", "n_signals", "n_predictions")} | |
| grid_results[ratio_key] = {"results": per_stock_results, "aggregate": agg} | |
| print(f" {'→ avg':>14} {agg['dir_accuracy']:>5.1f} {agg['up_precision']:>7.1f} " | |
| f"{agg['n_signals']:>7}\n") | |
| # Aggregate table | |
| print("=== AGGREGATE BY PT_RATIO ===") | |
| agg_hdr = f"{'pt_ratio':>8} {'avg_dir%':>8} {'avg_↑prec%':>10} {'avg_signals':>11}" | |
| print(f"{agg_hdr}\n{'-'*len(agg_hdr)}") | |
| for pt_ratio in PT_RATIOS: | |
| a = grid_results[str(pt_ratio)]["aggregate"] | |
| print(f"{pt_ratio:>8.1f} {a['dir_accuracy']:>8.1f} {a['up_precision']:>10.1f} {a['n_signals']:>11.1f}") | |
| # Best: highest up_precision where avg_signals >= 15 | |
| baseline_agg = grid_results["1.0"]["aggregate"] | |
| best_ratio = None | |
| best_up_prec = -1.0 | |
| for pt_ratio in PT_RATIOS: | |
| a = grid_results[str(pt_ratio)]["aggregate"] | |
| if a["n_signals"] >= 15 and a["up_precision"] > best_up_prec: | |
| best_up_prec = a["up_precision"] | |
| best_ratio = pt_ratio | |
| best_agg = grid_results[str(best_ratio)]["aggregate"] if best_ratio is not None else {} | |
| dir_regression = baseline_agg["dir_accuracy"] - best_agg.get("dir_accuracy", 0) if best_ratio else 999.0 | |
| passed = ( | |
| best_ratio is not None | |
| and best_up_prec >= 57.0 | |
| and best_agg.get("n_signals", 0) >= 15 | |
| and dir_regression <= 3.0 | |
| ) | |
| output = { | |
| "grid": grid_results, | |
| "best_pt_ratio": best_ratio, | |
| "best_aggregate": best_agg, | |
| "baseline_aggregate": baseline_agg, | |
| "dir_regression_vs_baseline": round(dir_regression, 1) if best_ratio else None, | |
| "passed": passed, | |
| "pass_criterion": "up_precision >= 57.0 AND avg_signals >= 15 AND dir_regression <= 3.0pp", | |
| } | |
| def _clean(obj): | |
| if isinstance(obj, dict): | |
| return {k: _clean(v) for k, v in obj.items()} | |
| if isinstance(obj, list): | |
| return [_clean(v) for v in obj] | |
| if isinstance(obj, float) and (np.isnan(obj) or np.isinf(obj)): | |
| return None | |
| if isinstance(obj, (np.integer,)): | |
| return int(obj) | |
| if isinstance(obj, (np.floating,)): | |
| return None if np.isnan(obj) else float(obj) | |
| if isinstance(obj, (np.bool_,)): | |
| return bool(obj) | |
| if hasattr(obj, 'item'): # any remaining numpy scalar | |
| return obj.item() | |
| return obj | |
| out_path = ROOT / "docs" / "c10_barrier_result.json" | |
| with open(out_path, "w") as f: | |
| json.dump(_clean(output), f, indent=2) | |
| print(f"\n=== RESULT ===") | |
| print(f" best_pt_ratio={best_ratio} up_precision={best_up_prec} " | |
| f"n_signals={best_agg.get('n_signals','?')} dir_regression={round(dir_regression,1) if best_ratio else 'N/A'}") | |
| print(f" PASSED: {passed}") | |
| print(f" Output: {out_path}") | |
| if __name__ == "__main__": | |
| main() | |