Most of what you know about option pricing depends on a model. Black-Scholes needs lognormal returns. Heston needs its own dynamics. Every one of them can be wrong, and when they are, the prices they produce are wrong with them.
Put-call parity is different. It holds without assuming anything about how the stock behaves — no distribution, no volatility, no dynamics at all. It’s true if returns are lognormal, true if they jump, true if the stock is manipulated by a cartel. That model-independence is what makes it the single most useful check you can run on a pricer, and why it’s usually the first test in any validation suite worth the name.
The relationship
For European options on a non-dividend-paying stock, with the same strike
and maturity
:
![]()
A call minus a put equals the stock minus the discounted strike. That’s it.
Why it’s true, with no model in sight
The argument takes about thirty seconds and uses nothing but arithmetic.
Build two portfolios today:
- Portfolio A: buy one call, sell one put.
- Portfolio B: buy one share, borrow
(so you’ll owe exactly
at
).
Now look at what each is worth at expiry, splitting on where the stock lands.
If
: your call is worth
, the put you sold expires worthless. Portfolio A pays
. Portfolio B: you hold a share worth
and repay
. Also
.
If
: your call expires worthless, the put you sold is exercised against you for
. Portfolio A pays
(a negative number). Portfolio B: share worth
, repay
. Again
. If
: both pay zero. The two portfolios have identical payoffs in every state of the world. So they must cost the same today, or someone buys the cheap one, sells the dear one, and books a risk-free profit. Set the costs equal and you have the relationship.
Notice what never entered that argument: the stock’s volatility, its drift, the shape of its distribution, whether it jumps. We only needed the payoffs to match state by state. That’s why parity survives when models don’t.
Check it against Black-Scholes
Since Black-Scholes is arbitrage-free by construction, parity has to hold in it exactly — not approximately.
import numpy as np
from scipy.stats import norm
S0, K, r, sigma, T = 100.0, 100.0, 0.05, 0.20, 1.0
def bs_call(S, K, r, s, T):
d1 = (np.log(S/K) + (r + 0.5*s**2)*T) / (s*np.sqrt(T))
d2 = d1 - s*np.sqrt(T)
return S*norm.cdf(d1) - K*np.exp(-r*T)*norm.cdf(d2)
def bs_put(S, K, r, s, T):
d1 = (np.log(S/K) + (r + 0.5*s**2)*T) / (s*np.sqrt(T))
d2 = d1 - s*np.sqrt(T)
return K*np.exp(-r*T)*norm.cdf(-d2) - S*norm.cdf(-d1)
C, P = bs_call(S0,K,r,sigma,T), bs_put(S0,K,r,sigma,T)
print(f"C - P = {C - P:.6f}")
print(f"S0 - K*e^-rT = {S0 - K*np.exp(-r*T):.6f}")
print(f"residual = {(C - P) - (S0 - K*np.exp(-r*T)):.2e}")
C - P = 4.877058
S0 - K*e^-rT = 4.877058
residual = 0.00e+00
Zero to machine precision. If your Black-Scholes implementation doesn’t do this, you have a bug — and you’ve just found it without needing to know what the right answer was.
The part that makes it a validation tool
Here’s why this matters more than it looks. Suppose you write a Monte Carlo pricer and it returns a put price of 5.88. Is that right?
You have no idea. It’s not obviously absurd. It’s the right order of magnitude. Without a benchmark you’re just hoping.
Now watch what parity does to a pricer with a realistic bug — forgetting to discount one leg:
np.random.seed(42)
n = 1_000_000
Z = np.random.standard_normal(n)
ST = S0 * np.exp((r - 0.5*sigma**2)*T + sigma*np.sqrt(T)*Z)
call_price = np.exp(-r*T) * np.maximum(ST - K, 0).mean()
put_price = np.maximum(K - ST, 0).mean() # discount missing
rhs = S0 - K*np.exp(-r*T)
print(f"call = {call_price:.4f}")
print(f"put = {put_price:.4f}")
print(f"parity residual = {call_price - put_price - rhs:+.4f}")
call = 10.4342
put = 5.8751
parity residual = -0.3180
The buggy put comes out at 5.88 against a true value of 5.59. Five per cent off. Nobody eyeballing that number catches it — it looks perfectly reasonable. Parity catches it immediately, and it does so without knowing the correct answer. That’s the property that makes it valuable: it’s a consistency check, not a comparison against a benchmark you might not have.
For exotics, where closed forms don’t exist, this is often the only check available.
Use the same paths
One implementation detail that trips people up. Run parity on a Monte Carlo pricer using different random draws for the call and the put and the residual will be dominated by simulation noise — you’ll be measuring your own sampling error rather than testing anything.
Price both legs on the same paths and most of the noise cancels, because both payoffs are functions of the same terminal prices. In the run above the residual came out at about
against a Monte Carlo standard error of around
— small, consistent with noise, and clearly distinguishable from the
that the real bug produced.
That’s the discipline: know what “passing” looks like before you run the test. A parity residual isn’t required to be zero in a simulation — it’s required to be within a few standard errors of zero.
Implied volatility must match, too
A consequence people miss: because parity ties the two prices together, a call and a put at the same strike and maturity must imply exactly the same volatility.
Call price 11.2028 -> implied vol 22.0000%
Put price 6.3258 -> implied vol 22.0000%
difference: 5.55e-16
This is worth internalising, because it explains something about volatility surfaces. When someone says “the 90 strike trades at 25 vol”, they don’t need to tell you whether they mean the call or the put. There’s only one implied vol per strike. If you ever build a surface where calls and puts disagree at the same strike, you haven’t found a market anomaly — you have a data problem, usually stale quotes on one side.
What a violation actually means
Suppose you observe a put trading fifty cents below where parity says it should be:
C - P (market) = 5.3771
S0 - K*e^-rT = 4.8771
gap = +0.5000
In principle: sell the call, buy the put, buy the stock, fund it by borrowing. At expiry every leg cancels and you keep the fifty cents, whatever the stock did. Free money.
In practice, if you see this on a screen, the overwhelmingly likely explanations are, in order: your data is stale, you’ve forgotten a dividend, the options are American rather than European, borrow costs on the stock are not what you assumed, or the quotes are wide enough that the gap disappears into the spread.
Which is exactly why parity is a good diagnostic. A violation is a signal that one of your inputs is wrong — and nine times out of ten, finding out which is more valuable than the trade would have been.
The version you’ll actually use
The clean formula above assumes no dividends and European exercise. Two adjustments cover most real cases.
With a continuous dividend yield
, the stock leg gets discounted too:
![]()
For discrete dividends, subtract their present value from the spot instead. And for index options,
is the index dividend yield, which is a real number you need to get right rather than a rounding error.
For American options, parity becomes an inequality rather than an equation, because early exercise breaks the payoff-matching argument. If you apply European parity to American options and find a violation, you’ve discovered early exercise value, not an arbitrage.
Why this belongs in your test suite
Put-call parity earns its place because of three properties that rarely come together:
- Model-free. It tests your implementation, not your model. It passes or fails the same way under Black-Scholes, Heston, or a jump-diffusion.
- No benchmark required. It’s an internal consistency check. You don’t need to already know the right price.
- Cheap. Two prices and a subtraction.
It won’t tell you your volatility is wrong or your model is unsuitable. It will reliably catch discounting errors, sign errors, wrong maturity conventions, dividend handling mistakes, and the whole family of bugs that produce numbers which look plausible and aren’t.
That’s the habit worth building, and it generalises well beyond this one identity: for every pricer you write, find a relationship that must hold regardless of the model, and test against it before you trust a number.
Building that suite properly — parity, martingale tests, convergence checks, benchmark comparisons — is a chunk of what we do in Risk-Neutral Valuation, because a price you can’t validate isn’t a price, it’s a guess with decimal places.
Get the Quant Reference Card — four pages covering stochastic calculus, measure change, the models you’ll actually meet, discretisation, rates, credit and XVA, with the practitioner notes that don’t make it into textbooks. Free.