1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
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);
}
|