Files
fxhnt/tests/integration/test_forward_migration.py
jgrusewski d7a9c32690 feat(0b): retire crypto_tstrend standalone track + stablecoin_rotation (not tradeable on Bybit); book sleeve unchanged
Neither is tradeable on Bybit: crypto_tstrend as a standalone Binance-perp forward
track was a -0.21 marginal-Sharpe drag/crash-amplifier, and stablecoin_rotation's
FDUSD/USDP pairs aren't listed on Bybit. Removes the two *_nav assets, their
registry entries, the orphaned migration_builders builders, and the
Binance-only StableRotationForward wrapper (stablecoin_runner.py's
StableReversionRunner stays -- it's still live via the Bybit stablecoin eval
paths). The crypto_tstrend SLEEVE inside the bybit_4edge deploy book
(_DEFAULT_BYBIT_SLEEVES, Bybit data) is untouched.

Updates dependent tests: deletes 5 whose subject (the retired asset/registry
entry/module) no longer exists, and swaps the retired sid for a still-registered
one (unlock/xsfunding/sixtyforty) in tests that only used crypto_tstrend as a
generic example sid.
2026-07-13 21:56:23 +02:00

129 lines
6.5 KiB
Python

import datetime as dt
from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo
from fxhnt.application.forward_migration import migrate_track
def _repo(tmp_path):
repo = ForwardNavRepo(f"sqlite:///{tmp_path}/mig.db")
repo.migrate()
return repo
class _RecomputeStrat:
def __init__(self, series):
self._series = series
def advance(self, last_date, extra):
return list(self._series), extra
def _reg(monkeypatch, sid, mode):
from fxhnt import registry
monkeypatch.setitem(registry.STRATEGY_REGISTRY, sid,
{"display_name": "T", "sleeve": "x", "gate_spec": {},
"definition": {"params": {"v": 1}, "version": 1, "record_mode": mode}})
def test_recompute_migration_preserves_t0_on_match(tmp_path, monkeypatch):
repo = _repo(tmp_path)
_reg(monkeypatch, "t_rec", "recompute")
series = [("2026-07-06", 0.01), ("2026-07-07", 0.02)]
# accumulated state whose nav MATCHES the recompute over [inception, ...]
nav = 1.0
days = []
for d, r in series:
nav *= 1 + r
days.append({"date": d, "ret": r, "nav": nav})
state = {"inception": "2026-07-06", "last_date": "2026-07-07", "nav": nav, "days": days, "extra": {}}
out = migrate_track(repo, "t_rec", state, _RecomputeStrat(series), today="2026-07-07",
at=dt.datetime(2026, 7, 7, 23, 30))
assert out["status"] == "match" and repo.active_anchor("t_rec").t0 == "2026-07-06"
def test_recompute_migration_flags_divergence(tmp_path, monkeypatch):
repo = _repo(tmp_path)
_reg(monkeypatch, "t_rec", "recompute")
series = [("2026-07-06", 0.01), ("2026-07-07", 0.02)]
# accumulated state that DRIFTED (wrong nav) → divergence flagged
state = {"inception": "2026-07-06", "last_date": "2026-07-07", "nav": 5.0,
"days": [{"date": "2026-07-06", "ret": 0.5, "nav": 1.5},
{"date": "2026-07-07", "ret": 2.0, "nav": 4.5}], "extra": {}}
out = migrate_track(repo, "t_rec", state, _RecomputeStrat(series), today="2026-07-07",
at=dt.datetime(2026, 7, 7, 23, 30))
assert out["status"] == "diverged" and out["max_abs_nav_diff"] > 0.02
def test_recompute_missing_dates_flags_divergence(tmp_path, monkeypatch):
repo = _repo(tmp_path)
_reg(monkeypatch, "t_rec", "recompute")
state = {"inception": "2026-07-06", "last_date": "2026-07-07", "nav": 100.0,
"days": [{"date": "2026-07-06", "ret": 9.0, "nav": 10.0},
{"date": "2026-07-07", "ret": 9.0, "nav": 100.0}], "extra": {}}
out = migrate_track(repo, "t_rec", state, _RecomputeStrat([]), today="2026-07-07",
at=dt.datetime(2026, 7, 7, 23, 30))
assert out["status"] == "diverged" and out["missing_dates"] == 2
def test_recompute_window_bounded_by_today(tmp_path, monkeypatch):
repo = _repo(tmp_path)
_reg(monkeypatch, "t_rec", "recompute")
series = [("2026-07-06", 0.01), ("2026-07-07", 0.02), ("2026-07-09", 0.5)] # 07-09 is beyond today
nav = 1.0
days = []
for d, r in series[:2]:
nav *= 1 + r
days.append({"date": d, "ret": r, "nav": nav})
state = {"inception": "2026-07-06", "last_date": "2026-07-07", "nav": nav, "days": days, "extra": {}}
out = migrate_track(repo, "t_rec", state, _RecomputeStrat(series), today="2026-07-07",
at=dt.datetime(2026, 7, 7, 23, 30))
assert out["status"] == "match" # the future 07-09 row is excluded from the reconcile window
def test_observed_migration_imports_verbatim(tmp_path, monkeypatch):
repo = _repo(tmp_path)
_reg(monkeypatch, "t_obs", "observed")
state = {"inception": "2026-07-06", "last_date": "2026-07-07", "nav": 1.03,
"days": [{"date": "2026-07-06", "ret": 0.01}, {"date": "2026-07-07", "ret": 0.02}],
"extra": {"positions": {"BTCUSDT": 0.5}}}
out = migrate_track(repo, "t_obs", state, _RecomputeStrat([]), today="2026-07-07",
at=dt.datetime(2026, 7, 7, 23, 30))
assert out["status"] == "imported" and out["n"] == 2
assert repo.record_series("t_obs") == [("2026-07-06", 0.01), ("2026-07-07", 0.02)]
assert repo.get_book_state("t_obs") == {"positions": {"BTCUSDT": 0.5}}
def test_recompute_track_migration_seeds_anchor_when_forward_summary_exists_but_no_anchor(tmp_path):
"""CRITICAL 1 regression (general, checked on a recompute track): production may have a pre-branch
forward_summary row (the old ingest wrote all tracks) but no anchor row. If a recompute track has no
entry in `track_builders`, `migrate-forward-anchors` skips it and the anchor is never seeded — the next
engine run's `active_anchor(sid)` is None while `has_forward_history(sid)` is True, tripping the
lost-anchor guard in `forward_engine.run_track` and crashing that track's nightly asset. Assert the
recompute track is in the builders map, and that running `migrate_track` on the crash precondition
(forward_summary row present, no anchor) seeds an anchor — so a subsequent engine run will not raise."""
from fxhnt.adapters.orchestration.migration_builders import track_builders
from fxhnt.application.forward_models import ForwardSummary
from fxhnt.config import get_settings
repo = _repo(tmp_path)
sid = "sixtyforty"
assert sid in track_builders(get_settings(), str(tmp_path)) # the CRITICAL-1 fix: recompute track has a builder
# seed the pre-branch forward_summary row (old ingest wrote all tracks) — no anchor exists yet.
repo.upsert_summary(
ForwardSummary(strategy_id=sid, as_of="2026-07-06", days=10, nav=1.05,
total_return=0.05, sharpe=1.0, maxdd=-0.02),
gate_status="WAIT", gate_reason="pre-branch", at=dt.datetime(2026, 7, 6, 23, 30))
assert repo.has_forward_history(sid) is True
assert repo.active_anchor(sid) is None # the exact crash precondition
series = [("2026-07-06", 0.01), ("2026-07-07", 0.02)]
state = {"inception": "2026-07-06", "last_date": "2026-07-07", "nav": 1.03,
"days": [{"date": "2026-07-06", "ret": 0.01, "nav": 1.01},
{"date": "2026-07-07", "ret": 0.02, "nav": 1.03}], "extra": {}}
migrate_track(repo, sid, state, _RecomputeStrat(series), today="2026-07-07",
at=dt.datetime(2026, 7, 7, 23, 30))
# anchor now exists — a subsequent engine run's active_anchor(sid) is no longer None, so the
# lost-anchor guard (active is None and has_forward_history) will not trip.
assert repo.active_anchor(sid) is not None