packages feed

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

-- | Coverage for 'QuantLib.Instrument' entry points ported from
-- @test/smoke/CheckAdditionalResults.hs@ (see CLAUDE.md: coverage is only measured over
-- @test\/hspec\/**@ + @test\/example\/**@, so this proven-correct smoke script was invisible
-- to the coverage number).
--
-- Checks 'additionalResults' end-to-end against two real QuantLib 1.43 engines that exercise
-- three of its four discriminants: the Bjerksund-Stensland American option engine writes
-- @exerciseType@ (std::string -> 'StringVal') and @strikeGamma@ (Real -> 'RealVal'); the Black
-- cap/floor engine writes @optionletsPrice@ as a @vector\<Real\>@ -> 'RealVectorVal',
-- exercising the vector marshalling branch the first check never touches. No shipped 1.43
-- engine stores a type this binding can't name, so the fourth discriminant ('UnsupportedVal',
-- the RTTI-name fallback) isn't exercised here -- its C++ side is a trivial, visibly-correct
-- @else@, and its Haskell side is a compiler-checked exhaustive @case@.
{-# LANGUAGE OverloadedLists #-}
module QuantLib.Spec.Instrument (spec) where

import Data.Time.Calendar(fromGregorian)
import Control.Monad(forM_)

import Test.Hspec

import QuantLib.Instrument
import QuantLib.Instrument.CapFloor
import QuantLib.Instrument.Option
import QuantLib.CashFlow hiding(npv, leg)
import QuantLib.Index.InterestRate(iborIndex, IborConstructor(Euribor6M))
import QuantLib.PricingEngine
import QuantLib.Process
import qualified QuantLib.Context as Context
import QuantLib.TermStructure.Volatility
import QuantLib.TermStructure.Yield
import QuantLib.Time.Calendar
import QuantLib.Time.Date
import QuantLib.Time.Schedule
import QuantLib.Quote
import QuantLib.InterestRate

spec :: Spec
spec = do
  describe "additionalResults" $ do
    it "Bjerksund-Stensland American option engine: exerciseType (StringVal) and strikeGamma (RealVal, > 0)" $
      Context.keepingSettingsGc $ do
        Context.setEvaluationDate $ Just (fromGregorian 1998 5 15)
        dc <- dayCounter Actual365FixedStandard
        let evalDate = 17 `may` 1998
            maturity = 17 `may` 1999
            under = 36; strike = 40; dividend = 0.0; riskFreeRate = 0.06; vol = 0.20
            optType = Put
        underQ <- simpleQuote under
        riskFreeQ <- simpleQuote riskFreeRate
        ts <- flatForward (ReferenceDate evalDate) riskFreeQ dc Continuous Annual
        divQ <- simpleQuote dividend
        divTS <- flatForward (ReferenceDate evalDate) divQ dc Continuous Annual
        volQ <- simpleQuote vol
        volTS <- calendar TARGET >>= \cal -> blackConstantVol (CalendarReferenceDate evalDate) cal volQ dc
        let payoff = PlainVanilla $ PlainVanillaPayoff optType strike
        bsmProc <- blackScholesMertonProcess underQ divTS ts volTS EulerDiscretization False
        americanOpt <- vanillaOption payoff (American Nothing maturity False)

        bjs <- bjerksundStenslandApproximationEngine bsmProc
        setPricingEngine americanOpt bjs
        _ <- npv americanOpt

        addl <- additionalResults americanOpt
        lookup "exerciseType" addl `shouldBe` Just (StringVal "American")
        case lookup "strikeGamma" addl of
          Just (RealVal g) -> g `shouldSatisfy` (> 0)
          other -> expectationFailure ("expected strikeGamma present as RealVal, got " ++ show other)
        length addl `shouldSatisfy` (> 0)

    it "Black cap/floor engine: optionletsPrice (RealVectorVal, non-empty and non-negative)" $
      Context.keepingSettingsGc $ do
        let today' = 11 `december` 2012
        Context.setEvaluationDate (Just today')
        cal <- calendar TARGET
        settle <- advance cal today' (2, Days) Following False
        discQ <- simpleQuote 0.02
        dc <- dayCounter Actual365FixedStandard
        discountTS <- flatForward (ReferenceDate today') discQ dc Continuous Annual
        idx <- iborIndex Euribor6M (Just discountTS)
        floatDC <- dayCounter (Actual360 False)
        floatSch <- schedule (Just settle) (11 `december` 2017) (6, Months) cal
          ModifiedFollowing ModifiedFollowing Forward False Nothing Nothing
        leg <- iborLeg floatSch idx [1000000] floatDC ModifiedFollowing [2] [1.0] [0.0] [] [] False False
        capfl <- cap leg [0.03]
        volQ <- simpleQuote 0.20
        vol0 <- constantOptionletVolatility (CalendarReferenceDate today') cal ModifiedFollowing volQ dc ShiftedLognormal 0
        eng <- blackCapFloorEngineFromVolatilityStructure discountTS vol0
        setPricingEngine capfl eng
        _ <- npv capfl

        addl <- additionalResults capfl
        case lookup "optionletsPrice" addl of
          Just (RealVectorVal xs) -> do
            xs `shouldSatisfy` (not . null)
            xs `shouldSatisfy` all (>= 0)
          other -> expectationFailure ("expected optionletsPrice present as RealVectorVal, got " ++ show other)

  describe "PerpetualFutures" $
    it "reproduces perpetualfutures.cpp's constant-parameter analytic values" $
      Context.keepingSettingsGc $ do
        let evalDate = fromGregorian 2024 1 2
            spot = 10000.0
            domesticRate = 0.04
            foreignRate = 0.02
            fundingRate = 0.01
            interestRateDiff = 0.005
            cases :: [(PerpetualFuturesPayoffType, PerpetualFuturesFundingType, (Int, TimeUnit))]
            cases =
              [ (PerpetualFuturesLinear, PerpetualFuturesFundingWithPreviousSpot, (3, Months))
              , (PerpetualFuturesLinear, PerpetualFuturesFundingWithCurrentSpot, (3, Months))
              , (PerpetualFuturesInverse, PerpetualFuturesFundingWithPreviousSpot, (3, Months))
              , (PerpetualFuturesInverse, PerpetualFuturesFundingWithCurrentSpot, (3, Months))
              , (PerpetualFuturesLinear, PerpetualFuturesFundingWithPreviousSpot, (0, Months))
              , (PerpetualFuturesInverse, PerpetualFuturesFundingWithPreviousSpot, (0, Months))
              ]
            expected payoff fundingType (frequency, _) =
              let dt = fromIntegral frequency / 12.0
               in if frequency == 0
                    then case payoff of
                      PerpetualFuturesLinear -> spot * (fundingRate - interestRateDiff) /
                        (foreignRate - domesticRate + fundingRate)
                      PerpetualFuturesInverse -> spot * (domesticRate - foreignRate + fundingRate) /
                        (fundingRate - interestRateDiff)
                      PerpetualFuturesQuanto -> error "Quanto is unsupported by the engine"
                    else case (payoff, fundingType) of
                      (PerpetualFuturesLinear, PerpetualFuturesFundingWithPreviousSpot) ->
                        spot * (fundingRate - interestRateDiff) * exp (foreignRate * dt) /
                        (exp (foreignRate * dt) - exp (domesticRate * dt) + fundingRate * exp (foreignRate * dt))
                      (PerpetualFuturesLinear, PerpetualFuturesFundingWithCurrentSpot) ->
                        spot * (fundingRate - interestRateDiff) * exp (domesticRate * dt) /
                        (exp (foreignRate * dt) - exp (domesticRate * dt) + fundingRate * exp (domesticRate * dt))
                      (PerpetualFuturesInverse, PerpetualFuturesFundingWithPreviousSpot) ->
                        spot * (exp (domesticRate * dt) - exp (foreignRate * dt) + fundingRate * exp (domesticRate * dt)) /
                        (fundingRate - interestRateDiff) / exp (domesticRate * dt)
                      (PerpetualFuturesInverse, PerpetualFuturesFundingWithCurrentSpot) ->
                        spot * (exp (domesticRate * dt) - exp (foreignRate * dt) + fundingRate * exp (foreignRate * dt)) /
                        (fundingRate - interestRateDiff) / exp (foreignRate * dt)
                      (_, _) -> error "Quanto is unsupported by the engine"
        Context.setEvaluationDate (Just evalDate)
        dc <- dayCounter ActualActualISDA
        cal <- calendar (Bespoke "PerpetualFutures" [])
        domesticQuote <- simpleQuote domesticRate
        foreignQuote <- simpleQuote foreignRate
        domesticCurve <- flatForward (ReferenceDate evalDate) domesticQuote dc Continuous Annual
        foreignCurve <- flatForward (ReferenceDate evalDate) foreignQuote dc Continuous Annual
        spotQuote <- simpleQuote spot
        forM_ cases $ \(payoff, fundingType, frequency) -> do
          future <- perpetualFutures payoff fundingType frequency cal dc
          engine <- discountingPerpetualFuturesEngine domesticCurve foreignCurve spotQuote
            [(0, fundingRate, interestRateDiff)] PerpetualFuturesPiecewiseConstant 60
          setPricingEngine future engine
          actual <- npv future
          abs (actual / expected payoff fundingType frequency - 1) `shouldSatisfy` (< 1.0e-6)