Calibrate an arbitrage-checked implied volatility surface across multiple expiries, by fitting a raw SVI (Stochastic Volatility Inspired, Gatheral 2004) curve to the market-implied smile (see get_implied_volatility) at each expiry, and checking the fitted surface for calendar-spread arbitrage.

A single per-expiry smile only tells you the shape of the market’s volatility skew at that one maturity. Stitching several calibrated SVI slices together instead gives a full surface, which is what is needed to price/interpolate options at maturities or strikes that don’t trade directly, and to check for term-structure inconsistencies (see Notes).

Before fitting, strikes whose market-implied volatility is a statistical outlier relative to its neighbors (a common symptom of a stale or wide-bid/ask illiquid quote) are dropped via a median-absolute-deviation filter, since a single bad quote can otherwise dominate the least-squares SVI fit for that whole expiry. Each fit keeps the total variance non-negative through Gatheral’s condition a + b * sigma * sqrt(1 - rho^2) >= 0.

The option chains and the days to expiration are live, while the stock price is the close of the last historical date; a warning is logged when that date lies more than five days before today.

See: Gatheral, J. (2004), “A parsimonious arbitrage-free implied volatility parameterization with application to the valuation of volatility derivatives”, and Gatheral, J., & Jacquier, A. (2014), “Arbitrage-free SVI volatility surfaces”, Quantitative Finance, 14(1), 59-71.

Also known as: SVI surface, implied volatility surface.

Notes: A warning is logged (not raised) if the fitted surface has any calendar-spread arbitrage violations, i.e. total implied variance decreasing with time to expiration at some log-moneyness – this reflects genuine inconsistency in the underlying market quotes across expiries, not a fitting error, and is only checked, not corrected.

Volatility Surface in Python or via the MCP

Install the Finance Toolkit with:

pip install financetoolkit -U

Then call get_volatility_surface as shown below.

from financetoolkit import Toolkit

toolkit = Toolkit(["JPM"], api_key="FINANCIAL_MODELING_PREP_KEY")

volatility_surface = toolkit.options.get_volatility_surface(number_of_expirations=4)

volatility_surface.loc["JPM"]

Or, after setting up the Finance Toolkit MCP, ask:

What does the implied volatility surface of JPMorgan Chase's call options look like across strikes for its next four expirations?

Which returns:

Strike Price 2026-10-30 2026-11-06 2026-11-13 2026-11-20
155 nan nan nan 1.3993
160 nan nan nan 1.3572
165 nan nan nan 1.3159
170 nan nan nan 1.2756
180 nan nan nan 1.1971
185 nan nan nan 1.159
190 nan nan nan 1.1216
195 nan nan nan 1.0848
200 nan nan nan 1.0487
205 nan nan nan 1.0132
210 nan nan nan 0.9782
215 nan nan nan 0.9438
220 nan nan nan 0.91
230 nan nan nan 0.8439
235 nan nan nan 0.8117
240 nan nan nan 0.7799
245 nan nan nan 0.7487
250 nan nan nan 0.7179
255 nan nan nan 0.6876
260 nan nan nan 0.6578
265 nan nan nan 0.6286
270 nan nan nan 0.5998
280 nan 0.7398 nan 0.5438
285 nan nan nan 0.5166
290 nan nan nan 0.49
295 nan nan nan 0.464
300 0.4147 nan 0.33 0.4387
305 0.3937 0.5749 nan 0.4142
310 0.3722 0.5386 nan 0.3904
312.5 0.3613 nan nan nan
315 0.3502 0.5006 nan 0.3675
320 0.3276 0.4609 0.2749 0.3456
322.5 0.3161 nan nan nan
325 0.3045 0.4193 0.262 0.3249
327.5 0.2929 nan nan nan
330 0.2812 0.3755 0.25 0.3054
332.5 0.2698 nan nan nan
335 0.2586 0.3298 0.2392 0.2875
337.5 0.2479 nan nan nan
340 0.2382 0.2836 0.2303 0.2712
342.5 0.2299 nan nan nan
345 0.224 0.2408 0.2239 0.257
347.5 0.2215 nan nan nan
350 0.2238 0.2104 0.2211 0.245
352.5 0.2321 nan nan nan
355 0.2469 0.2032 0.2229 0.2355
357.5 0.2679 nan nan nan
360 0.2936 0.22 0.2303 0.2288
365 0.3515 0.2504 0.2439 0.2251
370 0.4099 0.2853 0.264 0.2244
375 0.4647 0.3202 0.2899 0.2267
380 0.5153 0.3537 0.3206 0.2318
385 0.5619 0.3852 0.3546 0.2396
390 0.6051 0.4149 nan 0.2497
395 0.6452 nan nan 0.2618
400 0.6829 nan nan 0.2756
405 0.7183 nan nan 0.2908
410 0.7517 0.5176 nan 0.3072
415 0.7835 nan nan 0.3246
420 nan nan nan 0.3428
425 0.8425 nan nan 0.3616
430 nan nan nan 0.3809
435 nan nan nan 0.4007
440 nan nan nan 0.4208
445 nan nan nan 0.4412
450 nan nan nan 0.4618
455 nan nan nan 0.4826
460 nan nan nan 0.5035
465 nan nan nan 0.5246
470 nan nan nan 0.5457

Parameters

get_volatility_surface accepts the following parameters.

ParameterTypeDescriptionExample
expiration_dateslist[str] | NoneThe expiration dates to fit the surface over. Defaults to None, meaning the first number_of_expirations available expiration dates.["2027-12-17", "2028-01-21"]
put_optionboolWhether to use put options instead of call options. Defaults to False.True
risk_free_ratefloatThe risk free rate to use for the calculation. Defaults to None, which uses the Toolkit’s risk-free rate on the pricing date: the 10-year Treasury yield unless the Toolkit’s risk_free_rate parameter selects ‘13w’, ‘5y’ or ‘30y’, converted from its semi-annual bond-equivalent quote to continuous compounding. A rate passed here is used as given, as a continuously compounded rate.0.04
dividend_yieldfloatThe dividend yield to use for the calculation. Defaults to None, which uses the dividend yield on the pricing date (the dividends of the 365 days up to it relative to its close, as the continuously compounded ln(1 + q)). A given value is used as a continuous yield.0.02
number_of_expirationsintThe number of near-term expiration dates to fit when expiration_dates is not given. Defaults to 6.4
outlier_thresholdfloatThe number of median absolute deviations from the median implied volatility beyond which a quote is treated as an outlier and dropped before fitting. Defaults to 5.0.3.0
roundingint | NoneThe number of decimals to round the results to. Defaults to None.2
Share