Spaces:
Running
Running
File size: 5,750 Bytes
845615a 15836c8 845615a 15836c8 845615a 15836c8 845615a 15836c8 845615a 15836c8 845615a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | """
Cross-stock parameter study — find the lookback/T range that generalises.
Instead of tuning per ticker in isolation, this runs the walk-forward validation across a
whole basket and aggregates: which lookback consistently gives the lowest MAPE while still
beating a random walk and staying in-distribution? The answer becomes the *global default*
for tickers that don't have their own profile yet.
Because the per-(ticker, config) walk-forward runs ARE the profile evidence, this also
writes each ticker's trust profile in the same pass — one expensive sweep, two outputs:
* us/out/study.csv (every ticker x config row, for analysis)
* us/out/profiles/<TICKER>_1d.json (per-ticker recommended config + trust card)
Example
-------
python us/study.py --pred_len 20 --n_windows 5 --n_paths 10 --lookbacks 60,120,180,240
"""
import argparse
import sys
import time
from collections import Counter
from pathlib import Path
import numpy as np
import pandas as pd
from predict_us import fetch_ohlcv
from engine import get_predictor, _RANGE_THRESH
from autoconfig import run_ticker, profile_path
# Diverse basket: mega-cap tech, ETFs, high-vol names, and non-tech sectors.
DEFAULT_BASKET = ["AAPL", "MSFT", "NVDA", "GOOGL", "AMZN", "META", "TSLA", "AMD",
"SPY", "QQQ", "JPM", "XOM", "WMT", "KO"]
def main():
try:
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
ap = argparse.ArgumentParser()
ap.add_argument("--tickers", default=",".join(DEFAULT_BASKET))
ap.add_argument("--interval", default="1d")
ap.add_argument("--period", default="5y")
ap.add_argument("--pred_len", type=int, default=20)
ap.add_argument("--n_windows", type=int, default=8)
ap.add_argument("--n_outer", type=int, default=3)
ap.add_argument("--n_paths", type=int, default=10)
ap.add_argument("--lookbacks", default="60,120,180,240")
ap.add_argument("--temps", default="0.7")
ap.add_argument("--top_p", type=float, default=0.9)
ap.add_argument("--target_coverage", type=float, default=0.8)
ap.add_argument("--objective", default="blend")
ap.add_argument("--seed", type=int, default=123)
args = ap.parse_args()
tickers = [t.strip().upper() for t in args.tickers.split(",") if t.strip()]
lookbacks = [int(x) for x in args.lookbacks.split(",") if x.strip()]
temps = [float(x) for x in args.temps.split(",") if x.strip()]
predictor = get_predictor()
outdir = Path(__file__).resolve().parent / "out"
prof_dir = outdir / "profiles"; prof_dir.mkdir(parents=True, exist_ok=True)
n_cfg = len(lookbacks) * len(temps)
n_pred = len(tickers) * n_cfg * args.n_windows * args.n_paths
print(f"[study] {len(tickers)} tickers x {n_cfg} configs x {args.n_windows} win "
f"x {args.n_paths} paths ≈ {n_pred} predicts (nested {args.n_windows-args.n_outer}/{args.n_outer})\n")
rows = []
best_lb, best_T, grades = [], [], []
t0 = time.time()
for tk in tickers:
try:
df = fetch_ohlcv(tk, args.interval, args.period)
prof = run_ticker(predictor, df, tk, args.interval, args.period, args.pred_len,
lookbacks, temps, args.top_p, args.n_paths, args.n_windows,
args.n_outer, args.target_coverage, args.objective, args.seed)
except Exception as e:
print(f"[skip] {tk}: {e}")
continue
profile_path(prof_dir, tk, args.interval).write_text(
__import__("json").dumps(prof, indent=2, ensure_ascii=False), encoding="utf-8")
rec = prof["recommended"]; tr = prof["track_record"]; v = prof["verdict"]
best_lb.append(rec["lookback"]); best_T.append(rec["T"]); grades.append(v["grade"])
for c in prof["all_configs"]: # inner (selection) metrics per config, for the range study
rows.append({"ticker": tk, **c})
print(f"[{tk:5}] best lb={rec['lookback']:>3} T={rec['T']} grade {v['grade']} | "
f"HONEST(outer): MAPE={tr['MAPE%']}% skill={tr['skill_vs_rw']} "
f"IC={tr['IC']} Dir={tr['DirAcc%']}%")
# ---- write raw table ----
df_rows = pd.DataFrame(rows)
csv = outdir / "study.csv"
df_rows.to_csv(csv, index=False)
# ---- aggregate: which lookback generalises? ----
print("\n" + "=" * 64)
print("CROSS-STOCK SUMMARY")
print("=" * 64)
if not df_rows.empty:
ind = df_rows[df_rows["in_distribution"]]
print("\nMean MAPE by lookback (in-distribution only):")
by_lb = ind.groupby("lookback").agg(
MAPE_mean=("MAPE%", "mean"), MAPE_median=("MAPE%", "median"),
skill_mean=("skill_vs_rw", "mean"), n=("MAPE%", "size")).round(3)
print(by_lb.to_string())
# per-ticker best lookback by MAPE (in-dist)
print("\nPer-ticker best lookback (min MAPE, in-distribution):")
picks = (ind.sort_values("MAPE%").groupby("ticker").first()
.reset_index()[["ticker", "lookback", "T", "MAPE%", "skill_vs_rw"]])
print(picks.to_string(index=False))
lb_counts = Counter(picks["lookback"].tolist())
print("\nBest-lookback distribution:", dict(sorted(lb_counts.items())))
winners = ind.groupby("lookback")["MAPE%"].mean().sort_values()
print(f"Lowest mean-MAPE lookback: {int(winners.index[0])} "
f"(MAPE {winners.iloc[0]:.2f}%)")
print(f"\ngrades: {dict(Counter(grades))}")
print(f"[done] {time.time()-t0:.0f}s table -> {csv} profiles -> {prof_dir}")
if __name__ == "__main__":
main()
|