packages feed

hasquant-0.6.0.0: test/hspec/QuantLib/Spec/Instrument/Energy.hs

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

import Test.Hspec
import Data.Time.Calendar(addDays)

import qualified QuantLib.Settings as Settings
import QuantLib.Time.Date
import QuantLib.Time.Calendar(calendar, CalendarConstructor(..))
import QuantLib.Time.Schedule(dayCounter, DayCounterConstructor(..), Frequency(..))
import QuantLib.InterestRate(Compounding(..))
import QuantLib.Quote(simpleQuote)
import QuantLib.TermStructure.Yield(YieldTermStructure, flatForward)
import QuantLib.Commodity
import QuantLib.TermStructure.Commodity(commodityCurve)
import QuantLib.Index.Commodity(CommodityIndex, commodityIndex)
import QuantLib.Index(addFixing)
import QuantLib.Instrument(npv)
import QuantLib.Instrument.Energy

-- |A flat, zero-rate discount curve -- with pay/receive/discount legs all pointing at the same
-- curve, a swap's paid and received legs discount identically, so a zero @uDelta@ (the legs'
-- undiscounted difference) implies a zero NPV regardless of the curve's own rate (see CLAUDE.md's
-- TARF-style martingale-check precedent). Rate 0 just keeps the by-hand arithmetic trivial.
flatZeroCurve :: Day -> IO YieldTermStructure
flatZeroCurve evalDate = do
  q <- simpleQuote 0.0
  dc <- dayCounter Actual365FixedStandard
  flatForward evalDate q dc Continuous Annual

-- |A flat @price@ 'CommodityIndex' over @ct@\/@bbl@: one historical fixing well before
-- @evalDate@ (so every date in the pricing window below falls after 'lastQuoteDate' and is priced
-- off the forward curve, per energyvanillaswap.cpp's own per-day branch) backed by a forward curve
-- quoting the same flat @price@ across the whole window -- so every quote used anywhere in a
-- test is exactly @price@, regardless of which date it's fetched for.
flatIndex :: CommodityType -> UnitOfMeasure -> Day -> Double -> IO CommodityIndex
flatIndex ct bbl evalDate price = do
  usd <- commoditySettingsCurrency
  cal <- calendar Null
  curve <- commodityCurve "flat curve" ct usd bbl cal [evalDate, addDays 400 evalDate] [price, price]
             =<< dayCounter Actual365FixedStandard
  idx <- commodityIndex "flat index" ct usd bbl cal 1 (Just curve)
  addFixing idx (addDays (-30) evalDate) price False
  pure idx

spec :: Spec
spec = do
  describe "EnergyFuture" $ do
    it "nets to zero when the trade price matches the index's flat quote" $
      Settings.keepingSettings' $ do
        evalDate <- today
        Settings.setEvaluationDate (Just evalDate)
        ct <- commodityType "CL" "Crude Oil"
        bbl <- barrelUnitOfMeasure
        usd <- commoditySettingsCurrency
        idx <- flatIndex ct bbl evalDate 100
        fut <- energyFuture 1 (ct, bbl, 1000) (100, usd, bbl) idx ct []
        npv fut `shouldReturn` 0

    it "control: a below-market trade price nets to a positive, formula-predicted NPV" $
      Settings.keepingSettings' $ do
        evalDate <- today
        Settings.setEvaluationDate (Just evalDate)
        ct <- commodityType "CL" "Crude Oil"
        bbl <- barrelUnitOfMeasure
        usd <- commoditySettingsCurrency
        idx <- flatIndex ct bbl evalDate 100
        -- buySell=1, quantity 1000, trade price 90 vs a 100 flat quote, lot quantity 1:
        -- delta = (quote - tradePrice) * quantity * lotQuantity^2 * buySell = (100-90)*1000*1*1
        fut <- energyFuture 1 (ct, bbl, 1000) (90, usd, bbl) idx ct []
        npv fut `shouldReturn` 10000

  describe "EnergyVanillaSwap" $ do
    it "nets to zero (before financing cost) when the fixed price matches the flat floating quote" $
      Settings.keepingSettings' $ do
        evalDate <- today
        Settings.setEvaluationDate (Just evalDate)
        ct <- commodityType "CL" "Crude Oil"
        bbl <- barrelUnitOfMeasure
        usd <- commoditySettingsCurrency
        cal <- calendar Null
        idx <- flatIndex ct bbl evalDate 100
        ts <- flatZeroCurve evalDate
        let pp = pricingPeriod (addDays 10 evalDate) (addDays 14 evalDate) (addDays 20 evalDate) (ct, bbl, 1000)
        swp <- energyVanillaSwap True cal (100, usd) bbl idx usd usd [pp] ct [] ts ts ts
        npv swp `shouldReturn` 0

    it "control: paying a below-market fixed price nets to a positive NPV" $
      Settings.keepingSettings' $ do
        evalDate <- today
        Settings.setEvaluationDate (Just evalDate)
        ct <- commodityType "CL" "Crude Oil"
        bbl <- barrelUnitOfMeasure
        usd <- commoditySettingsCurrency
        cal <- calendar Null
        idx <- flatIndex ct bbl evalDate 100
        ts <- flatZeroCurve evalDate
        let pp = pricingPeriod (addDays 10 evalDate) (addDays 14 evalDate) (addDays 20 evalDate) (ct, bbl, 1000)
        swp <- energyVanillaSwap True cal (90, usd) bbl idx usd usd [pp] ct [] ts ts ts
        n <- npv swp
        n `shouldSatisfy` (> 0)

  describe "EnergyBasisSwap" $ do
    it "nets to zero (before financing cost) with a zero basis and matching flat quotes" $
      Settings.keepingSettings' $ do
        evalDate <- today
        Settings.setEvaluationDate (Just evalDate)
        ct <- commodityType "CL" "Crude Oil"
        bbl <- barrelUnitOfMeasure
        usd <- commoditySettingsCurrency
        cal <- calendar Null
        idx <- flatIndex ct bbl evalDate 100
        ts <- flatZeroCurve evalDate
        let pp = pricingPeriod (addDays 10 evalDate) (addDays 14 evalDate) (addDays 20 evalDate) (ct, bbl, 1000)
        swp <- energyBasisSwap cal idx idx idx True usd usd [pp] (0, usd, bbl) ct [] ts ts ts
        npv swp `shouldReturn` 0

    it "control: a nonzero basis added to the pay leg nets to a negative NPV" $
      Settings.keepingSettings' $ do
        evalDate <- today
        Settings.setEvaluationDate (Just evalDate)
        ct <- commodityType "CL" "Crude Oil"
        bbl <- barrelUnitOfMeasure
        usd <- commoditySettingsCurrency
        cal <- calendar Null
        idx <- flatIndex ct bbl evalDate 100
        ts <- flatZeroCurve evalDate
        let pp = pricingPeriod (addDays 10 evalDate) (addDays 14 evalDate) (addDays 20 evalDate) (ct, bbl, 1000)
        swp <- energyBasisSwap cal idx idx idx True usd usd [pp] (5, usd, bbl) ct [] ts ts ts
        n <- npv swp
        n `shouldSatisfy` (< 0)

  describe "createPricingPeriods" $ do
    it "splits a date range into monthly PerMonth periods, per the paymentTerm's offset" $ do
      ct <- commodityType "CL" "Crude Oil"
      bbl <- barrelUnitOfMeasure
      cal <- calendar Null
      pt <- paymentTerm "5 days after pricing end" PricingDate 5 cal
      let startDate = 1 `january` 2024
          endDate = 1 `april` 2024
      pps@(st: _) <- createPricingPeriods startDate endDate (ct, bbl, 100) DeliveryMonthly PerMonth pt
      length pps `shouldBe` 3
      pricingPeriodStartDate st `shouldBe` startDate

    it "silently returns no periods for a DeliverySchedule createPricingPeriods doesn't implement" $ do
      ct <- commodityType "CL" "Crude Oil"
      bbl <- barrelUnitOfMeasure
      cal <- calendar Null
      pt <- paymentTerm "5 days after pricing end" PricingDate 5 cal
      pps <- createPricingPeriods (1 `january` 2024) (1 `april` 2024) (ct, bbl, 100) DeliveryQuarterly PerQuarter pt
      pps `shouldBe` []

-- vim: set ff=unix ts=8 sts=2 sw=2 et: