Skip to content

Repository files navigation

SPY Options Pricing & Implied Volatility Smirk Model

A python tool which prices SPY options using the Black-Scholes model. Using this it calculates implied violatilty using live data and plots the volatiltiy smirk against historical volatiltiy.

Overview

Built as a self-directed project to develop quantitative and data-analysis skills. An end-to-end options-pricing model running against live market data, from data sampling through to a full volatility curve.

Sample output

Black-Scholes price against live data

Volatility smirk example

Example render — the market-status annotation, spot price, and curve shape will differ each time the notebook is run, since the data is pulled live rather than from a fixed dataset.

What it does

  • Historical volatility — downloads SPY daily prices (2020–present) via yfinance and computes a 30-day rolling annualised historical volatility from daily returns.
  • Market-aware spot pricing — detects whether US markets are currently open if not then the most recent live data is taken.
  • Live options chain — pulls the SPY options chain nearest to a 30-day expiry via yfinance and the "market price" is created by averaging the bid/ask price for each call and put.
  • Black-Scholes pricing — computes theoretical call/put prices from the Black-Scholes formula, using the 30-day historical volatility as the volatility input.
  • Implied volatility solver — inverts the Black-Scholes formula with Brent's method to calculate each contract's implied volatility from its market price.
  • Liquidity & moneyness filtering — restricts to OTM contracts only (calls above spot, puts below spot) with positive ask and open interest, avoiding the unstable, near-zero-vega solves that deep-ITM contracts produce.
  • Volatility smirk plot — combines the filtered puts and calls into one continuous series and plots implied volatility against strike, with historical volatility as a reference line.

Black-Scholes Model Assumptions

These are the assumptions that the Black-Scholes Model uses:

  • The short term interest is known and is constant through time
  • The stock follows a 'random walk' in continuous time with a variance rate proportional to the square of the stock price. So the distribution of possible stock prices at the end of any finite interval is log-normal and the variance rate of the return on the stock is constant.
  • The stock pays no dividends or other distributions
  • The option is "European" and can only be exercised at maturity.
  • There are no transaction costs when buying or selling the stock or the option
  • It is possible to borrow any fraction of the price of a security to buy it or to hold it, at the short-term interest rate
  • There are no penalties to short selling. A seller who does not own a security will simply accept the price of the security from a buyer, and will agree to settle with the buyer on some future date by paying him an amount equal to the price of the security on that date.

Modeling assumptions & limitations

Beyond the Black-Scholes assumptions I made the following choices when building this model

  • Volatility input, Risk-free rate and time to expiry

I use a fixed r = 0.05 instead of pulling a live risk-free rate from Treasury yields or SOFR. I did this as the true risk-free rate fluctuates and because r appears in the discounting term of the pricing formula. However using a stale or approximate rate introduces a small bias into the implied volatility I later solve for. I also fix T = 30/365 for every option in the chain, so that each contract does not have different times remaining until they expire, and I select the expiry date closest to 30 days out so T is an approximation of the time to expiry for the contract I'm using. For the initial Black-Scholes pricing, I use a single 30-day trailing historical volatility, applied across every strike. This treats historical volatility as a replacement for the market implied volatility, which I use as an assumption for the rest of the notebook until implied volatility is calculated.

  • No dividend adjustment and "American style" stock

I didn't include a dividend yield term, creating the assumption that SPY pays no dividends, even though it actually does. This is the most impactful simplification, as it skews the implied volatilities I solve for. I also price these as European-style, despite SPY options being American-style options. For calls this would not affect the result much, except that early exercise can become optimal just before an ex-dividend date. For puts the result may differ, as the incentive to exercise early comes from capturing interest on the strike price rather than from dividends. This incentive is present more persistently rather than spiking around a specific date. So the put side of my combined implied volatility curve will likely carry more approximation error than the call side.

  • OTM filtering for implied volatility

When solving for implied volatility, I only used Out-Of-The-Money options, combining OTM puts and OTM calls into a single continuous series. I made this choice because deep In-The-Money options have near-zero Vega so the Implied Volatility solver would not be able to find a unique value for the corresponding strike price. As well as this, the moneyness cutoff sits exactly at the spot price, rather than using a wider band around it.

  • Data and price inputs

I use a single spot price snapshot, taken as the most recent close from either a 2-day or 5-day price history depending on whether markets are open, this is not synchronised to the exact timestamp of each option quote, so there is a small timing inaccuracy between S and the bid/ask I compare it against. I also use the mid-price = (bid + ask) / 2, as a substitute for the market price, which assumes the midpoint fairly represents value even though spread width varies across strikes. For liquidity, I only filter on a nonzero ask price and nonzero open interest; I did not filter on trading volume or spread width so some options passing this filter may still be thinly traded.

  • Implied volatility solver bounds

I bound Brent's method to search for a solution between 0% and 500% volatility. Any option whose true implied volatility falls outside this range fails to converge and is recorded as NaN, then discarded from the analysis.

Technical challenges

Built iteratively over several weeks, the process surfaced a handful of genuine data-quality problems worth documenting.

Discontinuous IV curve. Plotting OTM puts and OTM calls as two separate series left a visible gap at the money. Fixed by combining both into a single DataFrame with pd.concat, sorting by strike, and plotting as one continuous line.

Flat / stale IV readings. Some contracts returned constant or clearly wrong implied vols, traced to stale quotes. Fixed with liquidity gates — ask > 0 and openInterest > 0 — applied before solving.

Staircase artefacts in the curve. Including deep in-the-money contracts produced jagged, non-smooth jumps in implied vol as deep-ITM options have near-zero vega. Fixed by restricting to OTM only (strike >= spot for calls, strike <= spot for puts).

Solver convergence failures. Brent's method occasionally fails to converge, typically near-zero vega or at the edges of the strike range. it was then labelled NaN, then dropped before plotting, rather than letting one bad contract crash the rest of the code.

Pre-market spot mispricing. Using the last traded price outside market hours could misrepresent the true current spot. Added explicit market-hours detection to choose between the live intraday price and last close.

Possible extensions

Modeling

  • Calibrate a SABR model to the implied volatility smirk, producing a smooth curve that captures market-implied volatility across strike prices.
  • Extend the model from a 2D IV smirk to a full 3D implied volatility surface across both strike price and expiry.
  • Add a local volatility surface, this lets volatility depend on both the current stock price and time rather than have one IV for all options.

Comparison

  • Compare Black-Scholes implied volatility with other models such as binomial trees or stochastic volatility models. Compare how different models value the same option and what implied volatilities they produce.
  • Swap in SPX for a genuinely European-style comparison point (would need a data source beyond yfinance, e.g. CBOE DataShop).

Data

  • Compute the greeks (Delta, Gamma, Vega, Theta and Rho) for each option as well as implied volatility.
  • Pull a live risk-free rate (e.g. 3-month T-bill yield) instead of the current hardcoded 5%.

UI

  • Build an interactive dashboard that allows users to select expires and visualise implied volatility.

Tech stack

  • Python, Jupyter
  • numpy, pandas — data handling
  • scipy.stats.norm, scipy.optimize.brentq — pricing distribution & implied-vol root-finding
  • matplotlib — visualization
  • yfinance — market data (spot price, historical prices, options chains)
  • pytz — market-hours detection

Running it

Running the Project

  1. Clone the repository
git clone https://github.com/AlexY237/SPY-options-IV-model.git
cd SPY-options-IV-model
  1. Install the required packages
pip install -r requirements.txt
  1. Launch Jupyter Notebook
jupyter notebook

or

jupyter lab
  1. Open Black_Scholes_Model.ipynb Then run all cells top to bottom in Jupyter. The options chain and spot price are pulled live, so results reflect whatever SPY options are trading at runtime.

About

Python, Quantitive-Finance, Optio

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages