Markets: A Rigorous Course

Exercise · m06_dcf.py

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.

Tests (9)

  • test_dcf_reference_values
  • test_dcf_rejects_bad_terminal
  • test_dcf_equals_annuity_plus_gordon
  • test_growing_annuity_against_sum_and_limit
  • test_reverse_dcf
  • test_reverse_dcf_round_trip_random
  • test_value_driver_identity
  • test_growth_creates_value_iff_roic_exceeds_wacc
  • test_justified_pe_and_cost_of_capital

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

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.

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.