Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions src/skills/dcf/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,30 @@ Calculate WACC using `debt_to_equity` for capital structure weights.

## Step 5: Calculate Present Value

Discount all FCFs → sum for Enterprise Value → subtract Net Debt → divide by `outstanding_shares` for fair value per share.
Call the `dcf_calculator` tool for all DCF arithmetic.

**Do not calculate terminal value, enterprise value, equity value, value per share, or sensitivity tables manually.**

Pass:
- `baseFcf`: normalized current FCF or owner earnings
- `growthRates`: year-by-year growth assumptions as decimals
- `discountRate`: WACC / required return as a decimal
- `terminalGrowthRate`: terminal growth as a decimal
- `netDebt`: debt minus cash/investments; use a negative number for net cash
- `sharesOutstanding`: share count in the same scale as monetary values
- `units`: `billions`, `millions`, or `raw`

Use the calculator output as the source of truth for:
- projected FCFs
- PV of projected FCFs
- terminal value and PV of terminal value
- enterprise value
- equity value
- value per share

## Step 6: Sensitivity Analysis

Create 3×3 matrix: WACC (base ±1%) vs terminal growth (2.0%, 2.5%, 3.0%).
Use the `dcf_calculator` sensitivity output. For U.S. companies, use WACC (base ±1%) vs terminal growth (2.0%, 2.5%, 3.0%) unless better case-specific assumptions are justified.

## Step 7: Validate Results

Expand All @@ -119,6 +138,8 @@ Before presenting, verify these sanity checks:

3. **Per-share cross-check**: Compare to `free_cash_flow_per_share × 15-25` as rough sanity check

4. **Calculator reconciliation**: Final valuation tables must reconcile to the `dcf_calculator` output. If a manually drafted number differs from the calculator output, use the calculator output or rerun the tool with corrected inputs.

If validation fails, reconsider assumptions before presenting results.

## Step 8: Output Format
Expand Down
8 changes: 8 additions & 0 deletions src/tools/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { browserTool, BROWSER_DESCRIPTION } from './browser/browser.js';
import { readFileTool, READ_FILE_DESCRIPTION } from './filesystem/read-file.js';
import { writeFileTool, WRITE_FILE_DESCRIPTION } from './filesystem/write-file.js';
import { editFileTool, EDIT_FILE_DESCRIPTION } from './filesystem/edit-file.js';
import { dcfCalculatorTool, DCF_CALCULATOR_DESCRIPTION } from './valuation/dcf-calculator.js';
import { GET_FINANCIALS_DESCRIPTION } from './finance/get-financials.js';
import { GET_MARKET_DATA_DESCRIPTION } from './finance/get-market-data.js';
import { READ_FILINGS_DESCRIPTION } from './finance/read-filings.js';
Expand Down Expand Up @@ -69,6 +70,13 @@ export function getToolRegistry(model: string): RegisteredTool[] {
compactDescription: 'Screen stocks by financial criteria (P/E, growth, margins, etc.).',
concurrencySafe: true,
},
{
name: 'dcf_calculator',
tool: dcfCalculatorTool,
description: DCF_CALCULATOR_DESCRIPTION,
compactDescription: 'Deterministic DCF math: projected FCFs, terminal value, PVs, fair value per share, and sensitivity.',
concurrencySafe: true,
},
{
name: 'web_fetch',
tool: webFetchTool,
Expand Down
100 changes: 100 additions & 0 deletions src/tools/valuation/dcf-calculator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { describe, expect, test } from 'bun:test';
import { calculateDcf, dcfCalculatorTool } from './dcf-calculator.js';

describe('calculateDcf', () => {
test('reconciles the Apple owner-earnings case that exposed the terminal value bug', () => {
const result = calculateDcf({
baseFcf: 83.8,
growthRates: [0.05, 0.045, 0.04, 0.035, 0.03],
discountRate: 0.10,
terminalGrowthRate: 0.025,
netDebt: -20,
sharesOutstanding: 14.95,
units: 'billions',
});

expect(result.projections.map((projection) => projection.fcf)).toEqual([
87.99,
91.9495,
95.6275,
98.9745,
101.9437,
]);
expect(result.totalPvFcf).toBeCloseTo(358.7286, 4);
expect(result.terminalValue).toBeCloseTo(1393.2310, 4);
expect(result.pvTerminalValue).toBeCloseTo(865.0868, 4);
expect(result.enterpriseValue).toBeCloseTo(1223.8154, 4);
expect(result.equityValue).toBeCloseTo(1243.8154, 4);
expect(result.valuePerShare).toBeCloseTo(83.1984, 4);
});

test('computes the same base case when explicit projected FCFs are provided', () => {
const result = calculateDcf({
baseFcf: 83.8,
projectedFcfs: [88.0, 92.0, 95.7, 99.1, 102.1],
discountRate: 0.10,
terminalGrowthRate: 0.025,
netDebt: -20,
sharesOutstanding: 14.95,
units: 'billions',
});

expect(result.totalPvFcf).toBeCloseTo(359.0166, 4);
expect(result.terminalValue).toBeCloseTo(1395.3667, 4);
expect(result.pvTerminalValue).toBeCloseTo(866.4129, 4);
expect(result.valuePerShare).toBeCloseTo(83.3063, 4);
});

test('returns a sensitivity matrix from explicit rate grids', () => {
const result = calculateDcf({
baseFcf: 83.8,
growthRates: [0.05, 0.045, 0.04, 0.035, 0.03],
discountRate: 0.10,
terminalGrowthRate: 0.025,
netDebt: -20,
sharesOutstanding: 14.95,
discountRates: [0.09, 0.10, 0.11],
terminalGrowthRates: [0.02, 0.025, 0.03],
units: 'billions',
});

expect(result.sensitivity).toHaveLength(3);
expect(result.sensitivity[0]).toHaveLength(3);
expect(result.sensitivity[1][1].valuePerShare).toBeCloseTo(result.valuePerShare, 4);
expect(result.sensitivity[0][2].valuePerShare).toBeGreaterThan(result.sensitivity[2][0].valuePerShare);
});

test('rejects terminal growth at or above the discount rate', () => {
expect(() =>
calculateDcf({
baseFcf: 10,
growthRates: [0.03],
discountRate: 0.03,
terminalGrowthRate: 0.03,
sharesOutstanding: 1,
}),
).toThrow('discountRate must be greater than terminalGrowthRate');
});
});

describe('dcfCalculatorTool', () => {
test('serializes deterministic DCF output as a tool result', async () => {
const raw = await dcfCalculatorTool.func({
baseFcf: 83.8,
growthRates: [0.05, 0.045, 0.04, 0.035, 0.03],
discountRate: 0.10,
terminalGrowthRate: 0.025,
netDebt: -20,
sharesOutstanding: 14.95,
units: 'billions',
});
expect(typeof raw).toBe('string');
if (typeof raw !== 'string') {
throw new Error('Expected dcfCalculatorTool to return a string result.');
}
const parsed = JSON.parse(raw) as { data: { valuePerShare: number; pvTerminalValue: number } };

expect(parsed.data.valuePerShare).toBeCloseTo(83.1984, 4);
expect(parsed.data.pvTerminalValue).toBeCloseTo(865.0868, 4);
});
});
Loading
Loading