#!/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()])) # drop today's INCOMPLETE intraday bar if present — this is a daily-CLOSE strategy; an in-session # partial bar spikes the vol estimate and corrupts the leverage (e.g. halves it). Use completed closes. today = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d") if dates and dates[-1] == today: dates = dates[:-1] 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)) sub = np.nan_to_num(M[t - 63:t]); vmask = sub.std(0) > 1e-12 # only streams that vary (avoid /0 in corrcoef -> NaN) if int(vmask.sum()) >= 2: k = int(vmask.sum()); cm = np.corrcoef(sub[:, vmask].T); ac = float((np.nansum(cm) - k) / (k * k - k)) else: ac = 0.0 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()