41 lines
2.7 KiB
JavaScript
41 lines
2.7 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { calculate } from '../js/tools/stock-return-math.js';
|
|
|
|
const base = {price: 220, base_profit_100m: 860, shares_100m: 46.27, exit_pe: 13,
|
|
years: 3, base_year: 2026, target_return: .13, growth_rate: .1,
|
|
safety_margin: 0, annual_dividend_per_share: 0, dividend_growth_rate: 0, annual_dilution_rate: 0};
|
|
const near = (actual, expected, tolerance = 1e-8) => assert.ok(Math.abs(actual - expected) < tolerance, `${actual} ≠ ${expected}`);
|
|
const example = calculate(base);
|
|
near(example.required_growth.required_growth_rate, .0952, .0001);
|
|
near(example.required_growth.required_exit_price, 317.43734);
|
|
near(example.required_growth.required_terminal_profit_100m, 317.43734 * 46.27 / 13);
|
|
|
|
// Reverse/forward cash-flow consistency with dividends, dilution, negative rates and FX-normalized inputs.
|
|
for (const patch of [{}, {annual_dividend_per_share: 9.3, dividend_growth_rate: .08},
|
|
{annual_dilution_rate: .02}, {annual_dilution_rate: -.03, growth_rate: -.08},
|
|
{years: 5, target_return: .15}, {target_return: -.15},
|
|
{price: 398 * .85619, annual_dividend_per_share: 5.3 * .85619, base_profit_100m: 2300, shares_100m: 90.93, exit_pe: 15}]) {
|
|
for (const safety_margin of [0, .2, .5]) {
|
|
const input = {...base, ...patch, safety_margin};
|
|
const r = calculate(input);
|
|
const required = r.required_growth_with_safety_margin.required_growth_rate;
|
|
const forward = calculate({...input, growth_rate: required});
|
|
near(forward.max_purchase_price_with_safety_margin, input.price);
|
|
near(forward.margin_of_safety_at_purchase, safety_margin);
|
|
near(r.max_purchase_price_with_safety_margin, r.max_purchase_price_at_target_return * (1 - safety_margin));
|
|
near(calculate({...input, safety_margin: 0}).holding_period_irr, r.holding_period_irr);
|
|
const noMargin = calculate({...input, growth_rate: r.required_growth.required_growth_rate});
|
|
near(noMargin.holding_period_irr, input.target_return);
|
|
}
|
|
}
|
|
const covered = calculate({...base, annual_dividend_per_share: 100, safety_margin: .5});
|
|
assert.equal(covered.required_growth.required_growth_rate, null);
|
|
assert.notEqual(covered.required_growth_with_safety_margin.required_growth_rate, null);
|
|
assert.equal(calculate({...base, annual_dividend_per_share: 500}).required_growth_with_safety_margin.required_growth_rate, null);
|
|
for (const patch of [{price: 0}, {base_profit_100m: -1}, {shares_100m: 0}, {exit_pe: 0},
|
|
{years: 0}, {years: 3.2}, {years: 31}, {target_return: -1}, {annual_dilution_rate: -1},
|
|
{annual_dividend_per_share: -2}, {safety_margin: -1}, {safety_margin: 1}, {price: Infinity}]) {
|
|
assert.throws(() => calculate({...base, ...patch}));
|
|
}
|
|
console.log('✓ 估值示例、现金流反向/正向验证、安全边际、多币种、分红、摊薄及边界条件');
|