Volatility Surface
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.
| Parameter | Type | Description | Example |
|---|---|---|---|
expiration_dates | list[str] | None | The 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_option | bool | Whether to use put options instead of call options. Defaults to False. | True |
risk_free_rate | float | The 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_yield | float | The 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_expirations | int | The number of near-term expiration dates to fit when expiration_dates is not given. Defaults to 6. | 4 |
outlier_threshold | float | The 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 |
rounding | int | None | The number of decimals to round the results to. Defaults to None. | 2 |