hasquant-0.7.0.0: test/example/QuantLib/Example/LiborMarketModelMC.hs
-- |Monte-Carlo caplet pricing under a one-factor Libor market model, discounting each simulated
-- forward-rate vector with the process's own 'discountBond' rather than
-- with a deterministic curve.
--
-- Ported from QuantLib's test-suite\/libormarketmodelprocess.cpp::testMonteCarloCapletPricing
-- (its one-factor @process1@ leg): a 'liborForwardModelProcess' over 10 Euribor-1Y forwards,
-- wired to an 'QuantLib.Model.lfmHullWhiteParameterization' built from upstream's cap-vol curve
-- via 'QuantLib.Model.setCovarParam' -- without which the process holds no covariance
-- parameterization and cannot be simulated at all.
--
-- Each path yields one forward-rate vector, read at each rate's own fixing time; the caplet
-- payoff @max(r_k - 4%, 0) * tau_k@ is discounted by @discountBond@'s /k/-th cumulative factor.
-- The golden values are upstream's cached @capletNpv@ array. Upstream draws 250k paths and
-- compares within each estimator's own standard error; this runs a smaller Sobol sample and
-- asserts an absolute tolerance instead, since a low-discrepancy sample has no meaningful
-- per-path error estimate.
module QuantLib.Example.LiborMarketModelMC
(
Result(..)
, run
) where
import Control.Monad(foldM, forM)
import Data.List.NonEmpty(fromList)
import Data.Maybe(fromMaybe)
import qualified Data.Vector.Storable as V
import QuantLib.Index.InterestRate(iborIndex, IborConstructor(..))
import qualified QuantLib.Index.InterestRate as Ibor(fixingDays)
import QuantLib.InterestRate(VolatilityType(..))
import QuantLib.Math(SobolDirectionIntegers(..), Interpolation(..), timeGridFromVectorWithSteps, nonEmptyVector, points, boxedRealMatrix)
import QuantLib.Method(sobolPathGenerator, next, asset)
import QuantLib.Model(lfmHullWhiteParameterization, setCovarParam)
import QuantLib.Process(liborForwardModelProcess, fixingDates
, fixingTimes, accrualTimes
, discountBond, factors)
import qualified QuantLib.Context as Context
import QuantLib.TermStructure.Yield(interpolatedZeroCurve)
import qualified QuantLib.TermStructure.Volatility as Vol(capletVarianceCurve)
import QuantLib.Time.Calendar(adjust, advance, calendar, BusinessDayConvention(..), CalendarConstructor(..))
import QuantLib.Time.Date(september)
import QuantLib.Time.Schedule(dayCounter, DayCounterConstructor(..), TimeUnit(..))
data Result = Result
{ capletNpvs :: ![Double] -- ^simulated caplet NPVs, one per forward rate
, maxError :: !Double -- ^largest absolute deviation from upstream's cached values
} deriving Show
run :: IO Result
run = Context.keepingSettingsGc $ do
let fixtureDate = 4 `september` 2005
curveEndDate = 4 `september` 2018
len = 10 :: Word
cal <- calendar TARGET
evalDate <- adjust cal fixtureDate Following
Context.setEvaluationDate (Just evalDate)
dc <- dayCounter (Actual360 False)
emptyIndex <- iborIndex Euribor1Y Nothing
firstPillar <- advance cal evalDate (fromIntegral (Ibor.fixingDays emptyIndex), Days) Following False
rTS <- interpolatedZeroCurve (fromList [(firstPillar, 0.01), (curveEndDate, 0.08)]) dc cal [] Linear
idx <- iborIndex Euribor1Y (Just rTS)
-- the cap-vol curve is built off a len+1-sized process, as upstream's makeCapVolCurve does
volProcess <- liborForwardModelProcess (len + 1) idx
volDates <- fixingDates volProcess
volDc <- dayCounter ActualActualISDA
capletVol <- Vol.capletVarianceCurve evalDate
(fromList (zip (take (fromIntegral len) (drop 1 volDates)) capletVols)) volDc ShiftedLognormal 0.0
process <- liborForwardModelProcess len idx
parameterization <- lfmHullWhiteParameterization process capletVol emptyCorrelation 1
setCovarParam process parameterization
resetTimes <- fixingTimes process
accruals <- accrualTimes process
grid <- timeGridFromVectorWithSteps (fromMaybe (error "empty fixing times") (nonEmptyVector (V.fromList resetTimes))) 12
gridPts <- points grid
-- each rate is read at its own fixing time's index in the grid, as upstream's `location` does
let location = [fromMaybe (error "fixing time not on grid") (V.findIndex (== t) gridPts) | t <- resetTimes]
steps = fromIntegral (V.length gridPts) - 1
nFactors <- factors process
gen <- sobolPathGenerator JoeKuoD7 process grid 42 (nFactors * steps) False
totals <- foldM (\acc _ -> do
path <- next gen
rateVecs <- forM [0 .. len - 1] (asset path)
let rates = zipWith (V.!) rateVecs location
dfs <- discountBond process rates
let payoffs = zipWith3 (\df r (st, en) -> df * max 0.0 (r - capRate) * (en - st)) dfs rates accruals
pure (zipWith' (+) acc payoffs))
(replicate (fromIntegral len) 0.0) [1 .. nrTrails :: Int]
let npvs = map (/ fromIntegral nrTrails) totals
pure (Result npvs (maximum (map abs (zipWith (-) npvs expectedNpvs))))
where
zipWith' f (x:xs) (y:ys) = let z = f x y in z `seq` (z : zipWith' f xs ys)
zipWith' _ _ _ = []
emptyCorrelation = either error id (boxedRealMatrix 0 0 [])
capletVols = [0.1440, 0.1715, 0.1681, 0.1664, 0.1617, 0.1578, 0.1540, 0.1521, 0.1486, 0.1454]
capRate = 0.04 :: Double
nrTrails = 20000 :: Int
-- test-suite/libormarketmodelprocess.cpp's cached capletNpv[]
expectedNpvs =
[ 0.000000000000, 0.000002841629, 0.002533279333, 0.009577143571, 0.017746502618
, 0.025216116835, 0.031608230268, 0.036645683881, 0.039792254012, 0.041829864365 ]
-- vim: set ft=haskell ff=unix ts=8 sts=2 sw=2 et: