Skip to content

Core Math API

Time value of money, discount rate construction, and statistical functions.

Time Value of Money

time_value

Time value of money functions for valuation calculations.

Implements present value, future value, annuity, perpetuity, growing annuity, and terminal value calculations from Chapter 2 of the Ascent Partners textbook.

All functions return a ValuationResult dict with
  • value: The computed result
  • method: The calculation method used
  • formula_reference: Reference to the mathematical formula
  • steps: Step-by-step calculation breakdown
  • assumptions: List of assumptions made during calculation

Classes

ValuationResult(**data: Any)

Bases: BaseModel

Standardized result container for all valuation calculations.

Source code in src/core/time_value.py
def __init__(self, **data: Any) -> None:
    super().__init__(**data)
Functions
__getitem__(key: str) -> Any

Allow dict-style access to result fields.

Source code in src/core/time_value.py
def __getitem__(self, key: str) -> Any:
    """Allow dict-style access to result fields."""
    return getattr(self, key)
to_dict() -> dict[str, Any]

Convert to plain dictionary.

Source code in src/core/time_value.py
def to_dict(self) -> dict[str, Any]:
    """Convert to plain dictionary."""
    return self.model_dump()

TVMInputs

Bases: BaseModel

Validated inputs for time value of money calculations.

Functions

present_value(future_value: float, discount_rate: float, periods: int) -> ValuationResult

Calculate the present value of a single future cash flow.

Formula

PV = FV / (1 + r)^n

Parameters:

Name Type Description Default
future_value float

The future cash flow amount (FV)

required
discount_rate float

The discount rate per period (r), as a decimal

required
periods int

Number of periods (n)

required

Returns:

Type Description
ValuationResult

ValuationResult with computed present value

Raises:

Type Description
ValueError

If future_value is negative, discount_rate < -1, or periods < 0

Book Reference

Chapter 2, Section 2.1 — Present Value of a Single Sum Chapter 2, Basic Exercise Q1: PV of $500,000 in 8 years at 10% = $233,253

Source code in src/core/time_value.py
def present_value(
    future_value: float,
    discount_rate: float,
    periods: int,
) -> ValuationResult:
    """Calculate the present value of a single future cash flow.

    Formula:
        PV = FV / (1 + r)^n

    Parameters:
        future_value: The future cash flow amount (FV)
        discount_rate: The discount rate per period (r), as a decimal
        periods: Number of periods (n)

    Returns:
        ValuationResult with computed present value

    Raises:
        ValueError: If future_value is negative, discount_rate < -1, or periods < 0

    Book Reference:
        Chapter 2, Section 2.1 — Present Value of a Single Sum
        Chapter 2, Basic Exercise Q1: PV of $500,000 in 8 years at 10% = $233,253
    """
    inputs = TVMInputs(future_value=future_value, discount_rate=discount_rate, periods=periods)

    if inputs.future_value is None or inputs.discount_rate is None or inputs.periods is None:
        raise ValueError("future_value, discount_rate, and periods are required")

    if inputs.future_value < 0:
        raise ValueError("future_value must be non-negative")
    if inputs.discount_rate < -1:
        raise ValueError("discount_rate must be >= -1.0")
    if inputs.periods < 0:
        raise ValueError("periods must be non-negative")

    fv = inputs.future_value
    r = inputs.discount_rate
    n = inputs.periods

    discount_factor = (1 + r) ** n
    pv = fv / discount_factor

    return ValuationResult(
        value=round(pv, 2),
        method="Present Value of Single Sum",
        formula_reference="PV = FV / (1 + r)^n",
        steps=[
            f"Future Value (FV) = ${fv:,.2f}",
            f"Discount Rate (r) = {r:.2%}",
            f"Number of Periods (n) = {n}",
            f"Discount Factor = (1 + {r})^{n} = {discount_factor:,.6f}",
            f"PV = ${fv:,.2f} / {discount_factor:,.6f} = ${pv:,.2f}",
        ],
        assumptions=[
            "Cash flow occurs at end of period",
            "Discount rate is constant across all periods",
            "No intermediate cash flows",
        ],
    )

future_value(present_value: float, discount_rate: float, periods: int) -> ValuationResult

Calculate the future value of a present amount.

Formula

FV = PV * (1 + r)^n

Parameters:

Name Type Description Default
present_value float

The present amount (PV)

required
discount_rate float

The growth/discount rate per period (r), as a decimal

required
periods int

Number of periods (n)

required

Returns:

Type Description
ValuationResult

ValuationResult with computed future value

Raises:

Type Description
ValueError

If present_value is negative, discount_rate < -1, or periods < 0

Book Reference

Chapter 2, Section 2.2 — Future Value of a Single Sum Chapter 2, Basic Exercise Q2: FV $1M, PV $620,921, 5 years -> discount rate = 10%

Source code in src/core/time_value.py
def future_value(
    present_value: float,
    discount_rate: float,
    periods: int,
) -> ValuationResult:
    """Calculate the future value of a present amount.

    Formula:
        FV = PV * (1 + r)^n

    Parameters:
        present_value: The present amount (PV)
        discount_rate: The growth/discount rate per period (r), as a decimal
        periods: Number of periods (n)

    Returns:
        ValuationResult with computed future value

    Raises:
        ValueError: If present_value is negative, discount_rate < -1, or periods < 0

    Book Reference:
        Chapter 2, Section 2.2 — Future Value of a Single Sum
        Chapter 2, Basic Exercise Q2: FV $1M, PV $620,921, 5 years -> discount rate = 10%
    """
    inputs = TVMInputs(present_value=present_value, discount_rate=discount_rate, periods=periods)

    if inputs.present_value is None or inputs.discount_rate is None or inputs.periods is None:
        raise ValueError("present_value, discount_rate, and periods are required")

    if inputs.present_value < 0:
        raise ValueError("present_value must be non-negative")
    if inputs.discount_rate < -1:
        raise ValueError("discount_rate must be >= -1.0")
    if inputs.periods < 0:
        raise ValueError("periods must be non-negative")

    pv = inputs.present_value
    r = inputs.discount_rate
    n = inputs.periods

    compounding_factor = (1 + r) ** n
    fv = pv * compounding_factor

    return ValuationResult(
        value=round(fv, 2),
        method="Future Value of Single Sum",
        formula_reference="FV = PV * (1 + r)^n",
        steps=[
            f"Present Value (PV) = ${pv:,.2f}",
            f"Rate (r) = {r:.2%}",
            f"Number of Periods (n) = {n}",
            f"Compounding Factor = (1 + {r})^{n} = {compounding_factor:,.6f}",
            f"FV = ${pv:,.2f} * {compounding_factor:,.6f} = ${fv:,.2f}",
        ],
        assumptions=[
            "Compounding occurs at end of each period",
            "Rate is constant across all periods",
            "No intermediate withdrawals or additions",
        ],
    )

annuity_pv(payment: float, discount_rate: float, periods: int) -> ValuationResult

Calculate the present value of an ordinary annuity.

Formula

PV = PMT * [1 - (1 + r)^(-n)] / r

Parameters:

Name Type Description Default
payment float

The periodic payment amount (PMT)

required
discount_rate float

The discount rate per period (r), as a decimal

required
periods int

Number of periods (n)

required

Returns:

Type Description
ValuationResult

ValuationResult with computed annuity present value

Raises:

Type Description
ValueError

If payment is negative, discount_rate <= -1 or == 0, or periods < 0

Book Reference

Chapter 2, Section 2.3 — Present Value of an Annuity Chapter 2, Intermediate Exercise Q1: Annuity $50,000 for 10 years at 15% = $250,937

Source code in src/core/time_value.py
def annuity_pv(
    payment: float,
    discount_rate: float,
    periods: int,
) -> ValuationResult:
    """Calculate the present value of an ordinary annuity.

    Formula:
        PV = PMT * [1 - (1 + r)^(-n)] / r

    Parameters:
        payment: The periodic payment amount (PMT)
        discount_rate: The discount rate per period (r), as a decimal
        periods: Number of periods (n)

    Returns:
        ValuationResult with computed annuity present value

    Raises:
        ValueError: If payment is negative, discount_rate <= -1 or == 0, or periods < 0

    Book Reference:
        Chapter 2, Section 2.3 — Present Value of an Annuity
        Chapter 2, Intermediate Exercise Q1: Annuity $50,000 for 10 years at 15% = $250,937
    """
    inputs = TVMInputs(payment=payment, discount_rate=discount_rate, periods=periods)

    if inputs.payment is None or inputs.discount_rate is None or inputs.periods is None:
        raise ValueError("payment, discount_rate, and periods are required")

    if inputs.payment < 0:
        raise ValueError("payment must be non-negative")
    if inputs.discount_rate <= -1:
        raise ValueError("discount_rate must be > -1.0")
    if math.isclose(inputs.discount_rate, 0.0, abs_tol=1e-12):
        raise ValueError("discount_rate cannot be zero for annuity calculation")
    if inputs.periods < 0:
        raise ValueError("periods must be non-negative")

    pmt = inputs.payment
    r = inputs.discount_rate
    n = inputs.periods

    annuity_factor = (1 - (1 + r) ** (-n)) / r
    pv = pmt * annuity_factor

    return ValuationResult(
        value=round(pv, 2),
        method="Present Value of Ordinary Annuity",
        formula_reference="PV = PMT * [1 - (1 + r)^(-n)] / r",
        steps=[
            f"Payment (PMT) = ${pmt:,.2f}",
            f"Discount Rate (r) = {r:.2%}",
            f"Number of Periods (n) = {n}",
            f"Annuity Factor = [1 - (1 + {r})^(-{n})] / {r} = {annuity_factor:,.6f}",
            f"PV = ${pmt:,.2f} * {annuity_factor:,.6f} = ${pv:,.2f}",
        ],
        assumptions=[
            "Payments occur at end of each period (ordinary annuity)",
            "Payment amount is constant",
            "Discount rate is constant across all periods",
        ],
    )

perpetuity_pv(payment: float, discount_rate: float) -> ValuationResult

Calculate the present value of a perpetuity.

Formula

PV = PMT / r

Parameters:

Name Type Description Default
payment float

The periodic payment amount (PMT)

required
discount_rate float

The discount rate per period (r), as a decimal

required

Returns:

Type Description
ValuationResult

ValuationResult with computed perpetuity present value

Raises:

Type Description
ValueError

If payment is negative or discount_rate <= 0

Book Reference

Chapter 2, Section 2.4 — Present Value of a Perpetuity Chapter 3, Royalty Relief: $10M revenue, 4% royalty, 15% discount = $2,666,667

Source code in src/core/time_value.py
def perpetuity_pv(
    payment: float,
    discount_rate: float,
) -> ValuationResult:
    """Calculate the present value of a perpetuity.

    Formula:
        PV = PMT / r

    Parameters:
        payment: The periodic payment amount (PMT)
        discount_rate: The discount rate per period (r), as a decimal

    Returns:
        ValuationResult with computed perpetuity present value

    Raises:
        ValueError: If payment is negative or discount_rate <= 0

    Book Reference:
        Chapter 2, Section 2.4 — Present Value of a Perpetuity
        Chapter 3, Royalty Relief: $10M revenue, 4% royalty, 15% discount = $2,666,667
    """
    inputs = TVMInputs(payment=payment, discount_rate=discount_rate)

    if inputs.payment is None or inputs.discount_rate is None:
        raise ValueError("payment and discount_rate are required")

    if inputs.payment < 0:
        raise ValueError("payment must be non-negative")
    if inputs.discount_rate <= 0:
        raise ValueError("discount_rate must be positive for perpetuity calculation")

    pmt = inputs.payment
    r = inputs.discount_rate

    pv = pmt / r

    return ValuationResult(
        value=round(pv, 2),
        method="Present Value of Perpetuity",
        formula_reference="PV = PMT / r",
        steps=[
            f"Payment (PMT) = ${pmt:,.2f}",
            f"Discount Rate (r) = {r:.2%}",
            f"PV = ${pmt:,.2f} / {r} = ${pv:,.2f}",
        ],
        assumptions=[
            "Payments continue indefinitely (perpetual)",
            "Payment amount is constant",
            "Discount rate is constant",
            "First payment occurs one period from now",
        ],
    )

growing_annuity_pv(payment: float, discount_rate: float, growth_rate: float, periods: int) -> ValuationResult

Calculate the present value of a growing annuity.

Formula (when r != g): PV = PMT * [1 - ((1 + g) / (1 + r))^n] / (r - g)

Formula (when r == g): PV = PMT * n / (1 + r)

Parameters:

Name Type Description Default
payment float

The first period payment amount (PMT)

required
discount_rate float

The discount rate per period (r), as a decimal

required
growth_rate float

The growth rate of payments per period (g), as a decimal

required
periods int

Number of periods (n)

required

Returns:

Type Description
ValuationResult

ValuationResult with computed growing annuity present value

Raises:

Type Description
ValueError

If payment is negative, periods < 0, or r < -1, g < -1

Book Reference

Chapter 2, Section 2.5 — Present Value of a Growing Annuity

Source code in src/core/time_value.py
def growing_annuity_pv(
    payment: float,
    discount_rate: float,
    growth_rate: float,
    periods: int,
) -> ValuationResult:
    """Calculate the present value of a growing annuity.

    Formula (when r != g):
        PV = PMT * [1 - ((1 + g) / (1 + r))^n] / (r - g)

    Formula (when r == g):
        PV = PMT * n / (1 + r)

    Parameters:
        payment: The first period payment amount (PMT)
        discount_rate: The discount rate per period (r), as a decimal
        growth_rate: The growth rate of payments per period (g), as a decimal
        periods: Number of periods (n)

    Returns:
        ValuationResult with computed growing annuity present value

    Raises:
        ValueError: If payment is negative, periods < 0, or r < -1, g < -1

    Book Reference:
        Chapter 2, Section 2.5 — Present Value of a Growing Annuity
    """
    inputs = TVMInputs(
        payment=payment, discount_rate=discount_rate,
        growth_rate=growth_rate, periods=periods,
    )

    if (inputs.payment is None or inputs.discount_rate is None
            or inputs.growth_rate is None or inputs.periods is None):
        raise ValueError("payment, discount_rate, growth_rate, and periods are required")

    if inputs.payment < 0:
        raise ValueError("payment must be non-negative")
    if inputs.discount_rate < -1:
        raise ValueError("discount_rate must be >= -1.0")
    if inputs.growth_rate < -1:
        raise ValueError("growth_rate must be >= -1.0")
    if inputs.periods < 0:
        raise ValueError("periods must be non-negative")

    pmt = inputs.payment
    r = inputs.discount_rate
    g = inputs.growth_rate
    n = inputs.periods

    if math.isclose(r, g, abs_tol=1e-12):
        pv = pmt * n / (1 + r)
        formula_used = "PV = PMT * n / (1 + r) [special case: r = g]"
        steps = [
            f"Payment (PMT) = ${pmt:,.2f}",
            f"Discount Rate (r) = {r:.2%}",
            f"Growth Rate (g) = {g:.2%}",
            f"Number of Periods (n) = {n}",
            "Special case: r = g, using PV = PMT * n / (1 + r)",
            f"PV = ${pmt:,.2f} * {n} / (1 + {r}) = ${pv:,.2f}",
        ]
    else:
        ratio = (1 + g) / (1 + r)
        pv = pmt * (1 - ratio ** n) / (r - g)
        formula_used = "PV = PMT * [1 - ((1 + g) / (1 + r))^n] / (r - g)"
        steps = [
            f"Payment (PMT) = ${pmt:,.2f}",
            f"Discount Rate (r) = {r:.2%}",
            f"Growth Rate (g) = {g:.2%}",
            f"Number of Periods (n) = {n}",
            f"Ratio (1+g)/(1+r) = {ratio:.6f}",
            f"PV = ${pmt:,.2f} * [1 - {ratio:.6f}^{n}] / ({r} - {g}) = ${pv:,.2f}",
        ]

    return ValuationResult(
        value=round(pv, 2),
        method="Present Value of Growing Annuity",
        formula_reference=formula_used,
        steps=steps,
        assumptions=[
            "First payment occurs at end of period 1",
            "Payments grow at constant rate g",
            "Discount rate is constant",
            f"Payment grows from ${pmt:,.2f} in period 1 to ${pmt * (1 + g) ** (n - 1):,.2f} in period {n}",
        ],
    )

terminal_value(final_year_cashflow: float, perpetual_growth_rate: float, discount_rate: float, method: Literal['gordon_growth', 'exit_multiple'] = 'gordon_growth', exit_multiple: float | None = None) -> ValuationResult

Calculate terminal value using Gordon Growth or Exit Multiple method.

Gordon Growth Formula

TV = FCF * (1 + g) / (r - g)

Exit Multiple Formula

TV = FCF * Exit Multiple

Parameters:

Name Type Description Default
final_year_cashflow float

The final year projected cash flow (FCF)

required
perpetual_growth_rate float

The perpetual growth rate (g), as a decimal

required
discount_rate float

The discount rate (r), as a decimal

required
method Literal['gordon_growth', 'exit_multiple']

Calculation method ("gordon_growth" or "exit_multiple")

'gordon_growth'
exit_multiple float | None

Required when method="exit_multiple"

None

Returns:

Type Description
ValuationResult

ValuationResult with computed terminal value

Raises:

Type Description
ValueError

If parameters are invalid for the chosen method

Book Reference

Chapter 2, Section 2.6 — Terminal Value Calculations Chapter 5, DCF Methodology — Terminal Value in Multi-Period DCF

Source code in src/core/time_value.py
def terminal_value(
    final_year_cashflow: float,
    perpetual_growth_rate: float,
    discount_rate: float,
    method: Literal["gordon_growth", "exit_multiple"] = "gordon_growth",
    exit_multiple: float | None = None,
) -> ValuationResult:
    """Calculate terminal value using Gordon Growth or Exit Multiple method.

    Gordon Growth Formula:
        TV = FCF * (1 + g) / (r - g)

    Exit Multiple Formula:
        TV = FCF * Exit Multiple

    Parameters:
        final_year_cashflow: The final year projected cash flow (FCF)
        perpetual_growth_rate: The perpetual growth rate (g), as a decimal
        discount_rate: The discount rate (r), as a decimal
        method: Calculation method ("gordon_growth" or "exit_multiple")
        exit_multiple: Required when method="exit_multiple"

    Returns:
        ValuationResult with computed terminal value

    Raises:
        ValueError: If parameters are invalid for the chosen method

    Book Reference:
        Chapter 2, Section 2.6 — Terminal Value Calculations
        Chapter 5, DCF Methodology — Terminal Value in Multi-Period DCF
    """
    inputs = TVMInputs(
        final_year_cashflow=final_year_cashflow,
        perpetual_growth_rate=perpetual_growth_rate,
        discount_rate=discount_rate,
        exit_multiple=exit_multiple,
    )

    if inputs.final_year_cashflow is None or inputs.perpetual_growth_rate is None or inputs.discount_rate is None:
        raise ValueError("final_year_cashflow, perpetual_growth_rate, and discount_rate are required")

    if inputs.final_year_cashflow < 0:
        raise ValueError("final_year_cashflow must be non-negative")
    if inputs.discount_rate <= -1:
        raise ValueError("discount_rate must be > -1.0")

    fcf = inputs.final_year_cashflow
    g = inputs.perpetual_growth_rate
    r = inputs.discount_rate

    if method == METHOD_GORDON_GROWTH:
        if r <= g:
            raise ValueError(
                f"discount_rate ({r:.2%}) must be greater than perpetual_growth_rate ({g:.2%}) "
                "for Gordon Growth model"
            )

        tv = fcf * (1 + g) / (r - g)

        return ValuationResult(
            value=round(tv, 2),
            method="Gordon Growth Model",
            formula_reference="TV = FCF * (1 + g) / (r - g)",
            steps=[
                f"Final Year Cash Flow (FCF) = ${fcf:,.2f}",
                f"Perpetual Growth Rate (g) = {g:.2%}",
                f"Discount Rate (r) = {r:.2%}",
                f"Next Year Cash Flow = ${fcf:,.2f} * (1 + {g}) = ${fcf * (1 + g):,.2f}",
                f"TV = ${fcf * (1 + g):,.2f} / ({r} - {g}) = ${tv:,.2f}",
            ],
            assumptions=[
                "Cash flows grow at constant perpetual rate g",
                "Discount rate exceeds growth rate (r > g)",
                "Terminal value is calculated at end of projection period",
                "Growth rate is sustainable in perpetuity (typically <= long-term GDP growth)",
            ],
        )

    if method == METHOD_EXIT_MULTIPLE:
        if exit_multiple is None:
            raise ValueError("exit_multiple is required when method='exit_multiple'")
        if exit_multiple <= 0:
            raise ValueError("exit_multiple must be positive")

        tv = fcf * exit_multiple

        return ValuationResult(
            value=round(tv, 2),
            method="Exit Multiple Method",
            formula_reference="TV = FCF * Exit Multiple",
            steps=[
                f"Final Year Cash Flow (FCF) = ${fcf:,.2f}",
                f"Exit Multiple = {exit_multiple:.2f}x",
                f"TV = ${fcf:,.2f} * {exit_multiple:.2f} = ${tv:,.2f}",
            ],
            assumptions=[
                "Exit multiple is based on comparable company analysis",
                "Multiple reflects market conditions at terminal date",
                "Terminal value is calculated at end of projection period",
            ],
        )

    raise ValueError(f"Unknown method: {method}. Use 'gordon_growth' or 'exit_multiple'")

present_value_of_series(cash_flows: list[float], discount_rate: float) -> ValuationResult

Calculate present value of a series of uneven cash flows.

Formula

PV = sum(CF_t / (1 + r)^t) for t = 1 to n

Parameters:

Name Type Description Default
cash_flows list[float]

List of cash flows for each period (index 0 = period 1)

required
discount_rate float

The discount rate (r), as a decimal

required

Returns:

Type Description
ValuationResult

ValuationResult with computed present value and per-period breakdown

Raises:

Type Description
ValueError

If cash_flows is empty or discount_rate is invalid

Book Reference

Chapter 2, Section 2.1 — Present Value (general case) Chapter 4 — Used in all income-based methods

Source code in src/core/time_value.py
def present_value_of_series(
    cash_flows: list[float],
    discount_rate: float,
) -> ValuationResult:
    """Calculate present value of a series of uneven cash flows.

    Formula:
        PV = sum(CF_t / (1 + r)^t) for t = 1 to n

    Parameters:
        cash_flows: List of cash flows for each period (index 0 = period 1)
        discount_rate: The discount rate (r), as a decimal

    Returns:
        ValuationResult with computed present value and per-period breakdown

    Raises:
        ValueError: If cash_flows is empty or discount_rate is invalid

    Book Reference:
        Chapter 2, Section 2.1 — Present Value (general case)
        Chapter 4 — Used in all income-based methods
    """
    if not cash_flows:
        raise ValueError("cash_flows must not be empty")
    if discount_rate <= -1:
        raise ValueError("discount_rate must be > -1.0")

    pv = 0.0
    steps = []
    pv_by_period = []
    steps.append(f"Discount Rate (r) = {discount_rate:.2%}")
    steps.append(f"Number of Periods = {len(cash_flows)}")

    for t, cf in enumerate(cash_flows, 1):
        period_pv = cf / (1 + discount_rate) ** t
        pv += period_pv
        pv_by_period.append({
            "period": t,
            "cash_flow": cf,
            "present_value": round(period_pv, 2),
        })
        steps.append(f"Period {t}: CF=${cf:,.2f} / (1+{discount_rate})^{t} = ${period_pv:,.2f}")

    steps.append(f"Total PV = ${pv:,.2f}")

    return ValuationResult(
        value=round(pv, 2),
        method="Present Value of Cash Flow Series",
        formula_reference="PV = sum(CF_t / (1 + r)^t)",
        steps=steps,
        assumptions=[
            "Cash flows occur at end of each period",
            f"Discount rate is constant at {discount_rate:.2%}",
            f"Projection period is {len(cash_flows)} years",
        ],
        present_value=round(pv, 2),
        pv_by_period=pv_by_period,
    )

Discount Rates

discount_rates

Discount rate construction functions for valuation calculations.

Implements build-up method, CAPM, WACC, tax amortization benefit, control premium, DLOM (Finnerty), and currency-adjusted discount rates from Chapters 2, 3, and 4 of the Ascent Partners textbook.

All functions return a ValuationResult dict with
  • value: The computed discount rate or premium
  • method: The calculation method used
  • formula_reference: Reference to the mathematical formula
  • steps: Step-by-step calculation breakdown
  • assumptions: List of assumptions made during calculation

Classes

DiscountRateInputs

Bases: BaseModel

Validated inputs for discount rate calculations.

Functions

build_up_discount_rate(risk_free_rate: float, equity_risk_premium: float, size_premium: float = 0.0, industry_risk_premium: float = 0.0, specific_risk_premium: float = 0.0) -> ValuationResult

Calculate discount rate using the build-up method.

Formula

r = Rf + ERP + Size Premium + Industry Risk Premium + Specific Risk Premium

Parameters:

Name Type Description Default
risk_free_rate float

Risk-free rate (Rf), typically government bond yield

required
equity_risk_premium float

Equity risk premium (ERP)

required
size_premium float

Additional premium for company size risk (default 0)

0.0
industry_risk_premium float

Additional premium for industry-specific risk (default 0)

0.0
specific_risk_premium float

Company-specific risk premium (default 0)

0.0

Returns:

Type Description
ValuationResult

ValuationResult with computed discount rate

Raises:

Type Description
ValueError

If risk_free_rate or equity_risk_premium is negative

Book Reference

Chapter 2, Section 2.7 — Build-Up Method for Discount Rate Commonly used for private company valuations where beta is unavailable

Source code in src/core/discount_rates.py
def build_up_discount_rate(
    risk_free_rate: float,
    equity_risk_premium: float,
    size_premium: float = 0.0,
    industry_risk_premium: float = 0.0,
    specific_risk_premium: float = 0.0,
) -> ValuationResult:
    """Calculate discount rate using the build-up method.

    Formula:
        r = Rf + ERP + Size Premium + Industry Risk Premium + Specific Risk Premium

    Parameters:
        risk_free_rate: Risk-free rate (Rf), typically government bond yield
        equity_risk_premium: Equity risk premium (ERP)
        size_premium: Additional premium for company size risk (default 0)
        industry_risk_premium: Additional premium for industry-specific risk (default 0)
        specific_risk_premium: Company-specific risk premium (default 0)

    Returns:
        ValuationResult with computed discount rate

    Raises:
        ValueError: If risk_free_rate or equity_risk_premium is negative

    Book Reference:
        Chapter 2, Section 2.7 — Build-Up Method for Discount Rate
        Commonly used for private company valuations where beta is unavailable
    """
    inputs = DiscountRateInputs(
        risk_free_rate=risk_free_rate,
        equity_risk_premium=equity_risk_premium,
        size_premium=size_premium,
        industry_risk_premium=industry_risk_premium,
        specific_risk_premium=specific_risk_premium,
    )

    rf = inputs.risk_free_rate
    erp = inputs.equity_risk_premium
    sp = inputs.size_premium
    irp = inputs.industry_risk_premium
    srp = inputs.specific_risk_premium

    if rf is None or erp is None:
        raise ValueError("risk_free_rate and equity_risk_premium are required")
    if rf < 0:
        raise ValueError("risk_free_rate must be non-negative")
    if erp < 0:
        raise ValueError("equity_risk_premium must be non-negative")

    rate = rf + erp + sp + irp + srp

    return ValuationResult(
        value=round(rate, 6),
        method=METHOD_BUILD_UP,
        formula_reference="r = Rf + ERP + Size Premium + Industry RP + Specific RP",
        steps=[
            f"Risk-Free Rate (Rf) = {rf:.2%}",
            f"Equity Risk Premium (ERP) = {erp:.2%}",
            f"Size Premium = {sp:.2%}",
            f"Industry Risk Premium = {irp:.2%}",
            f"Specific Risk Premium = {srp:.2%}",
            f"Discount Rate = {rf:.2%} + {erp:.2%} + {sp:.2%} + {irp:.2%} + {srp:.2%} = {rate:.2%}",
        ],
        assumptions=[
            "Risk-free rate reflects long-term government bond yield",
            "Equity risk premium is based on historical market returns",
            "All risk premiums are additive (no interaction effects)",
            "Premiums reflect the specific risk profile of the asset",
        ],
    )

capm_discount_rate(risk_free_rate: float, beta: float, market_return: float) -> ValuationResult

Calculate discount rate using the Capital Asset Pricing Model (CAPM).

Formula

r = Rf + beta * (Rm - Rf)

Parameters:

Name Type Description Default
risk_free_rate float

Risk-free rate (Rf)

required
beta float

Systematic risk coefficient (beta)

required
market_return float

Expected market return (Rm)

required

Returns:

Type Description
ValuationResult

ValuationResult with computed cost of equity

Raises:

Type Description
ValueError

If inputs are invalid

Book Reference

Chapter 2, Section 2.8 — Capital Asset Pricing Model (CAPM) Standard approach for publicly traded company cost of equity

Source code in src/core/discount_rates.py
def capm_discount_rate(
    risk_free_rate: float,
    beta: float,
    market_return: float,
) -> ValuationResult:
    """Calculate discount rate using the Capital Asset Pricing Model (CAPM).

    Formula:
        r = Rf + beta * (Rm - Rf)

    Parameters:
        risk_free_rate: Risk-free rate (Rf)
        beta: Systematic risk coefficient (beta)
        market_return: Expected market return (Rm)

    Returns:
        ValuationResult with computed cost of equity

    Raises:
        ValueError: If inputs are invalid

    Book Reference:
        Chapter 2, Section 2.8 — Capital Asset Pricing Model (CAPM)
        Standard approach for publicly traded company cost of equity
    """
    inputs = DiscountRateInputs(
        risk_free_rate=risk_free_rate,
        beta=beta,
        market_return=market_return,
    )

    rf = inputs.risk_free_rate
    b = inputs.beta
    rm = inputs.market_return

    if rf is None or b is None or rm is None:
        raise ValueError("risk_free_rate, beta, and market_return are required")
    if rf < 0:
        raise ValueError("risk_free_rate must be non-negative")
    if rm < rf:
        raise ValueError("market_return must be >= risk_free_rate")

    erp = rm - rf
    rate = rf + b * erp

    return ValuationResult(
        value=round(rate, 6),
        method=METHOD_CAPM,
        formula_reference="r = Rf + beta * (Rm - Rf)",
        steps=[
            f"Risk-Free Rate (Rf) = {rf:.2%}",
            f"Beta = {b:.4f}",
            f"Market Return (Rm) = {rm:.2%}",
            f"Equity Risk Premium (Rm - Rf) = {rm:.2%} - {rf:.2%} = {erp:.2%}",
            f"Cost of Equity = {rf:.2%} + {b:.4f} * {erp:.2%} = {rate:.2%}",
        ],
        assumptions=[
            "Market is efficient and investors are rational",
            "Beta accurately measures systematic risk",
            "CAPM assumptions hold (no taxes, no transaction costs, etc.)",
            "Risk-free rate and market return are expected forward-looking values",
        ],
    )

wacc(equity_value: float, debt_value: float, cost_of_equity: float, cost_of_debt: float, tax_rate: float) -> ValuationResult

Calculate the Weighted Average Cost of Capital (WACC).

Formula

WACC = (E / V) * Re + (D / V) * Rd * (1 - Tc) where V = E + D

Parameters:

Name Type Description Default
equity_value float

Market value of equity (E)

required
debt_value float

Market value of debt (D)

required
cost_of_equity float

Cost of equity (Re)

required
cost_of_debt float

Pre-tax cost of debt (Rd)

required
tax_rate float

Corporate tax rate (Tc)

required

Returns:

Type Description
ValuationResult

ValuationResult with computed WACC

Raises:

Type Description
ValueError

If equity_value + debt_value is zero or values are negative

Book Reference

Chapter 2, Section 2.9 — Weighted Average Cost of Capital Used for enterprise value and firm-level discount rates

Source code in src/core/discount_rates.py
def wacc(
    equity_value: float,
    debt_value: float,
    cost_of_equity: float,
    cost_of_debt: float,
    tax_rate: float,
) -> ValuationResult:
    """Calculate the Weighted Average Cost of Capital (WACC).

    Formula:
        WACC = (E / V) * Re + (D / V) * Rd * (1 - Tc)
        where V = E + D

    Parameters:
        equity_value: Market value of equity (E)
        debt_value: Market value of debt (D)
        cost_of_equity: Cost of equity (Re)
        cost_of_debt: Pre-tax cost of debt (Rd)
        tax_rate: Corporate tax rate (Tc)

    Returns:
        ValuationResult with computed WACC

    Raises:
        ValueError: If equity_value + debt_value is zero or values are negative

    Book Reference:
        Chapter 2, Section 2.9 — Weighted Average Cost of Capital
        Used for enterprise value and firm-level discount rates
    """
    inputs = DiscountRateInputs(
        equity_value=equity_value,
        debt_value=debt_value,
        cost_of_equity=cost_of_equity,
        cost_of_debt=cost_of_debt,
        tax_rate=tax_rate,
    )

    e = inputs.equity_value
    d = inputs.debt_value
    re = inputs.cost_of_equity
    rd = inputs.cost_of_debt
    tc = inputs.tax_rate

    if e is None or d is None or re is None or rd is None:
        raise ValueError("equity_value, debt_value, cost_of_equity, and cost_of_debt are required")
    if e < 0 or d < 0:
        raise ValueError("equity_value and debt_value must be non-negative")
    if e + d == 0:
        raise ValueError("Total capital (equity + debt) must be positive")
    if tc < 0 or tc > 1:
        raise ValueError("tax_rate must be between 0 and 1")

    v = e + d
    weight_equity = e / v
    weight_debt = d / v
    after_tax_debt = rd * (1 - tc)
    result = weight_equity * re + weight_debt * after_tax_debt

    return ValuationResult(
        value=round(result, 6),
        method=METHOD_WACC,
        formula_reference="WACC = (E/V) * Re + (D/V) * Rd * (1 - Tc)",
        steps=[
            f"Equity Value (E) = ${e:,.2f}",
            f"Debt Value (D) = ${d:,.2f}",
            f"Total Capital (V) = ${v:,.2f}",
            f"Weight of Equity (E/V) = {weight_equity:.4f}",
            f"Weight of Debt (D/V) = {weight_debt:.4f}",
            f"Cost of Equity (Re) = {re:.2%}",
            f"Pre-tax Cost of Debt (Rd) = {rd:.2%}",
            f"Tax Rate (Tc) = {tc:.2%}",
            f"After-tax Cost of Debt = {rd:.2%} * (1 - {tc:.2%}) = {after_tax_debt:.2%}",
            f"WACC = {weight_equity:.4f} * {re:.2%} + {weight_debt:.4f} * {after_tax_debt:.2%} = {result:.2%}",
        ],
        assumptions=[
            "Capital structure weights are based on market values",
            "Cost of debt is the pre-tax yield to maturity",
            "Tax shield from debt interest is fully utilized",
            "Capital structure is stable over the projection period",
        ],
    )

tax_amortization_benefit(discount_rate: float, useful_life: int, tax_rate: float, asset_value: float) -> ValuationResult

Calculate the present value of the tax amortization benefit (TAB).

The TAB represents the present value of tax savings from amortizing an intangible asset over its useful life.

Formula

TAB = Asset Value * Tax Rate * [1 - (1 + r)^(-n)] / (r * n)

Parameters:

Name Type Description Default
discount_rate float

The discount rate (r), as a decimal

required
useful_life int

Amortization period in years (n)

required
tax_rate float

Corporate tax rate (Tc)

required
asset_value float

The value of the intangible asset

required

Returns:

Type Description
ValuationResult

ValuationResult with computed TAB value

Raises:

Type Description
ValueError

If inputs are invalid

Book Reference

Chapter 3, Section 3.4 — Tax Amortization Benefit Important adjustment in relief-from-royalty and multi-period excess earnings methods

Source code in src/core/discount_rates.py
def tax_amortization_benefit(
    discount_rate: float,
    useful_life: int,
    tax_rate: float,
    asset_value: float,
) -> ValuationResult:
    """Calculate the present value of the tax amortization benefit (TAB).

    The TAB represents the present value of tax savings from amortizing
    an intangible asset over its useful life.

    Formula:
        TAB = Asset Value * Tax Rate * [1 - (1 + r)^(-n)] / (r * n)

    Parameters:
        discount_rate: The discount rate (r), as a decimal
        useful_life: Amortization period in years (n)
        tax_rate: Corporate tax rate (Tc)
        asset_value: The value of the intangible asset

    Returns:
        ValuationResult with computed TAB value

    Raises:
        ValueError: If inputs are invalid

    Book Reference:
        Chapter 3, Section 3.4 — Tax Amortization Benefit
        Important adjustment in relief-from-royalty and multi-period excess earnings methods
    """
    inputs = DiscountRateInputs(
        discount_rate=discount_rate,
        useful_life=useful_life,
        tax_rate=tax_rate,
        asset_value=asset_value,
    )

    r = inputs.discount_rate
    n = inputs.useful_life
    tc = inputs.tax_rate
    av = inputs.asset_value

    if r is None or n is None or tc is None or av is None:
        raise ValueError("discount_rate, useful_life, tax_rate, and asset_value are required")
    if av < 0:
        raise ValueError("asset_value must be non-negative")
    if n <= 0:
        raise ValueError("useful_life must be positive")
    if tc < 0 or tc > 1:
        raise ValueError("tax_rate must be between 0 and 1")
    if r <= -1:
        raise ValueError("discount_rate must be > -1.0")

    if math.isclose(r, 0.0, abs_tol=1e-12):
        tab = av * tc
        steps = [
            f"Asset Value = ${av:,.2f}",
            f"Tax Rate = {tc:.2%}",
            "Discount rate ~ 0, TAB = Asset Value * Tax Rate",
            f"TAB = ${av:,.2f} * {tc:.2%} = ${tab:,.2f}",
        ]
    else:
        annuity_factor = (1 - (1 + r) ** (-n)) / (r * n)
        tab = av * tc * annuity_factor
        steps = [
            f"Asset Value = ${av:,.2f}",
            f"Tax Rate = {tc:.2%}",
            f"Discount Rate (r) = {r:.2%}",
            f"Useful Life (n) = {n} years",
            f"Annual Amortization = ${av:,.2f} / {n} = ${av / n:,.2f}",
            f"Annual Tax Savings = ${av / n:,.2f} * {tc:.2%} = ${av / n * tc:,.2f}",
            f"Annuity Factor = [1 - (1 + {r})^(-{n})] / ({r} * {n}) = {annuity_factor:.6f}",
            f"TAB = ${av:,.2f} * {tc:.2%} * {annuity_factor:.6f} = ${tab:,.2f}",
        ]

    return ValuationResult(
        value=round(tab, 2),
        method="Tax Amortization Benefit",
        formula_reference="TAB = Asset Value * Tax Rate * [1 - (1 + r)^(-n)] / (r * n)",
        steps=steps,
        assumptions=[
            "Straight-line amortization over useful life",
            "Tax rate is constant over amortization period",
            "Discount rate reflects the risk of tax savings",
            "Full utilization of tax deductions assumed",
        ],
    )

control_premium(minority_price: float, control_price: float) -> ValuationResult

Calculate the control premium percentage.

Formula

Control Premium = (Control Price - Minority Price) / Minority Price

Parameters:

Name Type Description Default
minority_price float

The trading price of minority shares

required
control_price float

The price paid for a controlling interest

required

Returns:

Type Description
ValuationResult

ValuationResult with computed control premium as a decimal

Raises:

Type Description
ValueError

If prices are non-positive or control_price < minority_price

Book Reference

Chapter 4, Section 4.3 — Control Premium Used to convert minority interest value to controlling interest value

Source code in src/core/discount_rates.py
def control_premium(
    minority_price: float,
    control_price: float,
) -> ValuationResult:
    """Calculate the control premium percentage.

    Formula:
        Control Premium = (Control Price - Minority Price) / Minority Price

    Parameters:
        minority_price: The trading price of minority shares
        control_price: The price paid for a controlling interest

    Returns:
        ValuationResult with computed control premium as a decimal

    Raises:
        ValueError: If prices are non-positive or control_price < minority_price

    Book Reference:
        Chapter 4, Section 4.3 — Control Premium
        Used to convert minority interest value to controlling interest value
    """
    inputs = DiscountRateInputs(
        minority_price=minority_price,
        control_price=control_price,
    )

    mp = inputs.minority_price
    cp = inputs.control_price

    if mp is None or cp is None:
        raise ValueError("minority_price and control_price are required")
    if mp <= 0:
        raise ValueError("minority_price must be positive")
    if cp <= 0:
        raise ValueError("control_price must be positive")
    if cp < mp:
        raise ValueError("control_price must be >= minority_price")

    premium = (cp - mp) / mp

    return ValuationResult(
        value=round(premium, 6),
        method="Control Premium",
        formula_reference="Control Premium = (Control Price - Minority Price) / Minority Price",
        steps=[
            f"Minority Share Price = ${mp:,.2f}",
            f"Control Price = ${cp:,.2f}",
            f"Premium = (${cp:,.2f} - ${mp:,.2f}) / ${mp:,.2f} = {premium:.2%}",
        ],
        assumptions=[
            "Minority price reflects market trading value",
            "Control price reflects value of decision-making power",
            "Premium captures synergies and control benefits",
        ],
    )

dlom_finnerty(restricted_period: float, volatility: float, risk_free_rate: float) -> ValuationResult

Calculate Discount for Lack of Marketability (DLOM) using Finnerty model.

The Finnerty model uses an average strike put option approach to estimate the DLOM based on the cost of hedging the restriction period.

Formula

DLOM = (1 / (r * T)) * [r * T * N(-d2) - (e^(rT) - 1) * N(-d1)] where: d1 = sigma * sqrt(T) / 2 d2 = -sigma * sqrt(T) / 2

Parameters:

Name Type Description Default
restricted_period float

Restriction period in years (T)

required
volatility float

Annualized stock volatility (sigma)

required
risk_free_rate float

Risk-free rate (r)

required

Returns:

Type Description
ValuationResult

ValuationResult with computed DLOM as a decimal

Raises:

Type Description
ValueError

If inputs are invalid

Book Reference

Chapter 4, Section 4.4 — Discount for Lack of Marketability (DLOM) Finnerty, J.D. "An Average-Strike Put Option Model for DLOM"

Source code in src/core/discount_rates.py
def dlom_finnerty(
    restricted_period: float,
    volatility: float,
    risk_free_rate: float,
) -> ValuationResult:
    """Calculate Discount for Lack of Marketability (DLOM) using Finnerty model.

    The Finnerty model uses an average strike put option approach to estimate
    the DLOM based on the cost of hedging the restriction period.

    Formula:
        DLOM = (1 / (r * T)) * [r * T * N(-d2) - (e^(rT) - 1) * N(-d1)]
        where:
            d1 = sigma * sqrt(T) / 2
            d2 = -sigma * sqrt(T) / 2

    Parameters:
        restricted_period: Restriction period in years (T)
        volatility: Annualized stock volatility (sigma)
        risk_free_rate: Risk-free rate (r)

    Returns:
        ValuationResult with computed DLOM as a decimal

    Raises:
        ValueError: If inputs are invalid

    Book Reference:
        Chapter 4, Section 4.4 — Discount for Lack of Marketability (DLOM)
        Finnerty, J.D. "An Average-Strike Put Option Model for DLOM"
    """
    inputs = DiscountRateInputs(
        restricted_period=restricted_period,
        volatility=volatility,
        risk_free_rate=risk_free_rate,
    )

    t = inputs.restricted_period
    sigma = inputs.volatility
    r = inputs.risk_free_rate

    if t is None or sigma is None or r is None:
        raise ValueError("restricted_period, volatility, and risk_free_rate are required")
    if t <= 0:
        raise ValueError("restricted_period must be positive")
    if sigma <= 0:
        raise ValueError("volatility must be positive")
    if sigma > 5.0:
        raise ValueError("volatility seems unreasonably high (> 500%)")
    if r < -1:
        raise ValueError("risk_free_rate must be >= -1.0")

    try:
        from math import exp, sqrt

        sigma_sqrt_t = sigma * sqrt(t)

        d1 = sigma_sqrt_t / 2
        d2 = -sigma_sqrt_t / 2

        n_d1 = _norm_cdf(-d1)
        n_d2 = _norm_cdf(-d2)

        exp_rt = exp(r * t)
        dlom = (1 / (r * t)) * (r * t * n_d2 - (exp_rt - 1) * n_d1)
        dlom = max(0.0, min(1.0, dlom))

    except (OverflowError, ValueError) as e:
        raise ValueError(f"Failed to compute Finnerty DLOM: {e}") from e

    return ValuationResult(
        value=round(dlom, 6),
        method=METHOD_FINNERTY,
        formula_reference="Finnerty Average-Strike Put Option Model",
        steps=[
            f"Restricted Period (T) = {t:.2f} years",
            f"Volatility (sigma) = {sigma:.2%}",
            f"Risk-Free Rate (r) = {r:.2%}",
            f"d1 = sigma*sqrt(T)/2 = {d1:.6f}",
            f"d2 = -sigma*sqrt(T)/2 = {d2:.6f}",
            f"N(-d1) = {n_d1:.6f}",
            f"N(-d2) = {n_d2:.6f}",
            f"DLOM = {dlom:.2%}",
        ],
        assumptions=[
            "Stock price follows geometric Brownian motion",
            "Volatility is constant over restriction period",
            "Restriction is absolute (no trading allowed)",
            "Model assumes European-style option characteristics",
        ],
    )

currency_adjusted_discount_rate(base_rate: float, currency_risk_premium: float = 0.0, country_risk_premium: float = 0.0) -> ValuationResult

Calculate a discount rate adjusted for currency and country risk.

Formula

r_adjusted = base_rate + Currency Risk Premium + Country Risk Premium

Parameters:

Name Type Description Default
base_rate float

The base discount rate (e.g., from CAPM or build-up)

required
currency_risk_premium float

Additional premium for currency risk

0.0
country_risk_premium float

Additional premium for country/sovereign risk

0.0

Returns:

Type Description
ValuationResult

ValuationResult with computed currency-adjusted discount rate

Raises:

Type Description
ValueError

If base_rate < -1 or premiums are negative

Book Reference

Chapter 4, Section 4.5 — International/Currency Risk Adjustments Used for cross-border valuations and emerging market assets

Source code in src/core/discount_rates.py
def currency_adjusted_discount_rate(
    base_rate: float,
    currency_risk_premium: float = 0.0,
    country_risk_premium: float = 0.0,
) -> ValuationResult:
    """Calculate a discount rate adjusted for currency and country risk.

    Formula:
        r_adjusted = base_rate + Currency Risk Premium + Country Risk Premium

    Parameters:
        base_rate: The base discount rate (e.g., from CAPM or build-up)
        currency_risk_premium: Additional premium for currency risk
        country_risk_premium: Additional premium for country/sovereign risk

    Returns:
        ValuationResult with computed currency-adjusted discount rate

    Raises:
        ValueError: If base_rate < -1 or premiums are negative

    Book Reference:
        Chapter 4, Section 4.5 — International/Currency Risk Adjustments
        Used for cross-border valuations and emerging market assets
    """
    inputs = DiscountRateInputs(
        risk_free_rate=base_rate,
        currency_risk_premium=currency_risk_premium,
        country_risk_premium=country_risk_premium,
    )

    base = inputs.risk_free_rate
    crp = inputs.currency_risk_premium
    ctrp = inputs.country_risk_premium

    if base is None:
        raise ValueError("base_rate is required")
    if base < -1:
        raise ValueError("base_rate must be >= -1.0")
    if crp < 0:
        raise ValueError("currency_risk_premium must be non-negative")
    if ctrp < 0:
        raise ValueError("country_risk_premium must be non-negative")

    adjusted = base + crp + ctrp

    return ValuationResult(
        value=round(adjusted, 6),
        method="Currency-Adjusted Discount Rate",
        formula_reference="r_adjusted = base_rate + Currency RP + Country RP",
        steps=[
            f"Base Discount Rate = {base:.2%}",
            f"Currency Risk Premium = {crp:.2%}",
            f"Country Risk Premium = {ctrp:.2%}",
            f"Adjusted Rate = {base:.2%} + {crp:.2%} + {ctrp:.2%} = {adjusted:.2%}",
        ],
        assumptions=[
            "Base rate reflects the asset's risk in local currency",
            "Currency risk premium captures exchange rate volatility",
            "Country risk premium captures sovereign and political risk",
            "Premiums are additive (no interaction effects)",
        ],
    )

Statistics

statistics

Statistical functions for valuation analysis.

Implements Monte Carlo simulation and decision tree analysis from Chapter 6 and Appendix B of the Ascent Partners textbook.

All functions return structured dicts with
  • value: The primary result
  • method: The calculation method used
  • formula_reference: Reference to the methodology
  • steps: Step-by-step calculation breakdown
  • assumptions: List of assumptions made during calculation

Classes

DistributionInput

Bases: BaseModel

Validated input distribution for Monte Carlo simulation.

TreeNode

Bases: BaseModel

Validated node in a decision tree.

TreeEdge

Bases: BaseModel

Validated edge in a decision tree.

DecisionTreeInput

Bases: BaseModel

Validated decision tree input.

Functions

monte_carlo_valuation(valuation_fn: Callable[..., float], input_distributions: list[dict[str, Any]], iterations: int = 10000, seed: int | None = None) -> dict[str, Any]

Perform Monte Carlo simulation for valuation uncertainty analysis.

Runs the valuation function multiple times with randomly sampled inputs from specified distributions to produce a probability distribution of valuation outcomes.

Parameters:

Name Type Description Default
valuation_fn Callable[..., float]

Function that takes named parameters and returns a float value

required
input_distributions list[dict[str, Any]]

List of dicts with keys: - name: Parameter name passed to valuation_fn - distribution: "normal", "uniform", or "triangular" - params: Distribution-specific parameters - normal: {"mean": float, "std": float} - uniform: {"low": float, "high": float} - triangular: {"low": float, "high": float, "mode": float}

required
iterations int

Number of simulation runs (default 10000)

10000
seed int | None

Random seed for reproducibility (default None)

None

Returns:

Type Description
dict[str, Any]

Dict with keys: - mean: Mean of simulated valuations - median: Median of simulated valuations - std: Standard deviation - percentile_5: 5th percentile - percentile_25: 25th percentile - percentile_75: 75th percentile - percentile_95: 95th percentile - min: Minimum value - max: Maximum value - method: "Monte Carlo Simulation" - formula_reference: Reference description - iterations: Number of iterations performed - seed: Random seed used - steps: Description of the simulation - assumptions: List of assumptions

Raises:

Type Description
ValueError

If input_distributions is empty or iterations < 1

Book Reference

Chapter 6, Section 6.2 — Monte Carlo Simulation for Valuation Appendix B — Statistical Methods in Valuation

Source code in src/core/statistics.py
def monte_carlo_valuation(
    valuation_fn: Callable[..., float],
    input_distributions: list[dict[str, Any]],
    iterations: int = 10000,
    seed: int | None = None,
) -> dict[str, Any]:
    """Perform Monte Carlo simulation for valuation uncertainty analysis.

    Runs the valuation function multiple times with randomly sampled inputs
    from specified distributions to produce a probability distribution of
    valuation outcomes.

    Parameters:
        valuation_fn: Function that takes named parameters and returns a float value
        input_distributions: List of dicts with keys:
            - name: Parameter name passed to valuation_fn
            - distribution: "normal", "uniform", or "triangular"
            - params: Distribution-specific parameters
                - normal: {"mean": float, "std": float}
                - uniform: {"low": float, "high": float}
                - triangular: {"low": float, "high": float, "mode": float}
        iterations: Number of simulation runs (default 10000)
        seed: Random seed for reproducibility (default None)

    Returns:
        Dict with keys:
            - mean: Mean of simulated valuations
            - median: Median of simulated valuations
            - std: Standard deviation
            - percentile_5: 5th percentile
            - percentile_25: 25th percentile
            - percentile_75: 75th percentile
            - percentile_95: 95th percentile
            - min: Minimum value
            - max: Maximum value
            - method: "Monte Carlo Simulation"
            - formula_reference: Reference description
            - iterations: Number of iterations performed
            - seed: Random seed used
            - steps: Description of the simulation
            - assumptions: List of assumptions

    Raises:
        ValueError: If input_distributions is empty or iterations < 1

    Book Reference:
        Chapter 6, Section 6.2 — Monte Carlo Simulation for Valuation
        Appendix B — Statistical Methods in Valuation
    """
    if not input_distributions:
        raise ValueError("input_distributions must contain at least one distribution")
    if iterations < 1:
        raise ValueError("iterations must be at least 1")

    validated_dists = [DistributionInput(**d) for d in input_distributions]

    if seed is not None:
        random.seed(seed)

    results: list[float] = []

    for _ in range(iterations):
        inputs = {}
        for dist in validated_dists:
            if dist.distribution == "normal":
                inputs[dist.name] = random.gauss(dist.params["mean"], dist.params["std"])
            elif dist.distribution == "uniform":
                inputs[dist.name] = random.uniform(dist.params["low"], dist.params["high"])
            elif dist.distribution == "triangular":
                inputs[dist.name] = _random_triangular(
                    dist.params["low"], dist.params["high"], dist.params["mode"],
                )

        try:
            result = valuation_fn(**inputs)
            results.append(result)
        except (TypeError, ValueError) as e:
            raise ValueError(f"valuation_fn failed with sampled inputs: {e}") from e

    results.sort()
    n = len(results)

    mean = sum(results) / n
    variance = sum((x - mean) ** 2 for x in results) / (n - 1) if n > 1 else 0.0
    std = math.sqrt(variance)

    def percentile(p: float) -> float:
        idx = int(p / 100 * (n - 1))
        return results[idx]

    distribution_descriptions = []
    for dist in validated_dists:
        if dist.distribution == "normal":
            distribution_descriptions.append(
                f"{dist.name} ~ N({dist.params['mean']}, {dist.params['std']})"
            )
        elif dist.distribution == "uniform":
            distribution_descriptions.append(
                f"{dist.name} ~ U({dist.params['low']}, {dist.params['high']})"
            )
        elif dist.distribution == "triangular":
            distribution_descriptions.append(
                f"{dist.name} ~ Triangular({dist.params['low']}, {dist.params['mode']}, {dist.params['high']})"
            )

    return {
        "mean": round(mean, 2),
        "median": round(percentile(50), 2),
        "std": round(std, 2),
        "percentile_5": round(percentile(5), 2),
        "percentile_25": round(percentile(25), 2),
        "percentile_75": round(percentile(75), 2),
        "percentile_95": round(percentile(95), 2),
        "min": round(results[0], 2),
        "max": round(results[-1], 2),
        "method": "Monte Carlo Simulation",
        "formula_reference": "Monte Carlo: Sample from input distributions, compute valuation, aggregate statistics",
        "iterations": iterations,
        "seed": seed,
        "steps": [
            f"Number of iterations: {iterations}",
            f"Input distributions: {', '.join(distribution_descriptions)}",
            f"Random seed: {seed if seed is not None else 'None (non-deterministic)'}",
            f"Mean result: ${mean:,.2f}",
            f"Standard deviation: ${std:,.2f}",
            f"5th-95th percentile range: ${percentile(5):,.2f} - ${percentile(95):,.2f}",
        ],
        "assumptions": [
            "Input distributions accurately represent parameter uncertainty",
            "Input parameters are independent (no correlation modeled)",
            "valuation_fn correctly computes the valuation for given inputs",
            f"{iterations} iterations provide sufficient convergence",
        ],
    }

decision_tree_valuation(tree: dict[str, Any]) -> dict[str, Any]

Evaluate a decision tree to compute expected values at each node.

The tree consists of three node types: - decision: A choice point where the optimal branch is selected (max value) - chance: A probabilistic branch where expected value is computed - terminal: An endpoint with a fixed value

Parameters:

Name Type Description Default
tree dict[str, Any]

Dict with keys: - nodes: List of node dicts with keys: id, type, label, value - edges: List of edge dicts with keys: from, to, probability, cost

required

Returns:

Type Description
dict[str, Any]

Dict with keys: - expected_value: Expected value at the root node - node_values: Dict mapping node ID to expected value - optimal_path: List of node IDs representing the optimal path - method: "Decision Tree Analysis" - formula_reference: Reference description - steps: Step-by-step evaluation - assumptions: List of assumptions

Raises:

Type Description
ValueError

If tree structure is invalid

Book Reference

Chapter 6, Section 6.3 — Decision Tree Analysis for Valuation Used for valuing assets with contingent outcomes (e.g., R&D projects, litigation)

Source code in src/core/statistics.py
def decision_tree_valuation(
    tree: dict[str, Any],
) -> dict[str, Any]:
    """Evaluate a decision tree to compute expected values at each node.

    The tree consists of three node types:
    - decision: A choice point where the optimal branch is selected (max value)
    - chance: A probabilistic branch where expected value is computed
    - terminal: An endpoint with a fixed value

    Parameters:
        tree: Dict with keys:
            - nodes: List of node dicts with keys: id, type, label, value
            - edges: List of edge dicts with keys: from, to, probability, cost

    Returns:
        Dict with keys:
            - expected_value: Expected value at the root node
            - node_values: Dict mapping node ID to expected value
            - optimal_path: List of node IDs representing the optimal path
            - method: "Decision Tree Analysis"
            - formula_reference: Reference description
            - steps: Step-by-step evaluation
            - assumptions: List of assumptions

    Raises:
        ValueError: If tree structure is invalid

    Book Reference:
        Chapter 6, Section 6.3 — Decision Tree Analysis for Valuation
        Used for valuing assets with contingent outcomes (e.g., R&D projects, litigation)
    """
    validated = DecisionTreeInput(**tree)

    nodes = {node.id: node for node in validated.nodes}
    edges_from: dict[str, list[TreeEdge]] = {}
    for edge in validated.edges:
        if edge.from_node not in edges_from:
            edges_from[edge.from_node] = []
        edges_from[edge.from_node].append(edge)

    node_values: dict[str, float] = {}
    evaluation_steps: list[str] = []

    def evaluate(node_id: str) -> float:
        node = nodes[node_id]

        if node.type == "terminal":
            node_values[node_id] = node.value
            evaluation_steps.append(f"Node '{node.label}' (terminal): value = ${node.value:,.2f}")
            return node.value

        if node_id not in edges_from or not edges_from[node_id]:
            node_values[node_id] = node.value
            evaluation_steps.append(f"Node '{node.label}' (leaf): value = ${node.value:,.2f}")
            return node.value

        if node.type == "decision":
            branch_values = []
            for edge in edges_from[node_id]:
                child_value = evaluate(edge.to)
                net_value = child_value - edge.cost
                branch_values.append((edge.to, net_value, edge))

            best = max(branch_values, key=lambda x: x[1])
            node_values[node_id] = best[1]
            evaluation_steps.append(
                f"Node '{node.label}' (decision): choose branch to '{nodes[best[0]].label}' "
                f"(value = ${best[1]:,.2f})"
            )
            return best[1]

        if node.type == "chance":
            total_prob = sum(edge.probability for edge in edges_from[node_id])
            if not math.isclose(total_prob, 1.0, abs_tol=1e-6):
                raise ValueError(
                    f"Probabilities for chance node '{node.label}' sum to {total_prob:.4f}, expected 1.0"
                )

            expected = 0.0
            for edge in edges_from[node_id]:
                child_value = evaluate(edge.to)
                net_value = child_value - edge.cost
                contribution = net_value * edge.probability
                expected += contribution
                evaluation_steps.append(
                    f"  Branch to '{nodes[edge.to].label}': "
                    f"P={edge.probability:.2%}, value=${net_value:,.2f}, "
                    f"contribution=${contribution:,.2f}"
                )

            node_values[node_id] = expected
            evaluation_steps.append(
                f"Node '{node.label}' (chance): expected value = ${expected:,.2f}"
            )
            return expected

        raise ValueError(f"Unknown node type: {node.type}")

    root_id = validated.nodes[0].id
    root_value = evaluate(root_id)

    optimal_path = _find_optimal_path(root_id, nodes, edges_from, node_values)

    return {
        "expected_value": round(root_value, 2),
        "node_values": {k: round(v, 2) for k, v in node_values.items()},
        "optimal_path": optimal_path,
        "method": "Decision Tree Analysis",
        "formula_reference": "Backward induction: EV(chance) = sum(P_i * V_i), EV(decision) = max(branches)",
        "steps": evaluation_steps,
        "assumptions": [
            "All probabilities at chance nodes sum to 1.0",
            "Decision nodes select the branch with maximum expected value",
            "Values are risk-neutral (no risk adjustment beyond discounting)",
            "Tree is acyclic (no loops)",
        ],
    }

Constants

constants

Industry default rates and constants for valuation calculations.