以下情境为教学假设,人物与数字不代表经核实的真实案例。
第 4 章讲买方怎么算,这一节讲买方按哪个分母算。卷一 11.1 三种估值口径 已经把 ARR、收入、SDE 三种口径的定义拆过;本节不重复定义,而是把它们放回交易现场,回答三个更具体的问题:这门生意该用哪个基数、同一个倍数换基数会差多少钱、什么情况下 EBITDA 才真的可用。
三个案例的锚经常被放在同一张表里:LedgerLite 是 3.2× ARR = $768,768,Tidecast 是 1.8× 收入 = $248,708,Nord 是 4.5× SDE = $154,440,折价后 3.0× SDE = $102,960。把它们并排看,最自然的读法是「LedgerLite 最贵、Tidecast 最便宜、Nord 居中」。这个读法在数学上不成立,因为三个倍数的分母不是同一种东西。
三种倍数,三种买家问题
倍数是一个比率,不是一个价格标签。3.2× 只有在说明「3.2 倍的什么」之后才有意义;离开分母,3.2、1.8、4.5 只是三个小数。三种常用倍数回答的是三个不同的问题:
| 倍数 | 分母 | 买家实际在问 | 适用主体 | 典型误用 |
|---|---|---|---|---|
| ARR | 年度经常性收入 | 续费基数值多少 | 订阅、续费可预测 | 把总收入当 ARR |
| 收入 | 净收入 | 当前收入流值多少 | 买断制、项目制 | 暗示明年还有同样的收入 |
| SDE | 卖方自由现金流 | 接手后能拿走多少现金 | 一人或小团队盈利资产 | 不说明所有者薪酬 |
| EBITDA | 息税折旧摊销前利润 | 规范化经营利润值多少 | 有规模、职业经理人经营 | 在一人公司上直接套 |
ARR 和收入是顶线,SDE 和 EBITDA 是利润线。顶线倍数和利润线倍数不能直接比大小,因为一个问「收入值多少钱」,另一个问「利润值多少钱」。同一个 3.2× 乘在 ARR 上是 $768,768,乘在 SDE 上只有 $416,768;乘在 EBITDA 上只有 $234,368。数字都来自同一个 3.2×,经济含义却完全不同。
ARR 只对经常性收入有意义。LedgerLite 有 260 家客户,ARPA $77/月,MRR $20,020,ARR $240,240,全部是订阅收入,所以 ARR 和收入恰好相等。Tidecast 是买断制,ARR 为零;Nord 只有产品订阅 90 家 × $39 × 12 = $42,120 可以计入 ARR,咨询服务 $112,200 不能计入。
EBITDA 的适用边界最容易被忽略。它不是「比 SDE 更高级」的口径,而是「所有者薪酬已经不再重要」的口径。当一门生意由职业经理人经营、所有者不参与日常、折旧摊销和利息有规模时,EBITDA 才能把不同资本结构的公司放在同一把尺子上。对一人公司,所有者薪酬往往是最重要的成本项;直接用 EBITDA,等于把老板的工资当成不存在。
同一门生意,换基数差多少
先看 LedgerLite。它的 ARR 和收入都是 $240,240,SDE 是 $130,240。EBITDA 需要一步一步推:SDE $130,240 − 创始人薪酬 $60,000 = 税前利润 $70,240;再加回折旧 $3,000(教学假设:固定资产 $3,000 在一年内折旧完毕,且没有利息费用),得到 EBITDA $73,240。
| 基数 | 金额 | 3.2× 的结果 | 与 ARR 结果的差 |
|---|---|---|---|
| ARR | $240,240 | $768,768 | $0 |
| 收入 | $240,240 | $768,768 | $0 |
| SDE | $130,240 | $416,768 | −$352,000 |
| EBITDA | $73,240 | $234,368 | −$534,400 |
同一个 3.2×,最高和最低相差 $534,400。这不是市场对 LedgerLite 的看法变了,而是分母变了。3.2× 只说明「每单位基数付 3.2 美元」,不说明基数是哪一条线。案例没有给出 EBITDA 倍数锚,所以这里的 3.2× EBITDA 只用于演示基数效应,不是报价。
Tidecast 的案例锚是 1.8× 净收入。净收入 $138,171 已经扣掉退款、Steam 30% 分成和官网 5% 支付费;SDE $121,171 又扣掉营销、服务器和工具 $17,000。
| 基数 | 金额 | 1.8× 的结果 | 与收入结果的差 |
|---|---|---|---|
| 收入 | $138,171 | $248,708 | $0 |
| SDE | $121,171 | $218,108 | −$30,600 |
差 $30,600,约为收入结果的 12.3%。案例选收入而不是 SDE,是因为买断制游戏的买方通常按当前作品的收入流定价;SDE 会把卖方自己的营销和运营选择带进价格,而这些选择买方未必会重复。1.8× 收入买的是这一部作品,不包含下一部作品的承诺。
Nord 的案例锚是 4.5× SDE = $154,440,折价锚是 3.0× SDE = $102,960。它的产品订阅年化收入是 90 × $39 × 12 = $42,120,总收入 $154,320,SDE $34,320。
| 基数 | 金额 | 4.5× 的结果 | 与 SDE 结果的差 |
|---|---|---|---|
| 产品 ARR | $42,120 | $189,540 | +$35,100 |
| SDE | $34,320 | $154,440 | $0 |
| 总收入 | $154,320 | $694,440 | +$540,000 |
$694,440 不是 Nord 的报价,而是「把 4.5× 套到以服务为主的收入上」这个错误的产物。服务收入占 72.7%、咨询续约率 70%,它不像订阅那样自动续期;用 ARR 式的倍数乘它,等于假设咨询服务可以像订阅一样续费。案例的折价锚 3.0× SDE = $102,960 才是买方在服务收入难以转移时愿意付的价格。
倍数选择决策表
倍数选择可以写成一个决策顺序。先问收入结构,再问所有者角色,最后才问倍数高低。顺序很重要:先选倍数再找分母,几乎一定会选错。
| 顺序 | 判断问题 | 如果为是 | 如果为否 |
|---|---|---|---|
| 1 | 收入是否至少有 90% 是经常性收入 | 用 ARR | 进入第 2 步 |
| 2 | 是否是买断制、项目制,没有续费基数 | 用收入 | 进入第 3 步 |
| 3 | 所有者是否亲自经营,薪酬是否显著影响利润 | 用 SDE | 进入第 4 步 |
| 4 | 是否有职业经理人,EBITDA 为正且已规范化 | 用 EBITDA | 用 SDE 或收入 |
90% 是教学阈值,不是行业标准。它的作用是把「大部分收入可续费」和「少数收入可续费」分开。低于 90% 的订阅生意,应该先把经常性部分拆出来,再决定用 ARR 还是 SDE。LedgerLite 的经常性收入是 100%,所以直接落在 ARR;Nord 的产品订阅只占 27.3%,所以落在 SDE。
这张表也解释了为什么同一门生意会得到不同报价。战略买方可能按 ARR 出价,财务买方按 EBITDA 或 SDE,个人买家按 SDE 或收入。LedgerLite 的 3.2× ARR 是 $768,768;如果个人买家按 4.5× SDE,只得到 $586,080。两个价格都合理,因为它们回答的问题不同:前者买续费基数,后者买可支配现金。
决策表的输出不是价格,而是基数。基数确定之后,倍数才有讨论的意义。接下来要问的是:这个基数上的 3.2×、1.8× 或 4.5× 是否合理,而不是继续在基数之间来回换。
把选择逻辑写成代码
下面的 multiples.ts 把决策顺序写成 selectBase:订阅且经常性 ≥90% 选 ARR;买断制选收入;所有者经营选 SDE;职业经理人且 EBITDA 为正选 EBITDA。compareBases 把同一个倍数套到所有正基数上,用 recommended 标出决策表选中的那个,用 spreadBps 量化「换基数」的价差。
// file: src/capital/multiples.ts
// tier: L1
// verify: node --test test/capital/multiples.test.ts
import { type Cents } from "../core/money.ts";
import { type Bps, applyRate, divide } from "../core/rate.ts";
import { type Result, fail, ok } from "../core/result.ts";
export type MultipleBase = "arr" | "revenue" | "sde" | "ebitda";
export type BusinessModel =
| "subscription"
| "transactional"
| "service"
| "company";
export const BASE_ORDER: MultipleBase[] = ["arr", "revenue", "sde", "ebitda"];
export type BusinessProfile = {
model: BusinessModel;
annualRevenueCents: Cents;
arrCents: Cents;
sdeCents: Cents;
ebitdaCents: Cents;
recurringShareBps: Bps;
ownerOperated: boolean;
ownerSalaryCents: Cents;
profitable: boolean;
};
export type BaseSelection = {
base: MultipleBase;
amountCents: Cents;
warnings: string[];
};
export type BaseQuote = {
base: MultipleBase;
baseCents: Cents;
multipleBps: Bps;
valueCents: Cents;
recommended: boolean;
};
export type BaseComparison = {
selection: BaseSelection;
quotes: BaseQuote[];
lowCents: Cents;
highCents: Cents;
spreadCents: Cents;
spreadBps: Bps;
};
function checkAmount(value: Cents, label: string): string | null {
if (!Number.isInteger(value) || value < 0) {
return `${label} must be a non-negative integer amount in cents`;
}
return null;
}
function checkBps(value: Bps, label: string): string | null {
if (!Number.isInteger(value) || value < 0 || value > 10000) {
return `${label} must be between 0 and 10000 bps`;
}
return null;
}
function checkProfile(profile: BusinessProfile): string | null {
const models: BusinessModel[] = [
"subscription",
"transactional",
"service",
"company",
];
if (!models.includes(profile.model)) {
return "unknown business model";
}
const amounts: Array<[Cents, string]> = [
[profile.annualRevenueCents, "annual revenue"],
[profile.arrCents, "arr"],
[profile.ownerSalaryCents, "owner salary"],
];
for (const [value, label] of amounts) {
const error = checkAmount(value, label);
if (error) {
return error;
}
}
if (profile.arrCents > profile.annualRevenueCents) {
return "arr cannot exceed annual revenue";
}
if (!Number.isInteger(profile.sdeCents)) {
return "sde must be an integer amount in cents";
}
if (!Number.isInteger(profile.ebitdaCents)) {
return "ebitda must be an integer amount in cents";
}
const shareError = checkBps(profile.recurringShareBps, "recurring share");
if (shareError) {
return shareError;
}
if (
typeof profile.ownerOperated !== "boolean" ||
typeof profile.profitable !== "boolean"
) {
return "owner-operated and profitable flags must be booleans";
}
return null;
}
/**
* Pick the base before picking the multiple. ARR needs recurring revenue, SDE
* needs an owner-operated profit base, EBITDA needs professional management,
* and a transactional product falls back to revenue.
*/
export function selectBase(
profile: BusinessProfile,
): Result<BaseSelection> {
const error = checkProfile(profile);
if (error) {
return fail(error);
}
const warnings: string[] = [];
if (profile.model === "subscription" && profile.recurringShareBps < 9000) {
warnings.push(
"recurring share below 90%: normalize the recurring amount before using ARR",
);
}
if (
profile.model === "subscription" &&
profile.recurringShareBps >= 9000 &&
profile.arrCents > 0
) {
if (profile.arrCents !== profile.annualRevenueCents) {
warnings.push(
"arr is lower than total revenue; quote only the recurring portion",
);
}
return ok({ base: "arr", amountCents: profile.arrCents, warnings });
}
if (profile.model === "transactional" && profile.annualRevenueCents > 0) {
warnings.push(
"revenue multiple buys the current title, not a renewal base",
);
return ok({
base: "revenue",
amountCents: profile.annualRevenueCents,
warnings,
});
}
if (
profile.model === "company" &&
profile.profitable &&
profile.ebitdaCents > 0
) {
warnings.push("normalize one-off items before applying an EBITDA multiple");
return ok({ base: "ebitda", amountCents: profile.ebitdaCents, warnings });
}
if (profile.ownerOperated && profile.sdeCents > 0) {
if (profile.ownerSalaryCents <= 0) {
warnings.push("owner salary is not stated; sde may not be comparable");
}
if (profile.ebitdaCents > 0) {
warnings.push(
"ebitda needs an owner-salary normalization before comparison",
);
}
return ok({ base: "sde", amountCents: profile.sdeCents, warnings });
}
if (profile.profitable && profile.ebitdaCents > 0) {
warnings.push("normalize one-off items before applying an EBITDA multiple");
return ok({ base: "ebitda", amountCents: profile.ebitdaCents, warnings });
}
if (profile.sdeCents > 0) {
return ok({ base: "sde", amountCents: profile.sdeCents, warnings });
}
if (profile.annualRevenueCents > 0) {
warnings.push("revenue multiple does not imply recurrence or profitability");
return ok({
base: "revenue",
amountCents: profile.annualRevenueCents,
warnings,
});
}
return fail("no positive base available");
}
/** Apply a multiple to one base; the caller is responsible for base choice. */
export function quoteBase(
base: MultipleBase,
baseCents: Cents,
multipleBps: Bps,
): Result<BaseQuote> {
if (!BASE_ORDER.includes(base)) {
return fail("unknown multiple base");
}
if (!Number.isInteger(baseCents) || baseCents <= 0) {
return fail("base must be a positive integer amount in cents");
}
if (!Number.isInteger(multipleBps) || multipleBps <= 0) {
return fail("multiple must be a positive integer in basis points");
}
return ok({
base,
baseCents,
multipleBps,
valueCents: applyRate(baseCents, multipleBps),
recommended: false,
});
}
/**
* Apply the same multiple to every positive base. This is a diagnostic, not a
* recommendation: it shows how far the price moves when the denominator
* changes while the headline multiple stays fixed.
*/
export function compareBases(
profile: BusinessProfile,
multipleBps: Bps,
): Result<BaseComparison> {
const selection = selectBase(profile);
if (!selection.ok) {
return selection;
}
const quotes: BaseQuote[] = [];
for (const base of BASE_ORDER) {
const amount =
base === "arr"
? profile.arrCents
: base === "revenue"
? profile.annualRevenueCents
: base === "sde"
? profile.sdeCents
: profile.ebitdaCents;
if (amount <= 0) {
continue;
}
const quote = quoteBase(base, amount, multipleBps);
if (!quote.ok) {
return quote;
}
quotes.push({
...quote.value,
recommended: base === selection.value.base,
});
}
if (quotes.length === 0) {
return fail("no positive base available");
}
let lowCents = quotes[0].valueCents;
let highCents = quotes[0].valueCents;
for (const quote of quotes) {
lowCents = Math.min(lowCents, quote.valueCents);
highCents = Math.max(highCents, quote.valueCents);
}
return ok({
selection: selection.value,
quotes,
lowCents,
highCents,
spreadCents: highCents - lowCents,
spreadBps: divide(highCents - lowCents, lowCents),
});
}
代进三个案例:LedgerLite 选中 ARR,3.2× 得到 $768,768;Tidecast 选中收入,1.8× 得到 $248,708;Nord 选中 SDE,4.5× 得到 $154,440。compareBases 还会给出价差:LedgerLite 在 3.2× 下最高与最低相差 $534,400,是最低值的 228.0%;Nord 在 4.5× 下相差 $540,000,是最低值的 349.7%;Tidecast 在 1.8× 下相差 $30,600,是最低值的 14.0%。价差越大,说明分母选择越关键。
代码不判断倍数高低,只判断基数是否配得上这门生意。倍数本身仍要由增长、毛利、集中度和买方类型决定。5.2 会换一个方向:不看同类交易,直接看未来现金流,用 DCF 做一次独立校验。
小结
- 三种倍数回答三个问题:ARR 买续费基数,SDE 买可支配现金,EBITDA 买规范化经营利润;收入倍数买当前收入流。
- 顶线倍数和利润线倍数不能直接比大小;同一个 3.2× 乘在 LedgerLite 的 ARR、SDE、EBITDA 上,分别得到 $768,768、$416,768、$234,368。
- Tidecast 的 1.8× 收入 = $248,708;改用 SDE 只有 $218,108,差 $30,600,因为买断制买方买的是当前作品,不是卖方运营选择。
- Nord 的 4.5× SDE = $154,440;把 4.5× 套到总收入上得到 $694,440,这不是报价,是分母错误的产物。
- 倍数选择按顺序问:经常性收入是否 ≥90%、是否买断制、所有者是否亲自经营、EBITDA 是否已规范化。
- 同一门生意换基数,价差可以很大:LedgerLite 的 3.2× 下相差 $534,400,Nord 的 4.5× 下相差 $540,000,Tidecast 的 1.8× 下相差 $30,600。
- 基数确定之后,倍数才有讨论意义;先选倍数再找分母,几乎一定会选错。
阅读导航:上一节:4.3 交易流程与时间表 · 下一节:5.2 DCF 与情景加权 。
常见错误与边界
- 把 3.2、1.8、4.5 直接比大小。 三个倍数的分母分别是 ARR、净收入和 SDE;不换分母就比大小,等于拿长度比重量。
- 把总收入当 ARR。 Nord 的总收入是 $154,320,可计入 ARR 的只有产品订阅 $42,120;用总收入乘 4.5× 会得到 $694,440 这种没有经济含义的数字。
- 对买断制用 ARR。 Tidecast 没有经常性收入,ARR 为零;它只能按收入或 SDE 定价,不能按 ARR 定价。
- 对一人公司直接套 EBITDA。 EBITDA 假设所有者薪酬已经不再重要;LedgerLite 的创始人薪酬是 $60,000,不先规范化就套 EBITDA,会把最主要的成本项当成不存在。
- SDE 不说明所有者薪酬是否扣除。 LedgerLite 的 SDE 在创始人工资之前,Nord 的 SDE 在两位合伙人工资之后;两个 4.5× 不是同一种 4.5×。
- 用收入倍数暗示持续性。 1.8× 收入可以给买断制定价,但不包含下一部作品的承诺;Tidecast 的 Q4 占全年销量 38%,季节性本身就是风险。
- 把战略买方的倍数套给个人买家。 3.2× ARR 是战略买方的锚;个人买家可能只按 4.5× SDE 出价,得到 $586,080。倍数不同,分母也不同。
- 把 ARR 当现金。 LedgerLite 年末递延收入 $22,000,占 ARR 的 9.2%;ARR 是续费基数,不是已经到手的现金。
- 把 EBITDA 当净利润。 EBITDA 还没扣利息、税、折旧和资本开支;它比 SDE 更「规范化」,但不等于股东能拿走的钱。
- 只选基数,不看倍数质量。 基数对了,倍数仍可能过高或过低;增长、毛利、集中度和买方类型会决定同一基数上的倍数区间。
5.1 只回答「用哪个基数」。5.2 换一个方向:不看同类交易,直接看未来现金流,用 DCF 做一次独立校验,并处理 DCF 与倍数差 30% 以上的情况。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。