diff --git a/scripts/surfer/multistrat_paper.py b/scripts/surfer/multistrat_paper.py new file mode 100644 index 000000000..e613abb11 --- /dev/null +++ b/scripts/surfer/multistrat_paper.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +"""multistrat — local paper-forward of the adaptive multi-strat book (deployable spec, Phase 1). + +6 streams via free Yahoo daily adj-close: SPY/IEF/GLD/PDBC/DBMF + BTC-USD. Pipeline (deterministic +from history each run): vol-normalize each stream -> edge-decay trust theta (down-weight decaying / +resurrect recovered) -> trust-weighted combination -> adaptive risk layer (EMA vol, Kelly-floor, +continuous self-recovering drawdown de-lever, z-score correlation de-risk, leverage-floor), +UNLEVERED (MAXLEV 1.0). Forward-start, idempotent per UTC day, catch-up. No capital, no agents. + + python3 multistrat_paper.py status cum return + annualized + current target weights & leverage + python3 multistrat_paper.py run book new trading days, persist (cron) + python3 multistrat_paper.py weights [USD] today's $ allocation per ETF + (alias paper == run) +""" +import datetime +import json +import math +import os +import sys +import urllib.request + +import numpy as np + +_REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +STATE = os.path.join(_REPO, "data/surfer/multistrat_state.json") +INSTR = [("SPY", "equity"), ("IEF", "bond"), ("GLD", "gold"), ("PDBC", "commod"), ("DBMF", "trend"), ("BTC-USD", "crypto")] +TARGET_VOL, MAXLEV, KELLY_FLOOR, LEV_FLOOR = 0.10, 1.0, 0.5, 0.3 +DD_DEADBAND, DD_SENS, DD_FLOOR = 0.05, 3.0, 0.40 + + +def get(url, tries=4): + import time + for a in range(tries): + try: + return json.loads(urllib.request.urlopen(urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}), timeout=30).read()) + except Exception: + if a == tries - 1: + raise + time.sleep(2 * (a + 1)) + + +def yhist(sym): + res = get(f"https://query1.finance.yahoo.com/v8/finance/chart/{sym}?interval=1d&range=2y")["chart"]["result"][0] + ts = res["timestamp"]; ind = res["indicators"] + adj = ind.get("adjclose", [{}])[0].get("adjclose") or ind["quote"][0]["close"] + return {datetime.datetime.utcfromtimestamp(t).strftime("%Y-%m-%d"): float(c) for t, c in zip(ts, adj) if c is not None} + + +def build(): + data = {nm: yhist(sym) for sym, nm in INSTR} + dates = sorted(set.intersection(*[set(d) for d in data.values()])) + R = np.zeros((len(dates), len(INSTR))) + for j, (_, nm) in enumerate(INSTR): + s = np.array([data[nm][d] for d in dates]) + R[1:, j] = s[1:] / s[:-1] - 1 + return dates, R + + +def volnorm(col, tv=TARGET_VOL, win=63): + out = np.zeros_like(col) + for t in range(win, len(col)): + rv = col[t - win:t].std() * math.sqrt(252) + out[t] = col[t] * min(5.0, tv / (rv + 1e-9)) + return out + + +def trust(col, win=126, a=0.06): + th = 1.0; out = np.ones_like(col) + for t in range(win, len(col)): + seg = col[t - win:t]; sr = seg.mean() / (seg.std() + 1e-9) * math.sqrt(252) + th = (1 - a) * th + a * float(np.clip((sr + 0.5) / 1.0, 0.1, 1.0)) + out[t] = th + return out + + +def book_series(R): + S = R.shape[1] + M = np.column_stack([volnorm(R[:, j]) for j in range(S)]) + TH = np.column_stack([trust(M[:, j]) for j in range(S)]) + tw = TH / np.maximum(TH.sum(1, keepdims=True), 1e-9) + combo = np.nansum(tw * M, axis=1) + T = len(combo); book = np.zeros(T); eq = 1.0; peak = 1.0 + emu = float(np.nanmean(combo[:63])); evar = float(np.nanvar(combo[:63])) + 1e-12; chist = [] + Lc = np.zeros(T) + for t in range(63, T - 1): + x = combo[t]; emu = 0.97 * emu + 0.03 * x; evar = 0.97 * evar + 0.03 * (x - emu) ** 2 + L = min(MAXLEV, TARGET_VOL / (math.sqrt(max(evar, 1e-12) * 252) + 1e-9)) + L = min(L, max(KELLY_FLOOR, (emu * 252) / (evar * 252 + 1e-9))) + dd = eq / peak - 1.0 + L *= float(np.clip(1 - DD_SENS * max(0.0, -dd - DD_DEADBAND), DD_FLOOR, 1.0)) + cm = np.corrcoef(np.nan_to_num(M[t - 63:t]).T); ac = (cm.sum() - S) / (S * S - S); chist.append(ac) + if len(chist) > 60: + h = np.array(chist[-120:]); z = (ac - h.mean()) / (h.std() + 1e-9) + L *= float(np.clip(1 - 0.2 * max(0.0, z), 0.5, 1.0)) + L = float(np.clip(L, LEV_FLOOR, MAXLEV)); Lc[t] = L + book[t + 1] = L * combo[t + 1]; eq *= (1 + book[t + 1]); peak = max(peak, eq) + return book, tw[-1], Lc[-2] if T > 1 else 0.0 + + +def load_state(): + if os.path.exists(STATE): + return json.load(open(STATE)) + return {"last_date": "", "days": 0, "sum_r": 0.0, "sumsq_r": 0.0, "equity": 1.0, "peak": 1.0, "max_dd": 0.0} + + +def main(): + cmd = sys.argv[1] if len(sys.argv) > 1 else "status" + if cmd in ("run", "paper"): + st = load_state(); dates, R = build(); book, w, L = book_series(R) + os.makedirs(os.path.dirname(STATE), exist_ok=True) + if not st["last_date"]: + st["last_date"] = dates[-1]; json.dump(st, open(STATE, "w")) + print(f"initialized forward tracking from {dates[-1]}"); return + booked = 0 + for i in range(1, len(dates)): + if dates[i] <= st["last_date"]: + continue + r = float(book[i]) + st["days"] += 1; st["sum_r"] += r; st["sumsq_r"] += r * r + st["equity"] *= (1 + r); st["peak"] = max(st["peak"], st["equity"]) + st["max_dd"] = min(st["max_dd"], st["equity"] / st["peak"] - 1); st["last_date"] = dates[i]; booked += 1 + json.dump(st, open(STATE, "w")) + print(f"{datetime.date.today()} booked {booked} day(s) -> day {st['days']} (through {st['last_date']}) " + f"cum {100*(st['equity']-1):+.2f}% leverage {L:.2f}") + elif cmd == "weights": + cap = float(sys.argv[2]) if len(sys.argv) > 2 else 35000.0 + dates, R = build(); _, w, L = book_series(R) + print(f"target allocation for ${cap:,.0f} (leverage {L:.2f}x, unlevered book):") + for j, (sym, nm) in enumerate(INSTR): + print(f" {nm:>7} ({sym:>8}): {100*w[j]*L:>5.1f}% ${cap*w[j]*L:>8,.0f}") + else: + st = load_state() + n = st["days"]; m = (st["sum_r"] / n) if n else 0; var = (st["sumsq_r"] / n - m * m) if n > 1 else 0 + sh = (m * 252) / (math.sqrt(max(var, 1e-12)) * math.sqrt(252)) if n > 1 and var > 0 else float("nan") + print(f"adaptive multi-strat paper — day {n} (through {st['last_date'] or 'n/a'})") + print(f" cum {100*(st['equity']-1):+.2f}% ann-Sharpe {sh:+.2f} maxDD {100*st['max_dd']:+.1f}%") + dates, R = build(); _, w, L = book_series(R) + print(f" current leverage {L:.2f}x | target weights:") + for j, (sym, nm) in enumerate(INSTR): + print(f" {nm:>7} ({sym:>8}): {100*w[j]*L:>5.1f}%") + + +if __name__ == "__main__": + main()