Files
fxhnt/tests/integration/test_forward_engine.py
jgrusewski 595dc5da0e feat(forward): pin re-homed sleeves' recovered OOS inception in git (reproducible on DB rebuild)
The Phase-0b venue re-home reset the crypto sleeves' forward OOS clock to 2026-07-14
(definition-hash change → auto-re-inception). Since these were faithful re-homes (same
edge + PIT data, record_mode=recompute), the pre-re-home days are still valid OOS; on
2026-07-15 they were recovered by backdating the active anchors' t0 in the DB (unlock
06-20 → recon-gate PASS +4.57%; xsfunding/positioning/bybit_4edge/levered → 8d).

That backdate lived ONLY in the DB — a full operational-Postgres rebuild would let the
anchors re-incept to `today` and silently lose the recovered window. This makes it
reproducible from git (the SSOT):

- forward_definition.inception_override(sid): returns a definition's declared
  `inception_t0.date`, VERSION-PINNED (honored only when its `version` == the
  definition's current version — a genuine re-tune bumps `version`, auto-ignoring a
  stale pin and correctly getting a fresh today-clock). Lives OUTSIDE `params` so it is
  NOT hashed — declaring it never itself re-inceptions (verified: the five sleeves'
  definition_hash is byte-identical to their live DB anchors).
- run_track: at inception, t0 = inception_override(sid) if present AND <= today, else
  today (never incept in the future — clock-skew/not-yet-reached pins fall back to today).
- registry: xsfunding/unlock/positioning/bybit_4edge/bybit_4edge_levered carry their
  pinned pre-re-home t0 (07-07/06-20/07-06/07-07/07-07, version 2).

Tests: inception_override version match/stale/absent + not-hashed + a regression pin on
the recovered dates; run_track honors the pin, ignores a version-stale pin, and falls
back to today for a future pin. 262 forward/registry tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 19:31:16 +02:00

215 lines
12 KiB
Python

import datetime as dt
from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo
from fxhnt.application.forward_engine import run_track
def _repo(tmp_path):
repo = ForwardNavRepo(f"sqlite:///{tmp_path}/eng.db")
repo.migrate()
return repo
class _RecomputeStrat:
"""A recompute strategy: full series every call (independent of last_date)."""
def __init__(self, series):
self._series = series
def advance(self, last_date, extra):
return list(self._series), extra
def test_first_run_inceptions_at_today(tmp_path, monkeypatch):
repo = _repo(tmp_path)
# register a throwaway recompute track definition
from fxhnt import registry
monkeypatch.setitem(registry.STRATEGY_REGISTRY, "t_rec",
{"display_name": "T", "sleeve": "x", "gate_spec": {},
"definition": {"params": {"v": 1}, "version": 1, "record_mode": "recompute"}})
strat = _RecomputeStrat([("2026-07-06", 0.01), ("2026-07-07", 0.02), ("2026-07-08", 0.03)])
out = run_track(repo, "t_rec", strat, today="2026-07-08", at=dt.datetime(2026, 7, 8, 23, 30))
a = repo.active_anchor("t_rec")
assert a.t0 == "2026-07-08" and out["reinception"] is True
# recompute filtered to >= t0 → only the 07-08 row is booked
assert out["days"] == 1 and repo.nav_history("t_rec")[-1].date == "2026-07-08"
def test_unchanged_hash_preserves_t0_and_recomputes(tmp_path, monkeypatch):
repo = _repo(tmp_path)
from fxhnt import registry
monkeypatch.setitem(registry.STRATEGY_REGISTRY, "t_rec",
{"display_name": "T", "sleeve": "x", "gate_spec": {},
"definition": {"params": {"v": 1}, "version": 1, "record_mode": "recompute"}})
# seed an anchor at an earlier T0 by running "in the past"
strat = _RecomputeStrat([("2026-07-06", 0.01), ("2026-07-07", 0.02), ("2026-07-08", 0.03)])
run_track(repo, "t_rec", strat, today="2026-07-06", at=dt.datetime(2026, 7, 6, 23, 30))
out = run_track(repo, "t_rec", strat, today="2026-07-08", at=dt.datetime(2026, 7, 8, 23, 30))
assert repo.active_anchor("t_rec").t0 == "2026-07-06" and out["reinception"] is False
assert out["days"] == 3 # recompute over [2026-07-06, today] = all 3 rows
def test_param_change_reinceptions(tmp_path, monkeypatch):
repo = _repo(tmp_path)
from fxhnt import registry
monkeypatch.setitem(registry.STRATEGY_REGISTRY, "t_rec",
{"display_name": "T", "sleeve": "x", "gate_spec": {},
"definition": {"params": {"v": 1}, "version": 1, "record_mode": "recompute"}})
strat = _RecomputeStrat([("2026-07-06", 0.01), ("2026-07-08", 0.03)])
run_track(repo, "t_rec", strat, today="2026-07-06", at=dt.datetime(2026, 7, 6, 23, 30))
# change a param → hash changes → re-inception at today
registry.STRATEGY_REGISTRY["t_rec"]["definition"]["params"] = {"v": 2}
out = run_track(repo, "t_rec", strat, today="2026-07-08", at=dt.datetime(2026, 7, 8, 23, 30))
assert out["reinception"] is True and repo.active_anchor("t_rec").t0 == "2026-07-08"
def test_inception_honors_override_date_not_today(tmp_path, monkeypatch):
repo = _repo(tmp_path)
from fxhnt import registry
# a faithful re-home pins its pre-re-home OOS start; inception must use it, not today, so the recovered
# window is recomputed (this is what a DB rebuild reproduces from git instead of a manual anchor backdate).
monkeypatch.setitem(registry.STRATEGY_REGISTRY, "t_rec",
{"display_name": "T", "sleeve": "x", "gate_spec": {},
"definition": {"params": {"v": 1}, "version": 2, "record_mode": "recompute",
"inception_t0": {"date": "2026-06-20", "version": 2}}})
strat = _RecomputeStrat([("2026-06-20", 0.01), ("2026-07-07", 0.02), ("2026-07-08", 0.03)])
out = run_track(repo, "t_rec", strat, today="2026-07-08", at=dt.datetime(2026, 7, 8, 23, 30))
assert repo.active_anchor("t_rec").t0 == "2026-06-20" and out["reinception"] is True
assert out["days"] == 3 # recompute over [2026-06-20, today] = all 3 rows, not just today
def test_inception_ignores_stale_override_on_version_bump(tmp_path, monkeypatch):
repo = _repo(tmp_path)
from fxhnt import registry
# the override is declared for v2, but the live definition is v3 (a genuine re-tune) → the OOS clock must
# legitimately restart at today; the version-stale pin is ignored.
monkeypatch.setitem(registry.STRATEGY_REGISTRY, "t_rec",
{"display_name": "T", "sleeve": "x", "gate_spec": {},
"definition": {"params": {"v": 3}, "version": 3, "record_mode": "recompute",
"inception_t0": {"date": "2026-06-20", "version": 2}}})
strat = _RecomputeStrat([("2026-06-20", 0.01), ("2026-07-08", 0.03)])
out = run_track(repo, "t_rec", strat, today="2026-07-08", at=dt.datetime(2026, 7, 8, 23, 30))
assert repo.active_anchor("t_rec").t0 == "2026-07-08" and out["reinception"] is True
assert out["days"] == 1
def test_inception_override_in_the_future_falls_back_to_today(tmp_path, monkeypatch):
repo = _repo(tmp_path)
from fxhnt import registry
# a pin LATER than today (clock skew, or a not-yet-reached inception) must not incept in the future —
# it falls back to today so the OOS clock starts sanely, never negative.
monkeypatch.setitem(registry.STRATEGY_REGISTRY, "t_rec",
{"display_name": "T", "sleeve": "x", "gate_spec": {},
"definition": {"params": {"v": 1}, "version": 2, "record_mode": "recompute",
"inception_t0": {"date": "2026-12-31", "version": 2}}})
strat = _RecomputeStrat([("2026-07-08", 0.03)])
out = run_track(repo, "t_rec", strat, today="2026-07-08", at=dt.datetime(2026, 7, 8, 23, 30))
assert repo.active_anchor("t_rec").t0 == "2026-07-08" # today, not the future pin
assert out["days"] == 1
class _ObservedStrat:
"""An observed strategy: books TODAY on the carried book, returns updated positions."""
def __init__(self, ret_by_day):
self._r = ret_by_day
def advance(self, last_date, extra):
# book today's return; carry a trivial position
day = extra.get("_today")
return [(day, self._r[day])], {"positions": {"X": 1.0}, "_today": day}
def test_observed_mode_appends_and_carries_state(tmp_path, monkeypatch):
repo = _repo(tmp_path)
from fxhnt import registry
monkeypatch.setitem(registry.STRATEGY_REGISTRY, "t_obs",
{"display_name": "T", "sleeve": "x", "gate_spec": {},
"definition": {"params": {"v": 1}, "version": 1, "record_mode": "observed"}})
strat = _ObservedStrat({"2026-07-07": 0.001, "2026-07-08": 0.002})
# NOTE: run_track injects today into extra as "_today" for observed strategies (see impl)
run_track(repo, "t_obs", strat, today="2026-07-07", at=dt.datetime(2026, 7, 7, 23, 30))
out = run_track(repo, "t_obs", strat, today="2026-07-08", at=dt.datetime(2026, 7, 8, 23, 30))
assert repo.record_series("t_obs") == [("2026-07-07", 0.001), ("2026-07-08", 0.002)]
assert repo.get_book_state("t_obs")["positions"] == {"X": 1.0}
assert out["days"] == 2
class _CapturingObservedStrat:
"""An observed strategy that records the `extra` it was called with, so a test can assert whether
carried book-state (e.g. `positions`) was reset before booking."""
def __init__(self, ret_by_day):
self._r = ret_by_day
self.seen_extra: dict | None = None
def advance(self, last_date, extra):
self.seen_extra = dict(extra)
day = extra.get("_today")
return [(day, self._r[day])], {"positions": {"Y": 2.0}, "_today": day}
def test_observed_reinception_resets_carried_book_state(tmp_path, monkeypatch):
repo = _repo(tmp_path)
from fxhnt import registry
monkeypatch.setitem(registry.STRATEGY_REGISTRY, "t_obs",
{"display_name": "T", "sleeve": "x", "gate_spec": {},
"definition": {"params": {"v": 1}, "version": 1, "record_mode": "observed"}})
strat = _ObservedStrat({"2026-07-07": 0.001})
run_track(repo, "t_obs", strat, today="2026-07-07", at=dt.datetime(2026, 7, 7, 23, 30))
assert repo.get_book_state("t_obs")["positions"] == {"X": 1.0} # carried book-state from day 1
# param change → re-inception; the carried book-state (positions built under the OLD definition) must
# be cleared before the next booking, not carried forward into the fresh log.
registry.STRATEGY_REGISTRY["t_obs"]["definition"]["params"] = {"v": 2}
strat2 = _CapturingObservedStrat({"2026-07-08": 0.002})
run_track(repo, "t_obs", strat2, today="2026-07-08", at=dt.datetime(2026, 7, 8, 23, 30))
assert "positions" not in strat2.seen_extra # empty carry — the stale {"X": 1.0} was reset
assert repo.get_book_state("t_obs")["positions"] == {"Y": 2.0} # the new definition's own booking
def test_reinception_purges_stale_nav_rows(tmp_path, monkeypatch):
repo = _repo(tmp_path)
from fxhnt import registry
monkeypatch.setitem(registry.STRATEGY_REGISTRY, "t_rec",
{"display_name": "T", "sleeve": "x", "gate_spec": {},
"definition": {"params": {"v": 1}, "version": 1, "record_mode": "recompute"}})
strat = _RecomputeStrat([("2026-07-06", 0.01), ("2026-07-08", 0.03)])
run_track(repo, "t_rec", strat, today="2026-07-06", at=dt.datetime(2026, 7, 6, 23, 30))
assert [r.date for r in repo.nav_history("t_rec")] == ["2026-07-06", "2026-07-08"]
# param change → re-inception at t0=2026-07-08; the stale 2026-07-06 row must be purged
registry.STRATEGY_REGISTRY["t_rec"]["definition"]["params"] = {"v": 2}
run_track(repo, "t_rec", strat, today="2026-07-08", at=dt.datetime(2026, 7, 8, 23, 30))
assert [r.date for r in repo.nav_history("t_rec")] == ["2026-07-08"] # clean start, no stale row
def test_invalid_record_mode_raises_before_persisting_anchor(tmp_path, monkeypatch):
import pytest
repo = _repo(tmp_path)
from fxhnt import registry
monkeypatch.setitem(registry.STRATEGY_REGISTRY, "t_bad",
{"display_name": "T", "sleeve": "x", "gate_spec": {},
"definition": {"params": {"v": 1}, "version": 1, "record_mode": "bogus"}})
with pytest.raises(ValueError):
run_track(repo, "t_bad", _RecomputeStrat([]), today="2026-07-08", at=dt.datetime(2026, 7, 8))
assert repo.active_anchor("t_bad") is None # no broken anchor persisted
def test_anchor_loss_fails_loud_not_silent_reinception(tmp_path, monkeypatch):
import pytest
from sqlalchemy.orm import Session
from fxhnt.adapters.persistence.cockpit_models import StrategyAnchorRow
repo = _repo(tmp_path)
from fxhnt import registry
monkeypatch.setitem(registry.STRATEGY_REGISTRY, "t_rec",
{"display_name": "T", "sleeve": "x", "gate_spec": {},
"definition": {"params": {"v": 1}, "version": 1, "record_mode": "recompute"}})
strat = _RecomputeStrat([("2026-07-06", 0.01)])
run_track(repo, "t_rec", strat, today="2026-07-06", at=dt.datetime(2026, 7, 6, 23, 30)) # inception + record
# simulate anchor-row loss (bad DB restore) while the forward record persists
with Session(repo._engine) as s:
s.query(StrategyAnchorRow).filter_by(strategy_id="t_rec").delete()
s.commit()
with pytest.raises(RuntimeError):
run_track(repo, "t_rec", strat, today="2026-07-07", at=dt.datetime(2026, 7, 7, 23, 30))
# and confirm no fresh anchor was written by the failed call
assert repo.active_anchor("t_rec") is None