hasquant-0.7.0.0: test/hspec/QuantLib/Spec/Statistics.hs
-- Risk-statistics checks against analytic Gaussian results.
module QuantLib.Spec.Statistics (spec) where
import Test.Hspec
import qualified Data.Vector.Storable as V
import QuantLib.Math
import QuantLib.Method(sobolGaussianRsg, nextSequence)
import QuantLib.Spec.Helpers(closePrec)
spec :: Spec
spec =
describe "risk statistics (RiskStatistics over a caller-supplied sample)" $
mapM_ scenarioSpec
[ (0.0, 1.0)
, (-1.0, 0.1)
]
-- |Enough Sobol draws for stable Gaussian moment estimates.
sampleSize :: Int
sampleSize = 8191
scenarioSpec :: (Double, Double) -> Spec
scenarioSpec (mean, sigma) =
describe ("N(" ++ show mean ++ ", " ++ show sigma ++ ")") $
it "matches the analytic gaussian moments and risk measures (riskstats.cpp::testResults)" $ do
sample <- gaussianSample sampleSize mean sigma
m <- riskStatisticsMean sample
m `shouldSatisfy` closePrec mean (relTol mean 1.0e-3)
v <- riskStatisticsVariance sample
v `shouldSatisfy` closePrec (sigma * sigma) (sigma * sigma * 1.0e-2)
sd <- riskStatisticsStandardDeviation sample
sd `shouldSatisfy` closePrec sigma (sigma * 1.0e-2)
-- percentile(0.5)/gaussianPercentile(0.5) of a gaussian sample is its own mean.
p50 <- riskStatisticsPercentile sample 0.5
p50 `shouldSatisfy` closePrec mean (relTol mean 1.0e-3)
gp50 <- riskStatisticsGaussianPercentile sample 0.5
gp50 `shouldSatisfy` closePrec mean (relTol mean 1.0e-3)
-- Two-sigma quantiles give the expected upside and downside thresholds.
let upperTail = mean + 2 * sigma
lowerTail = mean - 2 * sigma
twoSigma = normalCdf mean sigma upperTail
pu <- riskStatisticsPotentialUpside sample twoSigma
pu `shouldSatisfy` closePrec (max upperTail 0.0) (relTol (max upperTail 0.0) 1.0e-2)
gpu <- riskStatisticsGaussianPotentialUpside sample twoSigma
gpu `shouldSatisfy` closePrec (max upperTail 0.0) (relTol (max upperTail 0.0) 1.0e-2)
var <- riskStatisticsValueAtRisk sample twoSigma
let expVar = -(min lowerTail 0.0)
var `shouldSatisfy` closePrec expVar (relTol expVar 1.0e-2)
gvar <- riskStatisticsGaussianValueAtRisk sample twoSigma
gvar `shouldSatisfy` closePrec expVar (relTol expVar 1.0e-2)
-- expected shortfall's closed form (riskstats.cpp): -min(mean - sigma^2*phi(lowerTail)/(1-twoSigma), 0)
es <- riskStatisticsExpectedShortfall sample twoSigma
let expEs = -(min (mean - sigma * sigma * normalPdf mean sigma lowerTail / (1 - twoSigma)) 0.0)
es `shouldSatisfy` closePrec expEs (relTol expEs 1.0e-2)
ges <- riskStatisticsGaussianExpectedShortfall sample twoSigma
ges `shouldSatisfy` closePrec expEs (relTol expEs 1.0e-2)
-- shortfall(mean) is the probability of falling below the mean -- 0.5 for a gaussian.
sf <- riskStatisticsShortfall sample mean
sf `shouldSatisfy` closePrec 0.5 2.0e-3
where
relTol expected tol = if expected == 0.0 then tol else abs expected * tol
-- |Deterministic Sobol sample from @N(mean, sigma)@.
gaussianSample :: Int -> Double -> Double -> IO RealVector
gaussianSample n mean sigma = do
rsg <- sobolGaussianRsg Jaeckel 1 42
draws <- mapM (const (draw rsg)) [1 .. n]
pure (V.fromList draws)
where
draw rsg = do
(z, _weight) <- nextSequence rsg
pure (mean + sigma * V.head z)
-- Abramowitz-Stegun 7.1.26 approximation; avoids an extra dependency.
erf :: Double -> Double
erf x =
let a1 = 0.254829592; a2 = -0.284496736; a3 = 1.421413741
a4 = -1.453152027; a5 = 1.061405429; p = 0.3275911
sign = if x < 0 then -1 else 1
ax = abs x
t' = 1 / (1 + p * ax)
y = 1 - (((((a5 * t' + a4) * t') + a3) * t' + a2) * t' + a1) * t' * exp (-ax * ax)
in sign * y
normalCdf :: Double -> Double -> Double -> Double
normalCdf mean sigma x = 0.5 * (1 + erf ((x - mean) / (sigma * sqrt 2)))
normalPdf :: Double -> Double -> Double -> Double
normalPdf mean sigma x = exp (-(x - mean) * (x - mean) / (2 * sigma * sigma)) / (sigma * sqrt (2 * pi))