DockerSpace / scripts /backtest_c10.py
DennisChan0909's picture
feat(C10): asymmetric barrier grid search; FAILED
699a65e
Raw
History Blame Contribute Delete
6.19 kB
#!/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()