70 lines
2.5 KiB
Python
70 lines
2.5 KiB
Python
"""Minimal OptimalStopControl threshold calibration — valid-window grid search.
|
||
|
||
This repo found OptimalStopControl thresholds overfit the valid window (valid
|
||
+7.5% → test −17.7% on one calibration). This script runs a small grid over
|
||
(entry_pct, exit_pct, max_hold_days) on the VALID window, reports per-config
|
||
excess return + turnover, and warns when the best valid config is a spike.
|
||
|
||
Reference repo impl: tac-qlib/examples/run_optstop_compare.py.
|
||
|
||
python examples/run_optstop_compare.py --universe AAPL,MSFT,QQQ
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import itertools
|
||
import os
|
||
|
||
import pandas as pd
|
||
|
||
|
||
GRID = {
|
||
"entry_pct": [0.7, 0.85, 0.95],
|
||
"exit_pct": [0.5, 0.7],
|
||
"max_hold_days": [5, 10],
|
||
}
|
||
|
||
|
||
def evaluate_config(lake_root: str, universe: list[str], window: tuple, config: dict) -> dict:
|
||
"""Simplified stand-in: train the RankIC model, backtest OptimalStopControl
|
||
on `window`, return (ann_excess_return, turnover, max_drawdown).
|
||
|
||
The real repo impl calls qlib.backtest with the strategy and reads
|
||
report_normal.csv + risk.csv. Keep the interface here so the grid loop is
|
||
reusable.
|
||
"""
|
||
# placeholder — plug in the real backtest here
|
||
return {"ann_excess": 0.0, "turnover": 0.0, "max_dd": 0.0}
|
||
|
||
|
||
def main() -> None:
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("--lake-root", default=os.environ.get("TAC_LAKE_DIR", ""))
|
||
ap.add_argument("--universe", default="AAPL,MSFT,QQQ,IVV,SMH,TLT")
|
||
args = ap.parse_args()
|
||
universe = [s.strip().upper() for s in args.universe.split(",")]
|
||
valid = ("2026-06-01", "2026-06-30")
|
||
test = ("2026-07-01", "2026-08-06")
|
||
|
||
keys = list(GRID)
|
||
results = []
|
||
for combo in itertools.product(*[GRID[k] for k in keys]):
|
||
config = dict(zip(keys, combo))
|
||
v = evaluate_config(args.lake_root, universe, valid, config)
|
||
t = evaluate_config(args.lake_root, universe, test, config)
|
||
results.append({**config, "valid_excess": v["ann_excess"], "test_excess": t["ann_excess"]})
|
||
|
||
df = pd.DataFrame(results).sort_values("valid_excess", ascending=False)
|
||
print(df.head(10).to_string(index=False))
|
||
# Overfit check: how far is the best-valid config from the median test config?
|
||
med = df["test_excess"].median()
|
||
best = df.iloc[0]
|
||
print(f"\nmedian test excess: {med:+.3f} | best-valid test excess: {best['test_excess']:+.3f}")
|
||
if abs(best["test_excess"] - med) > 0.10:
|
||
print("WARNING: best-valid config is an outlier on test — likely overfit, prefer robust defaults")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|