Fixing Unsorted and Duplicate Expiries in a QuantLib Volatility Surface
Summary
The document diagnoses a QuantLib error raised while building a Black variance surface from option implied volatilities. The reported validation requires expiry dates to be unique and arranged in chronological order. In the supplied schedule, one-year expiry appears twice, and the eighteen-month expiry is listed after the one-year expiry, violating both requirements.
The proposed fix is to correct the mistaken maturity and sort all expiry dates before passing them to the surface constructor. The example concerns a volatility matrix indexed by expiry and strike, with the date issue preventing construction. It does not assess the volatility quotes, calibration quality, or the resulting surface; its guidance is limited to the input-date ordering and uniqueness error.
Key ideas
- QuantLib's Black variance surface requires expiry dates to be unique.
- Expiry dates must be sorted in chronological order before surface construction.
- The example schedule repeats a one-year expiry and places eighteen months after it.
- Correcting the maturity schedule addresses the reported date validation error.
Tags
Full text
# Heston calibration using Quantlib and Python: failure in BlackVarianceSurface function
# Heston calibration using Quantlib and Python: failure in BlackVarianceSurface function
I have an error when trying to use the fucntion BlackVarianceSurface from quantlib.
Can you help me?
the error is RunTime Error: dates must be sorted unique.
```
def IndexVolatilities2(calculation_date):
spot = 2184.56
strikePerc = [0.8,0.9,0.95,0.975,1,1.025,1.05,1.10,1.2]
risk_free_rate = 0.0213
dividend_rate = 0.006
#vol is a sample matrix of volatility quote by expiry and strike
#Following is a sample matrix of volatility quote by exipiry and strike.
#The volatilities are log-normal volatilities and can be interpolated to construct the implied volatility surface.
p1m= calculation_date + ql.Period(1, ql.Months)
p2m= calculation_date+ ql.Period(2, ql.Months)
p3m= calculation_date+ ql.Period(3, ql.Months)
p6m= calculation_date+ ql.Period(6, ql.Months)
p9m= calculation_date+ ql.Period(9, ql.Months)
p18m= calculation_date+ ql.Period(18, ql.Months)
p1Y = calculation_date + ql.Period(1, ql.Years)
p3Y = calculation_date + ql.Period(3, ql.Years)
p4Y = calculation_date + ql.Period(4, ql.Years)
p5Y = calculation_date + ql.Period(5, ql.Years)
p7Y = calculation_date + ql.Period(7, ql.Years)
p10Y = calculation_date + ql.Period(10, ql.Years)
expiration_dates = [p1m, p2m, p3m, p6m, p9m,p18m,p1Y,p3Y,p4Y,p5Y,p7Y,p10Y]
strikes = [p*spot for p in strikePerc]
data =[
[27.18000, 22.31000, 17.31000, 14.81000, 11.84000, 8.97000, 10.14000, 14.59000, 19.44000],
[26.14000, 20.64000, 16.78000, 15.05000, 12.81000, 10.48000, 9.91000, 13.03000, 17.77000],
[25.28000, 19.84000, 16.73000, 15.25000, 13.35000, 11.36000, 10.36000, 12.26000, 16.66000],
[22.48000, 18.76000, 16.62000, 15.45000, 14.19000, 12.92000, 11.85000, 11.09000, 12.06000],
[20.68000, 18.55000, 16.84000, 15.82000, 14.73000, 13.65000, 12.71000, 11.58000, 12.06000],
[20.26000, 18.43000, 16.93000, 16.05000, 15.13000, 14.23000, 13.41000, 12.24000, 12.18000],
[20.25000, 18.20000, 16.90000, 16.21000, 15.51000, 14.82000, 14.19000, 13.18000, 12.47000],
[20.03000, 18.02000, 16.84000, 16.23000, 15.63000, 15.05000, 14.50000, 13.61000, 12.78000],
[19.81000, 17.93000, 16.93000, 16.43000, 15.95000, 15.48000, 15.05000, 14.32000, 13.44000],
[19.74000, 17.99000, 17.12000, 16.69000, 16.28000, 15.89000, 15.53000, 14.90000, 14.08000],
[19.75000, 18.14000, 17.36000, 16.99000, 16.63000, 16.30000, 15.98000, 15.43000, 14.66000],
[19.98000, 18.59000, 17.95000, 17.65000, 17.36000, 17.10000, 16.84000, 16.40000, 15.74000],
[20.60000, 19.46000, 18.96000, 18.73000, 18.51000, 18.30000, 18.11000, 17.76000, 17.22000]]
return expiration_dates,strikes,data,spot, risk_free_rate,dividend_rate
calendar = ql.UnitedStates()
day_count = ql.Actual365Fixed()
calculation_date = ql.Date(25, 9, 2019)
#expiration_dates,strikes,data,spot, risk_free_rate,dividend_rate = IndexVolatilities1()
expiration_dates,strikes,data,spot, risk_free_rate,dividend_rate = IndexVolatilities2(calculation_date)
#build the vol surface (implied vols surface) and dividend and the interest rate curve
hestonParams= HestonParameters(calculation_date, calendar, day_count, spot, risk_free_rate, dividend_rate, len(expiration_dates), strikes, data)
dividend_ts, flat_ts, implied_vols = hestonParams.dividend_ts, hestonParams.flat_ts, hestonParams.implied_vols
# Now the Black volatility surface can be constructed using the BlackVarianceSurface method.
black_var_surface = ql.BlackVarianceSurface(calculation_date, calendar, expiration_dates, strikes,implied_vols, day_count)
```
## Answer by Luigi Ballabio (score 2)
https://quant.stackexchange.com/a/49303
"Dates must be sorted unique". "Unique", so you can't have repeated dates in the inputs you're passing. You have two copies of 1Y. You probably wanted the second to be 2Y.
"Sorted", so you can't have 18M before 1Y. They have to be in the correct order in time.Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.