DockerSpace / scripts /backtest_c19.py
DennisChan0909's picture
Add C19 adaptive triple-barrier labeling (PASSED 5-stock and 12-stock)
960fb5b
Raw
History Blame Contribute Delete
7.62 kB
#!/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())