packages feed

hasquant-0.8.0.0: test/hspec/QuantLib/Spec/CashFlow/FxReset.hs

module QuantLib.Spec.CashFlow.FxReset (spec) where

import Control.Exception(bracket_)
import Control.Monad(forM_)
import Data.List.NonEmpty(NonEmpty(..))
import Data.Time.Calendar(fromGregorian, addDays, diffDays)
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(..), VolatilityType(..))
import qualified QuantLib.Index.InterestRate as IR
import qualified QuantLib.Index as Index
import QuantLib.Quote
import QuantLib.TermStructure(CalendarReference(..))
import QuantLib.TermStructure.Yield
import QuantLib.TermStructure.Volatility(constantOptionletVolatility)
import qualified QuantLib.CashFlow as CF
import QuantLib.Spec.Helpers(quantLibAtMost143, unsupportedQuantLib144)

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

spec :: Spec
spec = describe "FX reset and stub cash flows 1.44" $ do
  it "calculates business-day observations with native validation" $ Context.keepingSettingsGc $ do
    cal <- calendar TARGET
    let valueDate = fromGregorian 2024 7 8
    if quantLibAtMost143 then
      CF.fxResetObservation (CF.FxResetConvention 2 (Just cal)) valueDate `shouldThrow` unsupportedQuantLib144 "fxResetObservation"
    else do
      reset <- CF.fxResetObservation (CF.FxResetConvention 2 (Just cal)) valueDate
      reset `shouldBe` CF.FxReset (fromGregorian 2024 7 4) valueDate
      CF.fxResetValueDate (CF.FxResetConvention 2 (Just cal)) (CF.fxResetFixingDate reset) `shouldReturn` valueDate
      CF.fxResetObservation (CF.FxResetConvention 0 Nothing) valueDate `shouldReturn` CF.FxReset valueDate valueDate
      CF.fxResetObservation (CF.FxResetConvention 2 Nothing) valueDate `shouldThrow` anyException

  it "scales overnight amounts and accruals, wires legs and retains pricers" $ Context.keepingSettingsGc $ do
    let today = fromGregorian 2024 7 1
        start = fromGregorian 2024 7 8
        end = fromGregorian 2024 10 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
    idx <- IR.overnightIborIndex IR.Sofr (Just usdCurve)
    underlying <- CF.overnightIndexedCoupon end 100 start end idx 1 0 Nothing Nothing dc False CF.AveragingCompound 0 0 False False Nothing Nothing Nothing Nothing
    spot <- simpleQuote 1.1
    let observation = CF.FxReset start start
    if quantLibAtMost143 then do
      CF.fxResetCoupon underlying 100 observation `shouldThrow` unsupportedQuantLib144 "fxResetCoupon"
      CF.fxResetNotionalExchange start 100 Nothing (Just observation) `shouldThrow` unsupportedQuantLib144 "fxResetNotionalExchange"
      CF.discountingFxResetPricer eur usd eurCurve usdCurve spot True Nothing `shouldThrow` unsupportedQuantLib144 "discountingFxResetPricer"
    else do
      coupon <- CF.fxResetCoupon underlying 100 observation
      CF.amount coupon `shouldThrow` anyException
      pricer <- CF.discountingFxResetPricer eur usd eurCurve usdCurve spot True Nothing
      CF.setFxResetPricer coupon pricer
      fx <- CF.fxResetRate pricer observation
      CF.couponNominal coupon >>= near 1e-10 (100 * fx)
      rawAmount <- CF.amount underlying
      CF.amount coupon >>= near 1e-10 (rawAmount * fx)
      let accrualDate = addDays 30 start
      rawAccrued <- CF.couponAccruedAmount underlying accrualDate
      CF.couponAccruedAmount coupon accrualDate >>= near 1e-10 (rawAccrued * fx)
      initExchange <- CF.fxResetNotionalExchange start 100 Nothing (Just observation)
      finalExchange <- CF.fxResetNotionalExchange end 100 (Just observation) Nothing
      nextExchange <- CF.fxResetNotionalExchange end 100 (Just observation) (Just (CF.FxReset end end))
      flows <- mapM CF.asCashFlow [initExchange, finalExchange, nextExchange]
      leg <- CF.cashFlowLeg flows
      CF.setFxResetLegPricer leg pricer
      CF.amount initExchange >>= near 1e-10 (-100 * fx)
      CF.amount finalExchange >>= near 1e-10 (100 * fx)
      nextFx <- CF.fxResetRate pricer (CF.FxReset end end)
      CF.amount nextExchange >>= near 1e-10 (100 * (fx - nextFx))
      CF.fxResetNotionalExchange end 100 Nothing Nothing `shouldThrow` anyException
      plain <- CF.cashFlowLeg [underlying]
      wrapped <- CF.cashFlowLeg [coupon]
      CF.fixingDependencies plain >>= \dependencies -> CF.fixingDependencies wrapped `shouldReturn` dependencies
      Context.collectGarbage
      CF.amount coupon >>= near 1e-10 (rawAmount * fx)
      _ <- setValue spot 1.2
      CF.amount coupon >>= near 1e-10 (rawAmount * fx * 1.2 / 1.1)

  it "uses historical direct, inverse and triangulated exchange rates" $ Context.keepingSettingsGc $
    bracket_ clearExchangeRates clearExchangeRates $ do
      let today = fromGregorian 2024 7 8
          fixing = fromGregorian 2024 7 1
          valueDate = fromGregorian 2024 7 3
      Context.setEvaluationDate (Just today)
      dc <- dayCounter (Actual360 False)
      eur <- currency EUR
      usd <- currency USD
      gbp <- currency GBP
      curve <- simpleQuote 0.02 >>= \q -> flatForward (ReferenceDate today) q dc Continuous Annual
      spot <- simpleQuote 1.5
      if quantLibAtMost143 then
        CF.discountingFxResetPricer eur usd curve curve spot True Nothing `shouldThrow` unsupportedQuantLib144 "discountingFxResetPricer"
      else do
        pricer <- CF.discountingFxResetPricer eur usd curve curve spot True Nothing
        let observation = CF.FxReset fixing valueDate
            register source target fx = exchangeRate source target fx >>= \rate -> addExchangeRate rate fixing fixing
        CF.fxResetRate pricer observation `shouldThrow` anyException
        register eur usd 1.1
        CF.fxResetRate pricer observation >>= near 1e-12 1.1
        clearExchangeRates
        register usd eur (1 / 1.1)
        CF.fxResetRate pricer observation >>= near 1e-12 1.1
        clearExchangeRates
        register eur gbp 0.8
        register gbp usd 1.375
        CF.fxResetRate pricer observation >>= near 1e-12 1.1

  it "constructs interpolated stub coupons and expands component dependencies" $ 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
    nzd <- currency NZD
    curve2 <- simpleQuote 0.02 >>= \q -> flatForward (ReferenceDate today) q dc Continuous Annual
    curve3 <- simpleQuote 0.04 >>= \q -> flatForward (ReferenceDate today) q dc Continuous Annual
    short <- IR.iborIndex (IR.Ibor "Bkbm" (2, Months) 0 nzd cal ModifiedFollowing False dc) (Just curve2)
    long <- IR.iborIndex (IR.Ibor "Bkbm" (3, Months) 0 nzd cal ModifiedFollowing False dc) (Just curve3)
    let selection = Just (CF.InterpolatedStubIndexes (short :| [long]))
        makeCoupon = CF.stubIborCoupon end 1 start end 0 selection 1 0 Nothing Nothing (Just dc) False Nothing Preceding
    if quantLibAtMost143 then makeCoupon `shouldThrow` unsupportedQuantLib144 "stubIborCoupon"
    else do
      coupon <- makeCoupon
      vol <- simpleQuote 0.2 >>= \q -> constantOptionletVolatility (CalendarReferenceDate today) cal Following q dc ShiftedLognormal 0
      pricer <- CF.blackIborCouponPricer vol CF.Black76 Nothing (Just True)
      CF.setFloatingRateCouponPricer coupon pricer
      fixing <- IR.fixingDate short start
      shortMaturity <- IR.maturityDate short start
      longMaturity <- IR.maturityDate long start
      shortRate <- Index.fixing short fixing False
      longRate <- Index.fixing long fixing False
      let weight = fromIntegral (diffDays end shortMaturity) / fromIntegral (diffDays longMaturity shortMaturity)
      CF.rate coupon >>= near 1e-12 (shortRate + (longRate - shortRate) * weight)
      leg <- CF.cashFlowLeg [coupon]
      dependencies <- CF.fixingDependencies leg
      names <- mapM Index.name [short, long]
      dependencies `shouldMatchList` [(name, fixing) | name <- names]
      forM_ [CF.ClosestStubIndex (short :| [long])] $ \closest -> do
        c <- CF.stubIborCoupon end 1 start end 0 (Just closest) 1 0 Nothing Nothing Nothing False Nothing Preceding
        CF.setFloatingRateCouponPricer c pricer
        CF.rate c >>= near 1e-12 longRate