packages feed

hasquant-0.5.0.2: 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.
--
-- The point of the port is that multi-curve bootstrapping needs no relinking at all,
-- which is what settles issue #1's "probably would need RelinkableHandle first". Upstream
-- holds both curves in RelinkableHandles but only ever calls linkTo once each, right after
-- its curve is bootstrapped -- semantically just a plain handle over that curve, which is
-- what every curve-taking binding here already builds. Relinking as an actual capability
-- is a separate feature, exercised by the "relinkable handles" block in
-- main/test/QuantLib/Spec/TermStructure.hs.
--
-- 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 QuantLib.Math
import qualified QuantLib.Index.InterestRate as IR
import QuantLib.Instrument
import QuantLib.Instrument.Swap hiding(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 hiding(years)
import QuantLib.Settings(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 (1, Days) settlDays cal Following False depositDC

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

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

  eoniaCurve <- TS.piecewiseYieldCurve' 0 cal (depoHelpers ++ oisHelpers ++ datedOisHelpers)
    termStructureDC [] TS.Discount monotonicLogCubic 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 (6, Months) 3 cal Following False depositDC
        fras <- forM fraQuotes $ \(monthsToStart, rate) -> do
          q <- simpleQuote rate
          TS.fraRateHelper q monthsToStart (monthsToStart + 6) 2 cal ModifiedFollowing
            False depositDC TS.LastRelevantDate Nothing True
        swaps <- forM swapQuotes $ \(years, rate) -> do
          q <- simpleQuote rate
          TS.swapRateHelper' q (years, Years) cal Annual Unadjusted fixedLegDC euribor6M
            Nothing (0, Days) discounting
            Nothing TS.LastRelevantDate Nothing False Nothing Nothing Nothing
            >>= TS.asRateHelper
        pure (d6M : fras ++ swaps)

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

  -- the negative control: same helpers, no discounting curve
  singleHelpers <- euriborHelpers (Nothing :: Maybe TS.YieldTermStructure)
  singleCurve <- TS.piecewiseYieldCurve' 2 cal singleHelpers termStructureDC []
    TS.Discount monotonicLogCubic 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
        swap <- vanillaSwap Payer 1000000 fixedSch 0.007 fixedLegDC floatSch idx 0
          depositDC Nothing Nothing
        engine <- discountingSwapEngine eoniaCurve Nothing Nothing Nothing
        setPricingEngine swap engine
        SwapResult <$> npv swap <*> fairSpread swap <*> fairRate swap

  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: