""" Can a $10k-$100k account lever the overnight book toward 50% a year (A), and do S&P 500 puts make that leverage safer (B)? PYTHONPATH=. uv run python -m research.lab.option_overlay fetch # once: Cboe indices + S&P 500 TR PYTHONPATH=. uv run python -m research.lab.option_overlay # v2: puts held to expiry PYTHONPATH=. uv run python -m research.lab.option_overlay --hedge diff # v1 (kept for the record) Book: three_sleeve_overnight (research/lab/overnight_book.py books.pkl, the paper-tested book: overnight momentum + lowrisk + trend_v7, equal shares), daily 2006-10-02..2026-09-24, 1x, costs inside. Reported: three_sleeve_overnight_12_1, two_sleeve_overnight_trend. Account rules (US retail Reg T margin account; portfolio margin needs $110k+ and is out of scope): stocks initial margin 50% (Reg T), so 2x 3x ETFs 75% (FINRA Notice 09-53: 3x the 25% maintenance). The trend sleeve holds them, so levering the whole book at L needs 0.5 x stock share x L + 0.75 x 3x-ETF share x L <= 1; equal thirds give L <= 1.71 margin loan T-bill + 1.5% (IBKR Pro, first $100k); reported + 2.5% (IBKR Lite) MES futures $5 x S&P 500 = $38.5k a contract (S&P 7,701, 2026-09-28); margin 8% of notional, taken from the same equity SPY/XSP options 100 x $778 = $77.8k a contract; MES options $38.5k whole contracts today's notional / account size is the exposure step: MES 3.85 / 1.54 / 0.77 / 0.385 of equity at $10k / $25k / $50k / $100k, SPY options 7.78 / 3.11 / 1.56 / 0.78 Routes (A), each on a grid of book leverage L (step 0.05) and index exposure k (whole contracts, k <= 3): A0 reference L x book at T-bill + 1%, no cap (overnight_book's line; what portfolio margin would allow) A1 Reg T L x book up to the margin cap, T-bill + 1.5% A2 Reg T + box A1 financed with SPX/XSP box spreads at T-bill + 0.5%. Reported only: sources say a short box belongs in a portfolio margin account; its Reg T treatment is unverified A3 Reg T + MES A1 plus k S&P 500 futures: S&P 500 TR minus T-bill minus 0.1%/yr; margin 0.08 k joins the cap A4 calls A3 with the futures replaced by 90%-strike SPY calls rolled quarterly (call = forward + put): futures + the put_10q hedge on k, financing T-bill + 0.5%, 11% of notional paid in cash, steps of 0.9 x the SPY-option notional Hedges (B), added to A1 and A3: puts on c = (L - 1) + k of S&P 500 notional (the borrowed exposure), rounded to whole contracts (c can round to 0): put_10q (gated) 10% OTM quarterly SPX puts: Cboe PPUT3M minus the S&P 500 TR (real SPX option prices: VWAP purchase, mid marks) put_5m 5% OTM monthly: Cboe PPUT minus the S&P 500 TR (neighbour) vix_tail Cboe VXTH minus the S&P 500 TR (reported; VIX calls are small contracts, so c is not rounded) retail cost buying at the ask instead of the VWAP: 3% of the premium on SPY/XSP, 10% on MES options, on an assumed premium of 1.0% (put_10q) / 0.8% (put_5m) of notional per roll Account sizes: $10k, $25k, $50k, $100k. Metrics per route x account size x hedge: M35 highest CAGR among configs with MaxDD >= -35% D50 among configs with CAGR >= 50%, the shallowest MaxDD (or none) halves (2006-10..2016-12, 2017-01..2026-09) CAGR, MaxDD and Calmar at the M35 config Gates (fixed before the Cboe hedge series were computed on): A a route is viable at an account size if D50 exists with MaxDD >= -40% (A0 is the reference and A2 is flagged, so neither counts) B PASS if at $100k on A3 with SPY/XSP contracts: put_10q M35 >= unhedged M35 + 2pp, put_5m M35 >= unhedged M35, and put_10q's M35 config has Calmar >= the unhedged M35 config's in both halves Decision: A viable at a size -> write the live spec for the best viable route there (paper first); none -> 50%/yr needs portfolio margin or a larger account; report M35 per size B PASS -> hedge spec (contract, strike, roll); FAIL -> no option hedge, cap leverage by drawdown Data status: the book returns and their uniform-leverage table (overnight_book, T-bill + 1%) were seen before this rule. The Cboe PPUT, PPUT3M and VXTH files were downloaded, and only their first and last rows were printed. The Cboe PUT (put-write), QYLD/XYLD/TAIL/SWAN/JEPI numbers were looked at the same day; put selling is not a hedge and is not tested here. Result (2026-10-12, v2): A: no route viable at any account size. B: FAIL (B1 and B3 fail; B2 passes). three_sleeve_overnight, 2006-10..2026-09. M35 is the best CAGR with MaxDD >= -35%; "-" means no contract fits. route $10k $25k $50k $100k config at $100k A0 reference (no cap, portfolio margin) 47.0% 47.0% 47.0% 47.0% L 2.20, MaxDD -34.6%; 50% needs L 2.35 (-36.8%) A1 Reg T margin (T-bill + 1.5%) 36.2% 36.2% 36.2% 36.2% L 1.70 (cap), MaxDD -27.3% A2 + box financing (Reg T unverified) 37.1% 37.1% 37.1% 37.1% L 1.70, MaxDD -27.0% A3 + MES futures 36.2% 36.2% 36.2% 36.2% best k = 0 A3 + MES + 10% OTM quarterly puts (SPY) - 35.6% 37.2% 37.2% L 1.60, k 0.77, puts 1.56, MaxDD -31.3% A3 + MES + 5% OTM monthly puts (SPY) - - 36.3% 36.3% L 1.65, k 0.39, puts 0.78, MaxDD -26.2% A1 + 10% OTM quarterly puts (SPY) - - - 33.6% L 1.70, puts 0.78, MaxDD -23.5% A4 90%-strike calls - - - 31.8% L 1.25, k 0.70, MaxDD -35.0% reported, $100k: three_sleeve_overnight_12_1 A1 40.9% (-25.8%), A3 + 5% puts 43.1%; two_sleeve_overnight_trend A1 45.8% (-32.7%), A3 reaches 50% only at MaxDD -52.5% - The ceiling is Reg T, not the drawdown budget. 3x ETFs need 75% and stocks 50%, so equal thirds cap L at 1.71. At the cap the book makes 36%/yr with MaxDD -27%, and options and futures do not lift that cap. - Futures add S&P 500 beta, not the book's alpha. Inside -35% they add nothing to the primary book (best k = 0) and +0.5pp to the 12_1 book. - The puts cost 2.0%/yr (10% OTM quarterly) and 2.5%/yr (5% OTM monthly) per unit of notional, against the S&P 500 TR. On A1, puts on the borrowed 0.78 cut MaxDD from -27.3% to -23.5% for -2.6pp of CAGR. Paired with futures, they buy +1.0pp (37.2%) at MaxDD -31.3%; the gate asked +2pp. The second half of that config is better and the first half worse (Calmar fails in 2006-16). - Small accounts: - At $10k nothing fits: one MES is 3.85x the account and one SPY option 7.8x. - At $25k one MES is 1.54x, and the nearest SPY put lot over-hedges by 2x. - v1 (option_overlay/results.json, B FAIL) built the hedge as the daily difference PPUT3M - S&P 500 TR. That holds the puts plus a short S&P position as large as the put's value, so it understated the 2008 payoff (+4.2% vs the index spread's +11.4pp). v2 holds the puts to expiry, reconstructed from the Cboe index (yearly rebuild error: median 0.30pp, 2008 +2.9pp in the hedge's favour). The A verdict does not depend on the hedge model. - Decision: - No options or futures in the live books. - Under Reg T, a $10k-$100k account gets about 36%/yr from three_sleeve_overnight and 41-46% from the 12_1 and two-sleeve variants. These are backtest numbers, before any live haircut. - 50% needs portfolio margin ($110k+ at IBKR, NEEDS EVIDENCE on current terms) or a different book mix: levering only the stock sleeves is not tested here. Boundary: k and c are held as constant fractions of equity (daily rebalanced); real contracts drift between rolls. The margin cap uses the book's base shares; the band blend drifts them by up to 25%. The overnight sleeve's 1 bp a side is not yet measured (overnight_paper.py). FINRA counts selling an overnight position and rebuying it the same day as no day trade, but a broker's matching can differ (PDT under $25k). """ from __future__ import annotations import argparse import json import os import numpy as np import pandas as pd from research.lab.factor_scan import RESULTS from research.lab.levered_book import stats, tbill OUT = os.path.join(RESULTS, "option_overlay") RAW = os.path.join("data", "research", "option_overlay") BOOKS = os.path.join(RESULTS, "overnight_book", "books.pkl") SPAN = ("2006-10-02", "2026-09-24") HALVES = [("2006-10-02", "2016-12-30"), ("2017-01-03", "2026-09-24")] PRIMARY = "three_sleeve_overnight" REPORTED_BOOKS = ["three_sleeve_overnight_12_1", "two_sleeve_overnight_trend"] LETF_SHARE = {"three_sleeve_overnight": 1 / 3, "three_sleeve_overnight_12_1": 1 / 3, "two_sleeve_overnight_trend": 1 / 2} ACCOUNTS = [10_000, 25_000, 50_000, 100_000] SPX_LEVEL, SPY_PRICE = 7701.08, 778.10 MES_NOTIONAL = 5 * SPX_LEVEL SPY_OPT_NOTIONAL = 100 * SPY_PRICE M_STOCK, M_LETF, M_FUT = 0.50, 0.75, 0.08 REF_SPREAD, MARGIN_SPREAD, MARGIN_SPREAD_HI, BOX_SPREAD = 0.01, 0.015, 0.025, 0.005 FUT_COST = 0.001 CALL_DELTA, CALL_CASH = 0.9, 0.11 HEDGES = {"put_10q": ("PPUT3M", 4, 0.010), "put_5m": ("PPUT", 12, 0.008), "vix_tail": ("VXTH", 0, 0.0)} ASK_COST = {"spy": 0.03, "mes": 0.10} L_GRID = np.round(np.arange(1.0, 3.0001, 0.05), 2) K_MAX = 3.0 DD_BUDGET, TARGET, DD_VIABLE = -0.35, 0.50, -0.40 CBOE = "https://cdn-api.cboe.com/api/global/us_indices/daily_prices/{}_History.csv" LONG = os.path.join("data", "research", "long_history.pkl") # v2: puts bought at each Cboe roll and held to expiry (source index, strike / S, roll months) STATIC = {"put_10q": ("PPUT3M", 0.90, (3, 6, 9, 12)), "put_5m": ("PPUT", 0.95, None)} PREREG = { "question": "Which leverage route reaches 50%/yr on three_sleeve_overnight in a $10k-$100k Reg T account, " "and do S&P 500 puts on the borrowed exposure raise the CAGR available at MaxDD >= -35%?", "span": SPAN, "halves": HALVES, "book": PRIMARY, "reported_books": REPORTED_BOOKS, "account_sizes": ACCOUNTS, "rules": {"stock_margin": M_STOCK, "letf_3x_margin": M_LETF, "futures_margin": M_FUT, "margin_loan": "T-bill + 1.5% (reported + 2.5%)", "box": "T-bill + 0.5% (reported, Reg T unverified)", "contract_notional": {"MES": MES_NOTIONAL, "SPY_XSP_option": SPY_OPT_NOTIONAL, "MES_option": MES_NOTIONAL}, "futures_cost": "0.1%/yr", "calls": "90% strike quarterly = futures + put_10q, T-bill + 0.5%, " "11% cash, steps 0.9 x SPY-option notional"}, "hedges": {"put_10q (gated)": "Cboe PPUT3M - S&P 500 TR", "put_5m": "Cboe PPUT - S&P 500 TR", "vix_tail (reported)": "Cboe VXTH - S&P 500 TR", "size": "c = (L - 1) + k, whole contracts", "ask_cost": "3% (SPY/XSP) / 10% (MES opt) of a " "1.0% / 0.8% premium per roll"}, "metrics": {"M35": "max CAGR with MaxDD >= -35%", "D50": "shallowest MaxDD with CAGR >= 50%"}, "gates": {"A": "route viable at a size if D50 exists with MaxDD >= -40% (A0 reference, A2 flagged: excluded)", "B": "at $100k on A3 (SPY/XSP): put_10q M35 >= unhedged + 2pp; put_5m M35 >= unhedged; " "put_10q M35 config Calmar >= unhedged M35 config Calmar in both halves"}, "decision": {"A viable": "live spec for the best viable route at that size, paper first", "A none": "50%/yr needs portfolio margin or a larger account; report M35 per size", "B PASS": "hedge spec (contract, strike, roll)", "B FAIL": "no option hedge; cap leverage by drawdown"}, "data_status": "book returns and their T-bill+1% leverage table seen before; Cboe PPUT/PPUT3M/VXTH downloaded, " "first/last rows only; PUT and covered-call/tail ETF summaries seen (not tested here)", } # --------------------------------------------------------------------------- # data # --------------------------------------------------------------------------- def fetch() -> None: import requests import yfinance as yf from research.lab.fetch_us import _proxy_env _proxy_env() os.makedirs(RAW, exist_ok=True) for name in ("PPUT", "PPUT3M", "VXTH"): r = requests.get(CBOE.format(name), headers={"User-Agent": "Mozilla/5.0"}, timeout=60) r.raise_for_status() with open(os.path.join(RAW, f"{name}.csv"), "wb") as fh: fh.write(r.content) print(f"{name}: {len(r.content)} bytes") h = yf.Ticker("^SP500TR").history(period="max", auto_adjust=True)["Close"] h.index = h.index.tz_localize(None).normalize() h.rename("SP500TR").to_csv(os.path.join(RAW, "SP500TR.csv")) print(f"SP500TR: {len(h)} rows") def load_levels(index: pd.DatetimeIndex) -> pd.DataFrame: cols = {} for name in ("PPUT", "PPUT3M", "VXTH"): d = pd.read_csv(os.path.join(RAW, f"{name}.csv")) d.index = pd.to_datetime(d["DATE"], format="%m/%d/%Y") cols[name] = pd.to_numeric(d[name], errors="coerce") s = pd.read_csv(os.path.join(RAW, "SP500TR.csv"), index_col=0, parse_dates=True).iloc[:, 0] cols["SP500TR"] = s lv = pd.DataFrame(cols).sort_index() return lv.reindex(lv.index.union(index)).ffill().reindex(index) def roll_days(index: pd.DatetimeIndex, months) -> pd.DatetimeIndex: """Third Friday of each roll month; on a holiday, the preceding session.""" out = [] for p in pd.period_range(index[0], index[-1], freq="M"): if months and p.month not in months: continue third = pd.date_range(p.start_time, p.end_time, freq="W-FRI")[2] prior = index[(index <= third) & (index >= p.start_time)] if len(prior): out.append(prior[-1]) return pd.DatetimeIndex(out) def static_put(lv: pd.DataFrame, spx_open: pd.Series, spx_close: pd.Series, src: str, mny: float, months) -> tuple[pd.Series, pd.Series, pd.Series]: """The Cboe index's put, held from one roll to the next, per unit of S&P 500 notional. Between rolls a and b the index holds S + P (Cboe methodology 3.1), so with y = I/I_a - T/T_a and s = T/T_a (I the Cboe index, T the S&P 500 TR) the put's value per unit of S_a is x = y (1 + p) + p s, where p = P_a / S_a. p is solved on the session before b from x = max(0, K - S) / S_a, with K = mny x S_a rounded up to 5 points; on b the put settles at max(0, K - open) / S_a. Returns dx (daily change of x inside a period), pid (period number, 0 before the first roll) and the premium p paid at each roll (on the roll day). """ idx = lv.index rolls = roll_days(idx, months) I, T = lv[src], lv["SP500TR"] S, O = spx_close.reindex(idx).ffill(), spx_open.reindex(idx).ffill() dx = pd.Series(0.0, index=idx) pid = pd.Series(0, index=idx) prem = pd.Series(0.0, index=idx) ps = [] bounds = list(zip(rolls[:-1], rolls[1:])) + [(rolls[-1], idx[-1])] for n, (a, b) in enumerate(bounds, start=1): seg = idx[(idx >= a) & (idx <= b)] if len(seg) < 2: continue y = I.loc[seg] / I.loc[a] - T.loc[seg] / T.loc[a] s = T.loc[seg] / T.loc[a] K = np.ceil(mny * S.loc[a] / 5) * 5 complete = b in rolls and len(seg) >= 3 if complete: pre = seg[-2] x_pre = max(0.0, K - S.loc[pre]) / S.loc[a] p = float(np.clip((x_pre - y.loc[pre]) / (y.loc[pre] + s.loc[pre]), 0.0, 0.5)) else: # the last, unfinished period p = float(np.median(ps[-4:])) if ps else 0.01 ps.append(p) x = y * (1 + p) + p * s x.loc[a] = p if complete: x.loc[b] = max(0.0, K - O.loc[b]) / S.loc[a] dx.loc[seg[1:]] = x.diff().loc[seg[1:]].values pid.loc[seg[1:]] = n prem.loc[a] = p return dx, pid, prem def with_static(r: pd.Series, dx: pd.Series, pid: pd.Series, prem: pd.Series, c: float, ask: float) -> pd.Series: """r plus puts on c x (equity at the last roll) of S&P 500 notional, held to expiry: E_t = E_t-1 (1 + r_t) + c E_roll dx_t, so E_t / E_roll = g_t (1 + c sum dx_u / g_u) with g the period's growth of r. The ask cost (ask x premium x c) is charged on each roll day.""" on = pid > 0 g = (1 + r.where(on, 0.0)).groupby(pid).cumprod() e = g * (1 + c * (dx / g).groupby(pid).cumsum()) prev = e.groupby(pid).shift(1).fillna(1.0) return (e / prev - 1).where(on, r) - c * ask * prem # --------------------------------------------------------------------------- # routes # --------------------------------------------------------------------------- def margin_used(L: float, letf: float, k: float = 0.0, k_margin: float = M_FUT) -> float: return M_STOCK * (1 - letf) * L + M_LETF * letf * L + k_margin * k def steps(step: float, cap: float = K_MAX) -> list[float]: return [round(n * step, 4) for n in range(int(cap / step) + 1)] if step <= cap else [0.0] def rounded(x: float, step: float) -> float: return float(np.floor(x / step + 0.5) * step) def summarize(r: pd.Series, rf: pd.Series) -> dict: s = stats(r, rf) d = {"cagr": float(s["cagr"]), "mdd": float(s["mdd"]), "sharpe": float(s["sharpe"]), "worst_yr": float(s["worst_yr"])} for i, (a, b) in enumerate(HALVES): h = stats(r.loc[a:b], rf) d[f"h{i + 1}_cagr"], d[f"h{i + 1}_mdd"] = float(h["cagr"]), float(h["mdd"]) return d def configs(book: str, r: pd.Series, rf_d: pd.Series, fut_x: pd.Series, hedge, hedges: dict, size: int) -> list[dict]: """Every feasible config of every route at one account size. ``hedge(r, name, c, contract)`` returns r with the named hedge on c of S&P 500 notional.""" letf = LETF_SHARE[book] fut_step = MES_NOTIONAL / size spy_step, mes_opt_step = SPY_OPT_NOTIONAL / size, MES_NOTIONAL / size call_step = CALL_DELTA * SPY_OPT_NOTIONAL / size out = [] def base(L, spread): return L * r - (L - 1) * (rf_d + spread / 252) def add(route, L, k, r_cfg, hedge="none", contract="-", c=0.0): out.append({"route": route, "L": float(L), "k": float(k), "hedge": hedge, "contract": contract, "c": float(c), "_r": r_cfg}) for L in L_GRID: add("A0_reference", L, 0.0, base(L, REF_SPREAD)) if margin_used(L, letf) <= 1 + 1e-9: add("A1_regt", L, 0.0, base(L, MARGIN_SPREAD)) add("A1_regt_lite_rate", L, 0.0, base(L, MARGIN_SPREAD_HI)) add("A2_regt_box", L, 0.0, base(L, BOX_SPREAD)) for k in steps(fut_step): if margin_used(L, letf, k) > 1 + 1e-9: continue r3 = base(L, MARGIN_SPREAD) + k * (fut_x - FUT_COST / 252) add("A3_regt_mes", L, k, r3) for hname in hedges: want = (L - 1) + k options = (("vix", None),) if hname == "vix_tail" else (("spy", spy_step), ("mes", mes_opt_step)) for contract, step in options: c = want if step is None else rounded(want, step) if c <= 0: continue for route, rr in (("A3_regt_mes", r3),) + ((("A1_regt", base(L, MARGIN_SPREAD)),) if k == 0 else ()): add(route, L, k, hedge(rr, hname, c, contract), hname, contract, c) for k in steps(call_step): if k == 0 or margin_used(L, letf, k, CALL_CASH) > 1 + 1e-9: continue r4 = base(L, MARGIN_SPREAD) + k * (fut_x - (FUT_COST + BOX_SPREAD) / 252) add("A4_calls", L, k, hedge(r4, "put_10q", k, "spy"), "embedded put_10q", "spy", k) return out def best(rows: list[dict]) -> tuple[dict | None, dict | None]: ok = [x for x in rows if x["mdd"] >= DD_BUDGET] m35 = max(ok, key=lambda x: x["cagr"]) if ok else None hit = [x for x in rows if x["cagr"] >= TARGET] d50 = max(hit, key=lambda x: x["mdd"]) if hit else None return m35, d50 def calmar(x: dict, h: int) -> float: return x[f"h{h}_cagr"] / abs(x[f"h{h}_mdd"]) if x[f"h{h}_mdd"] < 0 else float("inf") PREREG_V2 = { **PREREG, "version": 2, "hedge_model": "puts bought at each Cboe roll and held to expiry, sized on equity at the roll; the put's " "value per unit of S&P 500 notional is reconstructed from the Cboe index (static_put); the ask " "cost is ask x the reconstructed premium per roll; vix_tail dropped (no VIX roll model)", "why_v2": "v1 used the daily difference PPUT3M - S&P 500 TR, which holds puts plus a short S&P position equal " "to the put's value; after a crash that short is large and loses in the rebound (2008: v1 hedge " "+4.2% vs index spread +11.4pp). Found after reading v1's results; gates and every other rule unchanged.", "data_status": PREREG["data_status"] + "; v1 results (option_overlay/results.json) seen before v2", } def main(model: str = "static") -> None: out_dir = OUT if model == "diff" else OUT + "_v2" os.makedirs(out_dir, exist_ok=True) res_path = os.path.join(out_dir, "results.json") if os.path.exists(res_path): raise SystemExit(f"{res_path} exists; this study does not overwrite results") with open(os.path.join(out_dir, "prereg.json"), "w") as fh: json.dump(PREREG if model == "diff" else PREREG_V2, fh, indent=2) B = pd.read_pickle(BOOKS).loc[SPAN[0]:SPAN[1]] idx = B.index rf = tbill(idx).fillna(0.0) rf_d = rf / 252 lv = load_levels(idx) ret = lv.pct_change(fill_method=None).fillna(0.0) fut_x = ret["SP500TR"] - rf_d res = {"model": model, "hedge_series": {}, "books": {}} if model == "diff": hedges = list(HEDGES) hedge_x = {h: ret[src] - ret["SP500TR"] for h, (src, _, _) in HEDGES.items()} def hedge(r_, name, c, contract): _, rolls, prem = HEDGES[name] return r_ + c * (hedge_x[name] - rolls * ASK_COST[contract] * prem / 252) else: hedges = list(STATIC) L_ = pd.read_pickle(LONG) spx_o, spx_c = L_["open"]["^GSPC"].dropna(), L_["close"]["^GSPC"].dropna() paths = {h: static_put(lv, spx_o, spx_c, src, mny, months) for h, (src, mny, months) in STATIC.items()} def hedge(r_, name, c, contract): dx, pid, prem = paths[name] return with_static(r_, dx, pid, prem, c, ASK_COST[contract]) print("=== static puts: reconstruction check (S&P 500 TR + 1 put per unit vs the Cboe index)") for h, (src, mny, months) in STATIC.items(): dx, pid, prem = paths[h] p = prem[prem > 0] rebuilt = with_static(ret["SP500TR"], dx, pid, prem, 1.0, 0.0) yr = pd.DataFrame({"rebuilt": rebuilt, "cboe": ret[src], "sp500tr": ret["SP500TR"]}) yr = (1 + yr).groupby(yr.index.year).prod() - 1 err = (yr["rebuilt"] - yr["cboe"]).abs() d = {"rolls": int(len(p)), "premium_median": float(p.median()), "premium_max": float(p.max()), "premium_max_date": str(p.idxmax().date()), "yearly_abs_err_median": float(err.median()), "yearly_abs_err_max": float(err.max()), "worst_err_year": int(err.idxmax()), "cost_pa_vs_sp500tr": float(((1 + rebuilt).prod() ** (252 / len(rebuilt)) - (1 + ret["SP500TR"]).prod() ** (252 / len(rebuilt)))), "years": {int(y): {k: float(v) for k, v in row.items()} for y, row in yr.loc[[2008, 2009, 2020, 2022]].iterrows()}} res["hedge_series"][h] = d print(f" {h}: {d['rolls']} rolls, premium median {d['premium_median']*100:.2f}% max " f"{d['premium_max']*100:.1f}% ({d['premium_max_date']}); rebuilt vs Cboe yearly |err| median " f"{d['yearly_abs_err_median']*100:.2f}pp, max {d['yearly_abs_err_max']*100:.2f}pp ({d['worst_err_year']}); " f"hedged S&P vs S&P {d['cost_pa_vs_sp500tr']*100:+.2f}%/yr") print(" " + " ".join(f"{y}: rebuilt {v['rebuilt']*100:+.1f}% cboe {v['cboe']*100:+.1f}% " f"S&P {v['sp500tr']*100:+.1f}%" for y, v in d["years"].items())) hedge_x = {} if hedge_x: print("=== hedge series (per unit of S&P 500 notional, before retail costs)") for h, x in hedge_x.items(): yr = (1 + x).groupby(x.index.year).prod() - 1 d = {"mean_pa": float(x.mean() * 252), "y2008": float(yr.get(2008, np.nan)), "y2020": float(yr.get(2020, np.nan)), "y2022": float(yr.get(2022, np.nan)), "corr_book_monthly": float(((1 + x).resample("ME").prod() - 1).corr( (1 + B[PRIMARY]).resample("ME").prod() - 1))} res["hedge_series"][h] = d print(f" {h:9s} mean {d['mean_pa']*100:+5.2f}%/yr 2008 {d['y2008']*100:+5.1f}% 2020 {d['y2020']*100:+5.1f}% " f"2022 {d['y2022']*100:+5.1f}% monthly corr with book {d['corr_book_monthly']:+.2f}") all_rows = [] for book in [PRIMARY] + REPORTED_BOOKS: r = B[book] cap = 1 / (M_STOCK * (1 - LETF_SHARE[book]) + M_LETF * LETF_SHARE[book]) res["books"][book] = {"regt_cap_L": cap, "sizes": {}} print(f"\n=== {book}: Reg T cap L <= {cap:.2f}") for size in ACCOUNTS: rows = configs(book, r, rf_d, fut_x, hedge, hedges, size) for x in rows: x.update(summarize(x.pop("_r"), rf)) x.update({"book": book, "size": size}) all_rows += rows groups = {} for x in rows: groups.setdefault((x["route"], x["hedge"], x["contract"]), []).append(x) table = {} for key, g in sorted(groups.items()): m35, d50 = best(g) table["|".join(key)] = {"M35": m35, "D50": d50} res["books"][book]["sizes"][size] = table if book != PRIMARY and size != 100_000: continue print(f" -- ${size // 1000}k (MES step {MES_NOTIONAL / size:.2f}, SPY-option step " f"{SPY_OPT_NOTIONAL / size:.2f})") for key, t in table.items(): m, d = t["M35"], t["D50"] ms = (f"M35 {m['cagr']*100:5.1f}% (L {m['L']:.2f} k {m['k']:.2f} c {m['c']:.2f}, mdd {m['mdd']*100:5.1f}%," f" halves {m['h1_cagr']*100:5.1f}/{m['h2_cagr']*100:5.1f}%)") if m else "M35 none" ds = (f"D50 mdd {d['mdd']*100:5.1f}% (L {d['L']:.2f} k {d['k']:.2f} c {d['c']:.2f})") if d else "D50 none" print(f" {key:34s} {ms} {ds}") # gates on the primary book sizes = res["books"][PRIMARY]["sizes"] viable = {} for size in ACCOUNTS: cands = [] for key, t in sizes[size].items(): if key.startswith(("A0", "A2")) or "lite_rate" in key: continue d = t["D50"] if d and d["mdd"] >= DD_VIABLE: cands.append((d["mdd"], key, d)) cands.sort(key=lambda z: -z[0]) viable[size] = [{"route": k, "mdd": d["mdd"], "cagr": d["cagr"], "L": d["L"], "k": d["k"], "c": d["c"]} for _, k, d in cands] t100 = sizes[100_000] un, p10, p5 = (t100.get(k, {}).get("M35") for k in ("A3_regt_mes|none|-", "A3_regt_mes|put_10q|spy", "A3_regt_mes|put_5m|spy")) b_checks = { "B1_put10q_plus_2pp": bool(un and p10 and p10["cagr"] >= un["cagr"] + 0.02), "B2_put5m_not_worse": bool(un and p5 and p5["cagr"] >= un["cagr"]), "B3_calmar_both_halves": bool(un and p10 and all(calmar(p10, h) >= calmar(un, h) for h in (1, 2))), } res["verdict"] = { "A_viable_routes_by_size": viable, "A_decision": {s: (v[0]["route"] if v else "none") for s, v in viable.items()}, "B_checks": b_checks, "B_decision": "PASS" if all(b_checks.values()) else "FAIL", } print("\n=== verdict") for s, v in viable.items(): top = v[0] if v else None print(f" A ${s // 1000}k: " + (f"viable, best {top['route']} mdd {top['mdd']*100:.1f}% " f"(L {top['L']:.2f} k {top['k']:.2f} c {top['c']:.2f})" if top else "no viable route")) print(f" B {b_checks} -> {res['verdict']['B_decision']}") with open(res_path, "w") as fh: json.dump(res, fh, indent=2, default=float) pd.DataFrame(all_rows).to_csv(os.path.join(out_dir, "configs.csv.gz"), index=False) from research.lab.trials import append_trials trial_rows = [] for size, table in sizes.items(): for key, t in table.items(): if t["M35"]: trial_rows.append({"name": f"option_overlay{'' if model == 'diff' else '_v2'}:{key}:{size}", **{x: t["M35"][x] for x in ("cagr", "mdd", "sharpe", "L", "k", "c")}}) append_trials(os.path.join(RESULTS, "trials_us.jsonl"), trial_rows, {"study": "option_overlay" + ("" if model == "diff" else "_v2"), "role": "preregistered; M35 config per route/size"}) if __name__ == "__main__": ap = argparse.ArgumentParser() ap.add_argument("cmd", nargs="?", default="run", choices=["run", "fetch"]) ap.add_argument("--hedge", choices=["static", "diff"], default="static", help="static: puts held to expiry (v2, default); diff: daily index difference (v1)") a = ap.parse_args() if a.cmd == "fetch": fetch() else: main(a.hedge)