Exercise · m10_storage.py
Cost-of-carry arbitrage bounds and the theory of storage
not run yet
Not saved: sign up to keep your code"""M10 exercise: cost-of-carry arbitrage bounds and the theory of storage (notes §10.2).
Continuous compounding, T in years. Storage cost u is a proportional rate per year.
Cash-and-carry (anyone can do it): buy spot, pay storage, sell futures, deliver.
No-arbitrage => F <= S e^{(r+u)T} (10.2)
Reverse cash-and-carry needs the physical commodity: only an inventory holder
(or someone who can borrow metal at lease rate l) can sell spot and buy futures.
With inventory: F >= S e^{(r+u-l)T}, l = convenience yield given up (10.3)
Without inventory: no lower bound from arbitrage.
Check with
cd exercises && .venv/bin/python -m pytest modules/M10-commodities/tests/test_m10_storage.py
"""
from __future__ import annotations
from dataclasses import dataclass
import numpy as np # noqa: F401
@dataclass(frozen=True)
class ArbResult:
lower: float # lower bound on F (-inf if no reverse arbitrage is available)
upper: float # upper bound on F
signal: str # 'cash_and_carry', 'reverse_cash_and_carry' or 'none'
profit: float # arbitrage profit per unit at T (0 if none)
def carry_upper_bound(spot: float, r: float, u: float, T: float, cost: float = 0.0) -> float:
"""Highest futures price with no cash-and-carry profit: S (1+k) e^{(r+u)T} / (1-k).
k = cost, a proportional transaction cost on each leg (spot purchase, futures sale).
"""
raise NotImplementedError
def carry_lower_bound(spot: float, r: float, u: float, T: float, lease: float, cost: float = 0.0) -> float:
"""Lowest futures price with no reverse cash-and-carry profit, for an inventory holder:
S (1-k) e^{(r+u-l)T} / (1+k), with l = lease (convenience yield forgone).
"""
raise NotImplementedError
def check_arbitrage(
spot: float,
futures: float,
T: float,
r_borrow: float,
r_lend: float,
u: float,
has_inventory: bool,
lease: float = 0.0,
cost: float = 0.0,
) -> ArbResult:
"""Classify a (spot, futures) pair against the bounds.
upper = carry_upper_bound(spot, r_borrow, u, T, cost)
lower = carry_lower_bound(spot, r_lend, u, T, lease, cost) if has_inventory else -inf
If futures > upper: signal 'cash_and_carry',
profit = F(1-k) - S(1+k) e^{(r_borrow+u)T}.
elif has_inventory and futures < lower: signal 'reverse_cash_and_carry',
profit = S(1-k) e^{(r_lend+u-l)T} - F(1+k).
else: 'none', profit 0.0.
Raise ValueError if r_borrow < r_lend.
"""
raise NotImplementedError
def implied_convenience_yield(spot: float, futures: float, r: float, u: float, T: float) -> float:
"""y = r + u - ln(F/S)/T."""
raise NotImplementedError
def convenience_yield(inventory, y0: float, b: float):
"""Stylised Working-Brennan marginal convenience yield y(I) = y0 exp(-b I) (array in, array out)."""
raise NotImplementedError
def storage_spread(inventory, r: float, u: float, y0: float, b: float):
"""Annualised log spread ln(F/S)/T = r + u - y(I) along the supply-of-storage curve (eq. 10.6)."""
raise NotImplementedError
No runs yet
The stub's functions raise
NotImplementedError until you write them. Fill in one, press
Run tests, and watch its tests turn green.Reference solution
Available after your first run, with an account approved for code runs.