Markets: A Rigorous Course

Exercise · m10_storage.py

Cost-of-carry arbitrage bounds and the theory of storage

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

Tests (6)

  • test_upper_bound_and_cash_and_carry
  • test_no_lower_bound_without_inventory
  • test_transaction_costs_and_rate_spread_widen_band
  • test_gold_lease_futures_inside_band
  • test_implied_convenience_yield_roundtrip
  • test_supply_of_storage_curve_shape

Theory: M10 notes. Run with Ctrl/⌘+Enter.

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.

Create a free account

Reading is open to everyone. An account keeps your progress, answers, notes and code drafts, and gives you the study plan, the journals and the simulator. Running code needs an account approved for it.