Python 75.9%
TeX 24%
1"""2================================================================3Auteur : Simon-Pierre Boucher4Contact : contact@spboucher.ai5Projet : Prévision de volatilité réalisée multi-actifs6 (HAR-RV vs GARCH vs Machine Learning)7Fichier : test_evaluation.py8Description : Tests pytest de l'évaluation — propriétés des9 pertes, taille/puissance du test DM, MCS, Mincer-10 Zarnowitz, encompassing et combinaisons.11================================================================12"""1314from __future__ import annotations1516import sys17from pathlib import Path1819import numpy as np20import pandas as pd21import pytest2223sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))2425from wp12 import evaluation as ev # noqa: E4022627RNG = np.random.default_rng(20260810)282930class TestLosses:31 def test_qlike_zero_at_perfect_forecast(self) -> None:32 a = RNG.uniform(0.5, 2.0, 100)33 assert np.allclose(ev.qlike(a, a), 0.0)3435 def test_qlike_positive_and_asymmetric(self) -> None:36 a = np.ones(10)37 under = ev.qlike(a, 0.5 * a).mean()38 over = ev.qlike(a, 2.0 * a).mean()39 assert under > 0 and over > 040 assert under > over # QLIKE punishes under-prediction more4142 def test_mse_basic(self) -> None:43 assert ev.mse(np.array([2.0]), np.array([1.0]))[0] == 1.0444546class TestDieboldMariano:47 def test_dm_detects_better_model(self) -> None:48 n = 80049 actual = np.exp(RNG.normal(0, 0.5, n))50 good = actual * np.exp(RNG.normal(0, 0.1, n))51 bad = actual * np.exp(RNG.normal(0.5, 0.5, n))52 la, lb = ev.qlike(actual, good), ev.qlike(actual, bad)53 stat, p = ev.diebold_mariano(la, lb, h=1)54 assert stat < -2 and p < 0.055556 def test_dm_size_under_null(self) -> None:57 """Two equally noisy forecasts: rejection rate ~ nominal."""58 rejects = 059 for _ in range(200):60 actual = np.exp(RNG.normal(0, 0.5, 300))61 f1 = actual * np.exp(RNG.normal(0, 0.3, 300))62 f2 = actual * np.exp(RNG.normal(0, 0.3, 300))63 _, p = ev.diebold_mariano(ev.qlike(actual, f1), ev.qlike(actual, f2))64 rejects += p < 0.0565 assert rejects / 200 < 0.12666768class TestMCS:69 def test_mcs_keeps_good_removes_bad(self) -> None:70 n = 60071 actual = np.exp(RNG.normal(0, 0.5, n))72 losses = pd.DataFrame({73 "good1": ev.qlike(actual, actual * np.exp(RNG.normal(0, 0.1, n))),74 "good2": ev.qlike(actual, actual * np.exp(RNG.normal(0, 0.1, n))),75 "bad": ev.qlike(actual, actual * np.exp(RNG.normal(1.0, 0.5, n))),76 })77 out = ev.model_confidence_set(losses, alpha=0.10, n_boot=800)78 assert not out.loc[out["model"] == "bad", "in_mcs"].iloc[0]79 assert out["in_mcs"].sum() >= 180 good_kept = out.loc[out["model"].str.startswith("good"), "in_mcs"]81 assert good_kept.any()828384class TestMZAndEncompassing:85 def test_mz_unbiased_forecast(self) -> None:86 n = 100087 f = np.exp(RNG.normal(0, 0.5, n))88 actual = f * np.exp(RNG.normal(-0.005, 0.1, n))89 res = ev.mincer_zarnowitz(actual, f)90 assert res["beta"] == pytest.approx(1.0, abs=0.1)91 assert res["r2"] > 0.89293 def test_mz_biased_forecast_rejected(self) -> None:94 n = 100095 f = np.exp(RNG.normal(0, 0.5, n))96 actual = 2.0 * f * np.exp(RNG.normal(0, 0.1, n))97 res = ev.mincer_zarnowitz(actual, f)98 assert res["p_joint"] < 0.0199100 def test_encompassing_detects_added_info(self) -> None:101 n = 1000102 true = np.exp(RNG.normal(0, 0.5, n))103 actual = true * np.exp(RNG.normal(0, 0.2, n))104 f_base = true * np.exp(RNG.normal(0, 0.4, n)) # noisy version105 f_rival = 0.5 * f_base + 0.5 * true # contains extra info106 res = ev.forecast_encompassing(actual, f_base, f_rival)107 assert res["p_b2"] < 0.05108 assert res["b2"] > 0109110111class TestCombination:112 def test_combined_beats_worst(self) -> None:113 n = 700114 idx = pd.date_range("2020-01-01", periods=n, freq="B")115 actual = pd.Series(np.exp(RNG.normal(0, 0.5, n)), index=idx)116 f = pd.DataFrame({117 "m1": actual * np.exp(RNG.normal(0, 0.2, n)),118 "m2": actual * np.exp(RNG.normal(0.3, 0.4, n)),119 }, index=idx)120 combs = ev.combine_forecasts(f, actual, window=60)121 q_mean = ev.qlike(actual.to_numpy(), combs["Comb-Mean"].to_numpy()).mean()122 q_inv = np.nanmean(ev.qlike(actual.to_numpy(), combs["Comb-InvMSE"].to_numpy()))123 q_worst = ev.qlike(actual.to_numpy(), f["m2"].to_numpy()).mean()124 assert q_mean < q_worst125 assert q_inv < q_worst126