Exercise · m06a_bank_valuation.py
Valuing banks on book: residual income, the ROE tree, asset-quality identities
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.