packages feed

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

-- No upstream test-suite fixture exists for BTP or RendistatoCalculator (checked
-- ~/Src/QuantLib/test-suite for btp/rendistato, neither exists), so 'btp' is checked against a
-- hand-built 'fixedRateBond' using btp.cpp's own hardcoded constructor arguments verbatim: the
-- two must agree on everything except accruedAmount, where BTP additionally applies
-- ClosestRounding(5) via its C++ override.
module QuantLib.Spec.Instrument.Bond (spec) where

import Test.Hspec
import Data.List.NonEmpty(fromList)

import qualified QuantLib.Context as Context
import QuantLib.Time.Date
import QuantLib.Time.Calendar
import QuantLib.Time.Schedule
import QuantLib.Quote
import QuantLib.TermStructure.Yield(Reference(..), flatForward)
import QuantLib.InterestRate(Compounding(..))
import QuantLib.Instrument.Bond
import qualified QuantLib.CashFlow as CF

import QuantLib.Spec.Helpers(closePrec)

spec :: Spec
spec = describe "Bond (BTP, Rendistato)" $ do
  btpSpec
  rendistatoSpec

btpSpec :: Spec
btpSpec = describe "BTP" $ do
  it "matches a hand-built FixedRateBond using btp.cpp's own hardcoded conventions, except accruedAmount's ClosestRounding(5)" $
    Context.keepingSettingsGc $ do
      let maturity = 1 `september` 2030
          start = 1 `september` 2020
          fixedRate = 0.03
          settle = 15 `march` 2025
      Context.setEvaluationDate (Just start)
      target <- calendar TARGET
      nullCal <- calendar Null
      isma <- dayCounter ActualActualISMA
      sched <- schedule (Just start) maturity (6, Months) nullCal Unadjusted Unadjusted Backward True Nothing Nothing

      b <- btp maturity fixedRate (Just start) Nothing
      ref <- fixedRateBond 2 100.0 sched (fromList [fixedRate]) isma ModifiedFollowing 100.0 Nothing target
        (0, Days) target Unadjusted False isma

      bMat <- maturityDate b
      refMat <- maturityDate ref
      bMat `shouldBe` refMat

      bNotionals <- notionals b
      refNotionals <- notionals ref
      bNotionals `shouldBe` refNotionals

      rawAccrued <- accruedAmount ref settle
      bAccrued <- accruedAmount b settle
      let rounded = fromIntegral (round (rawAccrued * 1e5) :: Integer) / (1e5 :: Double)
      bAccrued `shouldSatisfy` closePrec rounded 1e-9
      -- and the rounding must have actually moved the value -- otherwise this doesn't exercise
      -- ClosestRounding(5) at all.
      abs (rawAccrued - bAccrued) `shouldSatisfy` (> 1e-9)

  it "btpWithRedemption overrides the par redemption" $ do
    b <- btpWithRedemption (1 `september` 2030) 0.03 95.0 (Just (1 `september` 2020)) Nothing
    reds <- redemptions b
    cfs <- CF.cashFlows reds Nothing Nothing
    map (\(_, amt, _) -> amt) cfs `shouldBe` [95.0]

-- Fixture: three BTPs of increasing maturity, equal outstanding, live clean-price quotes, priced
-- against a flat 3% EUR curve. No upstream cached values exist, so these are structural
-- invariants (upstream's own inline formulas for yield\/spread, plus the hardcoded 1..15Y swap
-- ladder, both genuinely pinnable) rather than golden values.
rendistatoSpec :: Spec
rendistatoSpec = describe "RendistatoBasket / RendistatoCalculator" $
  it "aggregates a basket of BTPs against a flat EUR curve" $
    Context.keepingSettingsGc $ do
      let today' = 1 `september` 2024
      Context.setEvaluationDate (Just today')
      dc <- dayCounter Actual365FixedStandard
      q <- simpleQuote 0.03
      curve <- flatForward (ReferenceDate today') q dc Continuous NoFrequency

      b1 <- btp (1 `september` 2027) 0.025 Nothing Nothing
      b2 <- btp (1 `september` 2030) 0.03 Nothing Nothing
      b3 <- btp (1 `september` 2034) 0.035 Nothing Nothing
      p1 <- simpleQuote 98.0
      p2 <- simpleQuote 97.0
      p3 <- simpleQuote 95.0
      basket <- rendistatoBasket (fromList [(b1, 100.0, p1), (b2, 100.0, p2), (b3, 100.0, p3)])
      rc <- rendistatoCalculator basket (6, Months) (Just curve) curve

      lengths <- rendistatoSwapLengths rc
      lengths `shouldBe` [1 .. 15]

      yields <- rendistatoYields rc
      length yields `shouldBe` 3

      durations <- rendistatoDurations rc
      length durations `shouldBe` 3

      y <- rendistatoYield rc
      rate <- rendistatoEquivalentSwapRate rc
      spread <- rendistatoEquivalentSwapSpread rc
      spread `shouldSatisfy` closePrec (y - rate) 1e-9

      len <- rendistatoEquivalentSwapLength rc
      lengthQuote <- rendistatoEquivalentSwapLengthQuote rc
      lengthQuoteValue <- value lengthQuote
      lengthQuoteValue `shouldSatisfy` closePrec len 1e-9

      spreadQuote <- rendistatoEquivalentSwapSpreadQuote rc
      spreadQuoteValue <- value spreadQuote
      spreadQuoteValue `shouldSatisfy` closePrec spread 1e-9

      _ <- rendistatoEquivalentSwap rc
      _ <- rendistatoSwapRates rc
      _ <- rendistatoSwapYields rc
      _ <- rendistatoSwapDurations rc
      pure ()