Files
fxhnt/tests/unit/test_levered_shadow.py
jgrusewski 713424a89d feat(fund): multistrat_levered — observe-only levered shadow of the ETF book + financing sensitivity band
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>
2026-07-18 11:15:55 +02:00

101 lines
5.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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 (L1)·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