packages feed

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

-- |Prices a 3-asset American basket put (payoff on the maximum of three correlated underlyings)
-- with a Haskell-defined payoff, generalizing "QuantLib.Example.AmericanLSM"'s backward-induction
-- pattern from the scalar 'QuantLib.Method.lsmRegress' to the multi-asset
-- 'QuantLib.Method.lsmRegressMulti'. Fixture and golden values are QuantLib's own
-- @test-suite\/basketoption.cpp@ @testBarraquandThreeValues@ case (Barraquand & Martineau 1995):
-- three assets at spot 40, strike 40, r=5%, q=0, vol 20%\/30%\/50%, zero correlation, maturity 1
-- month -- cached European reference 0.13, American reference 0.23.
module QuantLib.Example.BasketLSM
  (
    Result(..)
  , run
  ) where
import Control.Monad(replicateM, zipWithM)
import Data.Time.Calendar(addDays)
import qualified Data.Vector.Storable as V
import Data.List.NonEmpty(fromList)

import QuantLib.Instrument
import QuantLib.Instrument.Option
import QuantLib.InterestRate
import QuantLib.Math
import QuantLib.Method
import QuantLib.Process
import QuantLib.PricingEngine
import QuantLib.Quote
import QuantLib.Context
import QuantLib.Time.Calendar
import QuantLib.Time.Date
import QuantLib.Time.Schedule
import QuantLib.TermStructure.Yield
import QuantLib.TermStructure.Volatility

data Result = Result
  { lsmPrice :: !Double        -- ^custom multi-asset LSM loop, out-of-sample pricing paths priced against a fit from a separate calibration path set (unbiased)
  , calibPrice :: !Double      -- ^same loop, but pricing the calibration paths against their own (in-sample) fit -- the naive, biased single-pass estimate
  , mcPrice :: !Double         -- ^'mcAmericanBasketEngine' pricing the equivalent bound MaxBasketPayoff option
  , referencePrice :: !Double  -- ^upstream's own cached golden value for this exact fixture (Barraquand & Martineau 1995)
  , exerciseProb :: !Double    -- ^fraction of pricing paths exercised before maturity
  , simulatedForwards :: ![Double]  -- ^per-asset mean simulated terminal spot, pricing path set
  , impliedForwards :: ![Double]    -- ^per-asset curve-implied forward: spot \/ discount(T) (q=0)
  }

payoff :: Double -> [Double] -> Double
payoff strike ss = max (strike - maximum ss) 0

mean :: [Double] -> Double
mean xs = sum xs / fromIntegral (length xs)

-- |every asset's full simulated time series for one drawn path.
pathAssets :: Int -> PathGenerator -> IO [[Double]]
pathAssets dim gen = next gen >>= \s -> mapM (fmap V.toList . asset s) [0 .. fromIntegral dim - 1]

-- |all paths' state at exercise-date index @t@, one row (of @dim@ underlyings) per path.
statesAt :: Int -> Int -> [[[Double]]] -> [[Double]]
statesAt dim t paths = [ [ p !! a !! t | a <- [0 .. dim-1] ] | p <- paths ]

toMatrix :: Int -> [[Double]] -> RealMatrix
toMatrix dim rows = either error id $
  realMatrixFromVector (fromIntegral (length rows)) (fromIntegral dim) (V.fromList (concat rows))

-- |one backward-induction step, generalizing "QuantLib.Example.AmericanLSM"'s 'step' from a
-- scalar state to an @dim@-underlying state vector per path (via 'lsmRegressMulti' instead of
-- 'lsmRegress').
step :: PolynomialType -> Word -> Double -> Int -> Double -> Int -> [[[Double]]] -> [[[Double]]]
     -> [Double] -> [Double] -> [Bool] -> IO ([Double], [Double], [Bool])
step polyT order strike dim df t calibPaths pricePaths calibCF0 priceCF0 exFlags0 = do
  let calibCF = map (* df) calibCF0
      priceCF = map (* df) priceCF0
      calibS = statesAt dim t calibPaths
      priceS = statesAt dim t pricePaths
      calibEx = map (payoff strike) calibS
      priceEx = map (payoff strike) priceS
      (fitStates, fitTargets, _) = unzip3 $ filter (\(_, _, e) -> e > 0) $ zip3 calibS calibCF calibEx
      basisNeeded = fromIntegral (lsmBasisSize (fromIntegral dim) order)
  if length fitStates <= basisNeeded
    then return (calibCF, priceCF, exFlags0)
    else do
      let fitMat = toMatrix dim fitStates
      contCalib <- V.toList <$> lsmRegressMulti polyT order fitMat (V.fromList fitTargets) (toMatrix dim calibS)
      contPrice <- V.toList <$> lsmRegressMulti polyT order fitMat (V.fromList fitTargets) (toMatrix dim priceS)
      let calibCF' = zipWith3 (\cf ex cont -> if ex > 0 && ex > cont then ex else cf) calibCF calibEx contCalib
          decidePrice cf ex cont = if ex > 0 && ex > cont then (ex, True) else (cf, False)
          (priceCF', exercisedNow) = unzip $ zipWith3 decidePrice priceCF priceEx contPrice
          exFlags' = zipWith (||) exFlags0 exercisedNow
      return (calibCF', priceCF', exFlags')

-- |walk exercise dates strictly backward, from index @timeSteps-1@ down to 1 -- same range as
-- "QuantLib.Example.AmericanLSM"'s 'goBack', but indexing directly into the fully-stored
-- per-asset path lists (already available in full) rather than peeling a transposed state list.
goBack :: PolynomialType -> Word -> Double -> Int -> Int -> [Double] -> [[[Double]]] -> [[[Double]]]
       -> [Double] -> [Double] -> [Bool] -> IO ([Double], [Double], [Bool])
goBack polyT order strike dim i dfs calibPaths pricePaths calibCF priceCF exFlags
  | i < 1 = return (calibCF, priceCF, exFlags)
  | otherwise = do
      (calibCF', priceCF', exFlags') <- step polyT order strike dim (dfs !! i) i calibPaths pricePaths calibCF priceCF exFlags
      goBack polyT order strike dim (i - 1) dfs calibPaths pricePaths calibCF' priceCF' exFlags'

run :: IO Result
run = do
  setEvaluationDate $ Just evalDate
  dc <- dayCounter (Actual360 False)
  cal <- calendar TARGET
  underQs <- mapM simpleQuote spots
  riskFreeQ <- simpleQuote riskFreeRate
  ts <- flatForward (ReferenceDate evalDate) riskFreeQ dc Continuous Annual
  divQ <- simpleQuote 0.0
  divTS <- flatForward (ReferenceDate evalDate) divQ dc Continuous Annual
  volQs <- mapM simpleQuote vols
  volTSs <- mapM (\vq -> blackConstantVol (CalendarReferenceDate evalDate) cal vq dc) volQs
  procs1D <- zipWithM (\uq vts -> blackScholesMertonProcess uq divTS ts vts EulerDiscretization False) underQs volTSs
  let corrFlat = concat [ [ if i == j then 1 else assetCorrelation | j <- [0 .. dim-1] ] | i <- [0 .. dim-1] ]
      corrMat = either error id $ boxedRealMatrix (fromIntegral dim) (fromIntegral dim) corrFlat
  procs <- stochasticProcessArray (fromList procs1D) corrMat

  t <- yearFraction dc evalDate maturity Nothing Nothing
  grid <- timeGrid t timeSteps
  times <- V.toList <$> points grid
  discFactors@(df0h:_) <- mapM (\x -> discount ts (TimePoint x) False) times
  let dfs = zipWith (flip (/)) discFactors (drop 1 discFactors)

  genCalib <- pathGenerator PseudoRandom procs grid seedCalib (fromIntegral dim * (size grid - 1)) False
  calibPaths <- replicateM nCalib (pathAssets dim genCalib)
  genPrice <- pathGenerator PseudoRandom procs grid seedPrice (fromIntegral dim * (size grid - 1)) False
  pricePaths <- replicateM nPrice (pathAssets dim genPrice)

  let nSteps = fromIntegral timeSteps
      calibCF0 = map (payoff strike . map (!! nSteps)) calibPaths
      priceCF0 = map (payoff strike . map (!! nSteps)) pricePaths

  (calibFinal, priceFinal, exFlags) <- goBack polyT order strike dim (nSteps - 1) dfs
    calibPaths pricePaths calibCF0 priceCF0 (replicate nPrice False)

  let df0 = discFactors !! 1 / df0h
      lsmP = mean (map (* df0) priceFinal)
      calibP = mean (map (* df0) calibFinal)
      exProb = fromIntegral (length (filter id exFlags)) / fromIntegral nPrice

  let payoffQL = Max (Type (Striked (PlainVanilla (PlainVanillaPayoff Put strike))))
      americanEx = American Nothing maturity False
  amOpt <- basketOption payoffQL americanEx
  amEng <- mcAmericanBasketEngine PseudoRandom procs (Just timeSteps) Nothing False False Nothing (Just 0.02) Nothing seedCalib (Just (fromIntegral nCalib)) order polyT
  QuantLib.Instrument.setPricingEngine amOpt amEng
  mcA <- npv amOpt

  -- martingale self-consistency check: under the risk-neutral measure, each simulated asset's
  -- mean terminal spot should match the curve-implied forward spot\/discount(T) (q=0 here) --
  -- guards against a process/curve wiring mistake (e.g. a swapped foreign\/domestic curve) the
  -- way the TARF example's own check does.
  let terminalDF = discFactors !! nSteps
      impliedFwds = map (/ terminalDF) spots
      simFwds = [ mean (map (\p -> p !! a !! nSteps) pricePaths) | a <- [0 .. dim-1] ]

  return $ Result lsmP calibP mcA referenceAmerican exProb simFwds impliedFwds
  where
    evalDate = 1 `may` 2024
    dim = 3 :: Int
    spots = [40, 40, 40] :: [Double]
    vols = [0.20, 0.30, 0.50] :: [Double]
    assetCorrelation = 0.0 :: Double
    strike = 40 :: Double
    riskFreeRate = 0.05
    maturity = addDays 30 evalDate  -- upstream: today + 1 (month) * 30 days
    timeSteps = 50 :: Word
    order = 2 :: Word
    polyT = Monomial
    nCalib = 4096 :: Int
    nPrice = 8192 :: Int
    seedCalib = 42 :: Word
    seedPrice = 43 :: Word
    referenceAmerican = 0.23

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