Two ways to the same place
Build two portfolios. A: buy a call and put the strike's present value K * exp(-rT) in a bank (or a risk-free bond). B: buy a put and buy the stock. At expiry, if the stock ends above K, A is the call's S - K plus the bank's K, so S; B is the stock S and a worthless put, so S. If the stock ends below K, A is just the bank's K since the call expires worthless, and B is the put's K - S plus the stock S, so K. Either way the two portfolios are worth the same at expiry, so they must cost the same today.
import math
spot, strike, rate, t = 100, 100, 0.05, 1
pv_strike = strike * math.exp(-rate * t) # what must be put in the bank today to own K at expiry
print(round(pv_strike, 4)) # 95.1229
call = 10.4506 # a call price we observe
put = call - spot + pv_strike # the parity relation
print(round(put, 4)) # 5.5735
print(round(put + spot - pv_strike, 4)) # 10.4506 and back to the callWhen parity is broken: arbitrage
If the market quotes prices that do not satisfy the relation, a riskless profit exists. Define the gap (C - P) - (S - K e^(-rT)). A positive gap means the call is too expensive for the put: sell the call, buy the put, buy the stock and borrow the strike's present value. The portfolio costs nothing at the start, pays nothing at expiry whatever the stock does, and pockets the gap today. A negative gap reverses every leg. Real markets have costs and bid-ask spreads, so small gaps are not tradable: that is what the tolerance stands for.
def parity_gap(call, put, spot, strike, rate, t):
return (call - put) - (spot - strike * math.exp(-rate * t))
print(round(parity_gap(11.0, put, 100, 100, 0.05, 1), 4)) # 0.5494 the call at 11.00 is too expensive: sell it
print(round(parity_gap(call, put, 100, 100, 0.05, 1), 4)) # 0.0 consistent prices: nothing to do
def arbitrage_action(call, put, spot, strike, rate, t, tolerance=0.01):
gap = parity_gap(call, put, spot, strike, rate, t)
if gap > tolerance:
return "sell call, buy put, buy stock"
if gap < -tolerance:
return "buy call, sell put, sell stock"
return "none"
print(arbitrage_action(11.0, put, 100, 100, 0.05, 1)) # sell call, buy put, buy stock# The package of the arbitrage, at today's prices: sell the call at 11, buy the put, buy the stock, borrow the strike's present value
cash_today = 11.0 - put - 100 + 100 * math.exp(-0.05 * 1)
print(round(cash_today, 4)) # 0.5494 you are paid the gap now and owe nothing at expiry# A call minus a put is a synthetic forward: it behaves like the stock bought with borrowed money
print(round(10.4506 - put, 4), round(100 - pv_strike, 4)) # 4.8771 4.8771C + K * exp(-rT) = P + S: a call plus the bond equals a put plus the stock, soP = C - S + K * exp(-rT).- Gap
(C - P) - (S - K e^(-rT))above zero: sell the call, buy the put and the stock; below zero: the reverse. - Parity is model-free, so use it as a check on any pricing model; with dividends or early exercise it becomes an inequality.
Next up: the Black–Scholes formula, a closed-form price for the call and the put.
Lab · what's graded
- Get the put from the call with
parity_put_price25 pts - Get the call from the put with
parity_call_price25 pts - Measure how far prices are from parity with
parity_gap25 pts - Name the riskless trade with
arbitrage_action25 pts