Files
fxhnt/tests/unit/test_backtest_refs.py
jgrusewski fa70f06416 fix(backtest): model-ref handler returns None (no mislabeled ref) + vrp_exec ref alias
The model-ref branch in backtest_ref() delegated to backtest_ref(model_id, ...),
which is dead today but would silently overwrite the real model's backtest_summary
row under a mislabeled sid if Task 6 generalizes the caller to pass the exec twin's
own builder. Executed tracks reconcile via CockpitBacktestRefProvider._REF_ALIAS at
the gate layer, not by writing their own ref here, so return None instead.

Also add the missing vrp_exec -> vrp alias entry so vrp_exec's model-ref label is
functionally true (otherwise its gate would WAIT forever once armed).

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

74 lines
3.5 KiB
Python

from fxhnt.application.backtest_refs import backtest_ref, registry_backtest_kind
class _ReplayStrat:
def advance(self, last, extra):
# a 3-day inception series -> a real BacktestSummary
return [("2024-01-01", 0.01), ("2024-01-02", -0.005), ("2024-01-03", 0.008)], {}
def test_recompute_replay_produces_summary():
sm = backtest_ref("multistrat", store=None, build_strategy=lambda: _ReplayStrat())
assert sm is not None
assert sm.strategy_id == "multistrat"
assert sm.sharpe == sm.sharpe # finite (not NaN)
def test_none_kind_returns_none():
# sixtyforty is absolute-gated -> backtest.kind == "none" -> no ref
assert registry_backtest_kind("sixtyforty") == "none"
assert backtest_ref("sixtyforty", store=None) is None
def test_model_ref_kind_never_writes_its_own_summary():
# multistrat_exec is backtest.kind == "model-ref": it reconciles against its MODELED counterpart's ref,
# resolved at the gate layer (CockpitBacktestRefProvider._REF_ALIAS), NOT by recomputing/delegating here.
# Regression: an earlier version delegated to backtest_ref(model_id, ...), which — if this module's caller
# is ever generalized to pass the exec twin's OWN builder — would silently overwrite the real model's
# backtest_summary row under a mislabeled sid. Must return None regardless of build_strategy.
assert registry_backtest_kind("multistrat_exec") == "model-ref"
sm = backtest_ref("multistrat_exec", store=None, build_strategy=lambda: _ReplayStrat())
assert sm is None
def _epoch_day(iso: str) -> int:
import datetime as _dt
return (_dt.date.fromisoformat(iso) - _dt.date(1970, 1, 1)).days
def test_xsfunding_sleeve_produces_ref_bug_A(monkeypatch):
"""Regression for the live bug: xsfunding is reconciliation-gated but had NO backtest ref (gate stuck at
WAIT forever). Its `sleeve` provider must yield a BacktestSummary from the funding panel — via the REAL
`sleeve_returns_from_store(store, edge, ...) -> {epoch_day: ret}` contract (store FIRST, edge SECOND;
NOT `sleeve_returns_from_store(edge, store) -> [(iso_date, ret), ...]` as an earlier draft assumed)."""
import fxhnt.application.backtest_refs as mod
import fxhnt.application.bybit_book_eval as bbe
monkeypatch.setattr(mod, "registry_backtest_kind", lambda sid: "sleeve")
sentinel_store = object()
calls: list[tuple[object, str]] = []
# deterministic 3-day {epoch_day: ret} series, inserted OUT OF epoch-day order so the test also catches a
# missing/incorrect sort in the epoch_day -> iso_date row conversion.
fake_series = {
_epoch_day("2024-03-03"): 0.003,
_epoch_day("2024-03-01"): 0.002,
_epoch_day("2024-03-02"): 0.0015,
}
def _fake_sleeve_returns_from_store(store, edge, **kwargs):
calls.append((store, edge))
return dict(fake_series)
monkeypatch.setattr(bbe, "sleeve_returns_from_store", _fake_sleeve_returns_from_store)
sm = mod.backtest_ref("xsfunding", store=sentinel_store)
assert sm is not None and sm.strategy_id == "xsfunding"
# the signature fix: store passed FIRST, the strategy_id (edge) SECOND — mirrors
# funding_dispersion_eval's sleeve_returns_from_store(bybit_store, "xsfunding", ...) call convention.
assert calls == [(sentinel_store, "xsfunding")]
# the {epoch_day: ret} dict was sorted by epoch_day before conversion to (iso_date, ret) rows: the last
# row (the summary's `as_of`) must be the LATEST date, not the last-inserted dict entry.
assert sm.as_of == "2024-03-03"