packages feed

hasquant-0.6.0.0: test/hspec/QuantLib/Spec/Commodity.hs

module QuantLib.Spec.Commodity (spec) where

import Test.Hspec

import Data.Time.Calendar(addDays)
import QuantLib.Time.Date
import QuantLib.Time.Calendar(calendar, adjust, CalendarConstructor(..), BusinessDayConvention(..))
import QuantLib.Currency(code)
import QuantLib.Commodity

spec :: Spec
spec = do
  describe "CommodityType" $ do
    it "round-trips code/name" $ do
      ho <- commodityType "HO" "Heating Oil"
      commodityTypeCode ho `shouldBe` "HO"
      commodityTypeName ho `shouldBe` "Heating Oil"
      commodityTypeEmpty ho `shouldBe` False

    it "equality is by code" $ do
      ho1 <- commodityType "HO" "Heating Oil"
      ho2 <- commodityType "HO" "Heating Oil (again)"
      wti <- commodityType "CL" "WTI Crude"
      ho1 `shouldBe` ho2
      ho1 `shouldNotBe` wti

    it "has a fixed null placeholder" $ do
      n1 <- nullCommodityType
      n2 <- nullCommodityType
      n1 `shouldBe` n2
      commodityTypeCode n1 `shouldBe` "<NULL>"

  describe "UnitOfMeasure" $ do
    it "round-trips name/code/type" $ do
      u <- unitOfMeasure "Barrels" "BBL" Volume
      unitOfMeasureName u `shouldBe` "Barrels"
      unitOfMeasureCode u `shouldBe` "BBL"
      unitOfMeasureType u `shouldBe` Volume
      unitOfMeasureEmpty u `shouldBe` False

    it "equality is by code" $ do
      u1 <- unitOfMeasure "Barrels" "BBL" Volume
      u2 <- unitOfMeasure "Barrels (again)" "BBL" Volume
      u1 `shouldBe` u2

    it "renames the Quantity tag to QuantityUnit to avoid clashing with the Quantity class" $ do
      u <- unitOfMeasure "Lots" "LOT" QuantityUnit
      unitOfMeasureType u `shouldBe` QuantityUnit

    it "has the petroleum and lot predefined units, each with the expected code" $ do
      lot <- lotUnitOfMeasure
      bbl <- barrelUnitOfMeasure
      mt <- mtUnitOfMeasure
      mb <- mbUnitOfMeasure
      gal <- gallonUnitOfMeasure
      litre <- litreUnitOfMeasure
      kl <- kilolitreUnitOfMeasure
      tkl <- tokyoKilolitreUnitOfMeasure
      map unitOfMeasureCode [lot, bbl, mt, mb, gal, litre, kl, tkl]
        `shouldBe` ["Lot", "BBL", "MT", "MB", "GAL", "l", "kl", "KL_tk"]

  describe "PaymentTerm" $ do
    it "round-trips name/eventType/offsetDays/calendar" $ do
      cal <- calendar BrazilSettlement
      pt <- paymentTerm "Pricing end + 5 days" PricingDate 5 cal
      paymentTermName pt `shouldBe` "Pricing end + 5 days"
      paymentTermEventType pt `shouldBe` PricingDate
      paymentTermOffsetDays pt `shouldBe` 5
      paymentTermEmpty pt `shouldBe` False
      cal' <- paymentTermCalendar pt
      show cal' `shouldBe` show cal

    it "computes the payment date as the calendar-adjusted offset" $ do
      cal <- calendar BrazilSettlement
      pt <- paymentTerm "Trade + 2 days" TradeDate 2 cal
      let d = 3 `january` 2024
      expected <- adjust cal (addDays 2 d) Following
      paymentTermGetPaymentDate pt d `shouldReturn` expected

  describe "Quantity" $ do
    it "rounds the amount per the unit of measure's rounding convention, unit/type unchanged" $ do
      ho <- commodityType "HO" "Heating Oil"
      bbl <- barrelUnitOfMeasure
      let (ct, uom, amt) = roundedQuantity (ho, bbl, 1.006)
      amt `shouldBe` 1.0
      ct `shouldBe` ho
      uom `shouldBe` bbl

    it "considers equal-unit quantities with the same amount close" $ do
      nullCt <- nullCommodityType
      bbl <- barrelUnitOfMeasure
      close <- closeQuantity (nullCt, bbl, 1000) (nullCt, bbl, 1000) 42
      close `shouldBe` True

    it "throws comparing quantities in different units with no conversion setting configured" $ do
      nullCt <- nullCommodityType
      bbl <- barrelUnitOfMeasure
      gal <- gallonUnitOfMeasure
      closeQuantity (nullCt, bbl, 1) (nullCt, gal, 42) 42 `shouldThrow` anyException

  describe "UnitOfMeasureConversion" $ do
    it "round-trips source/target/commodityType/type/factor/code" $ do
      nullCt <- nullCommodityType
      mb <- mbUnitOfMeasure
      bbl <- barrelUnitOfMeasure
      conv <- unitOfMeasureConversion nullCt mb bbl 1000
      src <- unitOfMeasureConversionSource conv
      tgt <- unitOfMeasureConversionTarget conv
      ct <- unitOfMeasureConversionCommodityType conv
      src `shouldBe` mb
      tgt `shouldBe` bbl
      ct `shouldBe` nullCt
      unitOfMeasureConversionType conv `shouldBe` UomDirect
      unitOfMeasureConversionFactor conv `shouldBe` 1000

    it "converts a quantity from source to target" $ do
      nullCt <- nullCommodityType
      mb <- mbUnitOfMeasure
      bbl <- barrelUnitOfMeasure
      conv <- unitOfMeasureConversion nullCt mb bbl 1000
      (ct, uom, amt) <- convertQuantity conv (nullCt, mb, 1000)
      ct `shouldBe` nullCt
      uom `shouldBe` bbl
      amt `shouldBe` 1000000

    -- Ported from test-suite/commodityunitofmeasure.cpp's testDirect: a direct conversion built
    -- by hand must agree with the manager's pre-populated lookup for the same unit pair.
    it "agrees with the pre-populated manager lookup (ported testDirect, MB to BBL leg)" $ do
      nullCt <- nullCommodityType
      mb <- mbUnitOfMeasure
      bbl <- barrelUnitOfMeasure
      direct <- unitOfMeasureConversion nullCt mb bbl 1000
      actual <- convertQuantity direct (nullCt, mb, 1000)
      looked <- lookupUomConversion nullCt bbl mb UomDirect
      calc <- convertQuantity looked (nullCt, mb, 1000)
      isClose <- closeQuantity calc actual 42
      isClose `shouldBe` True

    it "agrees with the pre-populated manager lookup (ported testDirect, BBL to Gallon leg)" $ do
      nullCt <- nullCommodityType
      bbl <- barrelUnitOfMeasure
      gal <- gallonUnitOfMeasure
      direct <- unitOfMeasureConversion nullCt bbl gal 42
      actual <- convertQuantity direct (nullCt, gal, 1000)
      looked <- lookupUomConversion nullCt bbl gal UomDirect
      calc <- convertQuantity looked (nullCt, gal, 1000)
      isClose <- closeQuantity calc actual 42
      isClose `shouldBe` True

    it "chains two conversions sharing a common unit" $ do
      nullCt <- nullCommodityType
      mb <- mbUnitOfMeasure
      bbl <- barrelUnitOfMeasure
      gal <- gallonUnitOfMeasure
      mbToBbl <- unitOfMeasureConversion nullCt mb bbl 1000
      bblToGal <- unitOfMeasureConversion nullCt bbl gal 42
      chained <- chainUnitOfMeasureConversion mbToBbl bblToGal
      unitOfMeasureConversionType chained `shouldBe` UomDerived
      (_, uom, amt) <- convertQuantity chained (nullCt, mb, 1)
      uom `shouldBe` gal
      amt `shouldBe` 42000

  describe "UnitOfMeasureConversionManager" $ do
    it "resolves petroleum conversions via triangulation, pre-populated before any add" $ do
      nullCt <- nullCommodityType
      litre <- litreUnitOfMeasure
      gal <- gallonUnitOfMeasure
      conv <- lookupUomConversion nullCt litre gal UomDerived
      src <- unitOfMeasureConversionSource conv
      tgt <- unitOfMeasureConversionTarget conv
      [src, tgt] `shouldSatisfy` \l -> litre `elem` l && gal `elem` l

    it "add/clear round-trip: a direct conversion registered via addUomConversion is found by a\
       \ direct lookup, and clear discards it" $ do
      ho <- commodityType "HO" "Heating Oil"
      lot <- lotUnitOfMeasure
      bbl <- barrelUnitOfMeasure
      conv <- unitOfMeasureConversion ho lot bbl 500
      addUomConversion conv
      found <- lookupUomConversion ho lot bbl UomDirect
      unitOfMeasureConversionFactor found `shouldBe` 500
      clearUomConversions
      lookupUomConversion ho lot bbl UomDirect `shouldThrow` anyException

  describe "CommoditySettings" $ do
    it "defaults to USD and barrels, and round-trips a new setting" $ do
      origCcy <- commoditySettingsCurrency
      origUom <- commoditySettingsUnitOfMeasure
      code origCcy `shouldBe` "USD"
      unitOfMeasureCode origUom `shouldBe` "BBL"

      mt <- mtUnitOfMeasure
      setCommoditySettingsUnitOfMeasure mt
      commoditySettingsUnitOfMeasure `shouldReturn` mt
      setCommoditySettingsUnitOfMeasure origUom
      commoditySettingsUnitOfMeasure `shouldReturn` origUom