Files
fxhnt/tests/integration/test_forward_tracker.py
jgrusewski bb4ec3e2ae feat(surfer): wire regime kill-switch into the live combined-book forward track
The ForwardTracker now optionally throttles booked exposure off the live track's own
running drawdown: flatten to 0 once drawdown exceeds kill_dd, re-enter once it recovers
inside reenter_dd. The drawdown SIGNAL tracks shadow (un-throttled) equity so recovery is
observable while flat; the booked NAV reflects the throttled book. No-lookahead (each
day's decision uses prior-day kill-state); state is persisted (shadow_nav/shadow_peak/
killed) and lazy-inits for tracks that predate the switch.

Wired ON in both live entry points via new settings combined_book_kill_dd (0.15) /
combined_book_reenter_dd (0.07): the Dagster combined_forward_nav asset and the
forward-track CLI. Default OFF at the tracker/factory level so existing trackers and
tests are unchanged. Tests 54 passed (incl. new live-killswitch test); mypy clean on
changed lines (pre-existing debt untouched).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 23:22:15 +02:00

76 lines
3.4 KiB
Python

"""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