Exercise · m06_dcf.py
Discounted cash flow, value-driver identity, justified multiples, cost of capital
not run yet
Not saved: sign up to keep your code"""M06 · Discounted cash flow, value-driver identity, justified multiples, cost of capital.
Exercise stubs. Replace each ``raise NotImplementedError`` with your code, then run
cd exercises && .venv/bin/python -m pytest modules/M06-valuation-fundamentals/tests
See notes.md §6.3–§6.6.
Timing convention used throughout (it matters for the test values):
* ``fcf0`` is the cash flow of the year just ended (t = 0). It is NOT discounted.
* Explicit-period cash flow in year t is FCF_t = fcf0 * (1 + growth)**t, t = 1..N.
* Terminal value at the end of year N is TV_N = FCF_N * (1 + g_T) / (wacc - g_T),
i.e. a Gordon growing perpetuity whose first cash flow is FCF_{N+1}.
* All cash flows are discounted at (1 + wacc)**t with year-end timing.
"""
from __future__ import annotations
def growing_annuity_pv(c1: float, r: float, g: float, n: int) -> float:
"""Closed-form PV of n year-end cash flows c1, c1(1+g), ..., c1(1+g)^(n-1) at rate r.
PV = c1/(r-g) * [1 - ((1+g)/(1+r))**n] (r != g), PV = n*c1/(1+r) (r == g).
Derive it first: Proposition 6.3 in the notes. Use math.isclose(r, g) for the special case.
"""
raise NotImplementedError
def gordon_value(c1: float, r: float, g: float) -> float:
"""Growing perpetuity c1/(r-g) (Proposition 6.4). Raise ValueError if r <= g."""
raise NotImplementedError
def dcf_value(fcf0: float, growth: float, wacc: float, terminal_g: float, years: int = 5):
"""Two-stage FCFF DCF. Returns the tuple (value, tv_share).
value = sum_{t=1}^{N} FCF_t/(1+wacc)^t + TV_N/(1+wacc)^N (convention above)
tv_share = [TV_N/(1+wacc)^N] / value
Raise ValueError if wacc <= terminal_g.
Reference: dcf_value(100, 0.15, 0.12, 0.05, 5) -> (2253.5898, 0.7597).
"""
raise NotImplementedError
def reverse_dcf(target_value: float, fcf0: float, wacc: float, terminal_g: float,
years: int = 5, lo: float = -0.5, hi: float = 1.0) -> float:
"""Implied explicit-period growth g* with dcf_value(fcf0, g*, wacc, terminal_g, years)[0] == target_value.
Value is strictly increasing in growth when fcf0 > 0, so a bracketing root-finder on
[lo, hi] (scipy.optimize.brentq, or your own bisection) finds the unique root.
Use a tight tolerance (xtol ~ 1e-12).
Reference: reverse_dcf(3000, 100, 0.12, 0.05, 5) -> 0.224344.
"""
raise NotImplementedError
def value_driver(nopat1: float, g: float, roic: float, wacc: float) -> float:
"""Value-driver formula V = NOPAT_1 (1 - g/ROIC) / (WACC - g) (Proposition 6.6).
Raise ValueError if wacc <= g.
"""
raise NotImplementedError
def value_driver_by_summation(nopat1: float, g: float, roic: float, wacc: float,
horizon: int = 3000) -> float:
"""Brute-force check of Proposition 6.6.
Year t = 1..horizon: NOPAT_t = nopat1 (1+g)^(t-1); net investment I_t = (g/roic) NOPAT_t;
FCF_t = NOPAT_t - I_t. Return sum_t FCF_t / (1+wacc)^t as a float.
"""
raise NotImplementedError
def value_driver_dg(nopat1: float, g: float, roic: float, wacc: float) -> float:
"""Analytic derivative dV/dg = NOPAT_1 (1 - WACC/ROIC) / (WACC - g)^2 (Proposition 6.7)."""
raise NotImplementedError
def justified_pe(payout: float, g: float, r: float, trailing: bool = True) -> float:
"""Justified P/E from the Gordon model (Proposition 6.9).
Forward P0/E1 = payout/(r-g). Trailing P0/E0 = payout (1+g)/(r-g). Raise ValueError if r <= g.
Reference: justified_pe(0.5, 0.08, 0.13) -> 10.8.
"""
raise NotImplementedError
def cost_of_equity(rf: float, beta: float, mature_erp: float, crp: float, lam: float | None = None) -> float:
"""CAPM cost of equity with a country risk premium (§6.5).
lam None: k_e = rf + beta (mature_erp + crp). Otherwise: k_e = rf + beta*mature_erp + lam*crp.
Reference: cost_of_equity(0.065, 1.0, 0.042, 0.0272) -> 0.1342 (illustrative rf).
"""
raise NotImplementedError
def wacc(equity_value: float, debt_value: float, k_e: float, k_d: float, tax_rate: float) -> float:
"""WACC = E/(D+E) k_e + D/(D+E) k_d (1 - t), market-value weights (§6.5)."""
raise NotImplementedError
def fundamental_growth(reinvestment_rate: float, roic: float) -> float:
"""Sustainable growth g = reinvestment rate x ROIC (Proposition 6.5)."""
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.