hasquant-0.6.0.0: test/example/QuantLib/Example/Gaussian1dModels.hs
{-# LANGUAGE TupleSections #-}
module QuantLib.Example.Gaussian1dModels
(
Result(..)
, CalibrationCheck(..)
, run
) where
import Control.Monad (forM, forM_, replicateM)
import Data.Maybe (mapMaybe)
import qualified QuantLib.CashFlow as CF
import qualified QuantLib.Index.InterestRate as IR
import QuantLib.InterestRate
import QuantLib.Instrument
import QuantLib.Instrument.Option (BermudanExercise(..))
import QuantLib.Instrument.Swap
import QuantLib.Math (EndCriteria(..), OptimizationMethod(..))
import QuantLib.Model hiding(setPricingEngine)
import qualified QuantLib.Model as Model
import QuantLib.PricingEngine
import QuantLib.Quote
import QuantLib.TermStructure.Volatility
import qualified QuantLib.TermStructure.Yield as TS
import QuantLib.Time.Calendar
import QuantLib.Time.Date
import QuantLib.Time.Schedule
import QuantLib.Settings
-- |Per-helper (modelValue, marketValue, impliedVolatility) after calibrating a basket --
-- what upstream's @printModelCalibration@ prints, and the strongest reachable calibration
-- check: 'calibrationBasket' returns generic 'BlackCalibrationHelper's (upstream erases to
-- the base inside @basketgeneratingengine.cpp@ before the vector is returned), so nominal\/
-- strike\/expiry on individual basket elements are not recoverable from hasquant -- see
-- CLAUDE.md's "A C++ return type erased by upstream itself" note.
data CalibrationCheck = CalibrationCheck
{ ccModelValue :: Double
, ccMarketValue :: Double
, ccImpliedVol :: Double
} deriving Show
data Result = Result
{ basketNaiveLen :: Int -- ^calibration basket size, Naive mode (bermudan swaption)
, basketMsdgLen :: Int -- ^..., MaturityStrikeByDeltaGamma mode
, basketCalibration :: [CalibrationCheck] -- ^Naive basket, after calibrateVolatilitiesIterative
, npvAtmGsr :: Double -- ^bermudan swaption NPV, ATM-calibrated GSR
, npvDealStrikeGsr :: Double -- ^..., after recalibrating to the MaturityStrikeByDeltaGamma basket
, amortizingBasketLen :: Int -- ^MSDG basket size for the amortizing-nominal underlying
, callRightBasketLen0 :: Int -- ^MSDG basket size for the bond call right, oas=0
, npvCallRight0 :: Double -- ^bond call right NPV, oas=0
, callRightBasketLen100 :: Int -- ^..., oas=100bp
, npvCallRight100 :: Double -- ^bond call right NPV, oas=100bp
, underlyingCmsSwapNpv :: Double -- ^FloatFloatSwap underlying NPV (LinearTsrPricer/BlackIborCouponPricer)
, cmsLegNpv :: Double
, euriborLegNpv :: Double
, floatBasketNaiveLen :: Int -- ^Naive basket size for the CMS-vs-Euribor float swaption
, npvFloatGsr :: Double -- ^float swaption NPV under (Naive-basket-calibrated) GSR
, underlyingValueGsr :: Double -- ^float swaption's "underlyingValue" additional result, GSR
, npvFloatMarkov :: Double -- ^float swaption NPV under MarkovFunctional
, underlyingValueMarkovPreCalib :: Double -- ^"underlyingValue" before coterminal calibration
, underlyingValueMarkovPostCalib :: Double -- ^"underlyingValue" after coterminal calibration
} deriving Show
underlyingValueOf :: FloatFloatSwaption -> IO Double
underlyingValueOf i = do
results <- additionalResults i
case mapMaybe (\(k, v) -> if k == "underlyingValue" then Just v else Nothing) results of
[RealVal v] -> return v
_ -> error "underlyingValueOf: expected exactly one RealVal \"underlyingValue\" additional result"
calibrationCheck :: BlackCalibrationHelper -> IO CalibrationCheck
calibrationCheck h = do
mv <- Model.modelValue h
ccv <- Model.marketValue h
vol <- Model.impliedVolatility h mv 1.0e-6 1000 0.0 2.0
return CalibrationCheck { ccModelValue = mv, ccMarketValue = ccv, ccImpliedVol = vol }
lmMethod :: OptimizationMethod
lmMethod = LevenbergMarquardt 1.0e-8 1.0e-8 1.0e-8 False
lmEndCriteria :: EndCriteria
-- Only the max-iterations field is actually consulted by LevenbergMarquardt (per the
-- upstream example's own comment at ex/Gaussian1dModels.cpp:257-258).
lmEndCriteria = EndCriteria 1000 10 1.0e-8 1.0e-8 1.0e-8
run :: IO Result
run = do
cal <- calendar TARGET
setEvaluationDate (Just refDate)
dc365 <- dayCounter Actual365FixedStandard
thirty360bb <- dayCounter Thirty360BondBasis
act360 <- dayCounter (Actual360 False)
forward6mQuote <- simpleQuote forward6mLevel
oisQuote <- simpleQuote oisLevel
yts6m <- TS.flatForward refDate forward6mQuote dc365 Continuous Annual
ytsOis <- TS.flatForward refDate oisQuote dc365 Continuous Annual
euribor6m <- IR.iborIndex IR.Euribor6M (Just yts6m)
volQuote <- simpleQuote volLevel
swaptionVol <- constantSwaptionVolatility 0 cal ModifiedFollowing volQuote dc365 ShiftedLognormal 0.0
effectiveDate <- advance cal refDate (2, Days) Following False
maturity <- advance cal effectiveDate (10, Years) Following False
fixedSchedule <- schedule (Just effectiveDate) maturity (1, Years) cal ModifiedFollowing ModifiedFollowing Forward False Nothing Nothing
floatSchedule <- schedule (Just effectiveDate) maturity (6, Months) cal ModifiedFollowing ModifiedFollowing Forward False Nothing Nothing
-- 1. A standard 10y bermudan payer swaption, struck at 4%, exercisable yearly.
vanilla <- vanillaSwap Payer 1.0 fixedSchedule strike thirty360bb floatSchedule euribor6m 0.00 act360 Nothing Nothing
underlying <- nonstandardSwapFromVanilla vanilla
-- Exercise dates: fixedSchedule[1..9], each shifted back 2 business days -- upstream loops
-- i = 1..9 over the *schedule*'s own dates, not the underlying's coupon accrual starts.
fixedDates <- dates fixedSchedule
exerciseDates <- forM (take 9 (drop 1 fixedDates)) $ \d -> advance cal d (-2, Days) Following False
let ex = Bermudan (BermudanExercise exerciseDates False)
swaption1 <- nonstandardSwaption underlying ex Physical PhysicalOTC
-- One-factor GSR model with piecewise volatility adapted to the exercise dates.
let stepDates = init exerciseDates
gsrVolQuotes <- replicateM (length stepDates + 1) (simpleQuote 0.01)
gsrReversionQuote <- simpleQuote reversion
gsrModel <- gsr yts6m stepDates gsrVolQuotes gsrReversionQuote 60.0
gsrGm <- gsrAsGaussian1dModel gsrModel
swaptionEngine <- gaussian1dSwaptionEngine gsrGm 64 7.0 True False (Just ytsOis) None
nonstandardSwaptionEngine <- gaussian1dNonstandardSwaptionEngine gsrGm 64 7.0 True False Nothing (Just ytsOis) None
setPricingEngine swaption1 nonstandardSwaptionEngine
swapBase <- IR.liborSwapIndex IR.EuriborSwapIsdaFixA (10, Years) (Just yts6m) (Just ytsOis)
-- Naive calibration basket: ATM swaptions adapted to the exercise dates and maturity.
basket1 <- calibrationBasket swaption1 swapBase swaptionVol CalibrationBasketNaive
forM_ basket1 (`Model.setPricingEngine` swaptionEngine)
calibrateVolatilitiesIterative gsrModel basket1 lmMethod lmEndCriteria Nothing []
calib1 <- mapM calibrationCheck basket1
npv1 <- npv swaption1
-- MaturityStrikeByDeltaGamma basket: maturity/strike/nominal match the underlying's NPV,
-- delta and gamma at each exercise date -- recalibrate and reprice.
basket2 <- calibrationBasket swaption1 swapBase swaptionVol MaturityStrikeByDeltaGamma
forM_ basket2 (`Model.setPricingEngine` swaptionEngine)
calibrateVolatilitiesIterative gsrModel basket2 lmMethod lmEndCriteria Nothing []
npv2 <- npv swaption1
-- 2. Linear amortizing nominal schedule, same exercise dates.
let n = length fixedDates - 1
nominalFixed = [1.0 - fromIntegral i / fromIntegral n | i <- [0 .. n - 1]]
nominalFloating = concatMap (\x -> [x, x]) nominalFixed -- 6m vs. 1y: two float periods per fixed one
strikes = replicate n strike
underlying2 <- nonstandardSwap Payer nominalFixed nominalFloating fixedSchedule strikes thirty360bb
floatSchedule euribor6m 1.0 0.0 act360 False False Nothing
swaption2 <- nonstandardSwaption underlying2 ex Physical PhysicalOTC
setPricingEngine swaption2 nonstandardSwaptionEngine
basket3 <- calibrationBasket swaption2 swapBase swaptionVol MaturityStrikeByDeltaGamma
-- 3. Bond call right, modeled as a rebated-exercise swaption on a one-leg swap with
-- notional reimbursement at maturity.
let nominalFixed2 = replicate n 1.0
nominalFloating2 = replicate (2 * n) 0.0 -- null the second leg
underlying3 <- nonstandardSwap Receiver nominalFixed2 nominalFloating2 fixedSchedule strikes thirty360bb
floatSchedule euribor6m 1.0 0.0 act360 False True Nothing -- final capital exchange
let exercise2 = Rebated ex (-1.0) 2 cal Following
swaption3 <- nonstandardSwaption underlying3 exercise2 Physical PhysicalOTC
oasQuote <- simpleQuote 0.0
nonstandardSwaptionEngine2 <- gaussian1dNonstandardSwaptionEngine gsrGm 64 7.0 True False (Just oasQuote) Nothing None -- discount on 6m, not OIS
setPricingEngine swaption3 nonstandardSwaptionEngine2
basket4 <- calibrationBasket swaption3 swapBase swaptionVol MaturityStrikeByDeltaGamma
forM_ basket4 (`Model.setPricingEngine` swaptionEngine)
calibrateVolatilitiesIterative gsrModel basket4 lmMethod lmEndCriteria Nothing []
npv3 <- npv swaption3
-- Add a 100bp credit spread and regenerate the basket.
_ <- setValue oasQuote 0.01
basket5 <- calibrationBasket swaption3 swapBase swaptionVol MaturityStrikeByDeltaGamma
forM_ basket5 (`Model.setPricingEngine` swaptionEngine)
calibrateVolatilitiesIterative gsrModel basket5 lmMethod lmEndCriteria Nothing []
npv4 <- npv swaption3
-- 4. CMS 10y vs Euribor 6m float-float swaption, exercisable yearly.
underlying4 <- floatFloatSwap Payer 1.0 1.0 fixedSchedule swapBase thirty360bb floatSchedule euribor6m act360
defaultFloatFloatSwapOpts{ffsSpread2 = 0.0010}
swaption4 <- floatFloatSwaption underlying4 ex Physical PhysicalOTC
floatSwaptionEngine <- gaussian1dFloatFloatSwaptionEngine gsrGm 64 7.0 True False Nothing (Just ytsOis) True None
setPricingEngine swaption4 floatSwaptionEngine
reversionQuote <- simpleQuote reversion
cmsPricer <- CF.linearTsrPricer swaptionVol reversionQuote Nothing (CF.LinearTsrPricerSettings CF.LinearTsrRateBound Nothing)
-- Only the CMS leg (leg 0) needs an explicit pricer -- a plain (uncapped/unfloored) Euribor
-- leg computes its forecast fixing directly, with no pricer required (confirmed in the
-- increment 2 smoke test).
leg0 <- leg underlying4 0
CF.setCouponPricer leg0 cmsPricer
swapPricer <- discountingSwapEngine ytsOis Nothing Nothing Nothing
setPricingEngine underlying4 swapPricer
npv5 <- npv underlying4
legNpv0 <- legNPV underlying4 0
legNpv1 <- legNPV underlying4 1
basket6 <- floatFloatSwaptionCalibrationBasket swaption4 swapBase swaptionVol CalibrationBasketNaive
forM_ basket6 (`Model.setPricingEngine` swaptionEngine)
calibrateVolatilitiesIterative gsrModel basket6 lmMethod lmEndCriteria Nothing []
npv6 <- npv swaption4
underlyingValGsr <- underlyingValueOf swaption4
-- 5. The Markov-functional model: calibrates to the underlying's own smile directly
-- (via its step-date/tenor/swapIndexBase construction), unlike GSR which only sees the
-- coterminal calibration basket.
let markovStepDates = exerciseDates
markovSigmas = replicate (length markovStepDates + 1) 0.01
cmsTenors = replicate (length markovStepDates) (10, Years) :: [(Word, TimeUnit)]
markov <- markovFunctional yts6m reversion markovStepDates markovSigmas swaptionVol markovStepDates cmsTenors swapBase 16
markovGm <- markovFunctionalAsGaussian1dModel markov
swaptionEngineMarkov <- gaussian1dSwaptionEngine markovGm 8 5.0 True False (Just ytsOis) None
floatEngineMarkov <- gaussian1dFloatFloatSwaptionEngine markovGm 16 7.0 True False Nothing (Just ytsOis) True None
setPricingEngine swaption4 floatEngineMarkov
npv7 <- npv swaption4
underlyingValMarkovPre <- underlyingValueOf swaption4
-- Calibrate the Markov model's own sigma function to the coterminal ATM swaptions basket
-- (basket6, from above) -- this shouldn't move the underlying-smile match much, since that
-- was already fixed by the model's construction.
basket6ch <- mapM Model.asCalibrationHelper basket6
forM_ basket6 (`Model.setPricingEngine` swaptionEngineMarkov)
Model.calibrate markov (map (, 1.0) basket6ch) lmMethod lmEndCriteria Nothing []
underlyingValMarkovPost <- underlyingValueOf swaption4
return Result
{ basketNaiveLen = length basket1
, basketMsdgLen = length basket2
, basketCalibration = calib1
, npvAtmGsr = npv1
, npvDealStrikeGsr = npv2
, amortizingBasketLen = length basket3
, callRightBasketLen0 = length basket4
, npvCallRight0 = npv3
, callRightBasketLen100 = length basket5
, npvCallRight100 = npv4
, underlyingCmsSwapNpv = npv5
, cmsLegNpv = legNpv0
, euriborLegNpv = legNpv1
, floatBasketNaiveLen = length basket6
, npvFloatGsr = npv6
, underlyingValueGsr = underlyingValGsr
, npvFloatMarkov = npv7
, underlyingValueMarkovPreCalib = underlyingValMarkovPre
, underlyingValueMarkovPostCalib = underlyingValMarkovPost
}
where
refDate = 30 `april` 2014
forward6mLevel = 0.025
oisLevel = 0.02
volLevel = 0.20
strike = 0.04
reversion = 0.01
-- vim: set ft=haskell ff=unix ts=8 sts=2 sw=2 et: