An option gives the holder the right — but not the obligation — to buy or sell at a fixed strike price. This asymmetry is what you pay the premium for. Put-call parity ties calls, puts, forwards, and bonds into one equation.
Call and put payoffs
A call pays max(Sᵀ − K, 0) at expiration: the payoff is positive when the stock finishes above the strike K; profit requires covering the premium paid. A put pays max(K − Sᵀ, 0): you profit when the stock falls below K. The option buyer's loss is capped at the premium paid. The seller's loss is potentially unlimited (for calls) or capped at K (for puts).
Scheme
; Option payoffs at expiration
(define (call-payoff S K) (max (- S K) 0))
(define (put-payoff S K) (max (- K S) 0))
; Profit = payoff - premium
(define (call-profit S K premium) (- (call-payoff S K) premium))
(define (put-profit S K premium) (- (put-payoff S K) premium))
(define K 100)
(define call-premium 8)
(define put-premium 5)
(display "=== Call (K=100, premium=8) ===" ) (newline)
(for-each (lambda (S)
(display "S=") (display S)
(display " payoff=") (display (call-payoff S K))
(display " profit=") (display (call-profit S K call-premium))
(newline))
'(8090100108120))
(newline)
(display "=== Put (K=100, premium=5) ===" ) (newline)
(for-each (lambda (S)
(display "S=") (display S)
(display " payoff=") (display (put-payoff S K))
(display " profit=") (display (put-profit S K put-premium))
(newline))
'(809095100110))
Python
# Option payoffs at expirationdef call_payoff(S, K): returnmax(S - K, 0)
def put_payoff(S, K): returnmax(K - S, 0)
K = 100
call_premium, put_premium = 8, 5print("=== Call (K=100, premium=8) ===")
for S in [80, 90, 100, 108, 120]:
print(f"S={S:>3} payoff={call_payoff(S,K):>3} profit={call_payoff(S,K)-call_premium:>+4}")
print("\n=== Put (K=100, premium=5) ===")
for S in [80, 90, 95, 100, 110]:
print(f"S={S:>3} payoff={put_payoff(S,K):>3} profit={put_payoff(S,K)-put_premium:>+4}")
Put-call parity
For European options on the same underlying with the same strike and expiration: C − P = S₀ − K·e⁻rᵀ. This is an arbitrage relationship, not a model. It holds regardless of your view on volatility or the stock's direction. Violations are free money.
Scheme
; Put-call parity: C - P = S0 - K * e^(-rT); Rearranged: P = C - S0 + K * e^(-rT)
(define (parity-put C S0 K r T)
(+ (- C S0) (* K (exp (* -1 r T)))))
(define (parity-call P S0 K r T)
(+ P S0 (* -1 K (exp (* -1 r T)))))
; Example: S0=100, K=100, r=5%, T=1yr, C=12
(define S0 100) (define K 100)
(define r 0.05) (define T 1)
(define C 12)
(define P-implied (parity-put C S0 K r T))
(display "Given C=12, implied P = ")
(display (round (* P-implied 100))) ; display cents
(display " cents... P = $")
(display (/ (round (* P-implied 100)) 100)) (newline)
; Verify: C - P should equal S0 - K*e^(-rT)
(define lhs (- C P-implied))
(define rhs (- S0 (* K (exp (* -1 r T)))))
(display "C - P = ") (display (/ (round (* lhs 100)) 100)) (newline)
(display "S - Ke^(-rT) = ") (display (/ (round (* rhs 100)) 100)) (newline)
(display "Parity holds: ") (display (< (abs (- lhs rhs)) 0.001))
Python
importmathdef parity_put(C, S0, K, r, T):
"""Implied put price from put-call parity."""return C - S0 + K * math.exp(-r * T)
def parity_call(P, S0, K, r, T):
"""Implied call price from put-call parity."""return P + S0 - K * math.exp(-r * T)
S0, K, r, T = 100, 100, 0.05, 1.0
C = 12
P = parity_put(C, S0, K, r, T)
print(f"Given C=${C}, implied P = ${P:.2f}")
lhs = C - P
rhs = S0 - K * math.exp(-r * T)
print(f"C - P = {lhs:.4f}")
print(f"S - Ke^(-rT) = {rhs:.4f}")
print(f"Parity holds: {abs(lhs - rhs) < 0.001}")
Intrinsic vs time value
An option's price has two parts. Intrinsic value is the payoff if exercised right now: max(S − K, 0) for a call. Time value is the rest: the premium minus intrinsic value. Time value reflects the probability that the option will become more valuable before expiration. It decays as expiration approaches — this is theta decay.
Scheme
; Intrinsic value vs time value
(define (intrinsic-call S K) (max (- S K) 0))
(define (intrinsic-put S K) (max (- K S) 0))
(define (time-value premium intrinsic)
(- premium intrinsic))
; Call option: S=105, K=100, market price=9
(define S 105) (define K 100) (define call-price 9)
(define iv (intrinsic-call S K))
(define tv (time-value call-price iv))
(display "Call: S=105, K=100, price=$9") (newline)
(display " Intrinsic: $") (display iv) (newline)
(display " Time value: $") (display tv) (newline)
; OTM option: S=95, K=100, market price=3
(newline)
(define S2 95) (define call-price2 3)
(define iv2 (intrinsic-call S2 K))
(define tv2 (time-value call-price2 iv2))
(display "Call: S=95, K=100, price=$3") (newline)
(display " Intrinsic: $") (display iv2) (newline)
(display " Time value: $") (display tv2) (newline)
(display " Status: entirely time value (out-of-the-money)")
Python
# Intrinsic value vs time valuedef intrinsic_call(S, K): returnmax(S - K, 0)
# ITM call: S=105, K=100, price=9
S, K, price = 105, 100, 9
iv = intrinsic_call(S, K)
tv = price - iv
print(f"Call: S={S}, K={K}, price=${price}")
print(f" Intrinsic: ${iv} Time value: ${tv}")
# OTM call: S=95, K=100, price=3
S2, price2 = 95, 3
iv2 = intrinsic_call(S2, K)
tv2 = price2 - iv2
print(f"\nCall: S={S2}, K={K}, price=${price2}")
print(f" Intrinsic: ${iv2} Time value: ${tv2}")
print(f" Status: entirely time value (out-of-the-money)")
Black-Scholes formula
The Black-Scholes model prices European options under specific assumptions: log-normal stock prices, constant volatility, no dividends, continuous trading. The formula: C = S₀·N(d₁) − K·e⁻rᵀ·N(d₂), where d₁ and d₂ depend on S, K, r, T, and volatility σ. Derivation is Finance II — here we use it as a pricing tool.
Scheme
; Black-Scholes European call price; Uses rational approximation for N(x) (CDF of standard normal)
(define pi 3.141592653589793)
; Standard normal PDF
(define (norm-pdf x)
(* (/ 1 (sqrt (* 2 pi))) (exp (* -0.5 x x))))
; Standard normal CDF (Abramowitz & Stegun approximation)
(define (norm-cdf x)
(if (< x 0)
(- 1 (norm-cdf (- x)))
(let* ((b1 0.319381530) (b2 -0.356563782)
(b3 1.781477937) (b4 -1.821255978) (b5 1.330274429)
(t (/ 1 (+ 1 (* 0.2316419 x))))
(poly (+ (* b1 t) (* b2 t t) (* b3 t t t)
(* b4 t t t t) (* b5 t t t t t))))
(- 1 (* (norm-pdf x) poly)))))
(define (black-scholes S K r T sigma)
(let* ((d1 (/ (+ (log (/ S K)) (* (+ r (* 0.5 sigma sigma)) T))
(* sigma (sqrt T))))
(d2 (- d1 (* sigma (sqrt T))))
(call (- (* S (norm-cdf d1))
(* K (exp (* -1 r T)) (norm-cdf d2))))
(put (+ (- call S) (* K (exp (* -1 r T))))))
(list call put)))
; Price options: S=100, K=100, r=5%, T=1yr, sigma=20%
(define result (black-scholes 1001000.0510.20))
(display "Call: $") (display (/ (round (* (car result) 100)) 100)) (newline)
(display "Put: $") (display (/ (round (* (cadr result) 100)) 100)) (newline)
; Higher volatility: sigma=40%
(define result2 (black-scholes 1001000.0510.40))
(display "Call (vol=40%): $") (display (/ (round (* (car result2) 100)) 100)) (newline)
(display "Put (vol=40%): $") (display (/ (round (* (cadr result2) 100)) 100))
Python
importmathdef norm_cdf(x):
"""Standard normal CDF (Abramowitz & Stegun)."""if x < 0: return1 - norm_cdf(-x)
b = [0.319381530, -0.356563782, 1.781477937, -1.821255978, 1.330274429]
t = 1 / (1 + 0.2316419 * x)
pdf = math.exp(-0.5 * x * x) / math.sqrt(2 * math.pi)
poly = sum(b[i] * t**(i+1) for i inrange(5))
return1 - pdf * poly
def black_scholes(S, K, r, T, sigma):
d1 = (math.log(S/K) + (r + 0.5*sigma**2)*T) / (sigma*math.sqrt(T))
d2 = d1 - sigma * math.sqrt(T)
call = S * norm_cdf(d1) - K * math.exp(-r*T) * norm_cdf(d2)
put = call - S + K * math.exp(-r*T) # put-call parityreturn call, put
# S=100, K=100, r=5%, T=1yrfor sigma in [0.20, 0.40]:
c, p = black_scholes(100, 100, 0.05, 1, sigma)
print(f"sigma={sigma:.0%}: Call=${c:.2f}, Put=${p:.2f}")