Markets: A Rigorous Course

Exercise · m06a_bank_valuation.py

Valuing banks on book: residual income, the ROE tree, asset-quality identities

Exercise stubs. Replace each raise NotImplementedError with your code, then run the tests in ../tests. See notes.md 6A.10 (Propositions 6A.14–6A.16).

Conventions#

  • Book value B_0 is today's book; ROE_t = NI_t / B_{t-1} (beginning-of-year book).
  • Book grows by retained earnings: B_t = B_{t-1} (1 + g_t), where g_t = b_t * ROE_t.
  • Residual income RI_t = (ROE_t - k) B_{t-1}; value V_0 = B_0 + sum_t RI_t / (1 + k)^t.
  • Terminal value at T: residual income from year T+1 on is (roe_T1 - k) B_T growing at g_T1, so TV_T = (roe_T1 - k) B_T / (k - g_T1). With roe_T1 = k the terminal value is zero.

Tests (10)

  • test_steady_state_pb_values
  • test_residual_income_matches_closed_form
  • test_residual_income_brute_force_sum
  • test_roe_equal_k_means_book
  • test_implied_roe_inverts_pb
  • test_roe_tree
  • test_nnpa_identity_and_limits
  • test_book_hit
  • test_pb_grid_monotone
  • test_two_stage_pb_between_bounds

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

not run yet

Not saved: sign up to keep your code
"""M06A · Valuing banks on book: residual income, the ROE tree, asset-quality identities.

Exercise stubs. Replace each ``raise NotImplementedError`` with your code, then run the tests in
../tests. See notes.md §6A.10 (Propositions 6A.14–6A.16).

Conventions
-----------
* Book value B_0 is today's book; ROE_t = NI_t / B_{t-1} (beginning-of-year book).
* Book grows by retained earnings: B_t = B_{t-1} (1 + g_t), where g_t = b_t * ROE_t.
* Residual income RI_t = (ROE_t - k) B_{t-1}; value V_0 = B_0 + sum_t RI_t / (1 + k)^t.
* Terminal value at T: residual income from year T+1 on is (roe_T1 - k) B_T growing at g_T1, so
  TV_T = (roe_T1 - k) B_T / (k - g_T1). With roe_T1 = k the terminal value is zero.
"""

from __future__ import annotations

import numpy as np


def steady_state_pb(roe: float, g: float, k: float) -> float:
    """Justified P/B with constant ROE and growth g < k: (ROE - g) / (k - g) (Proposition 6A.15).
    Raise ValueError if k <= g."""
    raise NotImplementedError


def implied_roe(pb: float, g: float, k: float) -> float:
    """Sustainable ROE implied by a market P/B: invert steady_state_pb. Raise ValueError if k <= g."""
    raise NotImplementedError


def residual_income_value(b0: float, roe, growth, k: float,
                          terminal_roe: float | None = None,
                          terminal_g: float = 0.0) -> float:
    """Residual-income value of equity (Proposition 6A.14).

    roe, growth : sequences of length T (explicit years 1..T); RI_t = (roe_t - k) B_{t-1},
                  B_t = B_{t-1} (1 + growth_t).
    terminal_roe: ROE from year T+1 on (default: k, i.e. no terminal residual income).
    terminal_g  : growth of book (and residual income) after T; must be < k (else ValueError).
    Raise ValueError if roe and growth differ in length. Return V_0.
    """
    raise NotImplementedError


def roe_from_drivers(nim: float, fee: float, opex: float, credit_cost: float,
                     tax: float, leverage: float) -> tuple[float, float]:
    """ROA and ROE from P&L drivers, each a fraction of average total assets:
    PBT/A = nim + fee - opex - credit_cost; ROA = PBT/A (1 - tax); ROE = ROA * leverage.
    Return (roa, roe)."""
    raise NotImplementedError


def nnpa_ratio(gnpa: float, pcr: float) -> float:
    """Net NPA ratio from the gross NPA ratio and the provision coverage ratio (Proposition 6A.16):
    NNPA = (GNPA - provisions) / (gross loans - provisions). Derive the closed form first.
    Raise ValueError unless 0 <= gnpa < 1 and 0 <= pcr <= 1."""
    raise NotImplementedError


def book_hit_from_provisioning(gnpa: float, pcr_now: float, pcr_target: float,
                               loans_to_equity: float, tax: float = 0.0) -> float:
    """Fractional reduction in book value if coverage rises from pcr_now to pcr_target:
    extra provisions (pcr_target - pcr_now) * gnpa * L, after tax, divided by E = L / loans_to_equity."""
    raise NotImplementedError


def pb_grid(roes, ks, g: float) -> np.ndarray:
    """Matrix of steady-state P/B: rows = ROE values, columns = cost-of-equity values.
    Raise ValueError if any k <= g."""
    raise NotImplementedError


def two_stage_pb(roe_high: float, g_high: float, T: int, roe_long: float,
                 g_long: float, k: float) -> float:
    """P/B when ROE = roe_high and growth g_high for T years, then roe_long and g_long forever
    (use residual_income_value with b0 = 1)."""
    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.