Skip to content

Terminal value, comparison of techniques (tv.compare)

Runs several terminal value techniques on one consistent set of inputs and returns each terminal value at the horizon N with its implied assumptions, side by side. The conclusion of a valuation often depends more on the technique than on any single input; this method shows by how much. Consistency is by construction: the rate of the terminal phase, the growth cap and the basis (enterprise or equity) are given once and apply to every technique.

How it works

Each technique is written as {"<method id>": {terms}}, where the terms are the inputs of that method without rate and growth_cap:

Technique Terms Shared inputs used
tv.gordon growth, next_flow or last_flow rate, growth cap
tv.value_driver nopat_next, growth, ronic rate, growth cap
tv.h_model last_flow, short_term_growth, long_term_growth, half_life rate, growth cap
tv.exit_multiple multiple, metric, metric_value, optional next_flow rate and growth cap when next_flow is given; the metric must be on the declared basis
tv.limited_horizon flow, periods, optional growth, residual_value rate, growth cap
tv.normalised_income income, capitalisation_rate, basis, optional growth growth cap; the capitalisation rate stays its own; basis must match the declared basis
tv.liquidation assets, liabilities, liquidation_costs, tax_on_gains, basis none; basis must match the declared basis

Each technique is computed by its own method, so a row of the comparison equals the result of that method run alone. Steps and conventions of each technique appear in explain, prefixed with its position and id, for example [1] tv.gordon: Terminal value at N. The first technique that fails returns its error; there are no partial results. A technique entry must have exactly one key, the method id; an entry with several keys is refused with a message that lists them.

Each row records method_version, the version of the technique that produced it. A numerical change in any tv.* method therefore requires a new version of tv.compare as well; a test pins the technique versions together with the version of tv.compare.

tv.normalised_income and tv.liquidation declare their own basis, checked against the basis of the comparison like the exit multiples (FM-E019 when they differ). Inside the comparison, tv.normalised_income keeps its own capitalisation rate, which is not the shared discount rate; explain.conventions states it. An exit multiple without next_flow has no implied growth, so it is not checked against the growth cap; explain.conventions states it when it applies.

Example

from decimal import Decimal

import fairmark as fm

r = fm.tv.compare(
    "0.10",
    "enterprise",
    [
        {"tv.gordon": {"growth": "0.02", "last_flow": 100}},
        {"tv.value_driver": {"nopat_next": 150, "growth": "0.02", "ronic": "0.12"}},
        {"tv.exit_multiple": {"multiple": "7.5", "metric": "ebitda", "metric_value": 180,
                              "next_flow": 95}},
    ],
    growth_cap="0.03",
)
values = [row["terminal_value"] for row in r.results]
assert r.results[0]["method_version"] == "1"
assert values[0] == Decimal("1275")
assert values[2] == Decimal("1350")
assert round(r.results[2]["output"]["implied_growth"], 6) == Decimal("0.029630")

try:  # P/E is an equity measure: never mixed into an enterprise comparison
    fm.tv.compare("0.10", "enterprise", [
        {"tv.exit_multiple": {"multiple": 12, "metric": "earnings", "metric_value": 50}},
    ])
except fm.FairmarkError as err:
    assert err.code == "FM-E019"

Inputs

Field Meaning Default
rate Discount rate of the terminal phase, decimal fraction per period, shared required
basis enterprise or equity, shared by every terminal value compared required
growth_cap Upper bound for the perpetual growth, decimal fraction per period, shared none, reported as absent
techniques List of {"<method id>": {terms}}, at least one; a technique may appear more than once required

Outputs

Field Meaning
basis The declared basis
results One row per technique, in the order requested: method, method_version, terminal_value and output, the technique's own output with its implied assumptions

Conventions (explicit, reported in explain.conventions)

Convention Options Default
Basis enterprise or equity, declared once none: must be given
Rate one rate for every technique that discounts; the capitalisation rate of tv.normalised_income is its own none: must be given
Growth cap one value for every growth-based technique, or none none, reported as absent

Errors

Code When
FM-E001 The list of techniques is empty, an entry has more than one key, a technique id is unknown, or a field is missing or unknown
FM-E019 An exit multiple, a normalised income or a liquidation is on the other basis than the one declared
Any code of a technique The first technique that fails returns its own error (for example FM-E004, FM-E018)

Accuracy

Each row has the accuracy of its method; the comparison adds no arithmetic.

Verified against

  • tests/golden/tv.compare/: seven techniques on one rate, basis and cap, each expected value computed on its own by an independent 50-digit script (scripts/reference_tv.py).
  • A Python test checks that each row equals the method run alone.