86 lines
3.4 KiB
Python
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"}
|