hasquant-0.7.0.0: test/example/QuantLib/Example/InflationInstruments.hs
{-# LANGUAGE OverloadedLists #-}
module QuantLib.Example.InflationInstruments
(
Result(..)
, run
) where
import Control.Monad(forM_)
import qualified QuantLib.CashFlow as CF
import QuantLib.Currency(currency, Ccy(GBP))
import QuantLib.Index(addFixing)
import qualified QuantLib.Index.InterestRate as I
import QuantLib.Index.Inflation
import QuantLib.InterestRate(Compounding(Continuous, Compounded), interestRate)
import QuantLib.Instrument
import QuantLib.Math(Interpolation(..))
import QuantLib.Instrument.Bond
import QuantLib.Instrument.Swap
import QuantLib.PricingEngine
import QuantLib.Quote
import QuantLib.Context
import QuantLib.TermStructure.Inflation
import QuantLib.TermStructure.Yield
import QuantLib.Time.Calendar
import QuantLib.Time.Date
import QuantLib.Time.Schedule
-- |Two 'ZeroInflationIndex' instances sharing a name/region/currency/frequency/lag share
-- fixings via QuantLib's global @IndexManager@ (keyed by name, not per-instance) -- verified
-- with a standalone scratch check before relying on it here. This lets us build a curve from
-- an *unlinked* index (@idx0@), then construct a second, curve-linked index (@idx1@) that can
-- forecast off it, entirely sidestepping the RelinkableHandle-based bootstrap upstream tests
-- use (no @RelinkableHandle@\/@linkTo@ concept exists in hasquant -- see README.md's TODO).
data Result = Result
{ zcisNpvBeforeFairRate :: Double
, zcisNpvAtFairRate :: Double
, cpiSwapNpvAtFairRate :: Double
, yoySwapNpvAtFairRate :: Double
, cpiBondDirtyMinusCleanAccrued :: Double
, cpiBondPriceLowInflation :: Double
, cpiBondPriceHighInflation :: Double
, cpiLegBondNpv :: Double
, yoyLegSwapNpv :: Double
} deriving Show
evalDate :: Day
evalDate = 2 `january` 2024
baseDate :: Day
baseDate = 1 `october` 2023
obsLag :: (Word, TimeUnit)
obsLag = (3, Months)
-- index availabilityLag must satisfy (swapObservationLag - indexPeriod) >= availabilityLag;
-- with a Monthly index and 3M swap observation lag, 1M availability lag is required.
obsLagI :: (Int, TimeUnit)
obsLagI = (1, Months)
maturity2Y :: Day
maturity2Y = 2 `january` 2026
maturity5Y :: Day
maturity5Y = 2 `january` 2029
flatRate :: Double
flatRate = 0.03
nominalRate :: Double
nominalRate = 0.02
nominal :: Double
nominal = 1000000.0
faceAmount :: Double
faceAmount = 100.0
couponRate :: Double
couponRate = 0.05
settlementDays :: Word
settlementDays = 2
-- |Prices the ZCIIS at its quoted rate and again at its own fair rate -- the
-- latter reprices to ~0 in one step, unlike the CPISwap refinement below.
priceZcis :: DayCounter -> Calendar -> ZeroInflationIndex -> PricingEngine -> IO (Double, Double)
priceZcis dc cal idx1 swapEngine = do
zcis0 <- zeroCouponInflationSwap Payer nominal evalDate maturity5Y cal Unadjusted dc flatRate idx1 obsLag CPILinear False cal Following
zcis0Inst <- asInstrument zcis0
setPricingEngine zcis0Inst swapEngine
npvBefore <- npv zcis0Inst
fairZcisRate <- fairRate zcis0
zcis1 <- zeroCouponInflationSwap Payer nominal evalDate maturity5Y cal Unadjusted dc fairZcisRate idx1 obsLag CPILinear False cal Following
zcis1Inst <- asInstrument zcis1
setPricingEngine zcis1Inst swapEngine
npvAtFair <- npv zcis1Inst
return (npvBefore, npvAtFair)
-- |Context shared by every iteration of the CPISwap fair-rate refinement below.
data CpiSwapContext = CpiSwapContext
{ cpiSwapDc :: DayCounter
, cpiSwapFloatSchedule :: Schedule
, cpiSwapFloatIdx :: I.IborIndex
, cpiSwapBaseCPI0 :: Double
, cpiSwapFixedSchedule :: Schedule
, cpiSwapIdx1 :: ZeroInflationIndex
, cpiSwapEngine :: PricingEngine
}
buildCpiSwap :: CpiSwapContext -> Double -> IO (CPISwap, Instrument)
buildCpiSwap ctx r = do
s <- cpiSwap Payer nominal True 0.0 (cpiSwapDc ctx) (cpiSwapFloatSchedule ctx) Unadjusted 0
(cpiSwapFloatIdx ctx) r (cpiSwapBaseCPI0 ctx)
(cpiSwapDc ctx) (cpiSwapFixedSchedule ctx) Unadjusted obsLag (cpiSwapIdx1 ctx) CPILinear Nothing
i <- asInstrument s
setPricingEngine i (cpiSwapEngine ctx)
_ <- npv i
pure (s, i)
-- |Rebuilding once at @CPISwap::fairRate()@'s single-step correction leaves a
-- non-trivial residual NPV for a CPI leg (unlike ZeroCouponInflationSwap, which
-- reprices to ~0 in one step) -- iterating the correction converges it to
-- machine-epsilon scale.
refineCpiSwapRate :: CpiSwapContext -> Int -> Double -> IO (Double, Double)
refineCpiSwapRate ctx 0 r = do
(_, i) <- buildCpiSwap ctx r
n <- npv i
pure (r, n)
refineCpiSwapRate ctx n r = do
(s, _) <- buildCpiSwap ctx r
r' <- fairRate s
refineCpiSwapRate ctx (n - 1) r'
run :: IO Result
run = do
setEvaluationDate $ Just evalDate
dc <- dayCounter Actual365FixedStandard
cal <- calendar Null
gbp <- currency GBP
reg <- customRegion "Wonderland" "WL"
-- unlinked index carrying only historical fixings, used to build the ZCIIS helpers
fixingDates <- mapM (\n -> advance cal (1 `january` 2022) (n, Months) Unadjusted False) [0 .. 24 :: Int]
idx0 <- customZeroInflationIndex "WL CPI" reg False Monthly obsLagI gbp Nothing
forM_ (zip [1 :: Double ..] fixingDates) $ \(i, d) -> addFixing idx0 d (260.0 + i) False
q1 <- simpleQuote flatRate
q2 <- simpleQuote flatRate
h1 <- zeroCouponInflationSwapHelper q1 obsLag maturity2Y cal Unadjusted dc idx0 CPILinear LastRelevantDate Nothing
h2 <- zeroCouponInflationSwapHelper q2 obsLag maturity5Y cal Unadjusted dc idx0 CPILinear LastRelevantDate Nothing
zeroCurve <- piecewiseZeroInflationCurve evalDate baseDate Monthly dc [h1, h2] Linear
-- curve-linked index (same name/region/etc, picks up idx0's fixings automatically)
idx1 <- customZeroInflationIndex "WL CPI" reg False Monthly obsLagI gbp (Just zeroCurve)
nominalQ <- simpleQuote nominalRate
nominalCurve <- flatForward (ReferenceDate evalDate) nominalQ dc Continuous Annual
swapEngine <- discountingSwapEngine nominalCurve Nothing Nothing Nothing
bondEngine <- discountingBondEngine nominalCurve Nothing
-- ZCIIS: reprices to ~0 once rebuilt at its own fair rate
(npvBefore, npvAtFair) <- priceZcis dc cal idx1 swapEngine
-- CPISwap: same self-consistency discipline, exercising the 19-arg shim end to end.
floatSchedule <- schedule (Just evalDate) maturity5Y (6, Months) cal Unadjusted Unadjusted Backward False Nothing Nothing
fixedSchedule <- schedule (Just evalDate) maturity5Y (6, Months) cal Unadjusted Unadjusted Backward False Nothing Nothing
floatIdx <- I.iborIndex (I.GbpLibor (6, Months)) (Just nominalCurve)
baseCPI0 <- fixing idx1 evalDate
let cpiSwapCtx = CpiSwapContext
{ cpiSwapDc = dc
, cpiSwapFloatSchedule = floatSchedule
, cpiSwapFloatIdx = floatIdx
, cpiSwapBaseCPI0 = baseCPI0
, cpiSwapFixedSchedule = fixedSchedule
, cpiSwapIdx1 = idx1
, cpiSwapEngine = swapEngine
}
(_, cpiSwapNpv) <- refineCpiSwapRate cpiSwapCtx (15 :: Int) flatRate
-- YoY inflation curve + swap, mirroring the same unlinked/linked-index trick
yidx0 <- customYoyInflationIndex "WL YoY CPI" reg False Monthly obsLagI gbp Nothing
forM_ (zip [1 :: Double ..] fixingDates) $ \(i, d) -> addFixing yidx0 d (flatRate + i * 0.0001) False
qy1 <- simpleQuote flatRate
qy2 <- simpleQuote flatRate
hy1 <- yearOnYearInflationSwapHelper qy1 obsLag maturity2Y cal Unadjusted dc yidx0 CPILinear nominalCurve LastRelevantDate Nothing
hy2 <- yearOnYearInflationSwapHelper qy2 obsLag maturity5Y cal Unadjusted dc yidx0 CPILinear nominalCurve LastRelevantDate Nothing
yoyCurve <- piecewiseYoyInflationCurve evalDate baseDate flatRate Monthly dc [hy1, hy2] Linear
yidx1 <- customYoyInflationIndex "WL YoY CPI" reg False Monthly obsLagI gbp (Just yoyCurve)
yoySchedule <- schedule (Just evalDate) maturity5Y (6, Months) cal Unadjusted Unadjusted Backward False Nothing Nothing
yoySwap0 <- yearOnYearInflationSwap Payer nominal fixedSchedule flatRate dc yoySchedule yidx1 obsLag CPILinear 0.0 dc cal Unadjusted
yoySwap0Inst <- asInstrument yoySwap0
setPricingEngine yoySwap0Inst swapEngine
fairYoyRate <- fairRate yoySwap0
yoySwap1 <- yearOnYearInflationSwap Payer nominal fixedSchedule fairYoyRate dc yoySchedule yidx1 obsLag CPILinear 0.0 dc cal Unadjusted
yoySwap1Inst <- asInstrument yoySwap1
setPricingEngine yoySwap1Inst swapEngine
yoySwapNpv <- npv yoySwap1Inst
-- CPIBond: dirty/clean/accrued self-consistency, plus a directional inflation bump
cpiBondSchedule <- schedule (Just evalDate) maturity5Y (6, Months) cal Unadjusted Unadjusted Backward False Nothing Nothing
cb <- cpiBond settlementDays faceAmount baseCPI0 obsLag idx1 CPILinear cpiBondSchedule [couponRate]
dc Unadjusted (Just evalDate) cal (0, Days) cal Unadjusted False
cbInst <- asBond cb >>= asInstrument
setPricingEngine cbInst bondEngine
_ <- npv cbInst
cbSettlement <- advance cal evalDate (fromIntegral settlementDays, Days) Following False
cbClean <- currentCleanPrice cb
cbDirty <- currentDirtyPrice cb
cbAccrued <- accruedAmount cb cbSettlement
q3 <- simpleQuote (flatRate + 0.02) -- higher expected inflation
h3 <- zeroCouponInflationSwapHelper q3 obsLag maturity5Y cal Unadjusted dc idx0 CPILinear LastRelevantDate Nothing
hiZeroCurve <- piecewiseZeroInflationCurve evalDate baseDate Monthly dc [h1, h3] Linear
hiIdx <- customZeroInflationIndex "WL CPI" reg False Monthly obsLagI gbp (Just hiZeroCurve)
cbHi <- cpiBond settlementDays faceAmount baseCPI0 obsLag hiIdx CPILinear cpiBondSchedule [couponRate]
dc Unadjusted (Just evalDate) cal (0, Days) cal Unadjusted False
cbHiInst <- asBond cbHi >>= asInstrument
setPricingEngine cbHiInst bondEngine
_ <- npv cbHiInst
cbHiClean <- currentCleanPrice cbHi
-- cpiLeg exercised directly via the generic Leg-based 'bond' constructor
cpiL <- CF.cpiLeg cpiBondSchedule idx1 baseCPI0 obsLag [faceAmount] [couponRate] dc Unadjusted cal CPILinear True
cpiLegBond <- bond settlementDays cal (Just evalDate) cpiL >>= asInstrument
setPricingEngine cpiLegBond bondEngine
cpiLegNpv <- npv cpiLegBond
-- yoyInflationLeg exercised via the generic Leg-based 'swapFromLegs' constructor
yoyL <- CF.yoyInflationLeg yoySchedule cal yidx1 obsLag CPILinear [nominal] dc Unadjusted [0] [1.0] [0.0] [] []
fixedIR <- interestRate flatRate dc Compounded Annual
fixedL <- CF.fixedRateLeg fixedSchedule [nominal] [fixedIR] Unadjusted dc cal
yoyLegSwap <- swapFromLegs [(fixedL, True), (yoyL, False)]
yoyLegSwapInst <- asInstrument yoyLegSwap
setPricingEngine yoyLegSwapInst swapEngine
yoyLegNpv <- npv yoyLegSwapInst
return Result
{ zcisNpvBeforeFairRate = npvBefore
, zcisNpvAtFairRate = npvAtFair
, cpiSwapNpvAtFairRate = cpiSwapNpv
, yoySwapNpvAtFairRate = yoySwapNpv
, cpiBondDirtyMinusCleanAccrued = cbDirty - (cbClean + cbAccrued)
, cpiBondPriceLowInflation = cbClean
, cpiBondPriceHighInflation = cbHiClean
, cpiLegBondNpv = cpiLegNpv
, yoyLegSwapNpv = yoyLegNpv
}
-- vim: set ft=haskell ff=unix ts=8 sts=2 sw=2 et: