packages feed

hasquant-0.8.0.0: test/hspec/QuantLib/Spec/Instrument/MtmCrossCurrency.hs

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

import Control.Monad(forM, forM_, void)
import qualified Data.List.NonEmpty as NE
import QuantLib.Math(Interpolation(..))
import Data.Time.Calendar(Day, fromGregorian, addGregorianYearsClip)
import qualified Data.Vector.Storable as V
import Test.Hspec
import qualified QuantLib.Context as Context
import QuantLib.Time.Calendar
import QuantLib.Time.Schedule
import QuantLib.Currency hiding (rate)
import QuantLib.InterestRate(Compounding(..))
import qualified QuantLib.Index.InterestRate as IR
import qualified QuantLib.Index as Index
import QuantLib.Quote
import QuantLib.TermStructure.Yield
import QuantLib.Instrument
import QuantLib.Instrument.Swap hiding (swap)
import QuantLib.PricingEngine
import qualified QuantLib.CashFlow as CF
import QuantLib.Spec.Helpers(quantLibAtMost143, unsupportedQuantLib144)

fixtureDate :: Day
fixtureDate = fromGregorian 2018 9 11

near :: Double -> Double -> Double -> Expectation
near tolerance expected actual = abs (actual - expected) `shouldSatisfy` (< tolerance)

spec :: Spec
spec = describe "MTM cross-currency 1.44" $ do
  it "prices both resettable legs and directions, and reprices at fair spreads" $ Context.keepingSettingsGc $ do
    Context.setEvaluationDate (Just fixtureDate)
    dc <- dayCounter (Actual360 False)
    usd <- currency USD
    eur <- currency EUR
    usdCurve <- simpleQuote 0.02 >>= \q -> flatForward (ReferenceDate fixtureDate) q dc Continuous Annual
    eurCurve <- simpleQuote 0.01 >>= \q -> flatForward (ReferenceDate fixtureDate) q dc Continuous Annual
    usdIndex <- IR.iborIndex (IR.UsdLibor (3, Months)) (Just usdCurve)
    eurIndex <- IR.iborIndex IR.Euribor3M (Just eurCurve)
    cal <- calendar TARGET
    start <- advance cal fixtureDate (2, Days) Following False
    sch <- schedule (Just start) (addGregorianYearsClip 5 start) (3, Months) cal ModifiedFollowing ModifiedFollowing Backward False Nothing Nothing
    spot <- simpleQuote 1.1
    let makeSwap direction resetBase baseSpread quoteSpread = mtmCrossCurrencyBasisSwap direction
          10000000 usd sch usdIndex baseSpread 1 (10000000 / 1.1) eur sch eurIndex quoteSpread 1 resetBase
    if quantLibAtMost143 then do
      makeSwap PayFxBaseCurrency False 0 0 `shouldThrow` unsupportedQuantLib144 "mtmCrossCurrencyBasisSwap"
      discountingMtmCrossCurrencyBasisSwapEngine usd usdCurve eur eurCurve spot Nothing Nothing Nothing Nothing
        `shouldThrow` unsupportedQuantLib144 "discountingMtmCrossCurrencyBasisSwapEngine"
    else do
      engine <- discountingMtmCrossCurrencyBasisSwapEngine usd usdCurve eur eurCurve spot Nothing Nothing Nothing Nothing
      forM_ [False, True] $ \resetBase -> do
        payer <- makeSwap PayFxBaseCurrency resetBase 0 0
        receiver <- makeSwap ReceiveFxBaseCurrency resetBase 0 0
        setPricingEngine payer engine
        setPricingEngine receiver engine
        price <- npv payer
        npv receiver >>= near 1e-6 (-price)
        fairBase <- fairFxBaseSpread payer
        fairQuote <- fairFxQuoteSpread payer
        fairPaySpread payer >>= near 1e-12 fairBase
        fairRecSpread payer >>= near 1e-12 fairQuote
        forM_ [(fairBase, 0), (0, fairQuote)] $ \(baseSpread, quoteSpread) -> do
          par <- makeSwap PayFxBaseCurrency resetBase baseSpread quoteSpread
          setPricingEngine par engine
          npv par >>= near 0.02 0
        rates <- fxResetRates payer
        notionals <- fxResetNotionals payer
        V.length rates `shouldBe` 20
        V.length notionals `shouldBe` V.length rates
        let notional = if resetBase then 10000000 / 1.1 else 10000000
        forM_ (zip (V.toList rates) (V.toList notionals)) $ \(rate, nominal) -> near 1e-6 (notional * rate) nominal
        _ <- legCurrency payer 0
        inCcyLegNpv payer 0 >>= (`shouldSatisfy` (\v -> not (isNaN v || isInfinite v)))
        inCcyLegBps payer 1 >>= (`shouldSatisfy` (/= 0))
        npvDateDiscounts payer 0 `shouldReturn` 1
        fxResetRates payer >>= \old -> do
          _ <- setValue spot 1.2
          changed <- fxResetRates payer
          changed `shouldSatisfy` (/= old)
          void (setValue spot 1.1)

  it "projects resets at value dates and respects spot settlement overrides" $ Context.keepingSettingsGc $ do
    let today = fromGregorian 2024 7 1
        start = fromGregorian 2024 7 8
        end = fromGregorian 2025 1 8
    Context.setEvaluationDate (Just today)
    dc <- dayCounter (Actual360 False)
    eur <- currency EUR
    usd <- currency USD
    eurCurve <- simpleQuote 0.01 >>= \q -> flatForward (ReferenceDate today) q dc Continuous Annual
    usdCurve <- simpleQuote 0.05 >>= \q -> flatForward (ReferenceDate today) q dc Continuous Annual
    eurIndex <- IR.iborIndex IR.Euribor3M (Just eurCurve)
    usdIndex <- IR.iborIndex (IR.UsdLibor (3, Months)) (Just usdCurve)
    cal <- calendar TARGET
    usCal <- calendar UnitedStatesSettlement
    fxCal <- calendar (Joint2 cal usCal JoinHolidays)
    sch <- schedule (Just start) end (3, Months) cal Following Following Forward False Nothing Nothing
    spot <- simpleQuote 1.1
    let makeSwap = mtmCrossCurrencyBasisSwapWithOptions PayFxBaseCurrency
          10000000 eur sch eurIndex 0 1 11000000 usd sch usdIndex 0 1 False
          defaultMtmCrossCurrencyBasisSwapOpts { mtmFxResetFixingDays = 2, mtmFxResetFixingCalendar = Just fxCal }
    if quantLibAtMost143 then makeSwap `shouldThrow` unsupportedQuantLib144 "mtmCrossCurrencyBasisSwap"
    else do
      swap <- makeSwap
      engine <- discountingMtmCrossCurrencyBasisSwapEngine usd usdCurve eur eurCurve spot Nothing Nothing Nothing Nothing
      setPricingEngine swap engine
      price <- npv swap
      valueDate <- CF.fxResetValueDate (CF.FxResetConvention 2 (Just fxCal)) today
      eurSpotDf <- discount eurCurve (DatePoint valueDate) False
      usdSpotDf <- discount usdCurve (DatePoint valueDate) False
      eurStartDf <- discount eurCurve (DatePoint start) False
      usdStartDf <- discount usdCurve (DatePoint start) False
      rates <- fxResetRates swap
      case V.toList rates of
        first:_ -> near 1e-10 (1.1 * usdSpotDf / eurSpotDf * eurStartDf / usdStartDf) first
        [] -> expectationFailure "no FX reset rates"
      explicit <- discountingMtmCrossCurrencyBasisSwapEngine usd usdCurve eur eurCurve spot Nothing Nothing Nothing (Just valueDate)
      setPricingEngine swap explicit
      npv swap >>= near 1e-6 price
      resettingLeg <- leg swap 1
      pricer <- CF.discountingFxResetPricer eur usd eurCurve usdCurve spot True (Just valueDate)
      CF.setFxResetLegPricer resettingLeg pricer
      CF.fixingDependencies resettingLeg >>= (`shouldSatisfy` (not . null))

  it "reprices instruments off MTM helper curves for all collateral, basis and reset flags" $ Context.keepingSettingsGc $
    if quantLibAtMost143 then pendingWith "MTM products require QuantLib 1.44" else do
      let today = fromGregorian 2013 9 6
          basisData = [(1, -0.00145), (2, -0.00205), (5, -0.00265)] :: [(Int, Double)]
      Context.setEvaluationDate (Just today)
      cal <- calendar TARGET
      dc <- dayCounter Actual365FixedStandard
      start <- advance cal today (2, Days) Following False
      eur <- currency EUR
      usd <- currency USD
      eurForecast <- simpleQuote 0.007 >>= \q -> flatForward (ReferenceDate today) q dc Continuous Annual
      usdForecast <- simpleQuote 0.015 >>= \q -> flatForward (ReferenceDate today) q dc Continuous Annual
      eurIndex <- IR.iborIndex IR.Euribor3M (Just eurForecast)
      usdIndex <- IR.iborIndex (IR.UsdLibor (3, Months)) (Just usdForecast)
      spot <- simpleQuote 1
      forM_ [(collateralBase, basisBase, resetBase) | collateralBase <- [False, True], basisBase <- [False, True], resetBase <- [False, True]] $ \(collateralBase, basisBase, resetBase) -> do
        let collateral = if collateralBase then eurForecast else usdForecast
        helpers <- forM basisData $ \(years, basis) -> do
          quote <- simpleQuote basis
          mtmCrossCurrencyBasisSwapRateHelper quote (years, Years) 2 cal Following False
            eurIndex usdIndex collateral collateralBase basisBase resetBase Nothing 0 Nothing 2 (Just cal) Nothing Nothing Nothing
        curve <- piecewiseYieldCurve (ReferenceDate today) (NE.fromList helpers) dc []
          (Iterative Discount LogLinear defaultIterativeBootstrapOpts) True
        let eurDiscount = if collateralBase then eurForecast else curve
            usdDiscount = if collateralBase then curve else usdForecast
        engine <- discountingMtmCrossCurrencyBasisSwapEngine usd usdDiscount eur eurDiscount spot Nothing Nothing Nothing Nothing
        forM_ basisData $ \(years, basis) -> do
          sch <- schedule (Just start) (addGregorianYearsClip (fromIntegral years) start) (3, Months) cal Following Following Backward False Nothing Nothing
          instrument <- mtmCrossCurrencyBasisSwapWithOptions PayFxBaseCurrency 1 eur sch eurIndex (if basisBase then basis else 0) 1
            1 usd sch usdIndex (if basisBase then 0 else basis) 1 resetBase
            defaultMtmCrossCurrencyBasisSwapOpts { mtmFxResetFixingDays = 2, mtmFxResetFixingCalendar = Just cal }
          setPricingEngine instrument engine
          npv instrument >>= near 1e-8 0

  it "wires overnight observation, payment and stub options into native legs" $ Context.keepingSettingsGc $ do
    let today = fromGregorian 2026 5 27
        start = fromGregorian 2026 5 29
        end = fromGregorian 2026 8 28
    Context.setEvaluationDate (Just today)
    cal <- calendar NewZealandWellington
    dc <- dayCounter Actual365FixedStandard
    usd <- currency USD
    nzd <- currency NZD
    usdCurve <- simpleQuote 0.015 >>= \q -> flatForward (ReferenceDate today) q dc Continuous Annual
    shortCurve <- simpleQuote 0.02 >>= \q -> flatForward (ReferenceDate today) q dc Continuous Annual
    nzdCurve <- simpleQuote 0.04 >>= \q -> flatForward (ReferenceDate today) q dc Continuous Annual
    sofr <- IR.overnightIborIndex IR.Sofr (Just usdCurve)
    short <- IR.iborIndex (IR.Ibor "Bkbm" (2, Months) 0 nzd cal ModifiedFollowing False dc) (Just shortCurve)
    long <- IR.iborIndex (IR.Ibor "Bkbm" (3, Months) 0 nzd cal ModifiedFollowing False dc) (Just nzdCurve)
    sch <- schedule (Just start) end (3, Months) cal ModifiedFollowing ModifiedFollowing Backward False Nothing Nothing
    spot <- simpleQuote 1.5
    let options = defaultMtmCrossCurrencyBasisSwapOpts
          { mtmFxBasePaymentLag = 2, mtmFxQuotePaymentLag = 3
          , mtmFxBasePaymentConvention = ModifiedFollowing
          , mtmFxBaseCompoundSpread = True
          , mtmFxBaseObservation = OvernightObservation (Just 1) 1 True
          , mtmUseIndexedCoupons = Just True
          , mtmFxQuoteStubIndexSelection = Just (CF.InterpolatedStubIndexes (short NE.:| [long])) }
        makeSwap spread = mtmCrossCurrencyBasisSwapWithOptions PayFxBaseCurrency 1000000 usd sch sofr spread 1
          1500000 nzd sch long 0 1 False options
    if quantLibAtMost143 then makeSwap 0.001 `shouldThrow` unsupportedQuantLib144 "mtmCrossCurrencyBasisSwap"
    else do
      instrument <- makeSwap 0.001
      engine <- discountingMtmCrossCurrencyBasisSwapEngine nzd nzdCurve usd usdCurve spot Nothing Nothing Nothing Nothing
      setPricingEngine instrument engine
      fairFxBaseSpread instrument >>= (`shouldSatisfy` (\parSpread -> not (isNaN parSpread || isInfinite parSpread)))
      baseLeg <- leg instrument 0
      baseFlows <- CF.cashFlows baseLeg Nothing Nothing
      payment <- advance cal end (2, Days) ModifiedFollowing False
      map (\(day, _, _) -> day) baseFlows `shouldSatisfy` elem payment
      discounted <- forM baseFlows $ \(day, cashAmount, _) -> do
        df <- discount usdCurve (DatePoint day) False
        pure (cashAmount * df)
      inCcyLegNpv instrument 0 >>= near 1e-6 (-sum discounted)
      quoteLeg <- leg instrument 1
      dependencies <- CF.fixingDependencies quoteLeg
      names <- mapM Index.name [short, long]
      forM_ names $ \name -> map fst dependencies `shouldSatisfy` elem name