Connect NOPAT, invested capital, ROIC spread, EVA, market value added, and enterprise value without mixing equity and enterprise claims.
The decision this tutorial makes visible
Economic Value Added is useful because it turns a set of explicit assumptions into a reconciled estimate while preserving the diagnostics needed to challenge that estimate.
The precise question is: How do after-tax operating profits become enterprise value after charging for all invested capital?
A practitioner needs to know what the diagnostic does and does not justify. A builder needs a contract that can be reproduced from the same point-in-time inputs in Python, TypeScript, a visual, and a browser lab.
Intuition before notation
Operating profit creates value only after covering the opportunity cost of every unit of invested capital; enterprise value is capital already invested plus the present value of future economic profit.
The result depends on the declared algorithm scope, input clocks, units, equality and rounding policies, and unsupported-state treatment. Change one of those and the output represents a different decision even when its field name is unchanged.
Scope and nearby methods
Enterprise EVA valuation using opening invested capital, explicit NOPAT and net-investment vectors, constant WACC, capital roll-forwards, and a stable-growth continuing EVA.
| Variant | Definition | Best use | Main limitation |
|---|---|---|---|
| Canonical repository convention | Enterprise EVA valuation using opening invested capital, explicit NOPAT and net-investment vectors, constant WACC, capital roll-forwards, and a stable-growth continuing EVA. | Reproducible teaching and parity | One explicit convention, not a universal default |
| MVA only | Report PV of future EVA without adding opening capital | Value-creation diagnostic | Not enterprise value |
| DCF cross-check | Discount FCFF on a consistent basis | Reconciliation and model audit | Requires FCFF construction |
What is sourced, selected, synthetic, and derived
| Role | Material claim | Evidence | Boundary |
|---|---|---|---|
| Definition source | EVA is operating profit after tax less a cost-of-capital charge on invested capital and can be expressed as the return-on-capital spread times capital. | S1: EVA definition and DCF relationship | Accounting adjustments and capital definitions remain implementation choices that must be disclosed consistently. |
| Professional-practice source | Residual income subtracts an equity charge from net income; equity value combines current book value and present value of expected residual income; EVA applies a WACC charge to total capital. | S2: Residual income definition, valuation, accounting adjustments, and EVA comparison | A curriculum summary is not a substitute for entity-specific accounting normalization or forecasts. |
| Definition source | Present value is built from expected cash flows discounted at a rate consistent with the claim being valued; firm cash flows pair with a firm discount rate and equity cash flows with an equity discount rate. | S3: Discounted cash flow valuation; equity versus firm valuation | Lecture material establishes the valuation framework, not the package's particular fixture, forecast horizon, or rates. |
| Implementation choice | Enterprise EVA valuation using opening invested capital, explicit NOPAT and net-investment vectors, constant WACC, capital roll-forwards, and a stable-growth continuing EVA. | Frozen family definition contract, code, fixtures, and parity checks | This is the repository convention, not the only accepted valuation variant. |
| Synthetic teaching input | Every financial amount, rate, date, and scenario state is repository-authored. | datasets/canonical-input.json and datasets/scenario-results.json | No value represents an observed issuer or provider record. |
| Author-derived calculation | The fixture produces period ROIC spreads and EVA, market value added, closing invested capital, and enterprise value. | Expected output, examples, and independent validation | Arithmetic correctness does not validate forecasts, discount rates, or market performance. |
The authoritative sources support only the exact facts named in the claim ledger. They do not certify the synthetic numbers in this tutorial. The repository fixture is deliberately invented for auditability, and the displayed output is author-derived under the selected implementation choice.
Formula, symbols, and numerical policy
EVA_t = NOPAT_t - WACC*IC_(t-1) = (ROIC_t-WACC)*IC_(t-1); IC_t=IC_(t-1)+NetInvestment_t; EV0=IC0+PV(future EVA)
| Symbol | Meaning | Unit | Policy |
|---|---|---|---|
| NOPAT_t | Net operating profit after tax | currency/period | Operating, financing-neutral basis |
| IC_(t-1) | Opening invested capital | currency | Same adjustment basis as NOPAT |
| ROIC_t | NOPAT divided by opening invested capital | decimal/period | Diagnostic |
| EVA_t | Economic value added | currency/period | NOPAT less capital charge |
- Calculate at full floating-point precision and round only for display.
- Periods and rates share one annual cadence unless an explicitly documented conversion is supplied.
- NOPAT and invested capital must share one disclosed adjustment basis; WACC matches the enterprise claim, terminal EVA growth stays below WACC, and an equity bridge is outside this topic.
- Reconcile all displayed components back to the headline value.
Read the formula in the same order as the algorithm. Validate identity, ordering, units, and supported state first. Apply the selected equality and window rules second. Calculate with unrounded numeric values. Round only at the declared presentation boundary, and preserve null as a diagnostic rather than coercing it to zero.
Build the algorithm
- Freeze the NOPAT/invested-capital adjustment basis
- Calculate the opening-capital charge and ROIC spread
- Derive and discount explicit EVA
- Roll invested capital forward with net investment
- Add PV future EVA to opening invested capital
Production-minded operational checklist
- Verify point-in-time statement availability and corporate-action/share-count basis.
- Match operating/equity cash flows to WACC/cost of equity.
- Stress discount and perpetual-growth assumptions rather than presenting one point estimate as truth.
- Reconcile independent arithmetic and Python/TypeScript output before publication.
The checklist is intentionally strict: an explicit rejection is safer than a plausible output built from stale, malformed, or unsupported state.
Worked synthetic example
The canonical fixture is synthetic teaching data, not an observed control
event, customer order, or broker execution. Its primary author-derived output,
enterprise_value, is period ROIC spreads and EVA, market value added, closing invested capital, and enterprise value. The complete input and output
are in datasets/canonical-input.json and datasets/expected-output.json.
The canonical synthetic fixture applies EVA_t = NOPAT_t - WACC*IC_(t-1) = (ROIC_t-WACC)*IC_(t-1); IC_t=IC_(t-1)+NetInvestment_t; EV0=IC0+PV(future EVA) and produces period ROIC spreads and EVA, market value added, closing invested capital, and enterprise value. The expected JSON is generated from the frozen contract and checked independently across both language implementations.
Counterfactual checkpoint
Economic Value Added material-assumption change. Move the primary discount or growth driver while holding the other frozen inputs constant. The output changes because A valuation is only interpretable when the source of a changed result is visible.
The structured result retains state and diagnostics in addition to the primary number. That makes the calculation independently reviewable and prevents a partial, null, rejected, or venue-bounded outcome from being mistaken for an unqualified value.
Boundary and counterexample workbook
The playground computes every scenario at 61 deterministic parameter states.
The table uses the declared focus step and states whether that focus reproduces
the canonical fixture. The full state ledger and compressed transition
segments are in datasets/scenario-results.json.
| Scenario | Review focus | Purpose | State | Primary output | Diagnostic | Decision segments |
|---|---|---|---|---|---|---|
| Canonical WACC sweep | Step 30 · canonical fixture | Move the capital charge around 9%. | valued | EV 1,365.31 · MVA 565.31 | State valued; EV 1,365.31 · MVA 565.31; terminal/continuing share 33.2%. | 1 |
| NOPAT scale | Step 30 · canonical fixture | Scale forecast operating profit. | valued | EV 1,365.31 · MVA 565.31 | State valued; EV 1,365.31 · MVA 565.31; terminal/continuing share 33.2%. | 1 |
| Opening-capital intensity | Step 30 · canonical fixture | Vary the capital base charged by WACC. | valued | EV 1,365.31 · MVA 565.31 | State valued; EV 1,365.31 · MVA 565.31; terminal/continuing share 33.2%. | 1 |
| Net-investment path | Step 30 · comparison focus | Change capital growth and later capital charges. | valued | EV 1,340.40 · MVA 540.40 | State valued; EV 1,340.40 · MVA 540.40; terminal/continuing share 32.2%. | 1 |
| Negative EVA | Step 30 · comparison focus | Set NOPAT below the capital charge. | valued | EV 295.12 · MVA -504.88 | State valued; EV 295.12 · MVA -504.88; terminal/continuing share -137.4%. | 1 |
| Continuing-EVA growth | Step 30 · canonical fixture | Stress continuing economic-profit growth. | valued | EV 1,365.31 · MVA 565.31 | State valued; EV 1,365.31 · MVA 565.31; terminal/continuing share 33.2%. | 1 |
| Adjustment-basis comparison | Step 30 · canonical fixture | Move already-adjusted NOPAT and capital together under a labeled alternate basis. | valued | EV 1,365.31 · MVA 565.31 | State valued; EV 1,365.31 · MVA 565.31; terminal/continuing share 33.2%. | 1 |
These rows are not backtest observations. They are controlled counterexamples that expose how one driver changes the state, output, or reason code while the rest of the contract stays fixed.
Visualize the boundary
Open this SVG at full size, or use the guided playground to compare the seven topic-specific canonical, boundary, policy, and failure scenarios.
The Mermaid flow answers where the selected calculation sits in the processing sequence. The SVG keeps the formula, output, decision boundary, and invariant visible together. The lab lets the reader step through the same structured states without changing the underlying definition.
Implementation walkthrough
The Python and TypeScript references begin with the same validation contract, reject malformed and unsupported state before calculation, preserve declared ordering and rounding policies, and return structured diagnostics rather than one context-free number.
The main implementation branches are:
- Input, timing, accounting, or claim contract fails — Reject, because A precise number would be misleading.
- Perpetual growth is greater than or equal to the matching discount rate — Reject, because The frozen stable-growth perpetuity is undefined or nonconvergent.
- Inputs are valid but sensitivity is extreme — Return value with diagnostics, because Fragility is information, not an excuse to hide output.
- All checks pass — Return structured valuation and audit trail, because The definition contract is satisfied.
Neither reference silently fetches data, mutates caller-owned inputs outside the declared engine behavior, guesses hidden state, or substitutes a provider default. Shared JSON fixtures make value, null, state, and reason-code drift visible across languages.
Testing and validation
Definition tests compare every canonical field, reject malformed state, and exercise the material boundary. Family validation recomputes every playground state from the reference function. Independent arithmetic is recorded beside the fixture rather than inferred only from implementation output.
The audit must preserve these invariants:
- Persist valuation date, currency, unit scale, claim level, forecast provenance, discount rate, growth rate, and adjustment basis.
- Return each period's opening balance where relevant, discount factor, undiscounted amount, and present value.
- Expose terminal or continuing value and its share of total value.
- Separate invalid input from a valid but economically fragile estimate.
Passing definition and parity checks proves that the implementation matches the selected contract. It does not prove production performance, universal applicability, or a later market outcome.
Failure modes and misuse
- NOPAT and invested capital often require judgment-heavy adjustments for leases, research, acquisitions, and non-operating items.
- Positive EVA is an accounting-and-capital-cost construct, not proof of a tradable mispricing.
Debugging order
When a result looks surprising, inspect the state in this order:
- Confirm identifiers, scope, side, and decision clock.
- Confirm units, ordering, and point-in-time inputs.
- Confirm equality, rounding, null, and reset policies.
- Recalculate the invariant and declared scenario focus before changing code.
Evidence and historical boundary
Historical decision: Not useful for the canonical worked example. The learning obstacle is assumption architecture and reconciliation. A named security would add unverifiable author forecasts and could imply false authority unless every input were reconstructed point in time.
The primary sources are Damodaran EVA, CFA residual income, Damodaran valuation framework. They support the source roles listed in the research ledger, not a redistributable historical observation, a private participant decision, production conformance certification, execution-quality result, profitability claim, or prediction claim.
Summary and next topic
You can now calculate, reconcile, stress, and explain Economic Value Added without hiding its claim, timing, or terminal assumptions. The learning flow is: Residual-Income Model → Economic Value Added → Relative Valuation. Carry the result forward only with its scope, clock, state, and evidence label.
Deep visual atlas
Outcome and pipeline
Timing and continuing-value boundary
Assumption sensitivity
Reconciliation bridge
Model boundaries
Use these visuals together: the pipeline establishes claim order, the timeline prevents off-by-one errors, the matrix exposes fragility, the bridge checks arithmetic, and the boundary card prevents the result from being mistaken for a recommendation.
Rendered from the canonical Mermaid sources linked by this article.
Economic Value Added calculation flow
This flow identifies the selected calculation stages and the structured output.
Takeaway: EVA values the enterprise only when operating profit and invested capital use the same adjustment basis.
ReferencesPrimary sources and evidence notesExpand the source trail, evidence role, and limitations behind the engineering choices.
Expand the source trail, evidence role, and limitations behind the engineering choices.
S1 — Economic Value Added
- Organization or authors: Aswath Damodaran
- Source type: Primary or authoritative technical source
- Publication or effective date: n.d.
- Version: n.d.
- URL or DOI: https://pages.stern.nyu.edu/~adamodar/New_Home_Page/lectures/eva.html
- Accessed: 2026-08-04
- Jurisdiction: International / educational framework
- Supports: EVA is operating profit after tax less a cost-of-capital charge on invested capital and can be expressed as the return-on-capital spread times capital.
- Limitations: Accounting adjustments and capital definitions remain implementation choices that must be disclosed consistently.
S2 — Residual Income Valuation
- Organization or authors: CFA Institute
- Source type: Primary or authoritative technical source
- Publication or effective date: 2026
- Version: 2026
- URL or DOI: https://www.cfainstitute.org/insights/professional-learning/refresher-readings/2026/residual-income-valuation
- Accessed: 2026-08-04
- Jurisdiction: International / educational framework
- Supports: Residual income subtracts an equity charge from net income; equity value combines current book value and present value of expected residual income; EVA applies a WACC charge to total capital.
- Limitations: A curriculum summary is not a substitute for entity-specific accounting normalization or forecasts.
S3 — Valuation
- Organization or authors: Aswath Damodaran
- Source type: Primary or authoritative technical source
- Publication or effective date: n.d.
- Version: n.d.
- URL or DOI: https://pages.stern.nyu.edu/~adamodar/New_Home_Page/lectures/val.html
- Accessed: 2026-08-04
- Jurisdiction: International / educational framework
- Supports: Present value is built from expected cash flows discounted at a rate consistent with the claim being valued; firm cash flows pair with a firm discount rate and equity cash flows with an equity discount rate.
- Limitations: Lecture material establishes the valuation framework, not the package's particular fixture, forecast horizon, or rates.
Evidence boundary
The sources establish the exact rule, interface, protocol, or research context named above. They do not verify the repository-authored synthetic fixture, thresholds, empirical usefulness, execution probability, or profitability. Package-selected choices remain labeled as implementation choices wherever they are used.
Full dependency-light reference implementations in both supported languages.
/** Canonical TypeScript parity implementation for D18-F02 Intrinsic Valuation. */
type Input = Record<string, unknown>;
type Output = Record<string, unknown>;
function numberValue(value: unknown, name: string): number {
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${name} must be a finite number`);
return value;
}
function rate(value: unknown, name: string): number {
const result = numberValue(value, name);
if (result <= 0 || result >= 1) throw new Error(`${name} must be strictly between 0 and 1`);
return result;
}
function growth(value: unknown, name: string, discountRate: number): number {
const result = numberValue(value, name);
if (result <= -1) throw new Error(`${name} must be greater than -1`);
if (result >= discountRate) throw new Error(`${name} must be below the matching discount rate`);
return result;
}
function series(value: unknown, name: string): number[] {
if (!Array.isArray(value) || value.length < 1) throw new Error(`${name} must contain at least 1 value`);
return value.map((item, index) => numberValue(item, `${name}[${index}]`));
}
function sameLength(left: unknown[], right: unknown[], leftName: string, rightName: string): void {
if (left.length !== right.length) throw new Error(`${leftName} and ${rightName} must have the same length`);
}
function discount(value: number, discountRate: number, period: number): [number, number] {
const factor = 1 / ((1 + discountRate) ** period);
return [factor, value * factor];
}
function fcffDcf(data: Input): Output {
const fcff = series(data.fcff, "fcff");
const wacc = rate(data.wacc, "wacc");
const terminalGrowth = growth(data.terminal_growth, "terminal_growth", wacc);
const cash = numberValue(data.cash_and_non_operating_assets, "cash_and_non_operating_assets");
const debt = numberValue(data.debt, "debt");
const preferred = numberValue(data.preferred_equity, "preferred_equity");
const minority = numberValue(data.noncontrolling_interest, "noncontrolling_interest");
const shares = numberValue(data.diluted_shares, "diluted_shares");
if (shares <= 0) throw new Error("diluted_shares must be positive");
let pvExplicit = 0;
const schedule = fcff.map((flow, index) => {
const period = index + 1;
const [discountFactor, presentValue] = discount(flow, wacc, period);
pvExplicit += presentValue;
return { period, fcff: flow, discount_factor: discountFactor, present_value: presentValue };
});
const terminalFcff = fcff.at(-1)! * (1 + terminalGrowth);
const terminalValue = terminalFcff / (wacc - terminalGrowth);
const [terminalDiscountFactor, pvTerminal] = discount(terminalValue, wacc, fcff.length);
const enterpriseValue = pvExplicit + pvTerminal;
const netEquityBridge = cash - debt - preferred - minority;
const equityValue = enterpriseValue + netEquityBridge;
return {
state: "valued", method: "fcff-enterprise-dcf", schedule,
pv_explicit_fcff: pvExplicit, terminal_fcff: terminalFcff, terminal_value: terminalValue,
terminal_discount_factor: terminalDiscountFactor, pv_terminal_value: pvTerminal,
terminal_value_share: enterpriseValue ? pvTerminal / enterpriseValue : 0,
enterprise_value: enterpriseValue, net_equity_bridge: netEquityBridge,
equity_value: equityValue, intrinsic_value_per_share: equityValue / shares,
};
}
function ddm(data: Input): Output {
const dividends = series(data.dividends, "dividends");
if (dividends.some((value) => value < 0)) throw new Error("dividends must be nonnegative");
const cost = rate(data.cost_of_equity, "cost_of_equity");
const terminalGrowth = growth(data.terminal_growth, "terminal_growth", cost);
let pvExplicit = 0;
const schedule = dividends.map((dividend, index) => {
const period = index + 1;
const [discountFactor, presentValue] = discount(dividend, cost, period);
pvExplicit += presentValue;
return { period, dividend, discount_factor: discountFactor, present_value: presentValue };
});
const terminalDividend = dividends.at(-1)! * (1 + terminalGrowth);
const terminalValue = terminalDividend / (cost - terminalGrowth);
const [terminalDiscountFactor, pvTerminal] = discount(terminalValue, cost, dividends.length);
const value = pvExplicit + pvTerminal;
return {
state: "valued", method: "multi-stage-dividend-discount", schedule,
pv_explicit_dividends: pvExplicit, terminal_dividend: terminalDividend,
terminal_value: terminalValue, terminal_discount_factor: terminalDiscountFactor,
pv_terminal_value: pvTerminal, terminal_value_share: value ? pvTerminal / value : 0,
intrinsic_value_per_share: value,
};
}
function gordon(data: Input): Output {
const dividendNext = numberValue(data.dividend_next, "dividend_next");
if (dividendNext < 0) throw new Error("dividend_next must be nonnegative");
const cost = rate(data.cost_of_equity, "cost_of_equity");
const perpetualGrowth = growth(data.perpetual_growth, "perpetual_growth", cost);
const spread = cost - perpetualGrowth;
return {
state: "valued", method: "gordon-growth", dividend_timing: "D1-next-period",
discount_growth_spread: spread, capitalization_multiple: 1 / spread,
intrinsic_value_per_share: dividendNext / spread,
};
}
function residualIncome(data: Input): Output {
const openingBook = numberValue(data.opening_book_value_per_share, "opening_book_value_per_share");
if (openingBook <= 0) throw new Error("opening_book_value_per_share must be positive");
const earnings = series(data.earnings_per_share, "earnings_per_share");
const dividends = series(data.dividends_per_share, "dividends_per_share");
const adjustments = series(data.nonowner_equity_adjustments_per_share, "nonowner_equity_adjustments_per_share");
sameLength(earnings, dividends, "earnings_per_share", "dividends_per_share");
sameLength(earnings, adjustments, "earnings_per_share", "nonowner_equity_adjustments_per_share");
const cost = rate(data.cost_of_equity, "cost_of_equity");
const terminalGrowth = growth(data.terminal_residual_income_growth, "terminal_residual_income_growth", cost);
let book = openingBook;
let pvExplicit = 0;
const residualValues: number[] = [];
const schedule = earnings.map((eps, index) => {
const period = index + 1;
const dividend = dividends[index];
const adjustment = adjustments[index];
if (dividend < 0) throw new Error("dividends_per_share must be nonnegative");
const opening = book;
const equityCharge = cost * opening;
const residual = eps - equityCharge;
const [discountFactor, presentValue] = discount(residual, cost, period);
const closing = opening + eps - dividend + adjustment;
if (closing <= 0) throw new Error("clean-surplus book-value roll-forward must remain positive");
pvExplicit += presentValue;
residualValues.push(residual);
book = closing;
return {
period, opening_book_value_per_share: opening, earnings_per_share: eps,
dividends_per_share: dividend, nonowner_equity_adjustment_per_share: adjustment,
equity_charge_per_share: equityCharge, residual_income_per_share: residual,
discount_factor: discountFactor, present_value: presentValue,
closing_book_value_per_share: closing,
};
});
const terminalResidual = residualValues.at(-1)! * (1 + terminalGrowth);
const continuingValue = terminalResidual / (cost - terminalGrowth);
const [terminalDiscountFactor, pvContinuing] = discount(continuingValue, cost, earnings.length);
const value = openingBook + pvExplicit + pvContinuing;
return {
state: "valued", method: "residual-income", schedule,
pv_explicit_residual_income: pvExplicit, terminal_residual_income_per_share: terminalResidual,
continuing_value: continuingValue, terminal_discount_factor: terminalDiscountFactor,
pv_continuing_value: pvContinuing, continuing_value_share: value ? pvContinuing / value : 0,
closing_book_value_per_share: book, intrinsic_value_per_share: value,
};
}
function eva(data: Input): Output {
const openingCapital = numberValue(data.opening_invested_capital, "opening_invested_capital");
if (openingCapital <= 0) throw new Error("opening_invested_capital must be positive");
const nopat = series(data.nopat, "nopat");
const investment = series(data.net_investment, "net_investment");
sameLength(nopat, investment, "nopat", "net_investment");
const wacc = rate(data.wacc, "wacc");
const terminalGrowth = growth(data.terminal_eva_growth, "terminal_eva_growth", wacc);
let capital = openingCapital;
let pvExplicit = 0;
const evaValues: number[] = [];
const schedule = nopat.map((periodNopat, index) => {
const period = index + 1;
const opening = capital;
const capitalCharge = wacc * opening;
const periodEva = periodNopat - capitalCharge;
const roic = periodNopat / opening;
const [discountFactor, presentValue] = discount(periodEva, wacc, period);
const closing = opening + investment[index];
if (closing <= 0) throw new Error("invested-capital roll-forward must remain positive");
pvExplicit += presentValue;
evaValues.push(periodEva);
capital = closing;
return {
period, opening_invested_capital: opening, nopat: periodNopat,
net_investment: investment[index], capital_charge: capitalCharge, roic,
roic_wacc_spread: roic - wacc, eva: periodEva,
discount_factor: discountFactor, present_value: presentValue,
closing_invested_capital: closing,
};
});
const terminalEva = evaValues.at(-1)! * (1 + terminalGrowth);
const continuingValue = terminalEva / (wacc - terminalGrowth);
const [terminalDiscountFactor, pvContinuing] = discount(continuingValue, wacc, nopat.length);
const marketValueAdded = pvExplicit + pvContinuing;
const enterpriseValue = openingCapital + marketValueAdded;
return {
state: "valued", method: "economic-value-added", schedule,
pv_explicit_eva: pvExplicit, terminal_eva: terminalEva, continuing_value: continuingValue,
terminal_discount_factor: terminalDiscountFactor, pv_continuing_value: pvContinuing,
market_value_added: marketValueAdded,
continuing_value_share: enterpriseValue ? pvContinuing / enterpriseValue : 0,
closing_invested_capital: capital, enterprise_value: enterpriseValue,
};
}
const calculators: Record<string, (data: Input) => Output> = {
"D18-F02-A01": fcffDcf,
"D18-F02-A02": ddm,
"D18-F02-A03": gordon,
"D18-F02-A04": residualIncome,
"D18-F02-A05": eva,
};
export function calculate(topicId: string, data: Input): Output {
const calculator = calculators[topicId];
if (!calculator) throw new Error(`unsupported topic_id: ${topicId}`);
if (!data || Array.isArray(data) || typeof data !== "object") throw new Error("data must be an object");
return calculator(data);
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.