From 75006af0ef07b1c06a4dcea30a4fa78d3e552e12 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Thu, 18 Jun 2026 23:41:38 +0200 Subject: [PATCH] =?UTF-8?q?fix(xsfunding):=20runner=20robust=20to=20panel?= =?UTF-8?q?=20gaps=20=E2=80=94=20use=20last-known=20qvol=20for=20cost=20(K?= =?UTF-8?q?eyError=20on=20real=20data)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- src/fxhnt/application/funding_backtest_runner.py | 4 +--- tests/integration/test_funding_backtest_runner.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/fxhnt/application/funding_backtest_runner.py b/src/fxhnt/application/funding_backtest_runner.py index e093fbb..6a7a2f7 100644 --- a/src/fxhnt/application/funding_backtest_runner.py +++ b/src/fxhnt/application/funding_backtest_runner.py @@ -64,9 +64,7 @@ class FundingBacktestRunner: borrowable = {s for s in elig if self._p[s].get(d, (0.0, 0.0, 0.0))[1] >= self._borrow_q} funding_next = {s: self._p[s][nxt][2] for s in elig if nxt in self._p[s]} held = set().union(*(prev_w[m] for m in _MODES)) if any(prev_w[m] for m in _MODES) else set() - cost = {s: self._coin_cost(self._p[s][d][1]) for s in elig} - for s in held - set(cost): - cost[s] = self._coin_cost(self._last_qvol(s, d)) # exit cost for dropped-out holdings + cost = {s: self._coin_cost(self._last_qvol(s, d)) for s in set(elig) | held} names_seen += len(elig) n_rebals += 1 for m in _MODES: diff --git a/tests/integration/test_funding_backtest_runner.py b/tests/integration/test_funding_backtest_runner.py index 51eb91e..a9e0d68 100644 --- a/tests/integration/test_funding_backtest_runner.py +++ b/tests/integration/test_funding_backtest_runner.py @@ -88,6 +88,20 @@ def test_dropped_out_holding_charged_nonzero_exit_cost(monkeypatch): assert exit_cost == 0.0008 + 0.005 # cost_bps 8 -> 8e-4 ; slip_cap 50bps -> 5e-3 +def test_runner_handles_panel_gaps_without_keyerror(): + panel = {} + fund = {"HI": 0.0015, "MID": 0.0008, "LO": 0.0001, "NEG": -0.0010} + for sym, f in fund.items(): + panel[sym] = {1000 + i: (1.0, 5e6, f) for i in range(200)} + # GAPCOIN: liquid + long history but MISSING several interior union-days (gaps) and stops early (delists) + panel["GAP"] = {1000 + i: (1.0, 5e6, 0.0012) for i in range(200) if i not in (50, 51, 120) and i < 180} + runner = FundingBacktestRunner(panel, min_qvol=1e6, min_history=20, lookback_days=7, + quantile=0.5, cost_bps=8.0, slip_coef=0.0005) + res = runner.run() # must NOT raise KeyError + for series in res.returns_by_mode.values(): + assert len(series) > 50 and np.isfinite(series).all() + + def test_book_carry_and_turnover(): # carry on the NEW book = sum w * funding_next w = {"A": 0.5, "B": -0.5}