Files
fxhnt/tests/unit/test_paper_sim.py
jgrusewski 994bb59641 test: parallelize (pytest-xdist -n auto) + optimize the slow tests
- pytest-xdist added to dev deps; addopts='-n auto' runs the ~1940-test suite across all
  cores (~6-9min -> ~1.5min); registered a 'slow' marker for a serial '-m "not slow"' loop.
- equity_backtest_runner liquidity fixture: trimmed the 3000-bar (8yr) LIVELONG/ILLIQUID
  histories to 800 bars (still >> min_history=300, multi-year for peak-yearly ranking) —
  the peak-yearly aggregation over 8yr was the cost: 50s/30s/19s tests -> ~5.8s each,
  assertions unchanged.
- paper_sim perf guards: made them parallel-robust (the wall-clock <1.6s bound flaked under
  -n auto CPU contention). full-history guard relaxed to <10s (it targets an O(D²) blowup =
  orders of magnitude, not a constant factor); cheap-reapply made RELATIVE (< full/10, ratio
  is contention-invariant). Both marked slow.

Full suite 1940 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 00:51:52 +02:00

477 lines
24 KiB
Python
Raw Permalink 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.
"""Unit tests for the pure `simulate_book` engine (paper_sim.py).
The load-bearing guarantee: with `controller="killswitch", kelly_fraction=1.0, cost_bps=0` the engine
reproduces the ESTABLISHED LIVE book math — the per-date overlay path the live backfill/snapshot use
(`paper_risk_overlay`/`IncrementalRiskOverlay` called on the strictly-prior window, eff_lev=0 on kill,
applied to that date) — so the sim never diverges from the live/replay book. The other tests cover
capital scaling, the date window, sleeve selection, the cost_bps turnover model, and hand-checked
metrics.
NOTE on the golden reference: the spec phrases the golden as "equal the existing `paper_book_returns`
(+killswitch)". In the actual code, `paper_book_returns` is the INTERNAL un-throttled regime signal
that feeds the killswitch — it is NOT what the live book compounds. The live book
(paper_backfill.py / paper_snapshot.py) compounds the OVERLAY's `(weights, eff_lev)` applied to each
date's sleeve returns. Those two differ at the vol-target warmup boundary (the overlay's leverage is
computed from the full trailing window via paper_risk_overlay, whereas paper_book_returns sizes off
its own incrementally-built throttled series). The engine is built against — and golden-tested
against — the live OVERLAY path, since that is the book the cockpit + live wiring must reproduce. See
the report.
"""
import json
import math
import pathlib
import random
import statistics as st
import time
import pytest
from fxhnt.application.paper_book import paper_risk_overlay
from fxhnt.application.paper_sim import (
apply_knobs,
overlay_pass,
simulate_book,
simulate_naive_eqwt,
)
_GOLDEN_PATH = pathlib.Path(__file__).parent / "data" / "golden_sim.json"
def _big_fixture() -> dict[str, dict[int, float]]:
"""A full-history-shaped fixture (~1473 days, 3 staggered sleeves) for the perf benchmark — mirrors
the production combined-book history that the interactive /paper/sim page recomputes per knob change."""
rng = random.Random(42)
return {
"crypto_tstrend": {1 + i: rng.gauss(0.0012, 0.012) for i in range(1473)},
"unlock": {30 + i: rng.gauss(-0.001, 0.022) for i in range(1400)},
"stablecoin_rotation": {10 + i: rng.gauss(0.0004, 0.004) for i in range(1450)},
}
# --- fixtures ------------------------------------------------------------------------------------
def _two_edge_fixture() -> dict[str, dict[int, float]]:
"""A 2-edge-shaped fixture (tstrend + unlock) with different vols + staggered starts + a
negative-drift sleeve so the killswitch actually fires (exercises the killed path). Deterministic."""
rng = random.Random(20260623)
return {
"crypto_tstrend": {1 + i: rng.gauss(0.0012, 0.012) for i in range(220)},
# Strong negative drift → the un-throttled book series breaches the 25% kill threshold.
"unlock": {16 + i: rng.gauss(-0.010, 0.022) for i in range(200)},
}
def _three_edge_fixture() -> dict[str, dict[int, float]]:
fx = _two_edge_fixture()
rng = random.Random(99)
fx["stablecoin_rotation"] = {6 + i: rng.gauss(0.0004, 0.004) for i in range(210)}
return fx
def _overlay_book_returns(returns_by_sleeve: dict[str, dict[int, float]]) -> dict[int, float]:
"""Faithful per-date THROTTLED book return via the LIVE overlay path (mirrors paper_backfill.py):
for each date d, call the stateless `paper_risk_overlay` on the strictly-prior window (keys < d),
take eff_lev = 0 if killed else lev, and book `eff_lev · Σ weights·return[d]`. This is exactly the
gross return the live book compounds → the ground-truth golden the engine must reproduce."""
all_days = sorted({d for r in returns_by_sleeve.values() for d in r})
out: dict[int, float] = {}
for d in all_days:
before = {s: {k: v for k, v in r.items() if k < d} for s, r in returns_by_sleeve.items()}
active = [s for s, r in before.items() if r]
w, lev, killed = paper_risk_overlay(before, active)
eff = 0.0 if killed else lev
out[d] = eff * sum(w.get(s, 0.0) * returns_by_sleeve.get(s, {}).get(d, 0.0) for s in w)
return out
# --- golden vs the established book math ----------------------------------------------------------
def test_golden_matches_overlay_book_returns_with_killswitch():
fx = _two_edge_fixture()
res = simulate_book(fx, capital=100_000.0, sleeves=["crypto_tstrend", "unlock"],
controller="killswitch", kelly_fraction=1.0, cost_bps=0.0)
ref = _overlay_book_returns(fx) # the live overlay path's per-date throttled book return
# The sim emits an entry for every union day (the first one is flat: overlay on empty → no weights).
assert res.dates == sorted(ref)
# Reconstruct the sim's per-day net return from its compounding equity curve and compare to ref.
eq = res.equity
dates = res.dates
cap = 100_000.0
for i, d in enumerate(dates):
prev = cap if i == 0 else eq[i - 1]
sim_ret = eq[i] / prev - 1.0
assert math.isclose(sim_ret, ref[d], rel_tol=0, abs_tol=1e-12), \
f"day {d}: sim {sim_ret!r} != book {ref[d]!r}"
# And the equity compounding itself matches a direct compound of the reference book returns.
expected_eq = cap
for d in dates:
expected_eq *= (1.0 + ref[d])
assert math.isclose(res.equity[-1], expected_eq, rel_tol=0, abs_tol=1e-6)
def test_killswitch_actually_fires_in_fixture():
# Guard: the golden test is only meaningful if the kill path is exercised.
fx = _two_edge_fixture()
res = simulate_book(fx, capital=100_000.0, sleeves=["crypto_tstrend", "unlock"])
assert any(res.killed), "fixture never killed — golden test not exercising the killed path"
assert all((lev == 0.0) for lev, k in zip(res.leverage, res.killed, strict=True) if k), \
"effective leverage must be 0 on killed days"
# --- capital scaling --------------------------------------------------------------------------------
def test_capital_scales_curve_linearly():
fx = _two_edge_fixture()
sleeves = ["crypto_tstrend", "unlock"]
a = simulate_book(fx, capital=10_000.0, sleeves=sleeves)
b = simulate_book(fx, capital=40_000.0, sleeves=sleeves)
assert a.dates == b.dates
for ea, eb in zip(a.equity, b.equity, strict=True):
assert math.isclose(eb, ea * 4.0, rel_tol=1e-12, abs_tol=1e-6)
# Shape-invariant stats are identical.
assert math.isclose(a.metrics["sharpe"], b.metrics["sharpe"], rel_tol=0, abs_tol=1e-12)
assert math.isclose(a.metrics["max_dd"], b.metrics["max_dd"], rel_tol=0, abs_tol=1e-12)
assert math.isclose(a.metrics["cagr"], b.metrics["cagr"], rel_tol=0, abs_tol=1e-12)
# --- date window ------------------------------------------------------------------------------------
def test_date_window_restricts_curve():
fx = _two_edge_fixture()
sleeves = ["crypto_tstrend", "unlock"]
full = simulate_book(fx, capital=100_000.0, sleeves=sleeves)
lo, hi = 50, 120
win = simulate_book(fx, capital=100_000.0, sleeves=sleeves, start=lo, end=hi)
assert win.dates == [d for d in full.dates if lo <= d <= hi]
assert all(lo <= d <= hi for d in win.dates)
# Equity restarts at `capital` on the first in-window date (it compounds from capital, not from the
# full-curve level at that date) — but the overlay state is still the full prior history (causal).
first = win.dates[0]
first_net = win.equity[0] / 100_000.0 - 1.0
# The first in-window day applies the THROTTLED overlay book return of that day to `capital`.
ref = _overlay_book_returns(fx)
assert math.isclose(first_net, ref[first], rel_tol=0, abs_tol=1e-12)
# --- sleeve selection -------------------------------------------------------------------------------
def test_sleeve_selection_distinct_curves():
fx = _three_edge_fixture()
two = simulate_book(fx, capital=100_000.0, sleeves=["crypto_tstrend", "unlock"])
three = simulate_book(fx, capital=100_000.0,
sleeves=["crypto_tstrend", "unlock", "stablecoin_rotation"])
assert two.equity[-1] != three.equity[-1], "2-edge and 3-edge curves should differ"
def test_single_sleeve_is_that_sleeve_overlaid_return():
fx = _three_edge_fixture()
res = simulate_book(fx, capital=100_000.0, sleeves=["crypto_tstrend"])
# A single sleeve gets weight 1.0; its net return each day = eff_lev · 1.0 · r[d] (killswitch +
# vol-target leverage still apply). Reproduce via the same single-sleeve overlay book math.
ref = _overlay_book_returns({"crypto_tstrend": fx["crypto_tstrend"]})
cap = 100_000.0
assert res.dates == sorted(ref)
for i, d in enumerate(res.dates):
prev = cap if i == 0 else res.equity[i - 1]
sim_ret = res.equity[i] / prev - 1.0
assert math.isclose(sim_ret, ref[d], rel_tol=0, abs_tol=1e-12), f"day {d}"
# Single active sleeve → its weight is 1.0 on days it is active.
for w in res.weights:
if w:
assert math.isclose(sum(w.values()), 1.0, rel_tol=0, abs_tol=1e-9)
def test_intersection_with_unknown_sleeve():
fx = _two_edge_fixture()
a = simulate_book(fx, capital=100_000.0, sleeves=["crypto_tstrend", "unlock"])
# Adding a sleeve absent from the data must not change the result (intersection).
b = simulate_book(fx, capital=100_000.0, sleeves=["crypto_tstrend", "unlock", "does_not_exist"])
assert a.dates == b.dates
assert a.equity == b.equity
# --- cost_bps ---------------------------------------------------------------------------------------
def test_cost_bps_zero_is_identical_to_gross():
fx = _two_edge_fixture()
sleeves = ["crypto_tstrend", "unlock"]
gross = simulate_book(fx, capital=100_000.0, sleeves=sleeves, cost_bps=0.0)
# Bit-identical: cost_bps=0 must take the no-cost branch.
again = simulate_book(fx, capital=100_000.0, sleeves=sleeves, cost_bps=0.0)
assert gross.equity == again.equity
def test_cost_bps_lowers_curve_by_turnover_charge():
fx = _two_edge_fixture()
sleeves = ["crypto_tstrend", "unlock"]
gross = simulate_book(fx, capital=100_000.0, sleeves=sleeves, cost_bps=0.0)
costed = simulate_book(fx, capital=100_000.0, sleeves=sleeves, cost_bps=10.0)
# There IS turnover (weights/leverage move day to day), so the costed curve ends lower.
assert costed.equity[-1] < gross.equity[-1]
# Every day, the costed net return <= gross net return (cost only subtracts).
cap = 100_000.0
for i in range(len(gross.dates)):
gp = cap if i == 0 else gross.equity[i - 1]
cp = cap if i == 0 else costed.equity[i - 1]
g_ret = gross.equity[i] / gp - 1.0
c_ret = costed.equity[i] / cp - 1.0
assert c_ret <= g_ret + 1e-15
def test_cost_bps_exact_turnover_on_two_day_fixture():
"""Hand-checked turnover charge on a tiny single-sleeve fixture where the math is traceable.
Single sleeve, within warmup so leverage is 1.0 throughout. Day 1 is flat (overlay over empty prior
view → no weights; eff_w = {}). Day 2: the sleeve is active with weight 1.0, eff_lev 1.0, so
eff_w = {sleeve: 1.0}; turnover vs prev_eff ({}) = |1.0 0| = 1.0 → cost = 1.0 · 25/1e4 = 0.0025.
Day 3: eff_w unchanged at {sleeve: 1.0} → turnover 0 → no cost."""
fx = {"crypto_tstrend": {1: 0.01, 2: 0.02, 3: -0.01}}
gross = simulate_book(fx, capital=100_000.0, sleeves=["crypto_tstrend"], cost_bps=0.0)
costed = simulate_book(fx, capital=100_000.0, sleeves=["crypto_tstrend"], cost_bps=25.0)
assert gross.dates == [1, 2, 3]
cap = 100_000.0
g_rets, c_rets = [], []
for i in range(3):
gp = cap if i == 0 else gross.equity[i - 1]
cp = cap if i == 0 else costed.equity[i - 1]
g_rets.append(gross.equity[i] / gp - 1.0)
c_rets.append(costed.equity[i] / cp - 1.0)
# Day 1: flat, no turnover → no cost; identical.
assert math.isclose(c_rets[0], g_rets[0], abs_tol=1e-15)
# Day 2: exactly 0.0025 of turnover cost (turnover 1.0 × 25bp).
assert math.isclose(g_rets[1] - c_rets[1], 0.0025, rel_tol=0, abs_tol=1e-12)
# Day 3: eff weights unchanged → zero turnover → no incremental cost.
assert math.isclose(c_rets[2], g_rets[2], rel_tol=0, abs_tol=1e-12)
# --- metrics ----------------------------------------------------------------------------------------
def test_metrics_hand_checked_on_deterministic_fixture():
"""Single sleeve, no killswitch action, leverage stays in warmup (1.0) so the book return == the
raw sleeve return for each day → metrics are hand-computable from those returns.
Direct-application semantics (mirroring the live backfill overlay path): the weights applied to day
d come from the overlay over data strictly before d. The first union day is FLAT (overlay over an
empty prior view → no weights). From the second day on, the single sleeve carries weight 1.0."""
# 5 days of a single sleeve.
rets = {1: 0.05, 2: -0.02, 3: 0.03, 4: 0.01, 5: 0.04}
fx = {"crypto_tstrend": rets}
res = simulate_book(fx, capital=100_000.0, sleeves=["crypto_tstrend"])
assert res.dates == [1, 2, 3, 4, 5]
# day 1 flat (overlay over empty prior view); days 2-5 carry r[d] (weight 1.0, leverage 1.0 warmup).
net = [0.0, rets[2], rets[3], rets[4], rets[5]]
cap = 100_000.0
eq = cap
peak = cap
dds = []
for r in net:
eq *= (1 + r)
peak = max(peak, eq)
dds.append((eq - peak) / peak)
assert math.isclose(res.equity[-1], eq, rel_tol=0, abs_tol=1e-6)
assert math.isclose(res.metrics["end_value"], eq, rel_tol=0, abs_tol=1e-6)
assert math.isclose(res.metrics["total_return"], eq / cap - 1.0, rel_tol=0, abs_tol=1e-12)
# max_dd hand-checked.
assert math.isclose(res.metrics["max_dd"], min(dds), rel_tol=0, abs_tol=1e-12)
# CAGR over the window span n_days = dates[-1] - dates[0] = 5 - 1 = 4.
years = (5 - 1) / 365
expected_cagr = (eq / cap) ** (1.0 / years) - 1.0
assert math.isclose(res.metrics["cagr"], expected_cagr, rel_tol=0, abs_tol=1e-9)
# Sharpe over the net-return series.
sd = st.pstdev(net)
expected_sharpe = st.mean(net) / sd * math.sqrt(365)
assert math.isclose(res.metrics["sharpe"], expected_sharpe, rel_tol=0, abs_tol=1e-9)
# All days fall in 1970 (epoch days 1-5) → worst==best==total compounded return.
assert math.isclose(res.metrics["worst_year"], eq / cap - 1.0, rel_tol=0, abs_tol=1e-12)
assert math.isclose(res.metrics["best_year"], eq / cap - 1.0, rel_tol=0, abs_tol=1e-12)
def test_worst_and_best_year_split_across_calendar_years():
"""Epoch days spanning two calendar years → distinct worst/best year returns.
Direct-application semantics: a single sleeve carries weight 1.0 from its SECOND observation on
(day 1 is flat: overlay over empty prior view). Lay out obs in each year so real returns land in each."""
from fxhnt.application.paper_sim import _epoch_day_to_year
# epoch day 1 = 1970-01-02 ... day 366 = 1971-01-02.
fx = {"crypto_tstrend": {1: -0.05, 2: -0.05, 3: -0.05, # 1970 (day 1 flat; days 2,3 live)
366: 0.10, 367: 0.10}} # 1971 (days 366,367 live)
assert _epoch_day_to_year(3) == 1970
assert _epoch_day_to_year(366) == 1971
res = simulate_book(fx, capital=100_000.0, sleeves=["crypto_tstrend"])
# 1970 live returns: day1=0 (flat), day2=0.05, day3=0.05 → (1)(0.95)(0.95)1 = 0.0975.
# 1971 live returns: day366=+0.10, day367=+0.10 → (1.10)(1.10)1 = +0.21.
assert res.metrics["worst_year"] < 0.0 < res.metrics["best_year"]
assert math.isclose(res.metrics["worst_year"], 0.95 * 0.95 - 1.0, rel_tol=0, abs_tol=1e-9)
assert math.isclose(res.metrics["best_year"], 1.10 * 1.10 - 1.0, rel_tol=0, abs_tol=1e-9)
def test_empty_inputs_return_seeded_metrics():
res = simulate_book({}, capital=100_000.0, sleeves=["crypto_tstrend"])
assert res.dates == []
assert res.equity == []
assert res.metrics["end_value"] == 100_000.0
assert res.metrics["total_return"] == 0.0
assert res.metrics["max_dd"] == 0.0
# --- bit-identical golden vs the PRE-OPTIMIZATION capture --------------------------------------------
def _golden_fixture() -> dict[str, dict[int, float]]:
"""The exact fixture the golden snapshot in tests/unit/data/golden_sim.json was captured from
(pre-optimization `simulate_book` output). Must reproduce these values bit-identically (abs_tol 1e-9)."""
rng = random.Random(20260623)
return {
"crypto_tstrend": {1 + i: rng.gauss(0.0012, 0.012) for i in range(220)},
"unlock": {16 + i: rng.gauss(-0.010, 0.022) for i in range(200)},
"stablecoin_rotation": {6 + i: rng.gauss(0.0004, 0.004) for i in range(210)},
}
def test_simulate_book_bit_identical_to_pre_optimization_golden():
"""The optimized engine reproduces the PRE-OPTIMIZATION `simulate_book` output (full SimResult —
equity, drawdown, weights, leverage, killed, metrics) bit-identically (abs_tol 1e-9) across every
controller / kelly / cost / capital combo captured in the frozen golden file."""
golden = json.loads(_GOLDEN_PATH.read_text())
fx = _golden_fixture()
sleeves = ["crypto_tstrend", "unlock", "stablecoin_rotation"]
assert golden, "golden snapshot is empty"
for g in golden:
res = simulate_book(fx, sleeves=sleeves, **g["cfg"])
assert res.dates == g["dates"], f"date axis changed for {g['cfg']}"
assert res.killed == g["killed"], f"killed changed for {g['cfg']}"
for a, b in zip(res.equity, g["equity"], strict=True):
assert math.isclose(a, b, rel_tol=0, abs_tol=1e-9), f"equity {g['cfg']}"
for a, b in zip(res.drawdown, g["drawdown"], strict=True):
assert math.isclose(a, b, rel_tol=0, abs_tol=1e-9), f"drawdown {g['cfg']}"
for a, b in zip(res.leverage, g["leverage"], strict=True):
assert math.isclose(a, b, rel_tol=0, abs_tol=1e-9), f"leverage {g['cfg']}"
for wa, wb in zip(res.weights, g["weights"], strict=True):
for k in set(wa) | set(wb):
assert math.isclose(wa.get(k, 0.0), wb.get(k, 0.0), rel_tol=0, abs_tol=1e-9), \
f"weight[{k}] {g['cfg']}"
for k, v in g["metrics"].items():
assert math.isclose(res.metrics[k], v, rel_tol=0, abs_tol=1e-9), f"metric {k} {g['cfg']}"
# --- expensive/cheap split seam ---------------------------------------------------------------------
def test_cheap_reapply_equals_full_simulate_book():
"""`apply_knobs(overlay_pass(...), capital, cost_bps)` is bit-identical to a full `simulate_book` with
the same capital/cost_bps — the seam Phase-2's page caches (expensive overlay once; cheap knobs per
drag). capital + cost_bps are the genuinely-cheap knobs (kelly is NOT — it's in the overlay pass)."""
fx = _three_edge_fixture()
sleeves = ["crypto_tstrend", "unlock", "stablecoin_rotation"]
for kelly in (1.0, 0.5):
op = overlay_pass(fx, sleeves=sleeves, kelly_fraction=kelly)
for cap in (100_000.0, 37_000.0):
for cost in (0.0, 10.0, 25.0):
cheap = apply_knobs(op, capital=cap, cost_bps=cost)
full = simulate_book(fx, capital=cap, sleeves=sleeves,
kelly_fraction=kelly, cost_bps=cost)
assert cheap.dates == full.dates
assert cheap.killed == full.killed
assert cheap.leverage == full.leverage
assert cheap.equity == full.equity, f"k={kelly} cap={cap} cost={cost}"
assert cheap.drawdown == full.drawdown
assert cheap.weights == full.weights
assert cheap.metrics == full.metrics
def test_overlay_pass_independent_of_capital_and_cost():
"""The expensive overlay pass depends ONLY on (returns, sleeves, controller, kelly, window) — NOT on
capital/cost_bps. Re-using ONE pass for all capital/cost combos must give identical curves to passes
that never knew those knobs (the property that makes caching it sound)."""
fx = _three_edge_fixture()
sleeves = ["crypto_tstrend", "unlock", "stablecoin_rotation"]
op = overlay_pass(fx, sleeves=sleeves, controller="killswitch", kelly_fraction=1.0)
# The pass has no capital/cost notion; the gross (killed-aware) returns are fixed.
a = apply_knobs(op, capital=10_000.0, cost_bps=0.0)
b = apply_knobs(op, capital=40_000.0, cost_bps=0.0)
for ea, eb in zip(a.equity, b.equity, strict=True):
assert math.isclose(eb, ea * 4.0, rel_tol=1e-12, abs_tol=1e-6)
# --- performance benchmark --------------------------------------------------------------------------
@pytest.mark.slow
def test_full_history_simulate_book_under_bound():
"""A full ~1473-day run must not regress to the O(D²) recomputation (which would be MINUTES on this
series). The bound is generous (10s) ON PURPOSE: it must survive `pytest -n auto` CPU contention +
slower CI hardware while still catching the only regression that matters here — a quadratic blowup,
which is orders of magnitude, not a constant factor."""
fx = _big_fixture()
sleeves = list(fx)
simulate_book(fx, capital=100_000.0, sleeves=sleeves) # warm imports/JIT-free, fair timing
runs = [_time(lambda: simulate_book(fx, capital=100_000.0, sleeves=sleeves)) for _ in range(3)]
best = min(runs)
assert best < 10.0, f"full-history simulate_book too slow: {best:.3f}s (O(D²) regression?)"
@pytest.mark.slow
def test_cheap_knob_reapply_is_effectively_instant():
"""After the expensive overlay pass is computed once, re-applying capital/cost_bps must be FAR cheaper
than a full simulate — so dragging the capital / cost sliders on /paper/sim doesn't recompute the whole
overlay. Asserted RELATIVE to a full run (contention-robust: both times inflate together under load, the
ratio holds), not an absolute ms bound that flakes under `-n auto`."""
fx = _big_fixture()
sleeves = list(fx)
full = min(_time(lambda: simulate_book(fx, capital=100_000.0, sleeves=sleeves)) for _ in range(2))
op = overlay_pass(fx, sleeves=sleeves)
apply_knobs(op, capital=100_000.0, cost_bps=10.0) # warm
best = min(_time(lambda cap=cap: apply_knobs(op, capital=cap, cost_bps=10.0))
for cap in (100_000.0, 50_000.0, 200_000.0))
assert best < full / 10.0, f"cheap re-apply not cheap: {best*1000:.1f}ms vs full {full*1000:.1f}ms"
def _time(fn) -> float:
t0 = time.perf_counter()
fn()
return time.perf_counter() - t0
# --- simulate_naive_eqwt: the Bybit book's naive equal-weight curve (no overlay) -----------------
def test_naive_eqwt_compounds_static_equal_weight():
"""Hand-checked: 2 sleeves, eq-wt → daily ret = (ra+rb)/2, equity compounds from capital, lev≡1."""
rbs = {"a": {0: 0.10, 1: 0.20}, "b": {0: 0.30, 1: -0.10}}
res = simulate_naive_eqwt(rbs, capital=1000.0, sleeves=["a", "b"], cost_bps=0.0)
assert res.dates == [0, 1]
assert math.isclose(res.equity[0], 1000.0 * 1.20) # (0.1+0.3)/2 = 0.20
assert math.isclose(res.equity[1], 1000.0 * 1.20 * 1.05) # (0.2-0.1)/2 = 0.05
assert res.leverage == [1.0, 1.0] and res.killed == [False, False]
def test_naive_eqwt_capital_scales_end_value_linearly():
rbs = {"a": {0: 0.10, 1: 0.20}, "b": {0: 0.30, 1: -0.10}}
one = simulate_naive_eqwt(rbs, capital=10_000.0, sleeves=["a", "b"])
two = simulate_naive_eqwt(rbs, capital=20_000.0, sleeves=["a", "b"])
assert math.isclose(two.metrics["end_value"], 2.0 * one.metrics["end_value"])
# total_return is capital-invariant (shape only).
assert math.isclose(two.metrics["total_return"], one.metrics["total_return"])
def test_naive_eqwt_cost_bps_reduces_return():
rbs = {"a": {0: 0.01, 1: 0.01, 2: 0.01}, "b": {0: 0.01, 1: 0.01, 2: 0.01}}
cheap = simulate_naive_eqwt(rbs, capital=1000.0, sleeves=["a", "b"], cost_bps=0.0)
pricey = simulate_naive_eqwt(rbs, capital=1000.0, sleeves=["a", "b"], cost_bps=50.0)
assert pricey.metrics["end_value"] < cheap.metrics["end_value"]
def test_naive_eqwt_start_window_restricts_dates():
rbs = {"a": {0: 0.01, 1: 0.02, 2: 0.03, 3: 0.04}}
res = simulate_naive_eqwt(rbs, capital=1000.0, sleeves=["a"], start=2)
assert res.dates == [2, 3]
def test_naive_eqwt_empty_is_graceful():
res = simulate_naive_eqwt({}, capital=1000.0, sleeves=["a"])
assert res.dates == [] and res.metrics["end_value"] == 1000.0