SPB Git forge

spb/wp12_uqo

Public
5commits 1branches 0releases
1.2 MBsize
maindefault branch
1 mo agolast push
Python 75.9% TeX 24%
4.6 KB · 126 lines python
Raw Blame History
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