← PortfolioTayte Choudhury
Quantitative research / Python · NumPy · SciPy

Arbitrage-Free Volatility Surface

I built a pipeline that turns option quotes into volatility smiles, with explicit checks at each step. Here is how I clean the inputs, recover the forwards, and fit the surface.

18Calibrated synthetic expiry slices
2,000Points per arbitrage-check grid
0.002Largest slice RMSE in volatility points
Dataset: synthetic validation

I use generated option quotes below to validate the implementation. These figures are not historical SPY results. The same pipeline accepts a synchronized market snapshot in the documented CSV format.

Calibrated SVI surface on a labeled synthetic dataset

Explore an expiry

What I built

I built a pipeline that takes an options snapshot and turns it into a set of calibrated volatility smiles. The surface plot is the visible result, but most of the work is in the steps before it: deciding which quotes to use, estimating the forward, solving for implied volatility, and checking whether the fitted slices are consistent with one another.

I used Python, NumPy, pandas, and SciPy so I could inspect the intermediate values at each step. The script writes a report with the filter counts, forwards, parameter estimates, fitting errors, and arbitrage diagnostics. That makes it possible to trace a strange-looking part of the surface back to the inputs or the fit.

The data behind this version

I am showing a generated validation snapshot here: 4,320 call and put quotes across 18 expiries, with 120 strikes per expiry and an underlying spot of 500. Maturities run from 0.05 to 1.50 years. I use this dataset to check that the numerical pipeline recovers a known construction. It is not the historical SPY dataset described in my resume, and the small fitting errors below should be read in that context.

I require one timestamp and one spot value for a snapshot, reject duplicate contracts, and check the numeric inputs before fitting anything. Mixing quotes from different moments would make the put-call comparisons difficult to interpret even if the optimizer still returned a smooth curve.

How I clean the quotes

I apply the screens in sequence and record how many rows each one removes. For this example, 50 quotes have zero bids and another 15 fail the relative-spread screen. That leaves 4,255 quotes after cleaning. The other screens remove zero rows in this particular snapshot; I keep those counts in the report rather than implying that every filter did something.

Sequential cleaning counts for the synthetic validation snapshot.
ScreenQuotes removedRule
Zero bids50Bid must be positive
Crossed or locked quotes0Ask must exceed bid
Wide relative spread15(ask − bid) / midpoint ≤ 30%
Short maturity0At least 7 days
Sparse expiry0At least 6 surviving strikes

Why I estimate a forward for each expiry

I pair calls and puts at the same strike and maturity, then regress their midpoint-price difference on strike. In the relation C − P = D(F − K), the slope gives me −D and the intercept gives me DF. From those two numbers I recover a discount factor and a forward for each slice.

In this validation run, the recovered forward rises from about 500.625 to 519.106, while the discount factor falls from about 0.9980 to 0.9418. I use those forwards in Black-76 and express the smile in log-forward-moneyness, log(K/F), so I am not treating the spot price as the forward for every maturity.

I then keep out-of-the-money calls and puts and solve for the volatility that reproduces each midpoint price. I use Brent’s method with a 1e-6 solver tolerance and a bracket from 1e-6 to 5 in volatility units. Prices outside the invertible bounds are counted and discarded. This run fits 2,095 OTM quotes and has zero inversion failures after selection.

How I constrain the SVI fit

Each slice has five parameters: a, b, ρ, m, and σ. They control the level, slope, skew, location, and curvature of total implied variance. I fit total variance rather than volatility directly and use SLSQP to keep the candidate parameters inside the constraints.

I check positive total variance, the wing-slope bounds, the Durrleman butterfly-density condition, and calendar monotonicity relative to the previous expiry. The density and calendar checks use 2,000 points from log-moneyness −1 to +1. The lowest checked density in this run is 0.3979, and the smallest adjacent-slice total-variance gap is 0.002931; both remain positive on that grid.

That is a useful numerical check, but it is still a finite grid. I am not claiming a proof that every possible strike between or beyond those points is arbitrage-free. I also have not added an American-exercise adjustment, so applying the same pricing assumptions to an American-style chain would require more care.

The full calibration results

I included every expiry below so the quality of the fit is visible across the surface. “Vol points” means percentage points of volatility: a move from 20% to 21% is one vol point. The largest slice RMSE is about 0.001682 vol points in this synthetic test.

All 18 calibrated slices. Each row comes directly from the downloadable calibration report.
Expiry (years)ForwardOTM quotesRMSE (vol points)Minimum density
0.050500.625630.0010180.4081
0.135501.6941120.0016820.4101
0.221502.7651200.0012680.4108
0.306503.8381200.0003390.4111
0.391504.9141200.0002930.4103
0.476505.9911200.0002290.4094
0.562507.0721200.0001680.4085
0.647508.1541200.0001510.4076
0.732509.2391200.0001590.4066
0.818510.3261200.0001440.4057
0.903511.4151200.0001300.4047
0.988512.5071200.0001100.4038
1.074513.6011200.0001080.4028
1.159514.6971200.0001000.4018
1.244515.7961200.0000930.4009
1.329516.8971200.0000880.3999
1.415518.0001200.0000830.3989
1.500519.1061200.0000770.3979

What this validates, and what comes next

I checked put-call parity, recovered known volatilities through inversion, tested rejection of invalid prices and mixed snapshots, and compared the analytical density expression with numerical derivatives. Those checks support the implementation. They do not measure how well it handles a real market with stale quotes, dividends, exercise effects, and inconsistent prices.

My next step would be to run the same reporting pipeline on a properly timestamped market snapshot and inspect the residuals expiry by expiry. I would also widen the arbitrage checks and test sensitivity to the liquidity thresholds. The point of keeping the intermediate data is that I can change those assumptions and see exactly what moved.

Synthetic option implied volatilities and fitted SVI slices
View source on GitHubCalibration reportSlice diagnostics CSVSVI methodology reference