Markets: A Rigorous Course

Exercise · m01_mechanics.py

Call auctions, index arithmetic, corporate actions

Three pieces of exchange arithmetic that every price series you download has already been through:

  • the pre-open and closing call auctions pick one price that maximises executable volume (notes.md 1.3, Proposition 1.1);
  • a free-float index is a ratio of market values to a divisor, and the divisor absorbs every basket change (notes.md 1.9, Proposition 1.4);
  • splits, bonuses and dividends make raw price series jump on the ex-date; adjusted series remove the jump with a multiplicative factor (notes.md 1.10, Proposition 1.5).

Check: cd exercises && .venv/bin/python -m pytest modules/M01-market-structure/tests

Tests (11)

  • test_schedule_worked_example
  • test_equilibrium_worked_example
  • test_no_cross
  • test_tie_breaks
  • test_equilibrium_maximises_volume_random_books
  • test_index_level_and_divisor
  • test_divisor_keeps_index_continuous_on_replacement
  • test_split_needs_no_divisor_change
  • test_factors
  • test_adjusted_series_small_example
  • test_adjusted_returns_equal_reinvested_total_return

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

not run yet

Not saved: sign up to keep your code
"""Exercise 1.5: call auctions, index arithmetic, corporate actions.

Three pieces of exchange arithmetic that every price series you download has
already been through:

  * the pre-open and closing call auctions pick one price that maximises
    executable volume (notes.md §1.3, Proposition 1.1);
  * a free-float index is a ratio of market values to a divisor, and the
    divisor absorbs every basket change (notes.md §1.9, Proposition 1.4);
  * splits, bonuses and dividends make raw price series jump on the ex-date;
    adjusted series remove the jump with a multiplicative factor
    (notes.md §1.10, Proposition 1.5).

Check:
    cd exercises && .venv/bin/python -m pytest modules/M01-market-structure/tests
"""

from __future__ import annotations

import numpy as np  # noqa: F401
import pandas as pd


# ----------------------------------------------------------------- call auction

def auction_schedule(buys, sells):
    """Cumulative demand and supply at every candidate price.

    buys, sells: lists of (price, qty). price None means a market order, which
    is willing to trade at any price.

    Candidate prices are the distinct LIMIT prices on either side, ascending.
    D(p) = market-buy qty + sum of buy-limit qty with limit >= p.
    S(p) = market-sell qty + sum of sell-limit qty with limit <= p.

    Returns (prices, demand, supply) as numpy float arrays of equal length.
    """
    raise NotImplementedError


def equilibrium_price(buys, sells, ref_price: float):
    """Single-price call auction (notes.md §1.3).

    Returns (price, volume, imbalance). Rules, applied in order:
      1. maximise executable volume V(p) = min(D(p), S(p));
      2. among ties, minimise the imbalance |D(p) - S(p)|;
      3. among ties, minimise |p - ref_price| (ref = previous close);
      4. if still tied, take the lower price.
    If no positive volume is possible (book does not cross, or empty),
    return (None, 0.0, None).
    """
    raise NotImplementedError


# ----------------------------------------------------------------- index arithmetic

def free_float_mcap(prices, shares, iwf) -> float:
    """Free-float market capitalisation sum_i P_i * N_i * f_i.

    P_i price, N_i shares outstanding, f_i investible weight factor in (0, 1].
    """
    raise NotImplementedError


def index_level(prices, shares, iwf, divisor: float) -> float:
    """Index level I = sum_i P_i N_i f_i / D (notes.md eq. (1.6))."""
    raise NotImplementedError


def new_divisor(old_divisor: float, mcap_before: float, mcap_after: float) -> float:
    """Divisor after a basket change (notes.md eq. (1.8)), chosen so the index level is unchanged:

        mcap_before / old_divisor = mcap_after / new_divisor.

    mcap_before and mcap_after are both evaluated at the SAME prices (the last
    close before the change), before and after the change in constituents,
    shares or IWFs.
    """
    raise NotImplementedError


# ----------------------------------------------------------------- corporate actions

def split_factor(old_face: float, new_face: float) -> float:
    """Price adjustment factor for a split of face value old_face -> new_face.

    A ₹10 -> ₹2 split multiplies the share count by 5, so pre-split prices are
    multiplied by 2/10 = 0.2.
    """
    raise NotImplementedError


def bonus_factor(new_shares: int, held_shares: int) -> float:
    """Factor for an a:b bonus (a new shares for every b held): b / (a + b).

    A 1:1 bonus gives 0.5.
    """
    raise NotImplementedError


def dividend_factor(cum_close: float, dividend: float) -> float:
    """Multiplicative dividend adjustment (P_cum - d) / P_cum (notes.md eq. (1.12)).

    P_cum is the last close on which the share still carried the dividend
    (the day before the ex-date).
    """
    raise NotImplementedError


def adjusted_prices(close: pd.Series, actions: list[tuple[str, float]]) -> pd.Series:
    """Back-adjust a close series indexed by dates.

    actions: list of (ex_date, factor). Every close strictly BEFORE ex_date is
    multiplied by factor; factors compound when there are several actions.
    Closes on or after the ex-date are unchanged. Return a new Series; do not
    modify the input.
    """
    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.