import assert from 'node:assert/strict'; import { calculateDCF } from '../js/tools/dcf-math.js'; const base = {basis: 'fcff', fcf: 100, year: 2026, rate: .1, terminal: .02, shares: 10, debt: 100, assets: 0, minority: 0, margin: .2, stages: [{years: 3, growth: .12}, {years: 4, growth: .08}, {years: 3, growth: .04}]}; const near = (actual, expected) => assert.ok(Math.abs(actual - expected) <= 1e-8 * Math.max(1, Math.abs(expected)), `${actual} ≠ ${expected}`); const r = calculateDCF(base); assert.equal(r.rows.length, 10); near(r.rows[0].cash, 112); near(r.rows[3].cash, 100 * 1.12 ** 3 * 1.08); near(r.rows.at(-1).cash, 100 * 1.12 ** 3 * 1.08 ** 4 * 1.04 ** 3); near(r.forecastPV, r.rows.reduce((sum, row) => sum + row.cash / 1.1 ** (row.year - base.year), 0)); near(r.terminalPV, r.rows.at(-1).cash * 1.02 / .08 / 1.1 ** 10); near(r.equityValue, r.operatingValue - 100); near(r.perShare, r.equityValue / 10); near(r.entryPrice, r.perShare * .8); // Each company's share count changes per-share valuation, never the company's total DCF. const diluted = calculateDCF({...base, shares: 20}); near(diluted.equityValue, r.equityValue); near(diluted.perShare, r.perShare / 2); near(diluted.entryPrice, r.entryPrice / 2); for (const years of [1, 10, 30]) { near(calculateDCF({...base, terminal: 0, stages: [{years, growth: 0}]}).operatingValue, 1000); near(calculateDCF({...base, stages: [{years, growth: .02}]}).operatingValue, 1275); } const fcfe = calculateDCF({...base, basis: 'fcfe', debt: NaN, assets: NaN, minority: NaN}); near(fcfe.equityValue, r.operatingValue); near(fcfe.adjustment, 0); const adjusted = calculateDCF({...base, debt: -50, assets: 20, minority: 10}); near(adjusted.equityValue, r.operatingValue + 60); const noEquity = calculateDCF({...base, debt: 1e6}); assert.ok(noEquity.equityValue < 0); const shrinking = calculateDCF({...base, stages: [{years: 10, growth: -.05}]}); assert.ok(shrinking.rows.at(-1).cash < 100); for (const patch of [{shares: 0}, {shares: -1}, {shares: NaN}, {fcf: 0}, {fcf: Infinity}, {year: 2026.5}, {basis: 'invalid'}, {terminal: .1}, {terminal: -1}, {rate: -1}, {margin: 1}, {margin: -.1}, {stages: []}, {stages: [{years: 31, growth: .1}]}, {stages: [{years: 1.5, growth: .1}]}, {stages: [{years: 1, growth: -1}]}, {stages: [{years: 1, growth: Infinity}]}, {debt: NaN}, {stages: [{years: 30, growth: 1e300}]}]) assert.throws(() => calculateDCF({...base, ...patch})); console.log('✓ DCF 分段复利、永续现金流、折现时点、股权调整、公司股本换算及无效输入'); // Price-zone thresholds are inclusive at both ends of the fair band. const { calculateDCFPriceRange, classifyDCFPrice } = await import('../js/tools/dcf-math.js'); const { buildDCFPriceOption } = await import('../js/tools/dcf-price-chart.js'); const range = calculateDCFPriceRange({perShare: 100, margin: .2, premium: .2}); assert.deepEqual(range, {low: 80, high: 120, perShare: 100}); for (const [price, zone] of [[79.99, '低估'], [80, '合理'], [100, '合理'], [120, '合理'], [120.01, '高估']]) { assert.equal(classifyDCFPrice(price, range).zone, zone); } near(classifyDCFPrice(80, range).deviation, -.2); const pointRange = calculateDCFPriceRange({perShare: 100, margin: 0, premium: 0}); assert.equal(classifyDCFPrice(100, pointRange).zone, '合理'); for (const patch of [{perShare: 0}, {perShare: -1}, {perShare: NaN}, {margin: -.1}, {margin: 1}, {premium: -.1}, {premium: Infinity}]) { assert.throws(() => calculateDCFPriceRange({perShare: 100, margin: .2, premium: .2, ...patch})); } for (const price of [0, -1, NaN, Infinity]) assert.throws(() => classifyDCFPrice(price, range)); const colors = {ink:'#333', muted:'#777', line:'#eee', paper:'#fff', accent:'#654', low:'#ded', fair:'#eed', high:'#edd'}; const unquoted = buildDCFPriceOption({range, colors, width: 320}); assert.equal(unquoted.series.some(series => series.type === 'scatter'), false); const quoted = buildDCFPriceOption({range, price: 200, colors, width: 320}); assert.ok(quoted.xAxis.max > 200); assert.deepEqual(quoted.series.at(-1).data, [[200, 0]]); near(quoted.series.slice(0, 3).reduce((sum, series) => sum + series.data[0], 0), quoted.xAxis.max); console.log('✓ DCF 低估/合理/高估边界、折溢价、可选股价及图表标记');