Code
import math
import numpy as np
import pandas as pd
import matplotlib.pyplot as pltimport math
import numpy as np
import pandas as pd
import matplotlib.pyplot as pltIn a previous workshops we studied what forwards, futures, and options are and how their payoffs look at expiration. In this workshop we go one level deeper: how are forward and futures prices determined in the first place?
The key insight is no-arbitrage pricing. If a forward price is “too high” or “too low,” traders can lock in a riskless profit — so competition quickly pushes the price to its fair value.
We distinguish two broad categories of underlying assets:
Key vocabulary:
Symbol Meaning S_0 Spot price today F_0 Forward / futures price today K Delivery price locked in a specific contract T Time to delivery (in years) r Continuously compounded risk-free rate f Current value of an existing forward contract
When the underlying pays no dividends or coupons and has no storage costs (e.g., a non-dividend-paying stock or a zero-coupon bond), no-arbitrage requires:
\boxed{F_0 = S_0 \, e^{rT}}
Intuition: buying the asset today and holding it until T costs you the financing of S_0 for T years. If F_0 were higher, you could borrow S_0, buy the asset, and short the forward — locking in a profit. If F_0 were lower, you could short the asset, invest the proceeds, and go long the forward.
def forward_no_income(S0, r, T):
"""No-arbitrage forward price for an asset with no income."""
return S0 * np.exp(r * T)
# Hull Example 5.1 — 4-month forward on a zero-coupon bond
S0 = 930 # current bond price
r = 0.06 # 4-month continuously compounded risk-free rate
T = 4/12 # 4 months in years
F0 = forward_no_income(S0, r, T)
print(f"Forward price (no income): ${F0:.2f}")Forward price (no income): $948.79
S0, r = 40, 0.05
T_range = np.linspace(0, 2, 200)
F0_range = S0 * np.exp(r * T_range)
plt.figure(figsize=(7,4))
plt.plot(T_range, F0_range, color='steelblue')
plt.axhline(S0, linestyle='--', color='gray', label=f'Spot S₀ = ${S0}')
plt.title("Forward Price vs. Time to Maturity (no income, r=5%)")
plt.xlabel("Time to Maturity T (years)")
plt.ylabel("Forward Price F₀ ($)")
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()Note: The forward price always lies above the spot price (for positive r) and grows exponentially. The gap equals the cost of financing the position.
When the asset pays predictable cash flows (coupons, dividends) with present value I, the formula becomes:
\boxed{F_0 = (S_0 - I)\, e^{rT}}
You subtract I from the spot price because the income accrues to the holder of the physical asset, not to the holder of the forward.
def pv_of_income(cash_flows, rates, times):
"""
PV of a list of known cash flows.
cash_flows : list of dollar amounts
rates : list of continuously compounded rates (same length)
times : list of times in years (same length)
"""
return sum(c * np.exp(-r * t) for c, r, t in zip(cash_flows, rates, times))
def forward_known_income(S0, I, r, T):
return (S0 - I) * np.exp(r * T)
# Hull Example 5.2 — 10-month forward on a stock paying quarterly dividends
S0 = 50.0
dividends = [0.75, 0.75, 0.75] # $ per share
div_times = [3/12, 6/12, 9/12] # in years
div_rates = [0.08, 0.08, 0.08] # same r for simplicity
r_main = 0.08
T = 10/12
I = pv_of_income(dividends, div_rates, div_times)
F0 = forward_known_income(S0, I, r_main, T)
print(f"PV of dividends I = ${I:.3f}")
print(f"Forward price F₀ = ${F0:.2f}")PV of dividends I = $2.162
Forward price F₀ = $51.14
When income is expressed as a continuous yield q (e.g., dividend yield on a stock index, or interest in a foreign currency):
\boxed{F_0 = S_0 \, e^{(r-q)T}}
Intuition: the yield q partially offsets the financing cost r. If q = r, the forward price equals the spot price.
def forward_known_yield(S0, r, q, T):
"""No-arbitrage forward price for asset with continuous yield q."""
return S0 * np.exp((r - q) * T)
# Hull Example 5.3 — 6-month forward, income = 2% of price semiannually
# Convert 4% p.a. semiannual to continuous compounding
q_semiannual = 0.04 # 4% per annum, semiannual compounding
q_continuous = 2 * math.log(1 + q_semiannual/2) # eq. 4.3 from Hull
S0 = 25.0
r = 0.10
T = 0.5
F0 = forward_known_yield(S0, r, q_continuous, T)
print(f"Continuous yield q = {q_continuous:.4f} ({q_continuous*100:.2f}%)")
print(f"Forward price F₀ = ${F0:.2f}")
# Plot: F0 as a function of yield q
q_range = np.linspace(0, 0.15, 200)
F0_range = forward_known_yield(S0, r, q_range, T)
plt.figure(figsize=(7,4))
plt.plot(q_range * 100, F0_range, color='darkorange')
plt.axhline(S0, linestyle='--', color='gray', label=f'Spot S₀ = ${S0}')
plt.axvline(r * 100, linestyle=':', color='red', label=f'r = {r*100}%')
plt.title("Forward Price vs. Dividend Yield q (r=10%, T=0.5)")
plt.xlabel("Yield q (%)")
plt.ylabel("Forward Price F₀ ($)")
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()Continuous yield q = 0.0396 (3.96%)
Forward price F₀ = $25.77
When q > r, the forward price falls below the spot price. This happens with high-yield currencies or high-dividend stocks.
A foreign currency is an asset that “yields” the foreign risk-free rate r_f. Substituting q = r_f into the known-yield formula gives interest rate parity:
\boxed{F_0 = S_0 \, e^{(r - r_f)T}}
where S_0 and F_0 are expressed in domestic currency per unit of foreign currency.
Intuition: if the domestic rate r is higher than the foreign rate r_f, the forward price of the foreign currency is above the spot — reflecting the expected appreciation of the lower-yield currency.
def fx_forward(S0, r_domestic, r_foreign, T):
"""Interest rate parity: forward FX rate."""
return S0 * np.exp((r_domestic - r_foreign) * T)
# A Mexican exporter (AgroGua-like company) wants to lock in an exchange rate
# Spot: 1 USD = 17.50 MXN
S0_mxn = 17.50 # MXN per USD (domestic = MXN, foreign = USD)
r_mxn = 0.10 # Mexico TIIE-like rate (continuously compounded)
r_usd = 0.05 # US Fed funds-like rate (continuously compounded)
T_months = [1, 3, 6, 9, 12]
print(f"{'Horizon':>10} {'F₀ (MXN/USD)':>15} {'Premium/Disc':>15}")
print("-" * 45)
for m in T_months:
T = m / 12
F0 = fx_forward(S0_mxn, r_mxn, r_usd, T)
pct = (F0 / S0_mxn - 1) * 100
print(f"{m:>8}m {F0:>15.4f} {pct:>+14.2f}%") Horizon F₀ (MXN/USD) Premium/Disc
---------------------------------------------
1m 17.5731 +0.42%
3m 17.7201 +1.26%
6m 17.9430 +2.53%
9m 18.1687 +3.82%
12m 18.3972 +5.13%
For a Mexican exporter: the forward USD/MXN rate is higher than the spot because Mexico’s interest rates are higher than the US’s. The peso-denominated forward price of the dollar rises — equivalently, the peso is expected to depreciate relative to interest rate differentials.
def fx_arbitrage_check(S0, r_d, r_f, T, F0_quoted):
"""
Returns the arbitrage profit (in domestic currency) per 1 unit of foreign currency
if the quoted forward deviates from the no-arbitrage price.
Positive = profit from borrowing domestic / investing foreign + short forward.
"""
F0_fair = fx_forward(S0, r_d, r_f, T)
if F0_quoted > F0_fair:
# Forward too high: borrow domestic, buy spot foreign, invest at r_f, sell forward
profit = F0_quoted - F0_fair
direction = "Borrow domestic, invest foreign, SELL forward"
else:
# Forward too low: borrow foreign, convert to domestic, invest at r_d, buy forward
profit = F0_fair - F0_quoted
direction = "Borrow foreign, invest domestic, BUY forward"
return F0_fair, profit, direction
S0, r_d, r_f, T = 17.50, 0.10, 0.05, 0.25 # 3-month
F0_quoted = 18.40 # hypothetical mispricing
F0_fair, profit, strategy = fx_arbitrage_check(S0, r_d, r_f, T, F0_quoted)
print(f"Fair forward (IRP): {F0_fair:.4f} MXN/USD")
print(f"Quoted forward : {F0_quoted:.4f} MXN/USD")
print(f"Arbitrage profit : {profit:.4f} MXN per USD")
print(f"Strategy : {strategy}")Fair forward (IRP): 17.7201 MXN/USD
Quoted forward : 18.4000 MXN/USD
Arbitrage profit : 0.6799 MXN per USD
Strategy : Borrow domestic, invest foreign, SELL forward
Once a forward contract is in place, its value f can become positive or negative as markets move.
\boxed{f = (F_0 - K)\, e^{-rT}}
where K is the delivery price locked in the original contract and F_0 is the current fair forward price.
For the three asset types:
| Asset type | Value of long forward f |
|---|---|
| No income | S_0 - K e^{-rT} |
| Known income I | S_0 - I - K e^{-rT} |
| Known yield q | S_0 e^{-qT} - K e^{-rT} |
def value_long_forward(S0, K, r, T, I=0, q=0):
"""
Value of an existing long forward contract.
Set I>0 for known-income asset, q>0 for known-yield asset.
"""
if I > 0:
return S0 - I - K * np.exp(-r * T)
elif q > 0:
return S0 * np.exp(-q * T) - K * np.exp(-r * T)
else:
return S0 - K * np.exp(-r * T)
# Hull Example 5.4 — 6-month forward on a non-dividend-paying stock
# Contract was entered at K=24 some time ago; stock is now $25
S0 = 25.0
K = 24.0
r = 0.10
T = 0.5
F0_current = forward_no_income(S0, r, T)
f = value_long_forward(S0, K, r, T)
print(f"Current fair forward price F₀ = ${F0_current:.4f}")
print(f"Value of existing long forward = ${f:.4f}")
print(f" (positive → contract benefits buyer; would cost this to buy out of contract)")
# --- Sensitivity: how f changes as the spot price moves today ---
S_range = np.linspace(15, 35, 200)
f_range = [value_long_forward(s, K, r, T) for s in S_range]
plt.figure(figsize=(7, 4))
plt.plot(S_range, f_range, color='steelblue')
plt.axhline(0, linestyle='--', color='gray')
plt.axvline(S0, linestyle=':', color='red', label=f'Current S₀=${S0}')
plt.title(f"Value of Long Forward (K={K}, r={r*100}%, T={T}y)")
plt.xlabel("Current Spot Price S₀ ($)")
plt.ylabel("Value f ($)")
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()Current fair forward price F₀ = $26.2818
Value of existing long forward = $2.1705
(positive → contract benefits buyer; would cost this to buy out of contract)
A stock index can be treated as a portfolio paying a continuous dividend yield q, so:
F_0 = S_0 \, e^{(r-q)T}
This is equation (5.8) from Hull. Index arbitrage enforces this relationship through program trading.
# Hull Example 5.5 — 3-month S&P 500 futures
S0 = 1300
r = 0.05 # risk-free
q = 0.01 # dividend yield
T = 0.25
F0_index = forward_known_yield(S0, r, q, T)
print(f"Index futures price: {F0_index:.2f}")
# Arbitrage signal: what if the quoted price deviates?
F0_quoted = 1320
F0_fair = F0_index
if F0_quoted > F0_fair:
profit = F0_quoted - F0_fair
print(f"\nQuoted {F0_quoted} > Fair {F0_fair:.2f}")
print(f"→ BUY index, SHORT futures. Locked profit ≈ {profit:.2f} index points")
else:
profit = F0_fair - F0_quoted
print(f"\nQuoted {F0_quoted} < Fair {F0_fair:.2f}")
print(f"→ SHORT index, LONG futures. Locked profit ≈ {profit:.2f} index points")Index futures price: 1313.07
Quoted 1320 > Fair 1313.07
→ BUY index, SHORT futures. Locked profit ≈ 6.93 index points
The cost of carry c unifies all the formulas:
F_0 = S_0 \, e^{cT}
| Asset | Cost of carry c |
|---|---|
| Non-dividend-paying stock | r |
| Stock index (div. yield q) | r - q |
| Foreign currency | r - r_f |
| Commodity (storage cost u, yield q) | r - q + u |
scenarios = {
"Non-div stock": {"r": 0.05, "q": 0.00, "rf": 0.00, "u": 0.00},
"Stock index (q=2%)": {"r": 0.05, "q": 0.02, "rf": 0.00, "u": 0.00},
"USD (MXN perspective, rf=5%)": {"r": 0.10, "q": 0.00, "rf": 0.05, "u": 0.00},
"Commodity (u=3%)": {"r": 0.05, "q": 0.00, "rf": 0.00, "u": 0.03},
}
S0, T = 100, 1.0
print(f"{'Asset':<35} {'c':>6} {'F₀':>8}")
print("-" * 55)
for name, p in scenarios.items():
c = p["r"] - p["q"] - p["rf"] + p["u"]
F0 = S0 * np.exp(c * T)
print(f"{name:<35} {c:>6.2%} {F0:>8.2f}")Asset c F₀
-------------------------------------------------------
Non-div stock 5.00% 105.13
Stock index (q=2%) 3.00% 103.05
USD (MXN perspective, rf=5%) 5.00% 105.13
Commodity (u=3%) 8.00% 108.33
This is one of the most conceptually interesting results in Chapter 5 (Section 5.14 of Hull).
The relationship between the futures price F_0 and the expected future spot price E(S_T) depends on the systematic risk of the underlying:
F_0 = E(S_T) \, e^{(r-k)T}
where k is the return required by investors in the asset.
| Systematic risk | k vs. r | F_0 vs. E(S_T) | Term |
|---|---|---|---|
| None | k = r | F_0 = E(S_T) | Unbiased |
| Positive | k > r | F_0 < E(S_T) | Normal backwardation |
| Negative | k < r | F_0 > E(S_T) | Contango |
def futures_vs_expected_spot(E_ST, r, k, T):
"""Implied futures price given expected spot and required return."""
return E_ST * np.exp((r - k) * T)
E_ST = 100 # Expected spot price in 1 year
r = 0.05 # Risk-free rate
T = 1.0
print(f"{'Scenario':<20} {'k':>6} {'F₀':>8} {'F₀ vs E(S_T)':>15}")
print("-" * 55)
for scenario, k in [("No syst. risk", 0.05), ("Positive syst.", 0.12), ("Negative syst.", 0.01)]:
F0 = futures_vs_expected_spot(E_ST, r, k, T)
relation = "= E(S_T)" if abs(F0 - E_ST) < 0.01 else ("< E(S_T)" if F0 < E_ST else "> E(S_T)")
print(f"{scenario:<20} {k:>6.1%} {F0:>8.2f} {relation:>15}")Scenario k F₀ F₀ vs E(S_T)
-------------------------------------------------------
No syst. risk 5.0% 100.00 = E(S_T)
Positive syst. 12.0% 93.24 < E(S_T)
Negative syst. 1.0% 104.08 > E(S_T)
Practical implication for hedgers: in normal backwardation, long hedgers (e.g., an airline buying oil futures) pay a risk premium to short speculators. Identifying the backwardation/contango regime helps companies evaluate the true cost of their hedging strategy.
AgroGua exports tomatoes to the US and receives USD. Their treasury must decide whether to hedge their FX exposure using a forward contract or leave it open.
Let’s price a forward and evaluate its value under different MXN scenarios.
# --- AgroGua parameters ---
S0_spot = 17.50 # USD/MXN spot rate (MXN per 1 USD)
r_mxn = 0.105 # Mexico Cetes rate (continuously compounded approx.)
r_usd = 0.053 # US T-bill rate (continuously compounded approx.)
T_values = [3/12, 6/12, 9/12, 12/12]
# Expected USD revenue per quarter (from Excel model, approx.)
usd_revenue_quarterly = 500_000 # USD per quarter
print("=== AgroGua FX Forward Pricing ===\n")
print(f"{'Horizon':>10} {'Forward Rate':>14} {'MXN Revenue':>14} {'vs Spot':>12}")
print("-" * 55)
for T in T_values:
F0 = fx_forward(S0_spot, r_mxn, r_usd, T)
mxn_locked = usd_revenue_quarterly * F0
vs_spot = usd_revenue_quarterly * (F0 - S0_spot)
months = int(T * 12)
print(f"{months:>8}m {F0:>14.4f} {mxn_locked:>14,.0f} {vs_spot:>+12,.0f}")
# --- Payoff diagram: hedged vs. unhedged ---
K = fx_forward(S0_spot, r_mxn, r_usd, 6/12) # 6-month forward
S_T_range = np.linspace(14, 22, 300)
revenue_unhedged = usd_revenue_quarterly * S_T_range
revenue_hedged = np.full_like(S_T_range, usd_revenue_quarterly * K)
revenue_short_fwd = usd_revenue_quarterly * K + usd_revenue_quarterly * (K - S_T_range) # short fwd payoff added
plt.figure(figsize=(8,5))
plt.plot(S_T_range, revenue_unhedged / 1e6, label="Unhedged (MXN revenue)", color='steelblue')
plt.axhline(usd_revenue_quarterly * K / 1e6, linestyle='--', color='darkorange',
label=f"Hedged with Forward (K={K:.2f})")
plt.axvline(S0_spot, linestyle=':', color='gray', alpha=0.7, label=f"Today's spot {S0_spot}")
plt.title("AgroGua: Hedged vs. Unhedged MXN Revenue (6-month Forward)")
plt.xlabel("USD/MXN Spot Rate at Delivery S_T")
plt.ylabel("MXN Revenue (millions)")
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()=== AgroGua FX Forward Pricing ===
Horizon Forward Rate MXN Revenue vs Spot
-------------------------------------------------------
3m 17.7290 8,864,493 +114,493
6m 17.9610 8,980,483 +230,483
9m 18.1960 9,097,992 +347,992
12m 18.4341 9,217,038 +467,038
Key takeaway: the forward contract eliminates the variability in MXN revenue — but it also eliminates any upside if the peso depreciates further. This is the classic hedge-or-not tradeoff.
import pandas as pd
summary = pd.DataFrame({
"Asset Type": [
"No income (stock, zero-coupon bond)",
"Known cash income I",
"Known continuous yield q",
"Foreign currency (IRP)",
"Commodity with storage cost u",
],
"Forward Price F₀": [
"S₀ e^{rT}",
"(S₀ − I) e^{rT}",
"S₀ e^{(r−q)T}",
"S₀ e^{(r−rf)T}",
"S₀ e^{(r+u)T}",
],
"Value of Long Forward f": [
"S₀ − K e^{−rT}",
"S₀ − I − K e^{−rT}",
"S₀ e^{−qT} − K e^{−rT}",
"S₀ e^{−rfT} − K e^{−rT}",
"(S₀+U) e^{rT} not exact for consumption",
]
})
summary.style.set_caption("Hull Ch. 5 — Forward & Futures Pricing Formulas")| Asset Type | Forward Price F₀ | Value of Long Forward f | |
|---|---|---|---|
| 0 | No income (stock, zero-coupon bond) | S₀ e^{rT} | S₀ − K e^{−rT} |
| 1 | Known cash income I | (S₀ − I) e^{rT} | S₀ − I − K e^{−rT} |
| 2 | Known continuous yield q | S₀ e^{(r−q)T} | S₀ e^{−qT} − K e^{−rT} |
| 3 | Foreign currency (IRP) | S₀ e^{(r−rf)T} | S₀ e^{−rfT} − K e^{−rT} |
| 4 | Commodity with storage cost u | S₀ e^{(r+u)T} | (S₀+U) e^{rT} not exact for consumption |
Work on the following problems. For each one, write the no-arbitrage formula, compute the answer in Python, and explain the economic intuition in 2–3 sentences.
Exercise 1 (Hull 5.2): You enter a 6-month forward on a non-dividend-paying stock when the spot price is $30 and the continuously compounded risk-free rate is 5% p.a. What is the forward price?
Exercise 2 (Hull 5.8): The risk-free rate is 7% p.a. (continuously compounded) and the dividend yield on a stock index is 3.2% p.a. The current index value is 150. What is the 6-month futures price?
Exercise 3 (Hull 5.12): The 2-month continuously compounded interest rates in Switzerland and the US are 1% and 2% p.a., respectively. The spot USD/CHF rate is $1.0500. The 2-month futures price is $1.0500. What arbitrage opportunity exists? Describe the exact trades.
Exercise 4 — AgroGua Context: AgroGua is negotiating a 9-month forward contract with Monex to sell USD 600,000 (their estimated revenue for the next three quarters). Use the parameters from Section 8 (r_mxn = 10.5%, r_usd = 5.3%, spot = 17.50).
# --- Starter code for Exercise 4 ---
S0 = 17.50
r_d = 0.105
r_f = 0.053
# (a) 9-month forward
T_9m = 9/12
F0_9m = fx_forward(S0, r_d, r_f, T_9m)
print(f"(a) 9-month fair forward: {F0_9m:.4f} MXN/USD")
# (b) Monex quote
K_contract = 18.20
print(f"(b) Monex quote {K_contract} vs fair {F0_9m:.4f} → {'favorable for AgroGua (they sell USD at a higher rate)' if K_contract > F0_9m else 'unfavorable'}")
# (c) Value of short forward 3 months later: spot is now 16.80, T remaining = 6/12
S0_new = 16.80
T_rem = 6/12
F0_new = fx_forward(S0_new, r_d, r_f, T_rem) # new fair forward for remaining term
r_disc = r_d # discount at domestic rate
# Short forward value = -(Long forward value) = -(F0_new - K)*e^{-rT}
f_long = (F0_new - K_contract) * np.exp(-r_disc * T_rem)
f_short = -f_long
print(f"\n(c) New fair 6-month forward: {F0_new:.4f}")
print(f" Value of SHORT forward (AgroGua): {f_short:+.4f} MXN per USD")
print(f" Total position value (600,000 USD): {f_short * 600_000:+,.0f} MXN")(a) 9-month fair forward: 18.1960 MXN/USD
(b) Monex quote 18.2 vs fair 18.1960 → favorable for AgroGua (they sell USD at a higher rate)
(c) New fair 6-month forward: 17.2425
Value of SHORT forward (AgroGua): +0.9085 MXN per USD
Total position value (600,000 USD): +545,101 MXN