Files
fxhnt/tests/unit/test_nav_summary.py
jgrusewski 375cd108bc feat(0b): remove the vestigial dual-venue PaperRepo abstraction (Task 7f)
Task 7e only renamed venue="binance" -> "legacy" to pass the substring guard;
the dual-venue repo machinery was left in place even though bybit is the ONLY
live venue. PaperRepo now hardcodes every discriminated read/write to a
_BYBIT_VENUE = "bybit" module constant instead of a constructor venue param
(removed) -- reads/writes ONLY the bybit rows, so the orphaned legacy/binance
rows from the retired combined-crypto book (Task 7d) are never touched.
nav_summary's venue arg is dropped the same way (every caller always passed
"bybit"). cockpit_models keeps the venue column (dropping it from the PK is a
separate prod schema migration, documented in the report for the merge
runbook) with reworded comments reflecting the single-venue reality.

Test fallout: deleted test_paper_repo_venue.py (tested the now-removed
dual-venue isolation) and 3 "does-not-touch-binance" tests that constructed
PaperRepo(venue="binance"); mechanically dropped the venue kwarg everywhere
else. Added a guard assertion that PaperRepo.__init__ has no venue param.
Full suite green (1853 passed); binance substring guard still 0 hits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 15:20:57 +02:00

83 lines
3.4 KiB
Python

"""refresh_nav_summary: MATERIALIZE the bybit headline nav-summary (stats + pre-rendered curve) from the
persisted paper_nav series, keyed by the nav's latest run_date (the version). The stored payload must be
byte-for-byte identical to the live `_overview_headline` compute (same `nav_backfill_stats` + `nav_sparkline`
helpers). In-memory SQLite — no network, no Postgres."""
from __future__ import annotations
import datetime as dt
from fxhnt.adapters.persistence.paper_repo import PaperRepo
from fxhnt.adapters.web.charts import nav_sparkline
from fxhnt.application.nav_stats import nav_backfill_stats
from fxhnt.application.nav_summary import refresh_nav_summary
_AT = dt.datetime(2026, 6, 24, 12, 0)
_PTS = [100_000.0, 140_000.0, 120_000.0, 210_000.0, 300_000.0, 360_000.0, 393_955.0]
def _seeded_repo(pts: list[float] = _PTS) -> PaperRepo:
repo = PaperRepo("sqlite://")
repo.migrate()
for i, eq in enumerate(pts):
rd = dt.date(2021, 1, 1) + dt.timedelta(days=120 * i)
repo.upsert_nav(rd.isoformat(), capital=100_000.0, realized=0.0, unrealized=0.0, equity=eq, at=_AT)
return repo
def test_refresh_matches_live_stats_and_curve() -> None:
repo = _seeded_repo()
payload = refresh_nav_summary(repo)
# Live compute via the EXACT helpers _overview_headline uses, on the same nav.
nav = repo.nav_history()
equities = [p.equity for p in nav]
stats = nav_backfill_stats(equities)
assert payload["final_equity"] == stats.final_equity
assert payload["total_return_pct"] == stats.total_return_pct
assert payload["sharpe"] == stats.sharpe
assert payload["max_dd_pct"] == stats.max_dd_pct
assert payload["curve_svg"] == nav_sparkline(equities, width=600, height=160)
assert payload["since"] == "2021"
assert payload["has_record"] is True
assert payload["nav_run_date"] == nav[-1].run_date
def test_read_back_equals_written_payload() -> None:
repo = _seeded_repo()
written = refresh_nav_summary(repo)
read = repo.read_nav_summary()
assert read is not None
for k in ("nav_run_date", "final_equity", "total_return_pct", "sharpe", "max_dd_pct",
"since", "has_record", "curve_svg"):
assert read[k] == written[k], k
def test_version_is_latest_run_date_and_goes_stale_on_new_nav() -> None:
repo = _seeded_repo()
refresh_nav_summary(repo)
# Fresh: stored version == the live latest run_date.
assert repo.read_nav_summary()["nav_run_date"] == repo.latest_nav_run_date()
# A new nav day → the stored summary is now stale (its version no longer matches).
repo.upsert_nav("2024-01-01", capital=100_000.0, realized=0.0, unrealized=0.0, equity=420_000.0, at=_AT)
assert repo.read_nav_summary()["nav_run_date"] != repo.latest_nav_run_date()
# Re-refresh re-matches and picks up the new final equity.
refresh_nav_summary(repo)
assert repo.read_nav_summary()["nav_run_date"] == repo.latest_nav_run_date()
assert repo.read_nav_summary()["final_equity"] == 420_000.0
def test_empty_nav_is_zeroed_summary() -> None:
repo = PaperRepo("sqlite://")
repo.migrate()
payload = refresh_nav_summary(repo)
assert payload["nav_run_date"] == ""
assert payload["since"] == ""
assert payload["has_record"] is False
assert payload["curve_svg"] == ""
assert payload["final_equity"] == 0.0
def test_missing_summary_reads_none() -> None:
repo = _seeded_repo()
assert repo.read_nav_summary() is None # nothing stored until refresh runs