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.
129 lines
6.5 KiB
Python
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
|