《独立开发者的商业指南·资本与退出》5.2 DCF 与情景加权

DCF 不是精确答案,而是把现金流、增长率、折现率和终值四个假设摆上桌。本节用 LedgerLite 的 SDE $130,240 推出自由现金流 $61,299,做悲观、基准、乐观三情景 DCF,加权得到 $896,776,并与 3.2× ARR = $768,768 对照,给出差距超过 30% 时的判断规则。

以下情境为教学假设,人物与数字不代表经核实的真实案例。

5.1 用倍数回答「同类资产按什么价格成交」,这一节用 DCF 回答「这门生意未来能产生多少现金」。两个问题没有谁更高级:倍数依赖可比交易,DCF 依赖经营假设;独立开发者通常没有足够多的可比交易,所以 DCF 的价值不在于给出一个精确价格,而在于把「价格依赖哪些假设」写清楚。

DCF 的公式不复杂,复杂的是四个输入:现金流起点、增长率、折现率和终值。四个输入里任何一个变动,结果都会大幅变化。本节用 LedgerLite 的 SDE $130,240 做三情景 DCF,再与 3.2× ARR = $768,768 的倍数结果对照,最后回答一个实际问题:当 DCF 和倍数差 30% 以上时,应该信哪个。

四个输入,四个可以争论的地方

DCF 的第一步不是折现,而是把 SDE 变成买方能拿走的自由现金流。LedgerLite 的 SDE 是 $130,240,它在创始人薪酬之前;买方接手后要雇一个管理者,所以先扣 $60,000 替代薪酬,得到税前经营利润 $70,240;再扣案例给出的教学所得税 $11,941,得到税后经营利润 $58,299(案例没有利息,所以它与净利润相同);最后加回折旧 $3,000(教学假设:资本开支与折旧相抵,营运资金变动按零处理),得到无杠杆自由现金流 $61,299。折现率按教学假设视为无杠杆资本成本,所以折现结果就是企业价值;现金和负债不在这个现金流里,留到 5.3 的桥处理。

这个 $61,299 与案例的净现金增加一致:净利润 $58,299 加回折旧 $3,000;换成现金流表的路径,是递延收入 +$8,000、应收 −$2,000、资本开支 −$3,000,三项合计 +$3,000。两条路径得到同一个数,但 DCF 只把第一年之后当成稳定状态,不假设递延收入每年都增加。

输入LedgerLite 取值为什么敏感本节处理
现金流起点SDE $130,240 → FCF $61,299起点差 10%,估值大致差 10%先扣替代管理者工资和税
增长率3% / 8% / 14%第 5 年 FCF 从 $71,062 到 $118,026教学假设,按情景给
折现率18% / 15% / 12%终值要折现 5 年,1 个点就会移动价格反映关键人、流失与集中度
终值倍数10× / 13× / 16× FCF终值占总价值 59.9%–76.8%教学假设,不称市场倍数

四个输入里,终值最容易被低估。五年后的终值要再折现回今天,而终值本身又依赖第 5 年的 FCF 和一个倍数。两个假设叠在一起,终值往往占总价值的大部分。下面的表会显示:三情景的终值占比分别是 59.9%、69.6% 和 76.8%;DCF 看起来在算五年现金流,实际上大半价格来自第五年之后的那一个倍数。

FCF_t = FCF_{t-1} × (1 + g)
企业价值 = Σ FCF_t ÷ (1 + r)^t + 终值 ÷ (1 + r)^5
终值 = FCF_5 × 终值倍数

三情景:悲观、基准、乐观

三情景不是三个故事,而是三组输入。悲观情景假设增长 3%、折现率 18%、终值 10× FCF;基准情景假设增长 8%、折现率 15%、终值 13× FCF;乐观情景假设增长 14%、折现率 12%、终值 16× FCF。增长率、折现率和终值倍数都是教学假设,不是市场预测。

情景增长率折现率终值倍数第 5 年 FCF终值企业价值终值占比
悲观3%18%10×$71,062$710,623$518,24759.9%
基准8%15%13×$90,068$1,170,888$837,00569.6%
乐观14%12%16×$118,026$1,888,416$1,394,84876.8%

三档的差距很大:悲观 $518,247,乐观 $1,394,848,相差 $876,601,是悲观值的 1.69 倍。这个差距不是模型算错了,而是输入不同。折现率从 18% 降到 12%,增长率从 3% 升到 14%,终值倍数从 10× 升到 16×,三个假设同时变好,结果自然大幅上升。

如果只取基准,会得到一个看起来精确的 $837,005;但这个数字的 69.6% 来自终值。换句话说,基准 DCF 的大部分价格不是由未来五年的现金流决定的,而是由「第五年之后还能按 13× FCF 卖出去」这个假设决定的。这就是为什么 DCF 不适合单独报价:它把一个远期假设包装成了一个当期数字。

情景加权把三档合成一个数。教学权重取悲观 25%、基准 50%、乐观 25%,加权结果是 0.25 × 518,247 + 0.50 × 837,005 + 0.25 × 1,394,848 = $896,776。加权终值占比是 71.0%。权重不是概率,而是卖方对三种经营路径的相对信心;换一组权重,结果就会变,所以权重必须和情景一起披露。

与倍数法对照

把 DCF 结果换算成 ARR 倍数,才能和 3.2× ARR = $768,768 放在同一把尺子上。LedgerLite 的 ARR 是 $240,240:

口径企业价值隐含 ARR 倍数与 3.2× ARR 的差
悲观 DCF$518,2472.16×−32.6%
基准 DCF$837,0053.48×+8.9%
乐观 DCF$1,394,8485.81×+81.4%
加权 DCF$896,7763.73×+16.7%
倍数法$768,7683.20×0

基准 DCF 只比倍数高 8.9%,两者在 30% 以内,可以视为相互印证。加权 DCF 高 16.7%,也在 30% 以内。但悲观和乐观各自越过了 30% 线:悲观低 32.6%,乐观高 81.4%。这正是 DCF 的用途:它不告诉你哪个数字对,它告诉你价格在什么假设下会落到什么位置。

差距超过 30% 时,判断规则不是「取平均」,而是看哪一方的假设更可验证。DCF 高于倍数时,先信倍数,因为高出来的部分通常来自终值倍数或增长率,而这两个假设最难验证;除非买方能指出可量化的协同,比如明确的交叉销售收入或渠道成本节省。DCF 低于倍数时,先信 DCF,因为现金流已经说明这门生意撑不起那个倍数;同样,除非买方能给出可量化的协同,否则倍数里的乐观假设没有依据。

这条规则是教学判断,不是投资建议。它之所以不对称,是因为两种错误的方向不同:DCF 高估时,错误藏在终值里,卖方容易把「五年后还能按 13× 卖」当成事实;DCF 低估时,错误藏在当期现金流里,而现金流比终值更容易核实。对独立开发者,这意味着:当 DCF 算出比市场倍数高很多的价格时,不要把 DCF 当成谈判武器;当 DCF 算出比市场倍数低很多的价格时,要认真看是不是自己的成本或流失假设太乐观。

把 DCF 写成代码

下面的 dcf.ts 把四个输入、三情景和加权写成一个模块。dcf 做五年预测、终值折现和终值占比;weightedScenarios 校验权重之和为 100%,返回加权值、最低值和最高值;crossCheck 把 DCF 与倍数比较,按 30% 容差给出 aligned、dcf-high 或 dcf-low 三个判断。crossCheck 的 note 只描述假设,不给投资建议。

// file: src/value/dcf.ts
// tier: L1
// verify: node --test test/value/dcf.test.ts

import { type Cents } from "../core/money.ts";
import { type Bps, applyRate, divide, ratio } from "../core/rate.ts";
import { type Result, fail, ok } from "../core/result.ts";

export type DcfInput = {
  startingFcfCents: Cents;
  growthBps: Bps;
  discountBps: Bps;
  terminalMultipleBps: Bps;
  years: number;
};

export type DcfProjection = {
  year: number;
  fcfCents: Cents;
  presentValueCents: Cents;
};

export type DcfResult = {
  projections: DcfProjection[];
  terminalValueCents: Cents;
  terminalPresentValueCents: Cents;
  enterpriseValueCents: Cents;
  terminalShareBps: Bps;
};

export type Scenario = {
  name: string;
  weightBps: Bps;
  input: DcfInput;
};

export type ScenarioResult = {
  name: string;
  weightBps: Bps;
  enterpriseValueCents: Cents;
  terminalShareBps: Bps;
};

export type WeightedDcf = {
  scenarios: ScenarioResult[];
  lowCents: Cents;
  highCents: Cents;
  weightedCents: Cents;
  weightedTerminalShareBps: Bps;
};

export type CrossCheckVerdict = "aligned" | "dcf-high" | "dcf-low";

export type CrossCheck = {
  dcfCents: Cents;
  multipleCents: Cents;
  gapBps: Bps;
  toleranceBps: Bps;
  verdict: CrossCheckVerdict;
  note: string;
};

function checkInput(input: DcfInput): string | null {
  if (
    !Number.isInteger(input.startingFcfCents) ||
    input.startingFcfCents <= 0
  ) {
    return "starting free cash flow must be a positive integer amount in cents";
  }
  if (!Number.isInteger(input.growthBps) || input.growthBps <= -10000) {
    return "growth must be an integer above -10000 bps";
  }
  if (!Number.isInteger(input.discountBps) || input.discountBps <= 0) {
    return "discount rate must be a positive integer in basis points";
  }
  if (input.discountBps > 10000) {
    return "discount rate above 10000 bps is outside this teaching model";
  }
  if (
    !Number.isInteger(input.terminalMultipleBps) ||
    input.terminalMultipleBps <= 0
  ) {
    return "terminal multiple must be a positive integer in basis points";
  }
  if (!Number.isInteger(input.years) || input.years < 1 || input.years > 10) {
    return "years must be an integer between 1 and 10";
  }
  return null;
}

/**
 * Five-year explicit forecast plus a terminal multiple. Growth, discount rate
 * and terminal multiple are teaching assumptions, not market data. The
 * terminal share is returned because a DCF whose value is mostly terminal is a
 * multiple in disguise.
 */
export function dcf(input: DcfInput): Result<DcfResult> {
  const error = checkInput(input);
  if (error) {
    return fail(error);
  }

  const growthFactor = 1 + ratio(input.growthBps);
  const discountFactor = 1 + ratio(input.discountBps);
  let currentCents = input.startingFcfCents;
  const projections: DcfProjection[] = [];
  let presentValueTotal = 0;

  for (let year = 1; year <= input.years; year += 1) {
    currentCents = Math.round(currentCents * growthFactor);
    const presentValueCents = Math.round(
      currentCents / Math.pow(discountFactor, year),
    );
    projections.push({ year, fcfCents: currentCents, presentValueCents });
    presentValueTotal += presentValueCents;
  }

  const terminalValueCents = applyRate(
    currentCents,
    input.terminalMultipleBps,
  );
  const terminalPresentValueCents = Math.round(
    terminalValueCents / Math.pow(discountFactor, input.years),
  );
  const enterpriseValueCents = presentValueTotal + terminalPresentValueCents;

  return ok({
    projections,
    terminalValueCents,
    terminalPresentValueCents,
    enterpriseValueCents,
    terminalShareBps: divide(
      terminalPresentValueCents,
      enterpriseValueCents,
    ),
  });
}

/**
 * Weighted scenarios are not a vote. The weights are teaching assumptions that
 * must be written down before the result is known, and the low/high values are
 * kept alongside the weighted value so one optimistic case cannot hide the
 * downside.
 */
export function weightedScenarios(
  scenarios: Scenario[],
): Result<WeightedDcf> {
  if (scenarios.length === 0) {
    return fail("at least one scenario is required");
  }

  let weightTotal = 0;
  let weightedEnterpriseValue = 0;
  let weightedTerminalValue = 0;
  let lowCents = Number.POSITIVE_INFINITY;
  let highCents = 0;
  const results: ScenarioResult[] = [];

  for (const scenario of scenarios) {
    if (typeof scenario.name !== "string" || scenario.name.trim() === "") {
      return fail("scenario name must be a non-empty string");
    }
    if (
      !Number.isInteger(scenario.weightBps) ||
      scenario.weightBps < 0 ||
      scenario.weightBps > 10000
    ) {
      return fail("scenario weight must be between 0 and 10000 bps");
    }
    const error = checkInput(scenario.input);
    if (error) {
      return fail(error);
    }
    const result = dcf(scenario.input);
    if (!result.ok) {
      return result;
    }
    weightTotal += scenario.weightBps;
    results.push({
      name: scenario.name,
      weightBps: scenario.weightBps,
      enterpriseValueCents: result.value.enterpriseValueCents,
      terminalShareBps: result.value.terminalShareBps,
    });
    weightedEnterpriseValue +=
      result.value.enterpriseValueCents * scenario.weightBps;
    weightedTerminalValue +=
      result.value.terminalPresentValueCents * scenario.weightBps;
    lowCents = Math.min(lowCents, result.value.enterpriseValueCents);
    highCents = Math.max(highCents, result.value.enterpriseValueCents);
  }

  if (weightTotal !== 10000) {
    return fail("scenario weights must add up to 10000 bps");
  }

  return ok({
    scenarios: results,
    lowCents,
    highCents,
    weightedCents: Math.round(weightedEnterpriseValue / 10000),
    weightedTerminalShareBps: divide(
      weightedTerminalValue,
      weightedEnterpriseValue,
    ),
  });
}

/**
 * When DCF and a market multiple disagree by more than the tolerance, the
 * question is not which number is more precise but which assumptions can be
 * verified. A high DCF usually means the terminal value is doing the work; a
 * low DCF means the multiple is paying for growth the cash flow cannot support.
 */
export function crossCheck(
  dcfCents: Cents,
  multipleCents: Cents,
  toleranceBps: Bps = 3000,
): Result<CrossCheck> {
  if (!Number.isInteger(dcfCents) || dcfCents <= 0) {
    return fail("dcf value must be a positive integer amount in cents");
  }
  if (!Number.isInteger(multipleCents) || multipleCents <= 0) {
    return fail("multiple value must be a positive integer amount in cents");
  }
  if (
    !Number.isInteger(toleranceBps) ||
    toleranceBps < 0 ||
    toleranceBps > 10000
  ) {
    return fail("tolerance must be between 0 and 10000 bps");
  }

  const gapBps = divide(dcfCents - multipleCents, multipleCents);

  if (Math.abs(gapBps) <= toleranceBps) {
    return ok({
      dcfCents,
      multipleCents,
      gapBps,
      toleranceBps,
      verdict: "aligned",
      note: "dcf and multiple are within tolerance; compare assumptions, not just the number",
    });
  }

  if (gapBps > toleranceBps) {
    return ok({
      dcfCents,
      multipleCents,
      gapBps,
      toleranceBps,
      verdict: "dcf-high",
      note: "dcf is far above the multiple; trust the multiple unless a buyer can name a measurable synergy",
    });
  }

  return ok({
    dcfCents,
    multipleCents,
    gapBps,
    toleranceBps,
    verdict: "dcf-low",
    note: "dcf is far below the multiple; trust the dcf unless the buyer can name a measurable synergy",
  });
}

代进三情景:悲观 $518,247、基准 $837,005、乐观 $1,394,848,25/50/25 加权 $896,776。crossCheck 对基准与 3.2× ARR 的比较返回 aligned(差 8.9%),对乐观返回 dcf-high(差 81.4%),对悲观返回 dcf-low(差 −32.6%)。这三个判断不是三个价格,而是三种需要继续追问的假设。

代码里最值得注意的不是折现公式,而是 terminalShareBps。它把「终值占总价值多少」变成一个必看指标。终值占比超过 70% 时,DCF 的精度主要取决于终值倍数,而不是前五年的现金流;这时应该把 DCF 和倍数法放在一起看,而不是相信 DCF 的小数点。

小结

  • DCF 的四个输入是现金流起点、增长率、折现率和终值;四个都是假设,不是事实。
  • LedgerLite 的 SDE $130,240 先扣替代管理者工资 $60,000 和税 $11,941,再加回折旧 $3,000,得到自由现金流 $61,299。
  • 三情景 DCF:悲观 $518,247、基准 $837,005、乐观 $1,394,848;25/50/25 加权 $896,776。
  • 终值占总价值 59.9%–76.8%,加权后 71.0%;DCF 的大部分价格来自第五年之后的终值倍数。
  • 换算成 ARR 倍数:悲观 2.16×、基准 3.48×、乐观 5.81×、加权 3.73×;倍数法是 3.20×。
  • 基准和加权都在 30% 容差内,与倍数相互印证;悲观和乐观各自越过 30% 线。
  • 差距超过 30% 时,DCF 高先信倍数,DCF 低先信 DCF;除非买方能给出可量化的协同。

阅读导航:上一节:5.1 ARR/SDE/EBITDA 倍数 · 下一节:5.3 估值调整项:现金、负债、营运资金 。

常见错误与边界

  1. 把 DCF 当精确答案。 基准 DCF 的 69.6% 来自终值;终值倍数变 1×,价格就会大幅移动。
  2. 用 SDE 直接当自由现金流。 SDE 在所有者薪酬之前,买方接手后要付替代管理者工资;LedgerLite 的 $130,240 要先扣 $60,000。
  3. 忘记税。 案例的所得税 $11,941 是教学示例;DCF 用的现金流必须是税后现金流,否则会高估。
  4. 把增长率当成长期承诺。 14% 的增长率只适用于乐观情景的五年;把它外推到终值,会把终值推到没有依据的位置。
  5. 折现率只反映时间价值。 对一人公司,折现率还要反映关键人、客户集中和平台依赖;18% 与 12% 的差距不是随意设定的。
  6. 终值倍数不标注假设。 10×、13×、16× 都是教学假设,不是市场倍数;不标注就会让读者以为这是可验证的数据。
  7. 用权重掩盖分歧。 25/50/25 是教学权重,不是概率;换一组权重,加权值就会变,所以权重必须和情景一起披露。
  8. 只报加权值,不报区间。 加权 $896,776 背后是 $518,247–$1,394,848 的区间;只报一个数会隐藏悲观情景。
  9. DCF 高于倍数就认为 DCF 对。 DCF 高出的部分通常来自终值;先问终值倍数和增长率能不能被验证。
  10. DCF 低于倍数就认为生意不值钱。 低 DCF 可能说明当期成本或流失假设太悲观;先核对现金流起点,再判断倍数是否过高。

5.2 把 DCF 和倍数放在一起对照,但两者都还是「企业价值」层面的数字。5.3 继续往下走:现金、负债、营运资金和一次性费用会把这个数字改成最终的股权价值或到手价格。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《移动游戏产品的架构·商业化与发行工程》附录 C 商业化与发行评审清单
  2. 《移动游戏产品的架构·商业化与发行工程》附录 B 双案例商业化事实表
  3. 《移动游戏产品的架构·商业化与发行工程》附录 A 商业化与发行术语速查