Files
fxhnt/tests/unit/test_state_reader.py
2026-06-18 20:56:50 +02:00

115 lines
4.5 KiB
Python

"""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, "funding_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 / "funding_state.json", "funding")
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)