packages feed

hasquant-0.8.0.0: test/example/QuantLib/Example/MulticurveBootstrapping.hs

-- Port of QuantLib's Examples/MulticurveBootstrapping/MulticurveBootstrapping.cpp.
--
-- Two curves, bootstrapped in sequence: an EONIA discount curve from deposits, dated
-- and undated OIS; then a Euribor 6M forecast curve from a deposit, FRAs and swaps
-- whose helpers discount off the EONIA curve. Two swaps are then priced on the pair.
--
-- Both curves are linked once after bootstrapping; the example needs no later relinking.
--
-- Two deliberate divergences from upstream:
--  * The bootstrap accuracy argument (upstream passes 1.0e-15 via an explicit
--    bootstrap_type) is not bound, so this runs at IterativeBootstrap's default 1e-12.
--    Expected values below are therefore recorded from an actual run of this code, not
--    copied from upstream's printed output.
--  * enableExtrapolation() has no binding (it would be a setter), so both curves are
--    built through piecewiseYieldCurve, which takes extrapolation as a construction
--    argument. Passing settlementDays 0 and 2 reproduces upstream's two reference
--    dates -- todaysDate for EONIA, settlementDate for Euribor 6M.
module QuantLib.Example.MulticurveBootstrapping
  (
    Result(..)
  , SwapResult(..)
  , run
  ) where
import Control.Monad(forM)
import Data.Time.Calendar(addGregorianYearsClip)
import Data.List.NonEmpty(fromList)

import QuantLib.Math
import qualified QuantLib.Index.InterestRate as IR
import QuantLib.Instrument
import QuantLib.Instrument.Swap
import QuantLib.PricingEngine
import QuantLib.Quote
import qualified QuantLib.TermStructure.Yield as TS
import QuantLib.Time.Calendar
import QuantLib.Time.Date
import QuantLib.Time.Schedule
import QuantLib.Context(setEvaluationDate)

data SwapResult = SwapResult { swapNpv :: Double
                             , swapFairSpread :: Double
                             , swapFairRate :: Double
                             } deriving Show

data Result = Result { spot5Y :: SwapResult
                     , forward1Y5Y :: SwapResult
                     -- Same two swaps, but with the Euribor helpers given no
                     -- discounting curve, so the forecast curve is bootstrapped
                     -- single-curve. A negative control: if these matched the dual
                     -- curve figures, the EONIA curve would not actually be reaching
                     -- the helpers and the whole example would prove nothing.
                     , singleCurveSpot5Y :: SwapResult
                     } deriving Show

todaysDate :: Day
todaysDate = 11 `december` 2012

run :: IO Result
run = do
  cal <- calendar TARGET
  setEvaluationDate (Just todaysDate)
  settlementDate <- advance cal todaysDate (2, Days) Following False

  termStructureDC <- dayCounter Actual365FixedStandard
  depositDC <- dayCounter (Actual360 False)
  fixedLegDC <- dayCounter Thirty360European

  -- EONIA curve: deposits, short OIS, dated OIS, long OIS
  eonia <- IR.overnightIborIndex IR.Eonia Nothing

  depoHelpers <- forM depoQuotes $ \(settlDays, rate) -> do
    q <- simpleQuote rate
    TS.depositRateHelper q (TS.DepositTenor (1, Days) settlDays cal Following False depositDC)

  oisHelpers <- forM (shortOisQuotes ++ longOisQuotes) $ \(tenor, rate) -> do
    q <- simpleQuote rate
    TS.oisRateHelper (TS.OisTenor 2 tenor (0, Days)) q eonia (Nothing :: Maybe TS.YieldTermStructure)
      >>= TS.asRateHelper

  datedOisHelpers <- forM datedOisQuotes $ \(start, end, rate) -> do
    q <- simpleQuote rate
    TS.oisRateHelper (TS.OisBetweenDates start end) q eonia (Nothing :: Maybe TS.YieldTermStructure)
      >>= TS.asRateHelper

  eoniaCurve <- TS.piecewiseYieldCurve (TS.SettlementDays 0 cal) (fromList (depoHelpers ++ oisHelpers ++ datedOisHelpers))
    termStructureDC [] (TS.Iterative TS.Discount monotonicLogCubic TS.defaultIterativeBootstrapOpts) True

  -- Euribor 6M curve: one deposit, FRAs, and swaps discounted off the EONIA curve
  euribor6M <- IR.iborIndex IR.Euribor6M Nothing

  let euriborHelpers discounting = do
        d6MQuote <- simpleQuote 0.00312
        d6M <- TS.depositRateHelper d6MQuote (TS.DepositTenor (6, Months) 3 cal Following False depositDC)
        fras <- forM fraQuotes $ \(monthsToStart, rate) -> do
          q <- simpleQuote rate
          TS.fraRateHelper q (TS.FraMonths monthsToStart (monthsToStart + 6) 2 cal ModifiedFollowing
            False depositDC) TS.LastRelevantDate Nothing True
        swaps <- forM swapQuotes $ \(yrs, rate) -> do
          q <- simpleQuote rate
          TS.swapRateHelper q (TS.SwapRateTenor (yrs, Years) cal Annual Unadjusted fixedLegDC euribor6M (0, Days) Nothing Nothing)
            Nothing discounting
            TS.LastRelevantDate Nothing False Nothing Nothing
            >>= TS.asRateHelper
        pure (d6M : fras ++ swaps)

  dualHelpers <- euriborHelpers (Just eoniaCurve)
  euriborCurve <- TS.piecewiseYieldCurve (TS.SettlementDays 2 cal) (fromList dualHelpers) termStructureDC []
    (TS.Iterative TS.Discount monotonicLogCubic TS.defaultIterativeBootstrapOpts) True

  -- the negative control: same helpers, no discounting curve
  singleHelpers <- euriborHelpers (Nothing :: Maybe TS.YieldTermStructure)
  singleCurve <- TS.piecewiseYieldCurve (TS.SettlementDays 2 cal) (fromList singleHelpers) termStructureDC []
    (TS.Iterative TS.Discount monotonicLogCubic TS.defaultIterativeBootstrapOpts) True

  let priceOn forecastCurve start = do
        idx <- IR.iborIndex IR.Euribor6M (Just forecastCurve)
        let maturity = addGregorianYearsClip 5 start
        fixedSch <- schedule (Just start) maturity (1, Years) cal Unadjusted Unadjusted
          Forward False Nothing Nothing
        floatSch <- schedule (Just start) maturity (6, Months) cal ModifiedFollowing
          ModifiedFollowing Forward False Nothing Nothing
        swp <- vanillaSwap Payer 1000000 fixedSch 0.007 fixedLegDC floatSch idx 0
          depositDC Nothing Nothing
        engine <- discountingSwapEngine eoniaCurve Nothing Nothing Nothing
        setPricingEngine swp engine
        SwapResult <$> npv swp <*> fairSpread swp <*> fairRate swp

  fwdStart <- advance cal settlementDate (1, Years) Following False

  Result
    <$> priceOn euriborCurve settlementDate
    <*> priceOn euriborCurve fwdStart
    <*> priceOn singleCurve settlementDate

-- QuantLib's MonotonicLogCubic: LogCubic(Spline, monotonic=true, SecondDerivative 0
-- at both ends), which is exactly what cbits/qlTermStructure.cpp emits for this pair.
monotonicLogCubic :: Interpolation
monotonicLogCubic = LogCubic (NaturalSpline True)

depoQuotes :: [(Word, Double)]  -- settlement days, rate
depoQuotes = [(0, 0.0004), (1, 0.0004), (2, 0.0004)]

shortOisQuotes :: [((Int, TimeUnit), Double)]
shortOisQuotes =
  [ ((1, Weeks), 0.00070), ((2, Weeks), 0.00069)
  , ((3, Weeks), 0.00078), ((1, Months), 0.00074) ]

datedOisQuotes :: [(Day, Day, Double)]
datedOisQuotes =
  [ (16 `january` 2013, 13 `february` 2013,  0.000460)
  , (13 `february` 2013, 13 `march` 2013,    0.000160)
  , (13 `march` 2013, 10 `april` 2013,      -0.000070)
  , (10 `april` 2013, 8 `may` 2013,         -0.000130)
  , (8 `may` 2013, 12 `june` 2013,          -0.000140) ]

longOisQuotes :: [((Int, TimeUnit), Double)]
longOisQuotes =
  [ ((15, Months), 0.00002), ((18, Months), 0.00008), ((21, Months), 0.00021)
  , ((2, Years), 0.00036), ((3, Years), 0.00127), ((4, Years), 0.00274)
  , ((5, Years), 0.00456), ((6, Years), 0.00647), ((7, Years), 0.00827)
  , ((8, Years), 0.00996), ((9, Years), 0.01147), ((10, Years), 0.01280)
  , ((11, Years), 0.01404), ((12, Years), 0.01516), ((15, Years), 0.01764)
  , ((20, Years), 0.01939), ((25, Years), 0.02003), ((30, Years), 0.02038) ]

fraQuotes :: [(Word, Double)]  -- months to start
fraQuotes =
  [ (1, 0.002930), (2, 0.002720), (3, 0.002600), (4, 0.002560), (5, 0.002520)
  , (6, 0.002480), (7, 0.002540), (8, 0.002610), (9, 0.002670), (10, 0.002790)
  , (11, 0.002910), (12, 0.003030), (13, 0.003180), (14, 0.003350)
  , (15, 0.003520), (16, 0.003710), (17, 0.003890), (18, 0.004090) ]

swapQuotes :: [(Int, Double)]  -- years
swapQuotes =
  [ (3, 0.004240), (4, 0.005760), (5, 0.007620), (6, 0.009540), (7, 0.011350)
  , (8, 0.013030), (9, 0.014520), (10, 0.015840), (12, 0.018090), (15, 0.020370)
  , (20, 0.021870), (25, 0.022340), (30, 0.022560), (35, 0.022950)
  , (40, 0.023480), (50, 0.024210), (60, 0.024630) ]

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