Skip to content

About

Black-Scholes greeks, Cox-Ross-Rubinstein and Leisen-Reimer trees, and an implied volatility solver that holds up across the wings of a live option chain

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Latest commit

 

History

6 Commits

Folders and files

Repository files navigation

Option pricer

European and American option pricing: Black-Scholes-Merton with closed-form greeks, a Cox-Ross-Rubinstein tree, a Leisen-Reimer tree, and an implied volatility solver that holds up across a whole option chain including the wings. Everything is checked against no-arbitrage relations and finite differences rather than against itself.

What is in it

Module Contents
options/analytic.py BSM price and the five greeks in closed form, vectorised over strike and volatility, with a continuous dividend yield.
options/binomial.py CRR tree for European and American exercise, Leisen-Reimer tree, early exercise premium.
options/implied.py Implied volatility by Newton-Raphson with a Brent fallback, no-arbitrage screening, smile construction.

Trees: why Leisen-Reimer

A CRR tree converges to Black-Scholes as O(1/n), but not monotonically. The price oscillates as steps are added, because whether a terminal node happens to land on the strike keeps changing. That means you cannot tell how accurate a CRR price is by adding a few steps and watching it settle, because it does not settle.

The Leisen-Reimer tree fixes this by constructing the up and down moves from d1 and d2 through the Peizer-Pratt inversion, so the strike always sits exactly on a terminal node. Same market, python examples/convergence.py:

 steps           CRR       error   Leisen-Reimer       error
------------------------------------------------------------
    11      8.558554    3.18e-02        8.523182    3.56e-03
    25      8.435421    9.13e-02        8.526001    7.40e-04
    51      8.537393    1.07e-02        8.526557    1.83e-04
   101      8.550042    2.33e-02        8.526693    4.74e-05
   201      8.524217    2.52e-03        8.526728    1.21e-05
   401      8.532518    5.78e-03        8.526737    3.04e-06
   801      8.528294    1.55e-03        8.526740    7.64e-07
  1601      8.528101    1.36e-03        8.526740    1.91e-07

The CRR column jumps around. The Leisen-Reimer column divides its error by 4 every time the step count doubles, which is O(1/n²) exactly as advertised. Leisen-Reimer with 101 steps is about 16 times more accurate than CRR with 1010 in this example (error 4.7e-05 against 7.5e-04), and a test asserts that it is more accurate.

Convergence and greeks

Implied volatility

Newton-Raphson on vega is fast near the money and unreliable away from it, because vega collapses towards zero in the wings and the step diff / vega blows up. The solver runs Newton, and switches to Brent on a bracketed interval whenever Newton leaves the sensible range or vega gets too small to divide by. Quotes below intrinsic value or above the no-arbitrage ceiling are returned as NaN rather than raising, so an entire chain can be inverted in one pass and the unusable strikes filtered out afterwards.

python examples/build_smile.py SPY pulls a live chain and inverts it:

SPY volatility smile

SPY  spot 761.69
1830 usable out-of-the-money quotes across 12 expiries

(one line per expiry: days to expiry, number of strikes, at-the-money vol)

Two choices in there matter more than the code:

Out-of-the-money options only, puts below the forward and calls above. That is what the market quotes, and it is what makes the picture readable: an in-the-money option has almost no vega, so a one-tick error in its price moves the implied volatility by several points. Inverting the full chain produces a smile that jumps between 20% and 50% for no economic reason. Restricting to OTM removes that entirely.

Bid-ask mids where a two-sided market exists, last trades otherwise. Yahoo's free feed returns empty bid and ask fields outside US trading hours, so a mid-only script simply does not run for most of the day. The fallback is capped at four days of staleness and the script prints how many quotes came from each source, because a smile built from stale last trades is worth less than one built from live mids and you should be able to see which you are looking at.

The shape is the usual equity skew: volatility falls as the strike rises, steeply at two days and flattening out by forty. That skew is also the reason a flat-volatility model misprices an autocall, since the capital barrier sits exactly in the expensive left-hand part of the surface.

Tests

python -m pytest tests/ -q
32 passed

Nothing is tested against a stored number produced by this code. Each test pins the implementation to something true independently of it:

  • put-call parity holds to 1e-10 across the chain
  • delta, gamma and vega match central finite differences of the price
  • calls and puts share gamma and vega, and their deltas differ by exp(-qT)
  • price is strictly increasing in volatility
  • a deep in-the-money call equals its discounted forward value
  • zero volatility gives intrinsic value
  • CRR converges to Black-Scholes; Leisen-Reimer converges quadratically and beats a ten times larger CRR tree
  • an American call on a non-dividend-paying stock equals the European one, and an American put is strictly worth more
  • a tree whose time step is too large for its volatility raises instead of returning a price built on a probability outside [0, 1]
  • implied volatility round-trips to 1e-6 across strikes and both option types, including a wing where the option is worth less than a cent
  • quotes below intrinsic or above the ceiling return NaN

The gamma test uses a step of 1e-3 rather than 1e-5, because the second difference divides by h² and a smaller step is destroyed by cancellation in double precision. That is a property of floating point, not of the model.

Limitations

One flat volatility per option. This prices vanillas correctly by construction, since implied volatility is just a change of variable on the price, but it gives no view on how the surface moves. There is no local volatility or stochastic volatility model here, so nothing in this repo can tell you what a barrier or an autocall is worth once skew matters. That is what the structured products pricer is for, and its own limitations section says the same thing from the other side.

Dividends are a continuous yield. That is right for an index and approximate for a single stock over a maturity that spans a discrete dividend.

Running it

pip install -r requirements.txt
python examples/convergence.py
python examples/build_smile.py SPY
python -m pytest tests/ -q

Licence

MIT.

About

Black-Scholes greeks, Cox-Ross-Rubinstein and Leisen-Reimer trees, and an implied volatility solver that holds up across the wings of a live option chain

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages