From 2525596329ce57d01b012a18b0505b05fa8011d7 Mon Sep 17 00:00:00 2001 From: Tyler Hoang Date: Mon, 29 Jun 2026 01:41:21 -0700 Subject: feat: add DCF verification script --- frontend/scripts/verify-dcf.ts | 129 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 frontend/scripts/verify-dcf.ts (limited to 'frontend/scripts') diff --git a/frontend/scripts/verify-dcf.ts b/frontend/scripts/verify-dcf.ts new file mode 100644 index 0000000..2787eb1 --- /dev/null +++ b/frontend/scripts/verify-dcf.ts @@ -0,0 +1,129 @@ +import { computeDcf, DcfInputs, DcfParams } from "../lib/dcf"; + +interface Scenario { + name: string; + inputs: DcfInputs; + params: DcfParams; + expected: { + intrinsicValuePerShare: number; + enterpriseValue: number; + equityValue: number; + fcfPvSum: number; + terminalValuePv: number; + }; +} + +// Independent reference implementation of the DCF math used by both backend +// (data_service._run_dcf) and frontend (lib/dcf.ts:computeDcf). +function expectedDcf( + baseFcf: number, + growthRate: number, + wacc: number, + terminalGrowth: number, + projectionYears: number, + equityBridge: number, + sharesOutstanding: number +) { + const projected: number[] = []; + for (let i = 1; i <= projectionYears; i++) { + projected.push(baseFcf * (1 + growthRate) ** i); + } + + const fcfPvSum = projected.reduce( + (sum, fcf, i) => sum + fcf / (1 + wacc) ** (i + 0.5), + 0 + ); + + const terminalFcf = projected[projected.length - 1] * (1 + terminalGrowth); + const terminalValue = terminalFcf / (wacc - terminalGrowth); + const terminalValuePv = terminalValue / (1 + wacc) ** (projectionYears - 0.5); + + const enterpriseValue = fcfPvSum + terminalValuePv; + const equityValue = enterpriseValue - equityBridge; + const intrinsicValuePerShare = equityValue / sharesOutstanding; + + return { + intrinsicValuePerShare, + enterpriseValue, + equityValue, + fcfPvSum, + terminalValuePv, + }; +} + +function makeScenario( + name: string, + baseFcf: number, + growthRate: number, + wacc: number, + terminalGrowth: number, + projectionYears: number, + sharesOutstanding: number, + netDebt: number, + preferredEquity = 0, + minorityInterest = 0 +): Scenario { + const equityBridge = netDebt + preferredEquity + minorityInterest; + return { + name, + inputs: { baseFcf, equityBridge, sharesOutstanding }, + params: { wacc, terminalGrowth, projectionYears, growthRate }, + expected: expectedDcf( + baseFcf, + growthRate, + wacc, + terminalGrowth, + projectionYears, + equityBridge, + sharesOutstanding + ), + }; +} + +const scenarios: Scenario[] = [ + makeScenario("1-year with equity bridge", 105, 0.05, 0.1, 0.03, 1, 10, 15, 2, 3), + makeScenario("3-year no claims", 105, 0.05, 0.1, 0.03, 3, 10, 0), + makeScenario("5-year default horizon", 105, 0.05, 0.1, 0.03, 5, 10, 0), + makeScenario("zero growth flat FCF", 100, 0.0, 0.1, 0.02, 5, 10, 0), +]; + +function closeEnough(actual: number, expected: number): boolean { + const tol = 1e-9 + 1e-6 * Math.abs(expected); + return Math.abs(actual - expected) <= tol; +} + +let failed = 0; + +for (const scenario of scenarios) { + const computed = computeDcf(scenario.inputs, scenario.params); + if ("error" in computed) { + console.error(`FAIL ${scenario.name}: returned error "${computed.error}"`); + failed++; + continue; + } + + const keys = Object.keys(scenario.expected) as Array; + const mismatches = keys.filter( + (key) => !closeEnough(computed[key], scenario.expected[key]) + ); + + if (mismatches.length === 0) { + console.log(`PASS ${scenario.name}`); + } else { + console.error(`FAIL ${scenario.name}`); + for (const key of mismatches) { + console.error( + ` ${key}: got ${computed[key]}, expected ${scenario.expected[key]}` + ); + } + failed++; + } +} + +if (failed === 0) { + console.log("\nAll frontend DCF scenarios passed."); + process.exit(0); +} else { + console.error(`\n${failed} frontend DCF scenario(s) failed.`); + process.exit(1); +} -- cgit v1.3-2-g0d8e