The multistrat ETF book is an UNDER-levered risk-parity book: it runs at ~5.7% vol vs its own 10% target because leverage is capped at 1x (MAXLEV=1.0), leaving return on the table. Add a return-defined observe-only shadow (cf. bybit_4edge_levered) that levers the SAME series to the 10% vol target minus the HONEST financing drag on the borrowed leg. - LeveredShadowStrategy (paper_strategies.py): L = min(max_lev, target_vol/vol), floored at 1x; levered_ret = L·r − (L−1)·financing/252. Never executed. - multistrat_levered_nav asset (deps=multistrat_nav) publishes its forward track + basis-matched backtest ref like the base; registered in the nightly job. - multistrat_levered registry entry + SIM_BOOKS (Backtest switcher). - financing_bps=530 GROUNDED in IBKR's real USD schedule (verified 2026-07-18: Fed funds 3.63% + Pro tiered spread → 5.83% <$100k / ~5.3% $100k-1M). - leverage_financing_sensitivity() + a cockpit BAND on the levered book's Backtest view: levered return/vol/Sharpe across 1..6.83% + the break-even (= the book's 7.7% return), so the verdict is never hostage to one rate. Honest finding it surfaces: at any realistic IBKR retail rate (~5-7%) levering this modest-return book LOWERS risk-adjusted return (Sharpe 1.35 -> ~0.9); it only pays at institutional financing (~1-3%). The financing drag is what makes the shadow honest (without it, free leverage would look like 13.5%/Sharpe 1.35). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
101 lines
5.2 KiB
Python
101 lines
5.2 KiB
Python
"""LeveredShadowStrategy — the observe-only levered shadow wrapper (multistrat_levered): levers a base
|
||
recompute-replay strategy's daily returns to the fund vol target (capped, floored at 1x) and charges the
|
||
honest financing drag on the borrowed leg. Return-defined; a pure transform of the base series."""
|
||
from __future__ import annotations
|
||
|
||
import math
|
||
from typing import Any
|
||
|
||
from fxhnt.application.paper_strategies import (
|
||
LeveredShadowStrategy,
|
||
leverage_financing_sensitivity,
|
||
)
|
||
|
||
|
||
class _Base:
|
||
"""A stub base strategy returning a fixed (date, ret) series + passing `extra` through unchanged."""
|
||
def __init__(self, rows: list[tuple[str, float]]) -> None:
|
||
self._rows = rows
|
||
|
||
def advance(self, last_date: str | None, extra: dict[str, Any]) -> tuple[list[tuple[str, float]], dict[str, Any]]:
|
||
return list(self._rows), extra
|
||
|
||
|
||
def _ann_vol(rets: list[float]) -> float:
|
||
mean = sum(rets) / len(rets)
|
||
return math.sqrt(sum((r - mean) ** 2 for r in rets) / (len(rets) - 1)) * math.sqrt(252)
|
||
|
||
|
||
def test_levers_a_low_vol_book_to_target_minus_financing() -> None:
|
||
# A low-vol base (well under the 10% target) LEVERS UP; each day = L·base − (L−1)·financing/252.
|
||
base_rows = [(f"d{i}", r) for i, r in enumerate([0.004, -0.004, 0.006, -0.005, 0.003, -0.004] * 30)]
|
||
s = LeveredShadowStrategy(_Base(base_rows), target_vol=0.10, financing_bps=550.0, max_leverage=2.0)
|
||
rows, extra = s.advance(None, {"carry": 1})
|
||
|
||
assert extra == {"carry": 1} # extra passes through untouched
|
||
rets = [r for _, r in base_rows]
|
||
lev = min(2.0, 0.10 / _ann_vol(rets))
|
||
lev = max(1.0, lev)
|
||
assert lev > 1.0 # a low-vol book levers up
|
||
daily_fin = (550.0 / 1e4) / 252.0 * (lev - 1.0)
|
||
for (d0, r0), (d1, r1) in zip(base_rows, rows):
|
||
assert d0 == d1
|
||
assert abs(r1 - (lev * r0 - daily_fin)) < 1e-12
|
||
# financing makes the levered MEAN strictly below the naive L·mean (leverage is not free)
|
||
assert sum(r for _, r in rows) / len(rows) < lev * (sum(rets) / len(rets))
|
||
|
||
|
||
def test_high_vol_book_is_not_de_levered_below_1x() -> None:
|
||
# A base ALREADY above the vol target: L is floored at 1.0 (a shadow only levers UP) -> no financing, the
|
||
# series is returned UNCHANGED.
|
||
base_rows = [(f"d{i}", r) for i, r in enumerate([0.05, -0.05] * 100)] # ann vol >> 10%
|
||
s = LeveredShadowStrategy(_Base(base_rows), target_vol=0.10, financing_bps=550.0, max_leverage=2.0)
|
||
rows, _ = s.advance(None, {})
|
||
assert rows == base_rows
|
||
|
||
|
||
def test_leverage_is_capped_at_max_leverage() -> None:
|
||
# A near-zero-vol base would want huge leverage; it is capped at max_leverage.
|
||
base_rows = [(f"d{i}", r) for i, r in enumerate([0.0001, -0.0001] * 100)]
|
||
s = LeveredShadowStrategy(_Base(base_rows), target_vol=0.10, financing_bps=550.0, max_leverage=2.0)
|
||
rows, _ = s.advance(None, {})
|
||
daily_fin = (550.0 / 1e4) / 252.0 * (2.0 - 1.0) # capped L = 2.0
|
||
assert abs(rows[0][1] - (2.0 * base_rows[0][1] - daily_fin)) < 1e-12
|
||
|
||
|
||
def test_short_series_passes_through_unchanged() -> None:
|
||
# < 2 rows: no vol to compute -> the base series is returned untouched (never a divide-by-zero).
|
||
s = LeveredShadowStrategy(_Base([("d0", 0.01)]), target_vol=0.10, financing_bps=550.0, max_leverage=2.0)
|
||
rows, _ = s.advance(None, {})
|
||
assert rows == [("d0", 0.01)]
|
||
|
||
|
||
def test_financing_sensitivity_band_break_even_and_current_flag() -> None:
|
||
# A low-vol base: levers up; the band shows the levered outcome at each rate. Break-even = the base book's
|
||
# own ann return (above it, leverage subtracts). The `current_bps` row is flagged and included.
|
||
base = [0.004, -0.004, 0.006, -0.005, 0.003, -0.004] * 30
|
||
b = leverage_financing_sensitivity(base, target_vol=0.10, max_leverage=2.0,
|
||
rates_bps=(100.0, 300.0, 683.0), current_bps=530.0)
|
||
assert b["leverage"] > 1.0
|
||
# break-even equals the base ann return
|
||
assert abs(b["break_even_pct"] - b["base_ann_return_pct"]) < 1e-9
|
||
# current_bps (530) is present + flagged even though not in rates_bps
|
||
cur = [r for r in b["rows"] if r["is_current"]]
|
||
assert len(cur) == 1 and abs(cur[0]["rate_pct"] - 5.30) < 1e-9
|
||
# higher financing -> lower levered return + Sharpe (monotone)
|
||
rets = [r["ann_return_pct"] for r in b["rows"]]
|
||
shs = [r["sharpe"] for r in b["rows"]]
|
||
assert rets == sorted(rets, reverse=True) and shs == sorted(shs, reverse=True)
|
||
# every row shares the SAME levered vol (= L * base vol) — only the financing drag differs
|
||
assert len({round(r["ann_vol_pct"], 6) for r in b["rows"]}) == 1
|
||
|
||
|
||
def test_financing_sensitivity_marks_rates_above_break_even_as_subtracting() -> None:
|
||
# A base whose ann return is small: a financing rate above it makes leverage SUBTRACT return (adds=False).
|
||
base = [0.0005, -0.0003] * 200 # tiny positive drift -> low break-even
|
||
b = leverage_financing_sensitivity(base, target_vol=0.10, max_leverage=3.0,
|
||
rates_bps=(50.0, 2000.0))
|
||
lo, hi = b["rows"][0], b["rows"][-1]
|
||
assert lo["adds"] is True # 0.5% financing < break-even
|
||
assert hi["adds"] is False # 20% financing >> break-even -> subtracts
|