packages feed

hasquant-0.5.0.2: test/example/QuantLib/Example/FittedBondCurve.hs

{-# LANGUAGE TemplateHaskell #-}
module QuantLib.Example.FittedBondCurve
  (
    Result(..)
  , Rate(..)
  , run
  ) where
import Prelude hiding(init, head, tail, last)
import Control.Monad(forM)
import Data.Time.Calendar
import Data.List.NonEmpty(NonEmpty(..), init, head, tail, last)

import qualified QuantLib.CashFlow as CF
import qualified QuantLib.InterestRate as IR
import QuantLib.Instrument.Bond
import QuantLib.Math
import QuantLib.Quote
import QuantLib.Settings
import QuantLib.TermStructure
import qualified QuantLib.TermStructure.Yield as TS
import QuantLib.Time.Calendar
import QuantLib.Time.Date
import QuantLib.Time.Schedule
import QuantLib.Syntax

data Result = Result { bondSettleR :: Day
  , rates1R :: Rate
  , rates2R :: Rate
  , rates3R :: Rate
  , rates4R :: Rate
  } deriving Show

data Rate = Rate{refDateR :: Day, numIterR :: [Int], tenorsR :: [Double], ratesR :: [[Double]]}
  deriving Show

run :: IO Result
run = do
  cal <- calendar Null
  tod1 <- today
  tod <- adjust cal tod1 Following
  setEvaluationDate $ Just tod
  dc <- dayCounter Simple

  bondSettle <- advance cal tod (bondSettleDays, Days) Following False
  cleanQuotes <- mapM simpleQuote cleanPrices

  (rates1, ts0, instrA, instrB, curves) <- step1 tod dc cal bondSettle cleanQuotes
  rates2 <- step2 tod dc cal ts0 instrA instrB curves
  let (iA, iB) = (drop 1 instrA, drop 1 instrB)

  newtod <- advance cal tod (24, Months) ModifiedFollowing False
  setEvaluationDate $ Just newtod
  newBondSettle <- advance cal newtod (bondSettleDays, Days) Following False

  (rates3, ts00, curves3) <- step3 newtod dc cal newBondSettle iA iB
  mapM_ (\(price, q, i) -> do
      b <- TS.bondHelperBond i
      ytm <- yieldFromPrice' b (price, Clean) dc IR.Compounded Annual newtod 1e-10 100 0.05
      dur <- duration b ytm dc IR.Compounded Annual CF.Modified newtod
      let dp = -dur * price * 5 / 10000
      setValue q (price + dp)) $
        zip3 (drop 1 cleanPrices) (drop 1 cleanQuotes) iA
  rates4 <- rates ts00 dc newBondSettle newtod curves3 iA

  return Result{bondSettleR = bondSettle, rates1R = rates1, rates2R = rates2, rates3R = rates3, rates4R = rates4}
  where
    bondSettleDays = 0
    curveSettleDays = 0
    cleanPrices = replicate 15 100.0
    lengths = [2, 4, 6, 8, 10, 12, 14, 16,
               18, 20, 22, 24, 26, 28, 30]
    coupons = [0.0200, 0.0225, 0.0250, 0.0275, 0.0300,
                0.0325, 0.0350, 0.0375, 0.0400, 0.0425,
                0.0450, 0.0475, 0.0500, 0.0525, 0.0550]
    tolerance = 1e-10
    maxEvals = 5000

    parRate :: TS.GenYieldTermStructure y -> NonEmpty Day -> DayCounter -> IO Double
    parRate ts ds dc = do
      dfs <- mapM (\(d1, d2) -> do
              dt <- years dc d1 d2 Nothing Nothing
              df <- TS.discount' ts d2 False
              return $ df * dt) $
                zip (init ds) (tail ds)
      df1 <- TS.discount' ts (head ds) False
      df2 <- TS.discount' ts (last ds) False
      return $ 100.0 * (df1 - df2) / sum dfs

    rates :: TS.YieldTermStructure -> DayCounter -> Day -> Day -> [TS.FittedBondDiscountCurve] -> [TS.BondHelper] -> IO Rate
    rates ts0 dc bondSettle tod curves instrA = do
      refDate <- referenceDate ts0
      numIter <- forM curves TS.numberOfIterations

      r <- forM instrA $
        \h -> do
          cfs <- TS.bondHelperBond h >>= cashFlows >>=
            $(free1st 'CF.cashFlows) (Just False) (Just bondSettle)
          let (ds, _, _) = unzip3 $ filter (\(_, _, oc) -> not oc) cfs
              -- `ds` comes from a filter and can be empty; taking the maximum over the
              -- NonEmpty that already includes bondSettle keeps this total, and shares
              -- the one value the two parRate calls below both need
              cfDates = bondSettle :| ds
          m <- years dc tod (maximum cfDates) Nothing Nothing
          r1 <- parRate ts0 cfDates dc
          r2 <- forM curves $ $(free1st' 3) parRate cfDates dc --before the migration off type classes an implicit cast to YieldTermStructure was needed
          return (m, r1:r2)
      let (tenors, rs) = unzip r
      return Rate {refDateR = refDate, numIterR = numIter, tenorsR = tenors, ratesR = rs}

    step1 :: Day -> DayCounter -> Calendar -> Day -> [SimpleQuote] -> IO (Rate, TS.YieldTermStructure, [TS.BondHelper], [TS.RateHelper], [TS.FittedBondDiscountCurve])
    step1 tod dc cal bondSettle cleanQuotes = do
      helpers <- mapM (\(q, l, c) -> do
        mat <- advance cal bondSettle (l, Years) Following False
        s <- schedule (Just bondSettle) mat (1, Years) cal
          ModifiedFollowing ModifiedFollowing Backward False Nothing Nothing

        hA <- TS.fixedRateBondHelper q (fromIntegral bondSettleDays) 100.0 s [c] dc ModifiedFollowing 100.0 Nothing
        hB <- TS.fixedRateBondHelper q (fromIntegral bondSettleDays) 100.0 s [c] dc ModifiedFollowing 100.0 Nothing
                >>= TS.asRateHelper
        return (hA, hB)) $
          zip3 cleanQuotes lengths coupons

      let (instrA, instrB) = unzip helpers

      ts0 <- TS.piecewiseYieldCurve' curveSettleDays cal instrB dc [] TS.Discount LogLinear False

      curves <- fitCurves cal dc instrA
      rs <- rates ts0 dc bondSettle tod curves instrA
      return (rs, ts0, instrA, instrB, curves)

    step2 :: Day -> DayCounter -> Calendar -> TS.YieldTermStructure -> [TS.BondHelper]
             -> [TS.RateHelper] -> [TS.FittedBondDiscountCurve] -> IO Rate
    step2 tod dc cal ts0 instrA _ curves = do
      newtoday <- advance cal tod (23, Months) ModifiedFollowing False
      setEvaluationDate $ Just newtoday
      bondSettle <- advance cal newtoday (bondSettleDays, Days) Following False

      rates ts0 dc bondSettle newtoday curves instrA


    step3 :: Day -> DayCounter -> Calendar -> Day -> [TS.BondHelper] -> [TS.RateHelper]
             -> IO (Rate, TS.YieldTermStructure, [TS.FittedBondDiscountCurve])
    step3 tod dc cal bondSettle iA iB = do
      ts00 <- TS.piecewiseYieldCurve' curveSettleDays cal iB dc [] TS.Discount LogLinear False

      curves <- fitCurves cal dc iA
      rs <- rates ts00 dc bondSettle tod curves iA
      return (rs, ts00, curves)

    -- the five fitting methods the example compares, and the curves fitted with them.
    -- step1 and step3 each used to spell this list out in full (including the eleven
    -- CubicBSplines knots) and repeat the mapM below verbatim.
    -- NB results depend on the optimization options used to build QLC.
    fitCurves :: Calendar -> DayCounter -> [TS.BondHelper] -> IO [TS.FittedBondDiscountCurve]
    fitCurves cal dc instr = mapM
        (\f -> TS.fittedBondDiscountCurve curveSettleDays cal instr dc f tolerance maxEvals [] 1.0)
        fittings
      where
        noCutoff = 1.0e6 :: Double -- stands in for QuantLib's QL_MAX_REAL default (effectively "no cutoff")
        fittings = [TS.ExponentialSplines True [] [] 0.0 noCutoff 9 Nothing Nothing,
                      TS.SimplePolynomial 3 True [] [] 0.0 noCutoff Nothing,
                      TS.NelsonSiegel [] [] 0.0 noCutoff Nothing,
                      TS.CubicBSplines [-30.0, -20.0,  0.0,  5.0, 10.0, 15.0, 20.0,  25.0, 30.0, 40.0, 50.0] True [] [] 0.0 noCutoff Nothing,
                      TS.Svensson [] [] 0.0 noCutoff Nothing]
-- vim: set ft=haskell ff=unix ts=8 sts=2 sw=2 et: