Files
fxhnt/tests/integration/test_compare_measured_precompute_cli.py

118 lines
5.0 KiB
Python

"""`fxhnt compare-measured-precompute` — the OWN-MEMORY writer of the measured-cost COMPARE artifact.
The per-coin measured-cost compare used to run LIVE in the cockpit web handler and OOM-killed the 4Gi
`fxhnt-dashboard` pod (exit 137). The compute now runs only here (own-memory Job), persisting
`compare_measured_cache`; the cockpit only reads it. This asserts the CLI computes + upserts the artifact,
is idempotent, and that the web handler no longer runs the heavy builder.
NO network: the operational DSN points at a temp sqlite db; the `features` warehouse + cockpit tables are
seeded on it directly.
"""
from __future__ import annotations
import datetime as dt
import inspect
from typer.testing import CliRunner
from fxhnt.cli import app
_DAY = 86_400
_AT = dt.datetime(2026, 6, 24, tzinfo=dt.UTC)
def _seed(dsn: str) -> None:
"""Seed a wide-spread Binance shadow book + sleeve returns on the `features` store, plus a Bybit
sleeve-ret series, on the temp operational DB — the inputs the precompute reads."""
from fxhnt.adapters.persistence.paper_repo import PaperRepo
from fxhnt.adapters.warehouse.timescale_feature_store import TimescaleFeatureStore
from fxhnt.domain.paper import Position
repo = PaperRepo(dsn)
repo.migrate()
store = TimescaleFeatureStore(dsn, table="features")
start = dt.date(2021, 1, 1)
days = 60
coins = ["AAAUSDT", "BBBUSDT"]
closes: dict[int, dict[str, float]] = {}
for ci, coin in enumerate(coins):
sign = 1.0 if ci % 2 == 0 else -1.0
px = 100.0
rows = []
for i in range(days):
px *= (1.0 + sign * 0.001)
half = 0.01 # 200bp wide spread
ts = (start + dt.timedelta(days=i)).toordinal() * _DAY
rows.append((ts, {"close": px, "high": px * (1 + half), "low": px * (1 - half)}))
closes.setdefault((start + dt.timedelta(days=i)).toordinal(), {})[coin] = px
store.write_features(coin, rows)
prior: list[Position] = []
from fxhnt.application.paper_book import sleeve_daily_return
for i in range(days):
d = (start + dt.timedelta(days=i)).isoformat()
close_d = closes[(start + dt.timedelta(days=i)).toordinal()]
if prior:
r = sleeve_daily_return(prior, close_d)
if r is not None:
repo.upsert_sleeve_ret(d, "crypto_tstrend", r, at=_AT)
shadow = [Position("crypto_tstrend", c, (0.5 / close_d[c]), close_d[c], d) for c in coins]
repo.replace_shadow_positions(d, shadow, at=_AT)
prior = shadow
bstart = dt.date(2021, 1, 20)
for i in range(30):
d = (bstart + dt.timedelta(days=i)).isoformat()
repo.upsert_bybit_sleeve_ret(d, "crypto_tstrend", 0.004 * (1.0 if i % 3 else -0.8), at=_AT)
store.close()
def _patch(monkeypatch, tmp_path) -> str:
dsn = f"sqlite:///{tmp_path / 'op.db'}"
monkeypatch.setenv("FXHNT_OPERATIONAL_DSN", dsn)
from fxhnt import cli as climod
climod.get_settings.cache_clear()
_seed(dsn)
return dsn
def test_cli_persists_compare_cache(monkeypatch, tmp_path) -> None:
dsn = _patch(monkeypatch, tmp_path)
res = CliRunner().invoke(app, ["compare-measured-precompute", "--capital", "100000"])
assert res.exit_code == 0, res.output
assert "persisted" in res.output.lower()
from fxhnt.adapters.persistence.paper_repo import PaperRepo
from fxhnt.application.compare_measured_cost import read_cached_measured_compare
payload = read_cached_measured_compare(PaperRepo(dsn))
assert payload is not None
assert payload["rows"] and {r["book"] for r in payload["rows"]} == {"binance_combined", "bybit_4edge"}
def test_cli_is_idempotent(monkeypatch, tmp_path) -> None:
dsn = _patch(monkeypatch, tmp_path)
runner = CliRunner()
r1 = runner.invoke(app, ["compare-measured-precompute", "--capital", "100000"])
assert r1.exit_code == 0, r1.output
from fxhnt.adapters.persistence.paper_repo import PaperRepo
from fxhnt.application.compare_measured_cost import read_cached_measured_compare
first = read_cached_measured_compare(PaperRepo(dsn))
r2 = runner.invoke(app, ["compare-measured-precompute", "--capital", "100000"])
assert r2.exit_code == 0, r2.output
second = read_cached_measured_compare(PaperRepo(dsn))
assert first is not None and second is not None
assert first["rows"] == second["rows"]
assert first["equity_a"] == second["equity_a"]
def test_web_compare_handler_has_no_live_measured_compute() -> None:
"""TRIPWIRE: the cockpit's _compare_context must NOT call the heavy `compare_measured_cost` builder —
it reads the cache / falls back. Read the create_app source and assert the live builder is absent from
the measured-read path (only `read_cached_measured_compare` is imported there)."""
from fxhnt.adapters.web import app as web_app
src = inspect.getsource(web_app.create_app)
# the measured-read path imports the READER, never the heavy builder.
assert "read_cached_measured_compare" in src
assert "import compare_measured_cost" not in src and "compare_measured_cost(" not in src