packages feed

hasquant-0.7.0.0: test/hspec/QuantLib/Spec/Instrument/CapFloor.hs

-- | Golden-value tests for 'QuantLib.Instrument.CapFloor' ('cap'\/'floor'\/'atmRate'\/
-- 'impliedVolatility'\/'optionlet'), ported from QuantLib's own test-suite/capfloor.cpp.
--
-- The cached NPVs in testCachedValue depend on QuantLib's global @IborCoupon::Settings@
-- \"at-par coupons\" toggle, which hasquant doesn't bind (there is no way to set it from
-- Haskell) -- both installed-QuantLib variants (par-coupon vs. index-fixing pricing) are
-- accepted rather than assuming one.
{-# LANGUAGE OverloadedLists #-}

module QuantLib.Spec.Instrument.CapFloor (spec) where

import Prelude hiding(floor)
import Control.Monad(forM_)

import Test.Hspec

import qualified QuantLib.Context as Context
import QuantLib.Time.Date
import QuantLib.Time.Calendar
import QuantLib.Time.Schedule
import QuantLib.InterestRate(Compounding(..), VolatilityType(..))
import QuantLib.Quote hiding(value)
import QuantLib.TermStructure.Yield
import QuantLib.CashFlow(iborLeg, Leg)
import QuantLib.Index.InterestRate(iborIndex, IborConstructor(Euribor6M))
import QuantLib.Instrument(npv, setPricingEngine, additionalResults, AdditionalResultVal(..))
import QuantLib.Instrument.Swap(vanillaSwap, SwapType(..))
import QuantLib.Instrument.CapFloor
import QuantLib.PricingEngine(blackCapFloorEngine, discountingSwapEngine)

import QuantLib.Spec.Helpers(closePrec)

-- |accepts either of the two cached values QuantLib's own test carries (index-fixing vs.
-- at-par coupon pricing), since hasquant has no binding to select between them
closeToEither :: Double -> Double -> Double -> Bool
closeToEither a b v = closePrec a 1.0e-8 v || closePrec b 1.0e-8 v

-- |Euribor6M-based 20Y cap/floor leg on a flat 5% Actual360 curve referenced at 18 March 2002,
-- the exact fixture test-suite/capfloor.cpp's CommonVars\/testCachedValue build (evaluation
-- date pinned to 14 March 2002).
cachedFixture :: IO Leg
cachedFixture = do
  Context.setEvaluationDate (Just (14 `march` 2002))
  cal <- calendar TARGET
  let startDate = 18 `march` 2002
  dc <- dayCounter (Actual360 False)
  q <- simpleQuote 0.05
  ts <- flatForward (ReferenceDate startDate) q dc Continuous Annual
  idx <- iborIndex Euribor6M (Just ts)
  endDate <- advance cal startDate (20, Years) ModifiedFollowing False
  sch <- schedule (Just startDate) endDate (6, Months) cal ModifiedFollowing ModifiedFollowing
    Forward False Nothing Nothing
  iborDC <- dayCounter (Actual360 False)
  iborLeg sch idx [100] iborDC ModifiedFollowing [2] [1.0] [0.0] [] [] False False

spec :: Spec
spec = do
  describe "testCachedValue" $
    it "Black cap/floor NPV reproduces capfloor.cpp's cached values" $
      Context.keepingSettingsGc $ do
        leg <- cachedFixture
        dc <- dayCounter (Actual360 False)
        volQ <- simpleQuote 0.20
        q <- simpleQuote 0.05
        ts <- flatForward (ReferenceDate (18 `march` 2002)) q dc Continuous Annual
        volDC <- dayCounter Actual365FixedStandard
        eng <- blackCapFloorEngine ts volQ volDC 0.0
        capfl <- cap leg [0.07]
        setPricingEngine capfl eng
        capNPV <- npv capfl
        capNPV `shouldSatisfy` closeToEither 6.87630307745 6.87570026732
        flr <- floor leg [0.03]
        setPricingEngine flr eng
        flrNPV <- npv flr
        flrNPV `shouldSatisfy` closeToEither 2.65796764715 2.65812927959

  describe "testCachedValueFromOptionLets" $
    it "sums additionalResults[optionletsPrice] to the same cached cap/floor NPVs" $
      Context.keepingSettingsGc $ do
        leg <- cachedFixture
        dc <- dayCounter (Actual360 False)
        volQ <- simpleQuote 0.20
        q <- simpleQuote 0.05
        ts <- flatForward (ReferenceDate (18 `march` 2002)) q dc Continuous Annual
        volDC <- dayCounter Actual365FixedStandard
        eng <- blackCapFloorEngine ts volQ volDC 0.0

        capfl <- cap leg [0.07]
        setPricingEngine capfl eng
        _ <- npv capfl
        capAddl <- additionalResults capfl
        case lookup "optionletsPrice" capAddl of
          Just (RealVectorVal xs) -> do
            length xs `shouldBe` 40
            sum xs `shouldSatisfy` closeToEither 6.87630307745 6.87570026732
          other -> expectationFailure ("expected optionletsPrice as RealVectorVal, got " ++ show other)

        flr <- floor leg [0.03]
        setPricingEngine flr eng
        _ <- npv flr
        flrAddl <- additionalResults flr
        case lookup "optionletsPrice" flrAddl of
          Just (RealVectorVal xs) -> sum xs `shouldSatisfy` closeToEither 2.65796764715 2.65812927959
          other -> expectationFailure ("expected optionletsPrice as RealVectorVal, got " ++ show other)

  describe "testATMRate" $
    it "cap atmRate == floor atmRate, and a VanillaSwap struck there reprices to ~0" $
      Context.keepingSettingsGc $ do
        Context.setEvaluationDate (Just (11 `december` 2012))
        cal <- calendar TARGET
        settle <- advance cal (11 `december` 2012) (2, Days) ModifiedFollowing False
        dc <- dayCounter Actual365FixedStandard
        q <- simpleQuote 0.05
        ts <- flatForward (ReferenceDate settle) q dc Continuous Annual
        idx <- iborIndex Euribor6M (Just ts)
        maturity <- advance cal settle (10, Years) ModifiedFollowing False
        sch <- schedule (Just settle) maturity (6, Months) cal ModifiedFollowing ModifiedFollowing
          Forward False Nothing Nothing
        iborDC <- dayCounter (Actual360 False)
        leg <- iborLeg sch idx [100] iborDC ModifiedFollowing [2] [1.0] [0.0] [] [] False False

        capfl <- cap leg [0.05]
        flr <- floor leg [0.05]
        capATM <- atmRate capfl ts
        floorATM <- atmRate flr ts
        floorATM `shouldSatisfy` closePrec capATM 1.0e-10

        swap <- vanillaSwap Payer 100 sch floorATM iborDC sch idx 0.0 iborDC Nothing Nothing
        swapEng <- discountingSwapEngine ts Nothing Nothing Nothing
        setPricingEngine swap swapEng
        swapNPV <- npv swap
        swapNPV `shouldSatisfy` closePrec 0 1.0e-8

  describe "testImpliedVolatility" $
    it "round-trips impliedVolatility against the vol used to build the cap's price" $
      Context.keepingSettingsGc $ do
        Context.setEvaluationDate (Just (11 `december` 2012))
        cal <- calendar TARGET
        settle <- advance cal (11 `december` 2012) (2, Days) ModifiedFollowing False
        dc <- dayCounter (Actual360 False)
        q <- simpleQuote 0.05
        ts <- flatForward (ReferenceDate settle) q dc Continuous Annual
        idx <- iborIndex Euribor6M (Just ts)
        maturity <- advance cal settle (10, Years) ModifiedFollowing False
        sch <- schedule (Just settle) maturity (6, Months) cal ModifiedFollowing ModifiedFollowing
          Forward False Nothing Nothing
        leg <- iborLeg sch idx [100] dc ModifiedFollowing [2] [1.0] [0.0] [] [] False False
        capfl <- cap leg [0.03]
        volDC <- dayCounter Actual365FixedStandard

        forM_ ([0.10, 0.20, 0.30] :: [Double]) $ \v -> do
          volQ <- simpleQuote v
          eng <- blackCapFloorEngine ts volQ volDC 0.0
          setPricingEngine capfl eng
          value <- npv capfl
          implVol <- impliedVolatility capfl value ts 0.10 1.0e-8 100 1.0e-7 4.0 ShiftedLognormal 0.0
          implVol `shouldSatisfy` closePrec v 1.0e-6