summaryrefslogtreecommitdiff
path: root/frontend/scripts/verify-dcf.ts
diff options
context:
space:
mode:
authorTyler Hoang <tyler@tylerhoang.xyz>2026-06-29 01:41:21 -0700
committerTyler Hoang <tyler@tylerhoang.xyz>2026-06-29 01:41:21 -0700
commit2525596329ce57d01b012a18b0505b05fa8011d7 (patch)
tree33c1eee6afb5971332d55e43a1375bc39b9ca41c /frontend/scripts/verify-dcf.ts
parentf1b1e85b53db87a04442181a40c530aca4791a14 (diff)
feat: add DCF verification script
Diffstat (limited to 'frontend/scripts/verify-dcf.ts')
-rw-r--r--frontend/scripts/verify-dcf.ts129
1 files changed, 129 insertions, 0 deletions
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<keyof typeof scenario.expected>;
+ 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);
+}