Markets: A Rigorous Course

Exercise · m07_carry.py

Cost of carry, discrete dividends, value of a forward, arbitrage

Implement each function, then press Run tests.

Conventions (notes 7.0): rates are continuously compounded, T is in years (ACT/365), q is a continuous dividend yield, and the basis is b = F - S (Hull uses S - F). Use only the standard library (math).

Run locally

From the repository root, with the exercises environment installed:

cd exercises && .venv/bin/python -m pytest modules/M07-forwards-futures/tests/test_m07_carry.py

Tests (9)

  • test_fair_forward_reference_value
  • test_fair_forward_limits
  • test_implied_carry_round_trip
  • test_discrete_dividends_hull_example
  • test_dividends_after_expiry_ignored
  • test_forward_value_hull_and_identity
  • test_arbitrage_detector_directions
  • test_locked_in_profit_is_path_independent
  • test_roll_and_calendar_carry_nifty_5nov2026

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

not run yet

Not saved: sign up to keep your code
"""M07 exercise 1: cost of carry, discrete dividends, value of a forward, arbitrage.

Implement each function, then run from the repo root:

    cd exercises && .venv/bin/python -m pytest modules/M07-forwards-futures/tests/test_m07_carry.py

Conventions (notes §7.0): rates are continuously compounded, T is in years
(ACT/365), q is a continuous dividend yield, and the basis is b = F - S
(Hull uses S - F). Use only the standard library (math).
"""

from __future__ import annotations

import math
from typing import Iterable, Sequence


def fair_forward(S: float, r: float, q: float, T: float) -> float:
    """No-arbitrage forward price F0 = S0 exp((r - q) T)  (notes Prop. 7.1, eq. 7.1).

    S: spot price; r: risk-free rate; q: dividend yield; T: years to delivery.
    """
    raise NotImplementedError


def pv_dividends(dividends: Iterable[tuple[float, float]], r: float, T: float) -> float:
    """Present value I = sum_i D_i exp(-r t_i) of the dividends paid during the life of the forward.

    dividends: iterable of (t_i, D_i) with t_i in years from today. Include only
    0 < t_i <= T; dividends after delivery do not affect the forward.
    """
    raise NotImplementedError


def fair_forward_discrete(S: float, r: float, T: float, dividends: Sequence[tuple[float, float]]) -> float:
    """Forward price with known cash dividends: F0 = (S0 - I) exp(r T)  (Prop. 7.2, eq. 7.3),
    where I = pv_dividends(dividends, r, T)."""
    raise NotImplementedError


def implied_carry(F: float, S: float, T: float, q: float = 0.0) -> float:
    """Implied financing rate r_impl = ln(F/S)/T + q (inverts fair_forward in r).

    With q = 0 this returns the implied net carry r - q.
    """
    raise NotImplementedError


def forward_value(K: float, S: float, r: float, q: float, T: float, position: int = 1) -> float:
    """Value today of an existing forward with delivery price K and T years left (Prop. 7.3).

    Long (position=+1): f = S e^{-qT} - K e^{-rT}, equivalently (F_t - K) e^{-rT}.
    Short (position=-1): the negative.
    """
    raise NotImplementedError


def arbitrage(F_mkt: float, S: float, r: float, q: float, T: float, cost: float = 0.0, lot_size: int = 1) -> dict:
    """Detect a cost-of-carry arbitrage and return the strategy that locks it in (§7.1.3, Prop. 7.4).

    cost: total frictions per unit of underlying, in rupees at time T. An
    arbitrage exists only when |F_mkt - fair| > cost.

    Return a dict with keys
      'strategy': 'cash_and_carry' if F_mkt - fair > cost (buy spot with borrowed
                  money, sell the forward), 'reverse_cash_and_carry' if
                  fair - F_mkt > cost (short spot, invest proceeds, buy the
                  forward), otherwise 'none';
      'fair': fair_forward(S, r, q, T);
      'profit_at_T': |F_mkt - fair| - cost per unit (0.0 if 'none');
      'profit_pv': profit_at_T * exp(-r T);
      'profit_per_lot_at_T': profit_at_T * lot_size;
      'spot_units': spot units held today per unit of forward: +e^{-qT} for
                    cash and carry, -e^{-qT} for the reverse, 0.0 for none.
    """
    raise NotImplementedError


def carry_trade_pnl(S0: float, S_T: float, r: float, q: float, T: float, F_mkt: float, strategy: str) -> float:
    """Terminal P&L per unit of the arbitrage trade, computed leg by leg.

    Cash and carry: borrow S0 e^{-qT}, buy e^{-qT} units of the asset and
    reinvest dividends so that exactly 1 unit is held at T (worth S_T); repay
    S0 e^{-qT} e^{rT}; the short forward pays F_mkt - S_T.
    'reverse_cash_and_carry' is the mirror image (negate every leg).
    Raise ValueError for any other strategy string.
    The answer must not depend on S_T.
    """
    raise NotImplementedError


def roll_cost(F_near: float, F_far: float, qty: float) -> float:
    """Rupee cost of rolling a long futures position: sell qty at F_near, buy qty at F_far.

    Returns (F_far - F_near) * qty; positive means the roll costs the long money (contango).
    """
    raise NotImplementedError


def calendar_implied_carry(F_near: float, F_far: float, T_near: float, T_far: float) -> float:
    """Annualised net carry r - q implied by a calendar spread: ln(F_far / F_near) / (T_far - T_near)."""
    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.