hasquant-0.7.0.0: test/example/QuantLib/Example/DiscreteHedging.hs
-- |Ported from QuantLib's @Examples/DiscreteHedging/DiscreteHedging.cpp@ (Derman & Kamal,
-- \"When You Cannot Hedge Continuously: The Corrections to Black-Scholes\"): a hedger sells a
-- European option at its Black-Scholes premium, then rehedges at @hedgesNum@ evenly spaced
-- dates using the Black-Scholes delta, and we look at the final profit\/loss of that strategy
-- across many simulated stock paths. Continuous rehedging would zero the P&L on every path;
-- discrete rehedging leaves a replication error whose standard deviation should shrink as the
-- number of hedges grows -- the qualitative point this module checks (upstream's own @main@ only
-- prints the numbers, it has no assertions of its own to port).
--
-- Two deliberate deviations from upstream, both already established elsewhere in this test suite
-- (see CLAUDE.md): upstream's own @PseudoRandom::make_sequence_generator(nTimeSteps, 0)@ seeds
-- from entropy (seed 0 has that special meaning for 'QuantLib.Method.pathGenerator''s underlying
-- Mersenne Twister), so a fixed nonzero seed is used here instead to make the result
-- reproducible; and the sample count is reduced from upstream's 50000 to keep a two-hedging-
-- frequency hspec run fast.
module QuantLib.Example.DiscreteHedging
(
Result(..)
, run
) where
import Control.Monad(replicateM)
import qualified Data.Vector.Storable as V
import QuantLib.Instrument.Option(OptionType(Call))
import QuantLib.Math(RngTrait(PseudoRandom), timeGrid)
import QuantLib.Method(pathGenerator, next, asset)
import QuantLib.Process(blackScholesMertonProcess, ProcessDiscretization(EulerDiscretization))
import QuantLib.PricingEngine(blackCalculator, StrikeSpec(..), value, delta)
import QuantLib.Quote(simpleQuote)
import QuantLib.Context(setEvaluationDate)
import QuantLib.Time.Date(today)
import QuantLib.Time.Schedule(dayCounter, DayCounterConstructor(Actual365FixedStandard), Frequency(Annual))
import QuantLib.InterestRate(Compounding(Continuous))
import QuantLib.TermStructure.Volatility(CalendarReference(..), blackConstantVol)
import QuantLib.TermStructure.Yield(Reference(..), flatForward)
import QuantLib.Time.Calendar(calendar, CalendarConstructor(TARGET))
data Result = Result
{ optionValue :: !Double -- ^Black-Scholes premium at t=0
, plMean21 :: !Double -- ^mean final P&L, 21 rehedging dates
, plStdDev21 :: !Double -- ^std. dev. of final P&L, 21 rehedging dates
, plMean84 :: !Double -- ^mean final P&L, 84 rehedging dates
, plStdDev84 :: !Double -- ^std. dev. of final P&L, 84 rehedging dates
}
mean :: [Double] -> Double
mean xs = sum xs / fromIntegral (length xs)
stdDev :: [Double] -> Double
stdDev xs = sqrt (mean [(x - m) * (x - m) | x <- xs]) where m = mean xs
run :: IO Result
run = do
evalDate <- today
setEvaluationDate (Just evalDate)
dc <- dayCounter Actual365FixedStandard
cal <- calendar TARGET
s0Q <- simpleQuote s0
rQ <- simpleQuote r
divQ <- simpleQuote 0.0
sigmaQ <- simpleQuote sigma
riskFreeTS <- flatForward (ReferenceDate evalDate) rQ dc Continuous Annual
dividendTS <- flatForward (ReferenceDate evalDate) divQ dc Continuous Annual
volTS <- blackConstantVol (CalendarReferenceDate evalDate) cal sigmaQ dc
process <- blackScholesMertonProcess s0Q dividendTS riskFreeTS volTS EulerDiscretization False
black0 <- blackCalculator (Strike Call strike) (s0 * exp (r * maturity)) (sqrt (sigma * sigma * maturity)) (exp (- r * maturity))
optValue <- value black0
(mean21, sd21) <- compute process 21 nSamples
(mean84, sd84) <- compute process 84 nSamples
return Result
{ optionValue = optValue
, plMean21 = mean21, plStdDev21 = sd21
, plMean84 = mean84, plStdDev84 = sd84
}
where
maturity = 1.0 / 12.0 -- 1 month
strike = 100
s0 = 100
sigma = 0.20
r = 0.05
nSamples = 3000 :: Int
seed = 42 :: Word
-- one path's replication P&L, following ReplicationPathPricer::operator() exactly: an
-- initial deal (sell the option, delta-hedge), n-1 rehedges at each interior path point,
-- then unwind against the option's payoff at the final point
plOfPath :: [Double] -> IO Double
plOfPath [] = error "plOfPath: empty path"
plOfPath path@(s00 : _) = do
let n = length path - 1
dt = maturity / fromIntegral n
black00 <- blackCalculator (Strike Call strike) s00 (sqrt (sigma * sigma * maturity)) (exp (- r * maturity))
premium <- value black00
delta0 <- delta black00 s00
(moneyAcct, stockAmount) <- rehedge dt (premium - delta0 * s00) delta0 0.0 (take (n - 1) (drop 1 path))
let finalStock = path !! n
optionPayoff = max (finalStock - strike) 0
return (moneyAcct * exp (r * dt) - optionPayoff + stockAmount * finalStock)
where
rehedge _dt moneyAcct stockAmount _t [] = return (moneyAcct, stockAmount)
rehedge dt moneyAcct stockAmount t (stock : rest) = do
let t' = t + dt
moneyAcct1 = moneyAcct * exp (r * dt)
timeToMaturity = maturity - t'
black <- blackCalculator (Strike Call strike) stock (sqrt (sigma * sigma * timeToMaturity)) (exp (- r * timeToMaturity))
hedgeDelta <- delta black stock
rehedge dt (moneyAcct1 - (hedgeDelta - stockAmount) * stock) hedgeDelta t' rest
compute process' nTimeSteps n = do
tg <- timeGrid maturity nTimeSteps
pg <- pathGenerator PseudoRandom process' tg seed nTimeSteps False
paths <- replicateM n (V.toList <$> (next pg >>= \sp -> asset sp 0))
pls <- mapM plOfPath paths
return (mean pls, stdDev pls)
-- vim: set ft=haskell ff=unix ts=8 sts=2 sw=2 et: