hasquant-0.5.0.2: test/example/QuantLib/Example/InflationInstruments.hs
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.Settings
import QuantLib.TermStructure.Inflation
import QuantLib.TermStructure.Yield
import QuantLib.Time.Calendar
import QuantLib.Time.Date hiding(today)
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
today :: Day
today = 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 today maturity5Y cal Unadjusted dc flatRate idx1 obsLag CPILinear False cal Following
zcis0Inst <- asInstrument zcis0
setPricingEngine zcis0Inst swapEngine
npvBefore <- npv zcis0Inst
fairZcisRate <- zcisFairRate zcis0
zcis1 <- zeroCouponInflationSwap Payer nominal today 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' <- cpiSwapFairRate s
refineCpiSwapRate ctx (n - 1) r'
run :: IO Result
run = do
setEvaluationDate $ Just today
dc <- dayCounter Actual365FixedStandard
cal <- calendar Null
gbp <- currency GBP
reg <- region' "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 <- zeroInflationIndex' "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 today baseDate Monthly dc [h1, h2] Linear
-- curve-linked index (same name/region/etc, picks up idx0's fixings automatically)
idx1 <- zeroInflationIndex' "WL CPI" reg False Monthly obsLagI gbp (Just zeroCurve)
nominalQ <- simpleQuote nominalRate
nominalCurve <- flatForward today 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 today) maturity5Y (6, Months) cal Unadjusted Unadjusted Backward False Nothing Nothing
fixedSchedule <- schedule (Just today) maturity5Y (6, Months) cal Unadjusted Unadjusted Backward False Nothing Nothing
floatIdx <- I.iborIndex (I.GbpLibor (6, Months)) (Just nominalCurve)
baseCPI0 <- fixing idx1 today
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 <- yoyInflationIndex' "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 today baseDate flatRate Monthly dc [hy1, hy2] Linear
yidx1 <- yoyInflationIndex' "WL YoY CPI" reg False Monthly obsLagI gbp (Just yoyCurve)
yoySchedule <- schedule (Just today) 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 <- yoyFairRate 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 today) maturity5Y (6, Months) cal Unadjusted Unadjusted Backward False Nothing Nothing
cb <- cpiBond settlementDays faceAmount baseCPI0 obsLag idx1 CPILinear cpiBondSchedule [couponRate]
dc Unadjusted (Just today) cal (0, Days) cal Unadjusted False
cbInst <- asBond cb >>= asInstrument
setPricingEngine cbInst bondEngine
_ <- npv cbInst
cbSettlement <- advance cal today (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 today baseDate Monthly dc [h1, h3] Linear
hiIdx <- zeroInflationIndex' "WL CPI" reg False Monthly obsLagI gbp (Just hiZeroCurve)
cbHi <- cpiBond settlementDays faceAmount baseCPI0 obsLag hiIdx CPILinear cpiBondSchedule [couponRate]
dc Unadjusted (Just today) 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 today) cpiL >>= asInstrument
setPricingEngine cpiLegBond bondEngine
cpiLegNpv <- npv cpiLegBond
-- yoyInflationLeg exercised via the generic Leg-based 'swap'' 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 <- swap' [(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: