packages feed

hasquant-0.5.0.2: test/example/QuantLib/Example/ShortRateModels.hs

{-# LANGUAGE TupleSections #-}
module QuantLib.Example.ShortRateModels
  (
    CalibrationResult(..)
  , SwapCheck(..)
  , ConvexityCheck(..)
  , DiscountCheck(..)
  , Result(..)
  , run
  ) where
import Control.Monad(forM, when)

import QuantLib.CashFlow(RateAveragingType(..))
import QuantLib.Index(fixingCalendar, addFixing)
import qualified QuantLib.Index.InterestRate as IR
import QuantLib.InterestRate hiding(rate)
import QuantLib.Instrument(npv, setPricingEngine)
import QuantLib.Instrument.Swap hiding(startDate)
import QuantLib.Math
import QuantLib.Model hiding (setPricingEngine, value)
import qualified QuantLib.Model as Model
import QuantLib.PricingEngine
import QuantLib.Quote
import qualified QuantLib.TermStructure.Yield as TS
import QuantLib.Time.Calendar
import QuantLib.Time.Date hiding(today)
import QuantLib.Time.Schedule
import QuantLib.Settings

-- |Shape shared by @testCachedHullWhite@/@testCachedHullWhite2@/
-- @testCachedHullWhiteFixedReversion@ in @ex/shortratemodels.cpp@.
-- @cachedA@/@cachedSigma@/@cachedValue@ are the upstream file's literal
-- expected values (the @usingAtParCoupons@ branch, matching this QuantLib
-- build's default -- @IborCoupon::Settings@ isn't bound in hasquant, so the
-- other branch's literals can't be selected at runtime).
data CalibrationResult = CalibrationResult
  { calculatedA, calculatedSigma :: Double
  , cachedA, cachedSigma :: Double
  , calculatedValue, cachedValue :: Double
  } deriving Show

data SwapCheck = SwapCheck
  { startMonths :: Int
  , lengthYears :: Int
  , swapRate :: Double
  , expectedNPV, calculatedNPV :: Double
  } deriving Show

data ConvexityCheck = ConvexityCheck
  { convexityT, convexityA, expectedForward, calculatedForward :: Double
  } deriving Show

data DiscountCheck = DiscountCheck { expectedDF, calculatedDF :: Double } deriving Show

data Result = Result
  { cachedHullWhite :: CalibrationResult
  , cachedHullWhiteFixedReversion :: CalibrationResult
  , cachedHullWhite2 :: CalibrationResult
  , swaps :: [SwapCheck]
  , futuresConvexityBias :: [ConvexityCheck]
  , extendedCirDiscountFactor :: DiscountCheck
  , vasicekDiscountFactorSmallMeanReversion :: DiscountCheck
  } deriving Show

run :: IO Result
run = do
  hw <- runCachedHullWhite [] 0.1 0.01 0.0464041 0.00579912
  hwFixed <- runCachedHullWhite fixedReversion 0.05 0.01 0.05 0.00585858
  hw2 <- runCachedHullWhite2
  sw <- runSwaps
  cirDF <- runExtendedCirDiscountFactor
  vasicekDF <- runVasicekSmallMeanReversion
  pure Result
    { cachedHullWhite = hw
    , cachedHullWhiteFixedReversion = hwFixed
    , cachedHullWhite2 = hw2
    , swaps = sw
    , futuresConvexityBias = futuresConvexityChecks
    , extendedCirDiscountFactor = cirDF
    , vasicekDiscountFactorSmallMeanReversion = vasicekDF
    }

calibrationToday :: Day
calibrationToday = 15 `february` 2002

calibrationSettlement :: Day
calibrationSettlement = 19 `february` 2002

calibrationData :: [(Word, Word, Double)] -- ^(start, length, volatility)
calibrationData =
  [ (1, 5, 0.1148), (2, 4, 0.1108), (3, 3, 0.1070), (4, 2, 0.1021), (5, 1, 0.1000) ]

-- |@testCachedHullWhite@/@testCachedHullWhiteFixedReversion@: calibrate a
-- 'HullWhite' model, constructed with the given starting @(a, sigma)@, to the
-- swaption grid in 'calibrationData', using Euribor6M with its usual start delay.
runCachedHullWhite :: [Bool] -- ^fixParameters, e.g. 'fixedReversion'
  -> Double -> Double -- ^model's starting (a, sigma)
  -> Double -> Double -- ^cached expected (a, sigma)
  -> IO CalibrationResult
runCachedHullWhite fixParams a0 sigma0 cachedAv cachedSigmaV = do
  setEvaluationDate (Just calibrationToday)
  ac365 <- dayCounter Actual365FixedStandard
  q <- simpleQuote 0.04875825
  ts <- TS.flatForward calibrationSettlement q ac365 Continuous Annual
  model <- hullWhite ts a0 sigma0
  index <- IR.iborIndex IR.Euribor6M (Just ts)
  runCalibration model index ts fixParams cachedAv cachedSigmaV

-- |@testCachedHullWhite2@: same as 'runCachedHullWhite' but against a
-- zero-fixing-days variant of the index (no start delay).
runCachedHullWhite2 :: IO CalibrationResult
runCachedHullWhite2 = do
  setEvaluationDate (Just calibrationToday)
  ac365 <- dayCounter Actual365FixedStandard
  q <- simpleQuote 0.04875825
  ts <- TS.flatForward calibrationSettlement q ac365 Continuous Annual
  model <- hullWhite ts 0.1 0.01
  index <- IR.iborIndex IR.Euribor6M (Just ts)
  tenr <- IR.tenor index
  ccy <- IR.currency index
  cal <- fixingCalendar index
  dc <- IR.dayCounter index
  let conv = IR.businessDayConvention index
      eom = IR.endOfMonth index
  index0 <- IR.iborIndex (IR.Ibor "Euribor" tenr 0 ccy cal conv eom dc) (Just ts)
  runCalibration model index0 ts [] 0.0482063 0.00582687

runCalibration :: HullWhite -> IR.IborIndex -> TS.YieldTermStructure -> [Bool] -> Double -> Double -> IO CalibrationResult
runCalibration model index ts fixParams cachedAv cachedSigmaV = do
  engine <- jamshidianSwaptionEngine model (Just ts)
  thirty360bb <- dayCounter Thirty360BondBasis
  act360 <- dayCounter (Actual360 False)
  helpers <- forM calibrationData $ \(s, l, v) -> do
    vol <- simpleQuote v
    h <- swaptionHelper (s, Years) (l, Years) vol index (1, Years) thirty360bb act360 ts RelativePriceError Nothing 1.0 ShiftedLognormal 0.0 Nothing AveragingCompound
    Model.setPricingEngine h engine
    asCalibrationHelper h
  let method = LevenbergMarquardt 1.0e-8 1.0e-8 1.0e-8 False
      ec = EndCriteria 10000 100 1e-6 1e-8 1e-8
  calibrate model (map (, 1.0) helpers) method ec Nothing fixParams
  ps@[calcA, calcSigma] <- params model
  calcValue <- Model.value model ps helpers
  cachedVal <- Model.value model [cachedAv, cachedSigmaV] helpers
  pure CalibrationResult
    { calculatedA = calcA, calculatedSigma = calcSigma
    , cachedA = cachedAv, cachedSigma = cachedSigmaV
    , calculatedValue = calcValue, cachedValue = cachedVal
    }

-- |@testSwaps@: reprice 27 (start, length, rate) combinations with a
-- discounting engine (expected) and a Hull-White tree engine (calculated).
runSwaps :: IO [SwapCheck]
runSwaps = do
  cal <- calendar TARGET
  today <- evaluationDate >>= \d -> adjust cal d Following
  setEvaluationDate (Just today)
  settlement <- advance cal today (2, Days) Following False
  ac365 <- dayCounter Actual365FixedStandard
  curveDates <- (settlement :) <$> mapM (\(n, u) -> advance cal settlement (n, u) Following False)
    [(1, Weeks), (1, Months), (3, Months), (6, Months), (9, Months)
    ,(1, Years), (2, Years), (3, Years), (5, Years), (10, Years), (15, Years)]
  let discounts =
        [1.0, 0.999258, 0.996704, 0.990809, 0.981798, 0.972570
        ,0.963430, 0.929532, 0.889267, 0.803693, 0.596903, 0.433022]
  ts <- TS.interpolatedDiscountCurve (zip curveDates discounts) ac365 cal [] LogLinear
  model <- hullWhite ts 0.1 0.01
  euribor <- IR.iborIndex IR.Euribor6M (Just ts)
  riskFreeEngine <- discountingSwapEngine ts Nothing Nothing Nothing
  treeEngine <- treeVanillaSwapEngine model 120 Nothing
  thirty360bb <- dayCounter Thirty360BondBasis
  act360 <- dayCounter (Actual360 False)
  results <- forM starts $ \s -> do
    startDate <- advance cal settlement (s, Months) Following False
    when (startDate < today) $ advance cal startDate (-2, Days) Following False >>= \fd -> addFixing euribor fd 0.03 False
    forM lengths $ \l -> do
      maturity <- advance cal startDate (l, Years) Following False
      fixedSchedule <- schedule (Just startDate) maturity (1, Years) cal Unadjusted Unadjusted Forward False Nothing Nothing
      floatSchedule <- schedule (Just startDate) maturity (6, Months) cal Following Following Forward False Nothing Nothing
      forM rates $ \r -> do
        swp <- vanillaSwap Payer 1000000.0 fixedSchedule r thirty360bb floatSchedule euribor 0.0 act360 (Just Following) Nothing
        setPricingEngine swp riskFreeEngine
        expected <- npv swp
        setPricingEngine swp treeEngine
        calculated <- npv swp
        pure SwapCheck
          { startMonths = s, lengthYears = l, swapRate = r
          , expectedNPV = expected, calculatedNPV = calculated
          }
  pure (concatMap concat results)
  where
    starts = [-3, 0, 3] :: [Int]
    lengths = [2, 5, 10] :: [Int]
    rates = [0.02, 0.04, 0.06]

-- |@testFuturesConvexityBias@: G. Kirikos, D. Novak, \"Convexity Conundrums\", Risk Magazine, March 1997.
futuresConvexityChecks :: [ConvexityCheck]
futuresConvexityChecks = map mkCheck convexityData
  where
    futureQuote = 94.0
    sigma = 0.015
    t = 5.0
    futureImpliedRate = (100.0 - futureQuote) / 100.0
    convexityData =
      [ (5.25, 0.03, 0.0573037), (5.25, 1e-4, 0.0568627), (5.25, 0.0, 0.0568611)
      , (5.001, 0.03, 0.0575736), (5.0, 0.03, 0.0575747) ]
    mkCheck (bigT, a, expected) = ConvexityCheck
      { convexityT = bigT, convexityA = a
      , expectedForward = expected
      , calculatedForward = futureImpliedRate - convexityBias futureQuote t bigT sigma a
      }

-- |@testExtendedCoxIngersollRossDiscountFactor@.
runExtendedCirDiscountFactor :: IO DiscountCheck
runExtendedCirDiscountFactor = do
  today <- evaluationDate
  ac365 <- dayCounter Actual365FixedStandard
  q <- simpleQuote rate
  rts <- TS.flatForward today q ac365 Continuous Annual
  model <- extendedCoxIngersollRoss rts rate 1.0 1e-4 rate True
  dNow <- TS.discount rts now False
  dMat <- TS.discount rts maturity False
  calculated <- discountBond model now maturity rate
  pure DiscountCheck { expectedDF = dMat / dNow, calculatedDF = calculated }
  where
    rate = 0.1
    now = 1.5
    maturity = 2.5

-- |@testVasicekDiscountFactorForSmallMeanReversion@ (closed-form reference, no curve involved).
runVasicekSmallMeanReversion :: IO DiscountCheck
runVasicekSmallMeanReversion = do
  model <- vasicek r0 a b sigma lambda
  calculated <- discountBond model now maturity r0
  pure DiscountCheck
    { expectedDF = exp (-r0 * maturity + sigma * sigma * maturity ** 3 / 6.0)
    , calculatedDF = calculated
    }
  where
    r0 = 0.05
    a = 1e-12
    b = 0.05
    sigma = 0.01
    lambda = 0.0
    now = 0.0
    maturity = 1.0

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