Option Prices (Using Put-Call Parity)¶
Background¶
Black Scholes Prices Parity
Educational script demonstrating black scholes prices parity concepts.
Code¶
```python """ Black Scholes Prices Parity
Educational script demonstrating black scholes prices parity concepts. """
============================================================================¶
black_scholes_OPTION_PRICES_USING_PUT_CALL_PARITY.py¶
============================================================================¶
import black_scholes as bs import numpy as np import matplotlib.pyplot as plt
Parameters¶
if name == "main": S = np.arange(50, 155, 5) K = 100 T = 1 r = 0.03 sigma = 0.2
# Print basic information
print(f"\n{'='*60}")
print("PUT-CALL PARITY DEMONSTRATION")
print("="*60)
print(f"Stock price range: ${S[0]} - ${S[-1]} (step: ${S[1]-S[0]})")
print(f"Strike price: ${K}")
print(f"Time to maturity: {T} year")
print(f"Risk-free rate: {r:.1%}")
print(f"Volatility: {sigma:.1%}")
print(f"Number of price points: {len(S)}")
# Compute Black-Scholes call and put prices using the wrapper
# We need to vectorize for array of S values
call_prices = []
put_prices = []
for s in S:
bs_model = bs.BlackScholes(s, K, T, r, sigma)
call_price, put_price = bs_model.price_analytical()
call_prices.append(call_price)
put_prices.append(put_price)
call = np.array(call_prices)
put = np.array(put_prices)
# Alternative: Use vectorized utility functions (more efficient)
call_vectorized = bs.bs_call_price(S, K, T, r, sigma)
put_vectorized = bs.bs_put_price(S, K, T, r, sigma)
# Verify both methods give same results
print(f"\nCalculation verification:")
print(f" Call prices match: {np.allclose(call, call_vectorized)}")
print(f" Put prices match: {np.allclose(put, put_vectorized)}")
print(f" Max difference: {np.max(np.abs(call - call_vectorized)):.2e}")
# Use the more efficient vectorized results
call = call_vectorized
put = put_vectorized
# Create figure
fig, ax = plt.subplots(figsize=(8, 6))
# Plot Black-Scholes put price
ax.plot(S, put, '-r', linewidth=2, label='Put using Black-Scholes Formula')
# Compute put price using put-call parity: P = C - S + K*exp(-rT)
put_parity = call - S + K * np.exp(-r * T)
ax.plot(S, put_parity, '*g', markersize=8, label='Put using Put-Call Parity')
# Plot intrinsic value (payoff at maturity)
S1 = np.sort(np.concatenate((S, [K])))
payoff = np.maximum(K - S1, 0)
ax.plot(S1, payoff, '--b', linewidth=2, alpha=0.7, label='Put Payoff (Intrinsic Value)')
# Add strike price reference line
ax.axvline(x=K, color='gray', linestyle=':', alpha=0.7, label=f'Strike = ${K}')
# Axis labels and legend
ax.set_xlabel('Stock Price ($)', fontsize=12)
ax.set_ylabel('Put Option Value ($)', fontsize=12)
ax.set_title('Put-Call Parity Verification\n' +
f'K=${K}, T={T}yr, r={r:.1%}, σ={sigma:.1%}', fontsize=14)
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
# Clean up plot appearance
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
plt.tight_layout()
plt.show()
# Verify put-call parity numerically
print(f"\nPut-Call Parity Verification:")
print(f" Formula: P = C - S + K*e^(-rT)")
print(f" Expected: Put prices should match parity calculation")
# Calculate differences
parity_differences = np.abs(put - put_parity)
max_diff = np.max(parity_differences)
mean_diff = np.mean(parity_differences)
print(f"\nNumerical Results:")
print(f" Maximum difference: ${max_diff:.2e}")
print(f" Mean difference: ${mean_diff:.2e}")
print(f" All differences < 1e-10: {np.all(parity_differences < 1e-10)}")
if max_diff < 1e-10:
print(f" ✓ Excellent! Put-call parity holds within numerical precision")
else:
print(f" ⚠ Differences detected - check calculation")
# Show some specific examples
print(f"\nSpecific Examples:")
print(f"{'Stock Price':<12}{'BS Put':<10}{'Parity Put':<12}{'Difference':<12}")
print("-" * 48)
# Show results for a few key points
key_indices = [0, len(S)//4, len(S)//2, 3*len(S)//4, -1]
for i in key_indices:
s_val = S[i]
put_bs = put[i]
put_par = put_parity[i]
diff = abs(put_bs - put_par)
print(f"${s_val:<11.0f}${put_bs:<9.4f}${put_par:<11.4f}${diff:<11.2e}")
print(f"\nAt-the-Money Analysis (S = K = ${K}):")
idx_atm = np.argmin(np.abs(S - K))
s_atm = S[idx_atm]
call_atm = call[idx_atm]
put_atm = put[idx_atm]
intrinsic_call = max(s_atm - K, 0)
intrinsic_put = max(K - s_atm, 0)
time_value_call = call_atm - intrinsic_call
time_value_put = put_atm - intrinsic_put
print(f" Stock Price: ${s_atm:.0f}")
print(f" Call Price: ${call_atm:.4f} (Intrinsic: ${intrinsic_call:.4f}, Time: ${time_value_call:.4f})")
print(f" Put Price: ${put_atm:.4f} (Intrinsic: ${intrinsic_put:.4f}, Time: ${time_value_put:.4f})")
# Demonstrate using wrapper for single calculation
print(f"\nWrapper Usage Example:")
atm_model = bs.BlackScholes(S0=K, K=K, T=T, r=r, sigma=sigma)
call_wrapper, put_wrapper = atm_model.price_analytical()
greeks = atm_model.calculate_greeks()
print(f" Using BlackScholes wrapper at S=${K}:")
print(f" Call: ${call_wrapper:.4f}")
print(f" Put: ${put_wrapper:.4f}")
print(f" Delta (Call/Put): {greeks['delta_call']:.4f} / {greeks['delta_put']:.4f}")
print(f" Gamma: {greeks['gamma']:.6f}")
print(f" Vega: {greeks['vega']:.4f}")
print("="*60)
```
Exercises¶
Exercise 1. State put-call parity for European options. Derive it using a no-arbitrage argument involving two portfolios.
Solution to Exercise 1
Put-call parity: \(C - P = S_0 - Ke^{-rT}\).
Portfolio A: Long call + \(Ke^{-rT}\) in bonds. At \(T\): if \(S_T > K\), exercise call for \(S_T - K + K = S_T\); if \(S_T \le K\), call expires, get \(K\).
Portfolio B: Long put + long stock. At \(T\): if \(S_T > K\), put expires, have \(S_T\); if \(S_T \le K\), exercise put for \(K - S_T + S_T = K\).
Both portfolios pay \(\max(S_T, K)\) at \(T\). By no-arbitrage, they must have equal present values: \(C + Ke^{-rT} = P + S_0\), giving \(C - P = S_0 - Ke^{-rT}\).
Exercise 2. Using put-call parity, if \(C = 8.50\), \(S_0 = 100\), \(K = 95\), \(T = 0.5\), \(r = 0.04\), compute \(P\).
Solution to Exercise 2
Exercise 3. Explain how put-call parity can be used to verify the correctness of option pricing implementations. What discrepancy would indicate a bug?
Solution to Exercise 3
After computing \(C\) and \(P\) independently (whether by formula, MC, or PDE), check that \(C - P = S_0 e^{-qT} - Ke^{-rT}\) (with dividends). Any discrepancy beyond numerical tolerance indicates a bug.
Common bugs detected:
- Incorrect discount factor (using \(e^{-rT}\) instead of \(e^{-(r-q)T}\) for dividends).
- Wrong sign in \(d_1\) or \(d_2\) formula.
- Boundary condition errors in PDE solvers.
- Payoff function errors (\(\max(S-K,0)\) vs \(\max(K-S,0)\)).
Exercise 4. Does put-call parity hold for American options? If not, what inequality replaces it?
Solution to Exercise 4
Put-call parity does not hold for American options because the early exercise feature breaks the equivalence between the two portfolios.
For American options on a non-dividend-paying stock: \(S_0 - K \le C_A - P_A \le S_0 - Ke^{-rT}\).
The upper bound comes from the European parity (since \(C_A = C_E\) for non-dividend stocks). The lower bound accounts for the possibility that the American put may be exercised early. With dividends, the bounds become: \(S_0 e^{-qT} - K \le C_A - P_A \le S_0 - Ke^{-rT}\).