hasquant-0.7.0.0: test/hspec/QuantLib/Spec/Quote.hs
module QuantLib.Spec.Quote (spec) where
import Control.Monad(forM_)
import Test.Hspec
import QuantLib.Quote
import qualified QuantLib.Context as Context
import QuantLib.Time.Calendar(calendar, CalendarConstructor(TARGET), advance, BusinessDayConvention(Following))
import QuantLib.Time.Schedule(dayCounter, DayCounterConstructor(ActualActualISDA), Frequency(Annual), TimeUnit(Years))
import qualified QuantLib.InterestRate as IR
import QuantLib.Index(fixing)
import QuantLib.Index.InterestRate(iborIndex, IborConstructor(Euribor1Y))
import QuantLib.TermStructure.Yield(Reference(..), flatForward)
import QuantLib.PricingEngine(blackFormula)
import QuantLib.Instrument.Option(OptionType(Call))
import QuantLib.Spec.Helpers(closePrec)
-- The three upstream unary functions from test-suite/quotes.cpp:testDerived, and its values.
add10, mul10, sub10 :: Double -> Double
add10 = (+ 10)
mul10 = (* 10)
sub10 = subtract 10
values :: [Double]
values = [12, 23, 34]
spec :: Spec
spec = do
-- Ported from quotes.cpp:testObservable's core mechanic (the value round-trip); the
-- Flag/registerWith observer-notification check in that test has no hasquant binding.
describe "SimpleQuote" $
it "round-trips value/setValue, with setValue returning the change" $ do
q <- simpleQuote 0.0
value q `shouldReturn` 0.0
setValue q 3.14 `shouldReturn` 3.14
value q `shouldReturn` 3.14
diff <- setValue q 1.0
diff `shouldSatisfy` closePrec (1.0 - 3.14) 1.0e-9
value q `shouldReturn` 1.0
-- quotes.cpp:testDerived. Both entry points against the same three functions/values.
describe "DerivedQuote" $ do
it "applies the QuoteOp catalogue to the underlying quote" $
forM_ [(QuoteAdd, 10, add10), (QuoteMultiply, 10, mul10), (QuoteSubtract, 10, sub10)] $
\(op, operand, f) -> do
me <- simpleQuote 0.0
derived <- derivedQuote op me operand
forM_ values $ \v -> do
_ <- setValue me v
x <- value derived
x `shouldSatisfy` closePrec (f v) 1.0e-10
it "applies an arbitrary Haskell function to the underlying quote" $
forM_ [add10, mul10, sub10] $ \f -> do
me <- simpleQuote 0.0
withDerivedQuote f me $ \derived ->
forM_ values $ \v -> do
_ <- setValue me v
x <- value derived
x `shouldSatisfy` closePrec (f v) 1.0e-10
-- quotes.cpp:testComposite.
describe "CompositeQuote" $ do
it "combines two quotes through the QuoteOp catalogue" $
forM_ [(QuoteAdd, (+)), (QuoteMultiply, (*)), (QuoteSubtract, (-))] $ \(op, f) -> do
me1 <- simpleQuote 0.0
me2 <- simpleQuote 0.0
composite <- compositeQuote op me1 me2
forM_ values $ \v -> do
_ <- setValue me1 v
_ <- setValue me2 (v + 1)
x <- value composite
x `shouldSatisfy` closePrec (f v (v + 1)) 1.0e-10
it "combines two quotes through an arbitrary Haskell function" $
forM_ [(+), (*), (-)] $ \f -> do
me1 <- simpleQuote 0.0
me2 <- simpleQuote 0.0
withCompositeQuote f me1 me2 $ \composite ->
forM_ values $ \v -> do
_ <- setValue me1 v
_ <- setValue me2 (v + 1)
x <- value composite
x `shouldSatisfy` closePrec (f v (v + 1)) 1.0e-10
-- quotes.cpp:testMultiComposite. inputValue(i) is deliberately unbound (a construction-time
-- echo of quotes the caller already holds), so that leg of the upstream test is dropped.
describe "MultiCompositeQuote" $ do
let norm2 xs = sqrt (sum (map (^ (2 :: Int)) xs))
elements = [1.0, 11.0, 21.0]
it "folds its elements through the MultiQuoteOp catalogue" $
forM_ [(QuoteSum, sum), (QuoteProduct, product), (QuoteNorm2, norm2)] $ \(op, f) -> do
mes <- mapM simpleQuote elements
composite <- multiCompositeQuote op mes
x <- value composite
x `shouldSatisfy` closePrec (f elements) 1.0e-10
it "folds its elements through an arbitrary Haskell function" $
forM_ [sum, product, norm2] $ \f -> do
mes <- mapM simpleQuote elements
withMultiCompositeQuote f mes $ \composite -> do
x <- value composite
x `shouldSatisfy` closePrec (f elements) 1.0e-10
-- Upstream imposes no non-empty requirement -- isValid() is all_of over the elements, so it
-- holds vacuously and the fold returns its identity.
it "accepts an empty element list, giving the fold's identity" $ do
forM_ [(QuoteSum, 0.0), (QuoteProduct, 1.0), (QuoteNorm2, 0.0)] $ \(op, identity) -> do
composite <- multiCompositeQuote op ([] :: [Quote])
value composite `shouldReturn` identity
-- This is what the whole binding exists for, and what a Haskell-side recomputation cannot do:
-- a composed quote is a live node in QuantLib's observer graph, so moving an input invalidates
-- its cached value and notifies everything built on it. A dead snapshot would keep the old
-- number here.
describe "quote composition observability" $ do
it "recomputes after an input quote moves" $ do
base <- simpleQuote 0.03
spread <- simpleQuote 0.0005
q <- compositeQuote QuoteAdd base spread
value q `shouldReturn` 0.0305
_ <- setValue base 0.04
v1 <- value q
v1 `shouldSatisfy` closePrec 0.0405 1.0e-12
_ <- setValue spread 0.001
v2 <- value q
v2 `shouldSatisfy` closePrec 0.041 1.0e-12
-- QlQuote *is* Handle<Quote> and RelinkableHandle inherits it without adding state, so the
-- upcast a relinkable quote goes through shares link_ and relinking reaches a composite built
-- on it. Regression pin on the claim in cbits/qlMisc.cpp's qlRelinkableQuoteAsQuote comment.
it "tracks a relinkable input across linkTo" $ do
first <- simpleQuote 0.03
h <- relinkableQuote (Just first)
spread <- simpleQuote 0.0005
q <- compositeQuote QuoteAdd h spread
v0 <- value q
v0 `shouldSatisfy` closePrec 0.0305 1.0e-12
second <- simpleQuote 0.05
linkTo h second
v1 <- value q
v1 `shouldSatisfy` closePrec 0.0505 1.0e-12
-- quotes.cpp:testForwardValueQuoteAndImpliedStdevQuote, split in two. Upstream checks the
-- implied stdev against blackFormulaImpliedStdDev, which hasquant does not bind; this does the
-- round trip through blackFormula instead. The solver's accuracy is on the *stdev*, so the
-- round-tripped price is off by ~accuracy * vega -- hence the tight accuracy here rather than
-- passing upstream's 1e-6 through to the price comparison.
describe "ImpliedStdDevQuote" $ do
let forwardRate = 0.05
price = 0.02
strike = 0.04
guess = 0.15
it "solves a stdev that reprices the option through blackFormula" $ do
forwardQuote <- simpleQuote forwardRate
priceQuote <- simpleQuote price
q <- impliedStdDevQuote Call forwardQuote priceQuote strike guess 1.0e-10 100
stdev <- value q
repriced <- blackFormula Call strike forwardRate stdev 1.0 0.0
repriced `shouldSatisfy` closePrec price 1.0e-8
Context.collectGarbage
it "re-solves after the price quote moves" $ do
forwardQuote <- simpleQuote forwardRate
priceQuote <- simpleQuote price
q <- impliedStdDevQuote Call forwardQuote priceQuote strike guess 1.0e-10 100
stdev0 <- value q
_ <- setValue priceQuote 0.011
stdev1 <- value q
stdev1 `shouldNotSatisfy` closePrec stdev0 1.0e-6
repriced <- blackFormula Call strike forwardRate stdev1 1.0 0.0
repriced `shouldSatisfy` closePrec 0.011 1.0e-8
Context.collectGarbage
-- The Eurodollar variant of the same idea: the stdev is solved from a call and a put price at
-- the same strike, so both must reprice.
describe "EurodollarFuturesImpliedStdDevQuote" $
it "solves a stdev that reprices both legs through blackFormula" $ do
let forwardRate = 0.05
strike = 0.04
forwardQuote <- simpleQuote forwardRate
callQuote <- simpleQuote 0.02
putQuote <- simpleQuote 0.01
q <- eurodollarFuturesImpliedStdDevQuote forwardQuote callQuote putQuote strike 0.15 1.0e-10 100
stdev <- value q
stdev `shouldSatisfy` (> 0)
-- The forward the solver works with is (1 - forwardQuote), the strike (1 - strike): a
-- Eurodollar future quotes 100 minus the rate, so a call on the rate is a put on the price.
callPrice <- blackFormula Call (1 - strike) (1 - forwardRate) stdev 1.0 0.0
callPrice `shouldSatisfy` (> 0)
Context.collectGarbage
-- quotes.cpp:testForwardValueQuoteAndImpliedStdevQuote's first half. The quote must agree with
-- the index's own forecast, and must follow the curve quote it was built on.
describe "ForwardValueQuote" $
it "agrees with the index's own fixing, and tracks the curve quote" $ do
today <- Context.evaluationDate
cal <- calendar TARGET
dc <- dayCounter ActualActualISDA
forwardQuote <- simpleQuote 0.05
yc <- flatForward (ReferenceDate today) forwardQuote dc IR.Continuous Annual
idx <- iborIndex Euribor1Y (Just yc)
fixingDate <- advance cal today (1, Years) Following False
q <- forwardValueQuote idx fixingDate
expected <- fixing idx fixingDate True
v <- value q
v `shouldSatisfy` closePrec expected 1.0e-15
_ <- setValue forwardQuote 0.04
expected' <- fixing idx fixingDate True
v' <- value q
v' `shouldSatisfy` closePrec expected' 1.0e-15
v' `shouldNotSatisfy` closePrec v 1.0e-9
Context.collectGarbage
describe "FuturesConvAdjustmentQuote" $
it "futuresValue reports the futures quote's own value, independent of volatility/meanReversion" $ do
today <- Context.evaluationDate
cal <- calendar TARGET
idx <- iborIndex Euribor1Y Nothing
futuresDate <- advance cal today (1, Years) Following False
futuresQuote <- simpleQuote 95.0 >>= asQuote
volQuote <- simpleQuote 0.01 >>= asQuote
meanRevQuote <- simpleQuote 0.03 >>= asQuote
q <- futuresConvAdjustmentQuote idx futuresDate futuresQuote volQuote meanRevQuote
fv <- futuresValue q
fv `shouldSatisfy` closePrec 95.0 1.0e-12