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>
83 lines
3.4 KiB
Python
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
|