"""pytest for the Python layer (requires the compiled core on sys.path: build with -DRAID_BUILD_PYTHON=ON).""" import os, sys import numpy as np import pytest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) raid = pytest.importorskip("raid") if not raid.HAS_CORE: pytest.skip("raid._core not built", allow_module_level=True) def test_devices_has_cpu(): kinds = [d["kind"] for d in raid.devices()] assert "cpu" in kinds def test_ols_matches_statsmodels_hc3(): sm = pytest.importorskip("statsmodels.api") rng = np.random.default_rng(3); X = np.column_stack([np.ones(400), rng.normal(size=400), rng.normal(size=400)]); y = X @ [1, 2, -1] + rng.normal(size=400) * (1 + X[:, 1] ** 2) e = raid.ols(X, y, cov="hc3"); f = sm.OLS(y, X).fit(cov_type="HC3") np.testing.assert_allclose(e["coefficients"], f.params, rtol=1e-10) np.testing.assert_allclose(e["inference"]["std_errors"], f.bse, rtol=1e-9) assert e["ok"] and e["backend"] == "cpu" and e["precision"] == "fp64" def test_ols_rejects_nan(): with pytest.raises(ValueError): raid.ols(np.array([[1.0, np.nan]]), np.array([1.0])) def test_realized_measures_cpu_matches_numpy(): rng = np.random.default_rng(5); r = rng.normal(0, 1e-3, size=390 * 10); off = np.arange(0, 3901, 390) m = raid.realized_measures(r, off, backend="cpu") for k in range(10): seg = r[off[k]:off[k + 1]] assert abs(m["rv"][k] - np.sum(seg ** 2)) <= 1e-15 * np.sum(seg ** 2) * 10 assert abs(m["rsv_neg"][k] + m["rsv_pos"][k] - m["rv"][k]) <= 1e-18 assert m["backend"] == "cpu" def test_garch_fit_converges_and_forecasts(): rng = np.random.default_rng(9); n = 2500; r = np.empty(n); s2 = 1e-4; e = 0.0 for t in range(n): s2 = 1e-6 + 0.08 * e * e + 0.9 * s2; e = np.sqrt(s2) * rng.standard_normal(); r[t] = e g = raid.garch_fit(r, model="garch", dist="gaussian", horizon=5) assert g["ok"] and g["convergence"]["converged"] assert abs(g["coefficients"][3] - 0.9) < 0.08 and len(g["extra"]["forecast_variance"]) == 5 def test_calendar_sessions_thanksgiving_week(): assert raid.calendar_sessions("2024-11-25", "2024-11-29") == ["2024-11-25", "2024-11-26", "2024-11-27", "2024-11-29"] def test_validate_bars_counts_missing(): # one session, 391 regular minutes 2024-11-27 (09:30–16:00 ET = 14:30–21:00 UTC), drop 3 bars base = np.datetime64("2024-11-27T14:30:00").astype("datetime64[us]").astype(np.int64) t = base + np.arange(391) * 60_000_000; keep = np.ones(391, bool); keep[[10, 20, 30]] = False; t = t[keep] p = 100 + np.cumsum(np.random.default_rng(1).normal(0, 0.01, size=t.size)) q = raid.validate_bars(t, p, p + 0.01, p - 0.01, p, np.full(t.size, 100.0)) assert q["n_sessions"] == 1 and q["missing_regular_bars"] == 3 and not q["has_errors"] def test_har_fit_matches_statsmodels_hac(): sm = pytest.importorskip("statsmodels.api") rng = np.random.default_rng(8); T = 800; lv = np.empty(T); lv[0] = np.log(1e-4) for t in range(1, T): lv[t] = 0.3 * np.log(1e-4) + 0.7 * lv[t - 1] + 0.4 * rng.standard_normal() rv = np.exp(lv); e = raid.har_fit(rv, horizon=1) rows = []; ys = [] for t in range(21, T - 1): rows.append([1.0, rv[t], rv[t - 4:t + 1].mean(), rv[t - 21:t + 1].mean()]); ys.append(rv[t + 1]) X = np.array(rows); y = np.array(ys); L = max(0, int(np.floor(4 * (len(y) / 100) ** (2 / 9)))) f = sm.OLS(y, X).fit(cov_type="HAC", cov_kwds={"maxlags": L, "use_correction": False}) np.testing.assert_allclose(e["coefficients"], f.params, rtol=1e-9); np.testing.assert_allclose(e["inference"]["std_errors"], f.bse, rtol=1e-8) assert e["forecast_next"] > 0 r = raid.har_rolling(rv, horizon=5, window=400, step=20); assert r["n_fits"] > 10 and r["qlike"] >= 0 b = raid.har_batch(np.concatenate([rv, rv * 2]), np.array([0, T, 2 * T]), backend="auto"); assert len(b["fits"]) == 2 and b["fits"][1]["ok"] def test_bootstrap_block_vs_iid(): rng = np.random.default_rng(2); n = 1500; x = np.empty(n); x[0] = 0 for t in range(1, n): x[t] = 0.7 * x[t - 1] + rng.standard_normal() blk = raid.bootstrap(x, "mean", "stationary", mean_block=20, replicas=399); iid = raid.bootstrap(x, "mean", "iid", replicas=399) assert blk["se"] > 1.5 * iid["se"] and blk["failures"] == 0 and blk["ci_lo"] < blk["point"] < blk["ci_hi"] def test_var_connectedness_matches_statsmodels_shape_and_bounds(): rng = np.random.default_rng(4); T, N = 600, 3; Y = np.zeros((T, N)); A = np.array([[0.5, 0.1, 0.0], [0.0, 0.4, 0.2], [0.1, 0.0, 0.3]]) for t in range(1, T): Y[t] = A @ Y[t - 1] + rng.standard_normal(N) c = raid.var_connectedness(Y, p=1, H=10, window=300, step=50) assert c["stable"] and 0 <= c["total"] <= 100 and np.allclose(c["table"].sum(axis=1), 1.0) and c["rolling_failures"] == 0 lw = raid.ledoit_wolf(Y); assert 0 <= lw["shrinkage"] <= 1 and lw["cov"].shape == (N, N)