"""The forward read-model DTOs and the state_reader that normalizes heterogeneous tracker state files.""" from __future__ import annotations import json import math from fxhnt.adapters.persistence.state_reader import ForwardStateReader from fxhnt.application.forward_models import ForwardNavRow, ForwardSummary def test_forward_summary_total_return_pct() -> None: s = ForwardSummary(strategy_id="x", as_of="2026-06-12", days=7, nav=1.05, total_return=0.05, sharpe=1.2, maxdd=-0.04) assert round(s.total_return_pct, 2) == 5.0 def test_forward_nav_row_is_immutable() -> None: r = ForwardNavRow(strategy_id="x", date="2026-06-05", ret=0.01, nav=1.01) assert r.strategy_id == "x" and r.nav == 1.01 def _write(tmp_path, name, obj): p = tmp_path / f"{name}.json" p.write_text(json.dumps(obj)) return p def test_reads_days_list_family(tmp_path) -> None: _write(tmp_path, "fxhnt_combined_forward", { "inception": "2026-06-04", "last_date": "2026-06-06", "nav": 1.0201, "days": [ {"date": "2026-06-05", "ret": 0.01, "nav": 1.01}, {"date": "2026-06-06", "ret": 0.0099, "nav": 1.0201}, ], }) reader = ForwardStateReader() summary, rows = reader.read(tmp_path / "fxhnt_combined_forward.json", "combined") assert summary.strategy_id == "combined" assert summary.days == 2 assert summary.as_of == "2026-06-06" assert math.isclose(summary.nav, 1.0201, rel_tol=1e-9) assert [r.date for r in rows] == ["2026-06-05", "2026-06-06"] assert all(r.strategy_id == "combined" for r in rows) def test_reads_welford_equity_family(tmp_path) -> None: _write(tmp_path, "multistrat_state", { "last_date": "2026-06-12", "days": 5, "sum_r": -0.0007245543702343088, "sumsq_r": 1.8807003187214318e-05, "equity": 0.9992663073361069, "peak": 1.001482883251981, "max_dd": -0.004599760548340659, }) reader = ForwardStateReader() summary, rows = reader.read(tmp_path / "multistrat_state.json", "multistrat") assert summary.days == 5 assert summary.as_of == "2026-06-12" assert math.isclose(summary.nav, 0.9992663073361069, rel_tol=1e-9) assert math.isclose(summary.total_return, -0.0007336926638931, rel_tol=1e-6) assert math.isclose(summary.maxdd, -0.004599760548340659, rel_tol=1e-9) # snapshot-only: one forward point at as_of with nav == equity assert len(rows) == 1 and rows[0].date == "2026-06-12" assert math.isclose(rows[0].nav, 0.9992663073361069, rel_tol=1e-9) def test_reads_cumulative_funding_family(tmp_path) -> None: _write(tmp_path, "crossvenue_state", { "positions": {"LAB": 0.2, "TON": 0.2}, "cum_gross": 0.0046, "cum_net": -6.11e-05, "days": 8, "last_run_date": "2026-06-14", }) reader = ForwardStateReader() summary, rows = reader.read(tmp_path / "crossvenue_state.json", "crossvenue") assert summary.days == 8 assert summary.as_of == "2026-06-14" assert math.isclose(summary.total_return, -6.11e-05, rel_tol=1e-9) assert math.isclose(summary.nav, 1.0 - 6.11e-05, rel_tol=1e-9) assert summary.sharpe == 0.0 # not derivable from cumulative-only assert len(rows) == 1 and rows[0].date == "2026-06-14" def test_unknown_shape_raises_valueerror(tmp_path) -> None: _write(tmp_path, "weird", {"foo": 1}) reader = ForwardStateReader() try: reader.read(tmp_path / "weird.json", "weird") assert False, "expected ValueError" except ValueError: pass def test_welford_sample_variance_sharpe(tmp_path) -> None: """Verify _welford uses SAMPLE variance (/(n-1)) matching the days-list _sharpe path. Fixture: days=2, r1=0.01, r2=0.03 sum_r = 0.04 sumsq_r = 0.01**2 + 0.03**2 = 0.001 mean = 0.02 sample var = (0.001 - 0.04**2/2) / (2-1) = (0.001 - 0.0008) / 1 = 0.0002 std = sqrt(0.0002) sharpe = (0.02 / sqrt(0.0002)) * sqrt(365) """ sum_r = 0.04 sumsq_r = 0.01 ** 2 + 0.03 ** 2 # = 0.001 mean = sum_r / 2 sample_var = (sumsq_r - sum_r ** 2 / 2) / (2 - 1) expected_sharpe = mean / math.sqrt(sample_var) * math.sqrt(365.0) _write(tmp_path, "welford_fixture", { "last_date": "2026-06-10", "days": 2, "sum_r": sum_r, "sumsq_r": sumsq_r, "equity": 1.04, "max_dd": 0.0, }) reader = ForwardStateReader() summary, _ = reader.read(tmp_path / "welford_fixture.json", "w") assert summary.sharpe > 0, "sharpe must be positive for two positive returns" assert math.isclose(summary.sharpe, expected_sharpe, rel_tol=1e-9)