"""Integration tests for the cross-sectional crypto PRICE-factor walk-forward runner + gauntlet matrix. Synthetic panels with a built-in edge; no I/O, no live network.""" import json from functools import partial import numpy as np from fxhnt.application.crypto_factor_runner import ( CryptoFactorRunner, FactorRunResult, evaluate_factor_matrix, ) from fxhnt.domain.crypto_factors import ( xs_illiquidity_score, xs_lowvol_score, xs_reversal_score, ) Panel = dict[str, dict[int, tuple[float, float, float]]] def _lowvol_edge_panel(n_days: int = 120) -> Panel: """Low-vol coins drift UP smoothly; high-vol coins oscillate around flat with no drift. The low-vol factor's long_tilt should therefore make money.""" panel: Panel = {} # calm uptrenders: tiny noise, positive drift for sym, drift in {"CALM1": 0.004, "CALM2": 0.0035, "CALM3": 0.003}.items(): closes = [] p = 100.0 for i in range(n_days): p *= (1.0 + drift + 0.0003 * ((-1) ** i)) closes.append(p) panel[sym] = {1000 + i: (closes[i], 5e6, 0.0) for i in range(n_days)} # wild flat coins: big oscillation, no net drift for sym, amp in {"WILD1": 8.0, "WILD2": 10.0, "WILD3": 12.0}.items(): panel[sym] = {1000 + i: (100.0 + amp * ((-1) ** i), 5e6, 0.0) for i in range(n_days)} return panel def _generic_panel(n_days: int = 120) -> Panel: panel: Panel = {} rng = np.random.default_rng(7) for k, sym in enumerate(["A", "B", "C", "D", "E", "F"]): p = 100.0 closes = [] for _ in range(n_days): p *= (1.0 + float(rng.normal(0.0005 * k, 0.01))) closes.append(max(1.0, p)) qv = 1e5 * (k + 1) ** 2 panel[sym] = {1000 + i: (closes[i], qv, 0.0) for i in range(n_days)} return panel def test_lowvol_factor_long_tilt_has_positive_edge(): runner = CryptoFactorRunner( _lowvol_edge_panel(), partial(xs_lowvol_score, window=30), min_qvol=1e6, min_history=20, quantile=0.5, cost_bps=0.0, slip_coef=0.0, ) res = runner.run() assert isinstance(res, FactorRunResult) assert set(res.returns_by_mode) == {"long_tilt", "market_neutral"} for series in res.returns_by_mode.values(): assert len(series) > 30 and np.isfinite(series).all() # the built-in low-vol edge => the long_tilt series is net positive assert float(np.sum(res.returns_by_mode["long_tilt"])) > 0.0 assert res.n_rebalances == len(res.returns_by_mode["long_tilt"]) assert res.n_names_avg > 0.0 def test_runner_books_price_returns_not_funding(): # funding is nonzero but the runner must book PRICE returns; a flat-price panel with # nonzero funding must produce ~zero return (proving it is NOT reading funding). panel: Panel = {} for sym in ["A", "B", "C", "D"]: panel[sym] = {1000 + i: (100.0, 5e6, 0.05) for i in range(60)} # huge funding, flat price res = CryptoFactorRunner( panel, partial(xs_lowvol_score, window=30), min_qvol=1e6, min_history=20, quantile=0.5, cost_bps=0.0, slip_coef=0.0, ).run() assert abs(float(np.sum(res.returns_by_mode["long_tilt"]))) < 1e-9 def test_runner_deterministic(): mk = lambda: CryptoFactorRunner( _lowvol_edge_panel(), partial(xs_lowvol_score, window=30), min_qvol=1e6, min_history=20, quantile=0.5, cost_bps=8.0, slip_coef=0.0005, ).run().returns_by_mode["long_tilt"] assert np.array_equal(mk(), mk()) def test_evaluate_factor_matrix_runs_all_cells(): factors = { "reversal": partial(xs_reversal_score, lookback=3), "lowvol": partial(xs_lowvol_score, window=30), "illiquidity": partial(xs_illiquidity_score, window=30), } report = evaluate_factor_matrix( _generic_panel(), factors, floors=[1e4, 1e6], quantile=0.5, cost_bps=8.0, slip_coef=0.0, oos_fraction=0.4, ) # 3 factors x 2 floors x 2 modes = 12 cells assert len(report["cells"]) == 12 assert report["n_trials"] == 12 for cell in report["cells"].values(): assert {"factor", "mode", "floor", "stats", "verdict"} <= set(cell) assert {"sharpe", "cagr", "max_drawdown", "n_obs"} <= set(cell["stats"]) assert {"passed", "dsr", "is_sharpe", "oos_sharpe", "n_trials"} <= set(cell["verdict"]) assert cell["verdict"]["n_trials"] == 12 json.dumps(report) # must be JSON-serializable (no NaN/np types) def test_evaluate_factor_matrix_n_trials_equals_total_cells(): factors = {"lowvol": partial(xs_lowvol_score, window=30)} report = evaluate_factor_matrix( _generic_panel(), factors, floors=[1e4], quantile=0.5, cost_bps=8.0, slip_coef=0.0, oos_fraction=0.4, ) # 1 factor x 1 floor x 2 modes = 2 cells assert report["n_trials"] == 2 assert len(report["cells"]) == 2 json.dumps(report)