"""Generic ForwardTracker: first-run freeze, forward-only booking, idempotency, extra round-trip.""" from __future__ import annotations from fxhnt.application.forward_tracker import ForwardTracker class _Recomputable: """Recomputable-series strategy: always returns the full known series.""" def __init__(self, series: list[tuple[str, float]]) -> None: self._series = series def advance(self, last_date, extra): return list(self._series), extra class _LiveBooking: """Live-booking strategy: books one row for `today`, carries a counter in extra.""" def __init__(self, today: str, ret: float) -> None: self._today, self._ret = today, ret def advance(self, last_date, extra): n = int(extra.get("runs", 0)) + 1 return [(self._today, self._ret)], {"runs": n} def test_first_run_freezes_and_books_nothing(tmp_path) -> None: p = str(tmp_path / "s.json") st = ForwardTracker(_Recomputable([("2026-01-01", 0.01), ("2026-01-02", 0.02)]), p).step() assert st.forward_days == 0 # freeze: nothing booked on first run assert st.inception == "2026-01-02" # inception = latest known date assert st.last_date == "2026-01-02" def test_second_run_books_only_forward_days(tmp_path) -> None: p = str(tmp_path / "s.json") strat = _Recomputable([("2026-01-01", 0.01), ("2026-01-02", 0.02)]) ForwardTracker(strat, p).step() # freeze at 2026-01-02 strat._series.append(("2026-01-03", 0.05)) # a new day appears st = ForwardTracker(strat, p).step() assert st.forward_days == 1 # only 2026-01-03 booked assert abs(st.forward_return_pct - 5.0) < 1e-9 assert st.last_date == "2026-01-03" def test_idempotent_rerun_books_nothing(tmp_path) -> None: p = str(tmp_path / "s.json") strat = _Recomputable([("2026-01-01", 0.01), ("2026-01-02", 0.02), ("2026-01-03", 0.05)]) ForwardTracker(strat, p).step() # freeze at 2026-01-03 st = ForwardTracker(strat, p).step() # nothing new assert st.forward_days == 0 def test_killswitch_flattens_live_track_and_reenters(tmp_path) -> None: p = str(tmp_path / "s.json") # ramp (peak) -> crash past kill_dd -> recovery; freeze on first run, then book forward series = [(f"2026-01-{i+1:02d}", 0.01) for i in range(10)] # ramp ForwardTracker(_Recomputable(series), p, kill_dd=0.15).step() # freeze at last ramp day series += [(f"2026-02-{i+1:02d}", -0.06) for i in range(5)] # ~-26% crash -> breach st = ForwardTracker(_Recomputable(series), p, kill_dd=0.15).step() import json days = json.load(open(p))["days"] crash = [d for d in days if d["date"].startswith("2026-02")] assert any(d["ret"] == 0.0 and d["killed"] for d in crash) # flattened during crash # a flattened day books 0 even though the underlying return was -0.06 assert any(d["killed"] and d["ret"] == 0.0 for d in crash) assert st.forward_days == 5 def test_extra_carry_state_round_trips(tmp_path) -> None: p = str(tmp_path / "s.json") ForwardTracker(_LiveBooking("2026-01-01", 0.0), p).step() # freeze, extra={"runs":1} ForwardTracker(_LiveBooking("2026-01-02", 0.01), p).step() # books 2026-01-02 import json extra = json.load(open(p))["extra"] assert extra["runs"] == 2 # advance saw prior extra and incremented