Add a `warmup_obs: int = _VOL_LOOKBACK` (60) keyword to vol_target_leverage and change the early guard from `len(days) < 2` to `len(days) < warmup_obs`. Returns 1.0 (unlevered) until there are >= warmup_obs book-return observations, so a thin cold-start window (tiny, unreliable vol estimate) no longer over-levers. The pv<=1e-12 / non-finite guard is unchanged. Existing leverage tests use 60-80 obs series so they still lever (verified). Per the foundation audit (D3, cold-start over-leverage). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
44 lines
1.9 KiB
Python
44 lines
1.9 KiB
Python
import math
|
|
from fxhnt.application.paper_book import vol_target_leverage
|
|
|
|
def _series(vals, start=1):
|
|
return {start + i: v for i, v in enumerate(vals)}
|
|
|
|
def test_no_history_cold_start_is_unlevered():
|
|
assert vol_target_leverage({"a": {}}, {"a": {}}) == 1.0 # weights, returns both empty -> 1.0
|
|
|
|
def test_lower_vol_gives_more_leverage():
|
|
lo = vol_target_leverage({"a": 1.0}, {"a": _series([0.002, -0.002] * 40)})
|
|
hi = vol_target_leverage({"a": 1.0}, {"a": _series([0.02, -0.02] * 40)})
|
|
assert lo > hi
|
|
|
|
def test_capped_at_max_leverage():
|
|
tiny = vol_target_leverage({"a": 1.0}, {"a": _series([1e-6, -1e-6] * 40)}, max_leverage=3.0)
|
|
assert tiny == 3.0
|
|
|
|
def test_formula_matches_book_allocator():
|
|
import numpy as np
|
|
r = _series([0.01, -0.01, 0.005, -0.005] * 20)
|
|
w = {"a": 1.0}
|
|
lev = vol_target_leverage(w, {"a": r}, target_vol=0.12, kelly_fraction=0.5,
|
|
periods_per_year=365, vol_lookback=60, max_leverage=3.0)
|
|
vals = [r[d] for d in sorted(r)][-60:]
|
|
pv = float(np.std(vals)); tvd = 0.12 / math.sqrt(365)
|
|
assert math.isclose(lev, min(3.0, 0.5 * tvd / pv), rel_tol=1e-9)
|
|
|
|
def test_nan_in_window_falls_back_to_unlevered():
|
|
# a NaN return must NOT pin leverage to max; fall back to 1.0 (unlevered)
|
|
r = {1: 0.01, 2: float("nan"), 3: -0.01, 4: 0.02, 5: -0.02}
|
|
lev = vol_target_leverage({"a": 1.0}, {"a": r})
|
|
assert lev == 1.0
|
|
|
|
def test_warmup_thin_history_is_unlevered():
|
|
# 30 obs (< warmup_obs default 60): cold-start, no leverage regardless of low vol
|
|
thin = vol_target_leverage({"a": 1.0}, {"a": _series([1e-6, -1e-6] * 15)}, max_leverage=3.0)
|
|
assert thin == 1.0
|
|
|
|
def test_levers_after_warmup():
|
|
# 70 obs (>= warmup_obs default 60) low-vol series: levers up past 1.0 after warmup
|
|
warm = vol_target_leverage({"a": 1.0}, {"a": _series([1e-6, -1e-6] * 35)}, max_leverage=3.0)
|
|
assert warm > 1.0
|