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()