feat: Enhance KupiecVaRBacktester with improved exception handling and fallback logic

This commit is contained in:
HimanshuJ16
2026-08-07 07:40:01 +05:30
parent a80d9cc8c7
commit 5bfc05d0ef
2 changed files with 51 additions and 13 deletions
+1
View File
@@ -2,4 +2,5 @@
pyyaml>=6.0
numpy>=1.21.0
pandas>=1.3.0
scipy>=1.7.0
pyotp>=2.6.0
@@ -1,25 +1,62 @@
"""
kupiec_var_backtester: Kupiec POF (Proportion of Failures) Likelihood Ratio test
for VaR model backtesting.
"""
from dataclasses import dataclass
import scipy.stats as st
import math
from typing import Optional
try:
import scipy.stats as st
except ImportError:
st = None
@dataclass
class KupiecResult:
p_value: float
is_rejected: bool
exceptions: int
stat: float = 0.0
class KupiecVaRBacktester:
def __init__(self, confidence_level: float = 0.99):
def __init__(self, confidence_level: float = 0.99, alpha: float = 0.05):
self.confidence_level = confidence_level
self.alpha = alpha
def run_test(self, total_observations: int, exceptions: int) -> KupiecResult:
if total_observations == 0:
return KupiecResult(1.0, False, 0)
p = 1 - self.confidence_level
# binomial test
try:
res = st.binomtest(exceptions, total_observations, p, alternative='two-sided')
p_val = res.pvalue
except AttributeError:
p_val = st.binom_test(exceptions, total_observations, p, alternative='two-sided')
is_rejected = p_val < 0.05
return KupiecResult(float(p_val), is_rejected, exceptions)
if total_observations <= 0:
return KupiecResult(1.0, False, 0, 0.0)
p = 1.0 - self.confidence_level
if st is not None:
try:
res = st.binomtest(exceptions, total_observations, p, alternative='two-sided')
p_val = float(res.pvalue)
except AttributeError:
p_val = float(st.binom_test(exceptions, total_observations, p, alternative='two-sided'))
is_rejected = p_val < self.alpha
return KupiecResult(p_val, is_rejected, exceptions)
# Pure Python Kupiec POF Likelihood Ratio test fallback
x = exceptions
N = total_observations
if x == 0:
lr_stat = -2.0 * (N * math.log(1.0 - p))
elif x == N:
lr_stat = -2.0 * (N * math.log(p))
else:
pi_hat = x / N
num = ((1.0 - p) ** (N - x)) * (p ** x)
den = ((1.0 - pi_hat) ** (N - x)) * (pi_hat ** x)
if den > 0 and num > 0:
lr_stat = -2.0 * math.log(num / den)
else:
lr_stat = 0.0
# Critical value for Chi-Square(1) at alpha=0.05 is 3.841459
is_rejected = lr_stat > 3.841459
p_val = math.exp(-lr_stat / 2.0) # Approx p-value under Chi2(1)
return KupiecResult(p_val, is_rejected, exceptions, stat=lr_stat)