packages feed

hasquant-0.7.0.0: test/example/QuantLib/Example/HestonHullWhiteMC.hs

-- |Monte-Carlo zero-bond and zero-bond-option pricing on the three-factor
-- 'hybridHestonHullWhiteProcess', discounting each simulated path by the process's own
-- 'numeraire' rather than by a deterministic curve.
--
-- Ported from QuantLib's test-suite\/hybridhestonhullwhiteprocess.cpp::testZeroBondPricing,
-- including its deliberately awkward zero curve (a sine-perturbed monthly 10y-20y grid plus a
-- 30y node) chosen upstream to exercise the joint process's drift and discounting. Two checks,
-- both against quantities the simulation never sees:
--
-- * the MC mean of @1 \/ numeraire(t, x_t)@ must reproduce the curve's own @P(0, t)@;
-- * the MC mean of that times a zero-bond call payoff must reproduce the closed-form
--   'QuantLib.Model.discountBondOption' of the matching 'QuantLib.Model.hullWhite' model.
--
-- Upstream draws 8191 Sobol-Brownian-bridge paths and allows 0.03 \/ 0.0035 absolute error; both
-- tolerances are kept. hasquant's 'sobolPathGenerator' stands in for upstream's
-- @SobolBrownianBridgeRsg@ with a plain Sobol sequence -- the bridge flag must be 'False' here,
-- since @MultiPathGenerator@ rejects it ("Brownian bridge not supported"); upstream gets its
-- bridging inside the sequence generator instead, which hasquant does not bind separately.
module QuantLib.Example.HestonHullWhiteMC
  (
    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.Instrument.Option(OptionType(..))
import QuantLib.Math(SobolDirectionIntegers(..), Interpolation(..), timeGridFromVector, nonEmptyVector)
import QuantLib.Method(sobolPathGenerator, next, asset)
import QuantLib.Model(hullWhite, asAffineModel, discountBond, discountBondOption)
import QuantLib.Process(hestonProcess, hullWhiteForwardProcess, setForwardMeasureTime
 , hybridHestonHullWhiteProcess, numeraire, factors
 , HestonProcessDiscretization(..), HybridHestonHullWhiteProcessDiscretization(..))
import QuantLib.Quote(simpleQuote)
import qualified QuantLib.Context as Context
import QuantLib.InterestRate(Compounding(..))
import QuantLib.TermStructure.Yield(Reference(..), TermPoint(..), interpolatedZeroCurve, flatForward, discount)
import QuantLib.Time.Calendar(calendar, CalendarConstructor(..))
import QuantLib.Time.Date(today, addPeriod)
import QuantLib.Time.Schedule(Frequency(..), dayCounter, yearFraction, DayCounterConstructor(..), TimeUnit(..))

data Result = Result
  { zeroBondError :: !Double    -- ^largest @|MC mean of 1\/numeraire - P(0,t)|@ over the grid
  , zeroOptionError :: !Double  -- ^largest @|MC mean of the option payoff - closed form|@ over the grid
  , gridPoints :: !Int          -- ^number of grid points checked
  } deriving Show

-- running sums of the two estimators at one grid point
data Acc = Acc !Double !Double

run :: IO Result
run = Context.keepingSettingsGc $ do
  evalDate <- today
  Context.setEvaluationDate (Just evalDate)
  dc <- dayCounter (Actual360 False)
  cal <- calendar TARGET

  let months = [120 .. 239 :: Int]
  monthDates <- mapM (\i -> addPeriod evalDate (i, Months)) months
  maturity <- addPeriod (last monthDates) (10, Years)
  let rates = 0.02 : [0.02 + 0.0002 * exp (sin (fromIntegral i / 8.0)) | i <- months] ++ [0.04]
      dates = evalDate : monthDates ++ [maturity]
  rTS <- interpolatedZeroCurve (fromList (zip dates rates)) dc cal [] Linear
  times <- mapM (\d -> yearFraction dc evalDate d Nothing Nothing) dates

  s0 <- simpleQuote 100.0
  -- a flat 0% dividend curve, as upstream: the joint process dereferences the handle, so it
  -- cannot be left empty here
  zeroQ <- simpleQuote 0.0
  qTS <- flatForward (ReferenceDate evalDate) zeroQ dc Continuous Annual
  hProcess <- hestonProcess rTS (Just qTS) s0 0.02 1.0 0.2 0.5 (-0.8) QuadraticExponentialMartingale
  hwFwd <- hullWhiteForwardProcess rTS hwA hwSigma
  -- must precede the joint process's construction, which captures T at that point
  setForwardMeasureTime hwFwd (last times)
  joint <- hybridHestonHullWhiteProcess hProcess hwFwd (-0.4) BSMHullWhite
  hwModel <- hullWhite rTS hwA hwSigma
  hwAffine <- asAffineModel hwModel

  -- the grid drops the final (maturity) node, as upstream's `times.end()-1` does
  let gridTimes = init times
      steps = length gridTimes - 1
  grid <- timeGridFromVector (fromMaybe (error "empty time grid") (nonEmptyVector (V.fromList gridTimes)))
  nFactors <- factors joint
  gen <- sobolPathGenerator JoeKuoD7 joint grid 0 (nFactors * fromIntegral steps) False

  let zero = replicate (m - 1) (Acc 0.0 0.0)
  totals <- foldM (\accs _ -> do
      path <- next gen
      states <- forM [0 .. nFactors - 1] (asset path)
      sample <- forM [1 .. m - 1] $ \j -> do
        let t = gridTimes !! j
            bigT = gridTimes !! (j + optionTenor)
        zeroBond <- recip <$> numeraire joint t [st V.! j | st <- states]
        bondAtT <- discountBond hwAffine t bigT [states !! 2 V.! j]
        pure (Acc zeroBond (zeroBond * max 0.0 (bondAtT - strike)))
      pure (zipWith' addAcc accs sample))
    zero [1 .. nrTrails :: Int]

  errs <- forM (zip [1 ..] totals) $ \(j, Acc zb zo) -> do
    let t = gridTimes !! j
        bigT = gridTimes !! (j + optionTenor)
        n = fromIntegral nrTrails
    expectedBond <- discount rTS (TimePoint t) False
    expectedOption <- discountBondOption hwAffine Call strike t Nothing bigT
    pure (abs (zb / n - expectedBond), abs (zo / n - expectedOption))

  pure (Result (maximum (map fst errs)) (maximum (map snd errs)) (length errs))
  where
    addAcc (Acc a b) (Acc c d) = Acc (a + c) (b + d)
    -- strict in the accumulated elements: a lazy zipWith over 8191 paths would build one thunk
    -- chain per grid point ('Acc' has strict fields, so a single seq forces both running sums)
    zipWith' f (x:xs) (y:ys) = let z = f x y in z `seq` (z : zipWith' f xs ys)
    zipWith' _ _ _ = []
    hwA = 0.05
    hwSigma = 0.05
    m = 90 :: Int
    optionTenor = 24 :: Int
    nrTrails = 8191 :: Int
    strike = 0.5 :: Double

-- vim: set ft=haskell ff=unix ts=8 sts=2 sw=2 et: