lballabio / QuantLib

The QuantLib C++ library
http://quantlib.org
Other
5.38k stars 1.79k forks source link

Strange impact of day counter on option valuation #47

Closed akasolace closed 8 years ago

akasolace commented 8 years ago

When running the unit test for Stulz engine, I noticed that option value was different if I was using ACT360 instead of ACT365fixed despite the fact that option maturity is expressed in years. Here below is an example with Quantlib-Python based on the unit test.

Thank you in advance for your help

import QuantLib as ql
calculation_date = ql.Date.todaysDate()
calendar = ql.Germany()

#/                                basketType,   optionType,    strike, s1,    s2,   q1,   q2,    r,    t,   v1,   v2,  rho, result, tol
#ew BasketOptionTwoData(BasketType.MinBasket,  Option.Type.Put,  100.0, 100.0, 100.0, 0.00, 0.00, 0.05, 0.50, 0.30, 0.30, 0.10, 11.893, 1.0e-3),
dc = ql.Actual360()
t = 0.5
r = 0.05

K = 100

S1 = 100
q1 = 0
v1 = 0.3

S2 = 100
q2 = 0
v2 = 0.3

rho = 0.1

expiryDate = calculation_date + int(360 * t + 0.5)

exercise = ql.EuropeanExercise(expiryDate)

r_curve = ql.YieldTermStructureHandle(ql.FlatForward(calculation_date, r, dc))
q1_curve = ql.YieldTermStructureHandle(ql.FlatForward(calculation_date, q1, dc))
v1_curve = ql.BlackVolTermStructureHandle(ql.BlackConstantVol(calculation_date, calendar, v1, dc))
q2_curve = ql.YieldTermStructureHandle(ql.FlatForward(calculation_date, q2, dc))
v2_curve = ql.BlackVolTermStructureHandle(ql.BlackConstantVol(calculation_date, calendar, v2, dc))

p1 = ql.BlackScholesMertonProcess(ql.QuoteHandle(ql.SimpleQuote(S1)), q1_curve, r_curve, v1_curve)
p2 = ql.BlackScholesMertonProcess(ql.QuoteHandle(ql.SimpleQuote(S2)), q1_curve, r_curve, v2_curve)

analyticEngine = ql.StulzEngine(p1, p2, rho);

payoff = ql.PlainVanillaPayoff(ql.Option.Put, K)
basketoption = ql.BasketOption(ql.MinBasketPayoff(payoff), exercise)
basketoption.setPricingEngine(analyticEngine)
result = basketoption.NPV()

print(result)

# returns 11.892 with dc = ql.Actual360()    and  expiryDate = calculation_date + int(360 * t + 0.5)      (pass unit test)
# returns 11.905 with dc = ql.Actual365Fixed()    and  expiryDate = calculation_date + int(365 * t + 0.5)   (fails unit test)
lballabio commented 8 years ago

Unfortunately, the actual/365 day counter can't give you t=0.5. With act/360 and expiryDate = calculation_date + int(360 * t + 0.5), you're adding 180 days and getting t = 180/360 = 0.5. With act/365, you're adding 183 days — because that's what int(365 * t + 0.5) gives you — and getting back t=183/365 = 0.50137 in the formula.

Long story short, if you want to reproduce textbook examples, I suggest you stick with act/360 (which, I suspect, was introduced precisely because 360 is exactly divisible by 2, 3, 4, 6, 12...)

akasolace commented 8 years ago

thank you for the answer.