Full 6-stream adaptive book live paper-forward: SPY/IEF/GLD/PDBC/DBMF + BTC-USD via Yahoo, with edge-decay trust allocation + adaptive risk layer (EMA vol/Kelly-floor/self-recovering DD/z-score corr), unlevered. status/run/weights modes, forward-start, idempotent, catch-up. Cron 12/15/18 UTC. Live weights sensible: trust down-weights bond(7%)+crypto(2%), gold/commod/trend healthy(~23%). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
145 lines
6.5 KiB
Python
145 lines
6.5 KiB
Python
#!/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()
|