Files
fxhnt/tests/integration/test_backtest_equity_cli.py
2026-06-18 09:09:01 +02:00

86 lines
3.4 KiB
Python

"""B3b: the `backtest-equity` CLI entrypoint runs the walk-forward equity backtest against the
DEDICATED backtest warehouse (settings.backtest_warehouse_path), evaluates the 3 price-only
constructions through the gauntlet, and writes a JSON report. No network: a synthetic warehouse
is built and the command is pointed at it via FXHNT_BACKTEST_WAREHOUSE_PATH."""
from __future__ import annotations
import datetime as dt
import json
from typer.testing import CliRunner
from fxhnt.adapters.warehouse.duckdb_feature_store import DuckDbFeatureStore
_SPD = 86_400
def _build_warehouse(path: str) -> DuckDbFeatureStore:
"""3 names over ~2 years of daily bars (mirrors the runner integration test): AAA strong uptrend,
BBB mild, CCC downtrend — a non-degenerate cross-section for long/ls/tilt."""
store = DuckDbFeatureStore(path)
start = dt.date(2022, 1, 3)
n = 520 # ~2 trading years
drifts = {"AAA": 0.0015, "BBB": 0.0003, "CCC": -0.0010}
items = []
members = []
for sym, dr in drifts.items():
rows = []
px = 100.0
for i in range(n):
day = start + dt.timedelta(days=i)
px *= (1.0 + dr)
ts = (day - dt.date(1970, 1, 1)).days * _SPD
rows.append((ts, {"adjclose": px, "close": px, "volume": 1_000_000.0}))
items.append((sym, rows))
members.append((sym, "NYSE", "2000-01-01", "2026-06-01"))
store.write_features_bulk(items)
store.upsert_membership(members)
return store
def test_cli_backtest_equity_writes_report(tmp_path, monkeypatch) -> None:
import fxhnt.cli as cli
wh = str(tmp_path / "wh.duckdb")
_build_warehouse(wh).close()
out = tmp_path / "report.json"
monkeypatch.setenv("FXHNT_BACKTEST_WAREHOUSE_PATH", wh)
cli.get_settings.cache_clear() # settings are lru_cached — pick up the env override
res = CliRunner().invoke(cli.app, ["backtest-equity", "--n", "10", "--out", str(out)])
cli.get_settings.cache_clear()
assert res.exit_code == 0, res.output
assert out.exists(), "backtest-equity must write the report JSON at --out"
report = json.loads(out.read_text())
assert set(report["constructions"]) == {"long", "ls", "tilt"}
for block in report["constructions"].values():
assert {"stats", "verdict"} <= set(block)
assert {"passed", "dsr", "is_sharpe", "oos_sharpe"} <= set(block["verdict"])
def test_cli_backtest_equity_bounded_candidate_flags(tmp_path, monkeypatch) -> None:
"""The bounded memory pre-filter flags (--candidate-top-k / --candidate-min-history) are exposed
and passed to the runner; with thresholds the 3 synthetic names qualify -> full report."""
import fxhnt.cli as cli
wh = str(tmp_path / "wh.duckdb")
_build_warehouse(wh).close()
out = tmp_path / "report.json"
monkeypatch.setenv("FXHNT_BACKTEST_WAREHOUSE_PATH", wh)
cli.get_settings.cache_clear() # settings are lru_cached — pick up the env override
res = CliRunner().invoke(
cli.app,
[
"backtest-equity", "--n", "10", "--out", str(out),
"--candidate-top-k", "100", "--candidate-min-history", "10",
],
)
cli.get_settings.cache_clear()
assert res.exit_code == 0, res.output
assert out.exists(), "backtest-equity must write the report JSON at --out"
report = json.loads(out.read_text())
assert set(report["constructions"]) == {"long", "ls", "tilt"}