以下情境为教学假设,人物与数字不代表经核实的真实案例。
11.1 说明了分母,11.2 拆开了驱动,但两者最终都要落到一个价格上。最容易犯的错误,是把价格写成一个点:LedgerLite 值 $768,768,Tidecast 值 $248,708,Nord 值 $154,440。点估值看起来精确,实际上把分母质量、驱动评分和交易结构的所有不确定性,压缩成了一个没有误差范围的数字。
这一节把点展开成区间。做法不是给三个数字取平均,而是用悲观、基准、乐观三档情景,分别对应不同的倍数假设,再看交易结构如何把 headline 价格转换成净得。区间不是犹豫,区间是把风险写进价格的方式。区间越宽,说明买卖双方对未来的分歧越大;分歧本身,就是谈判要处理的内容。
点估值为什么危险
点估值的第一個问题是锚定。一旦卖方说出 $768,768,后面的谈判就会围绕这个数字上下浮动,而不是回到「这门生意值多少」的问题。买方会说「太贵」,卖方会说「已经让了」,双方都在一个未经拆解的锚上做加减法。
点估值的第二个问题是隐藏假设。$768,768 来自 3.2× ARR,但 3.2× 本身包含了对续费、增长、毛利、集中度和创始人依赖的判断。这些判断在 11.2 里被拆成了五个驱动,在点估值里却被压缩成一个倍数。买方接受 3.2×,等于接受了这五个驱动的全部假设;卖方报出 3.2×,也等于承诺了这些假设。
点估值的第三个问题是忽略结构。$768,768 是 headline 价格,不是到手金额。LedgerLite 的交易结构是现金 70% + earn-out 30%,还有 10% 托管和 $3,000 营运资金下调。如果 earn-out 没拿到、托管被扣,实际净得会远低于 $768,768。把 headline 当净得,是谈判里最常见的误会之一。
| 视角 | 点估值 | 区间估值 |
|---|---|---|
| 表达方式 | 一个数字 | 低值、基准、高值 |
| 隐含假设 | 被压缩进倍数 | 每个情景对应一组假设 |
| 风险处理 | 事后争论 | 事前定价 |
| 交易结构 | 容易忽略 | 单独折算 |
| 谈判作用 | 锚定 | 划定可谈范围 |
区间估值不是把数字说得模糊,而是把「在什么假设下值多少」说清楚。一个诚实的区间,应该能让对方看到:低值对应哪些条件恶化,高值对应哪些条件兑现,基准值又依赖哪些假设。
三档情景:悲观、基准、乐观
三档情景的做法是:先选一个基准倍数,再给一个上下浮动幅度,得到悲观、基准、乐观三个倍数,最后乘到同一个分母上。浮动幅度不是行业统计,而是对驱动不确定性的教学假设。LedgerLite 的续费基数较稳,浮动可以窄一些;Tidecast 没有续费基数,浮动应该宽一些;Nord 依赖服务和合伙人,浮动也要宽。
| 案例 | 分母 | 悲观 | 基准 | 乐观 | 低值 | 基准值 | 高值 |
|---|---|---|---|---|---|---|---|
| LedgerLite | ARR $240,240 | 2.8× | 3.2× | 3.6× | $672,672 | $768,768 | $864,864 |
| Tidecast | 净收入 $138,171 | 1.4× | 1.8× | 2.2× | $193,439 | $248,708 | $303,976 |
| Nord Studio | SDE $34,320 | 3.0× | 3.75× | 4.5× | $102,960 | $128,700 | $154,440 |
这张表里的倍数都是教学情景,不是市场报价。LedgerLite 的 2.8×–3.6× 围绕 3.2× 上下浮动 0.4×;Tidecast 的 1.4×–2.2× 围绕 1.8× 上下浮动 0.4×;Nord 的 3.0×–4.5× 正好对应案例披露的折价区间,基准取中点 3.75×。
区间宽度用同一个公式复算:
区间宽度 = (高值 − 低值) ÷ 基准值
按这个公式,LedgerLite 的宽度是 (864,864 − 672,672) ÷ 768,768 = 25.0%;Tidecast 是 (303,976 − 193,439) ÷ 248,708 ≈ 44.4%;Nord 是 (154,440 − 102,960) ÷ 128,700 = 40.0%。三个宽度不同,不是随意设定的,而是反映了三个案例的风险结构不同。
| 案例 | 低值 | 基准值 | 高值 | 宽度 | 宽度背后的主要不确定性 |
|---|---|---|---|---|---|
| LedgerLite | $672,672 | $768,768 | $864,864 | 25.0% | 续费与扩张能否维持 |
| Tidecast | $193,439 | $248,708 | $303,976 | 44.4% | 单部作品销量与下一部作品 |
| Nord Studio | $102,960 | $128,700 | $154,440 | 40.0% | 服务收入与合伙人可转移性 |
三档情景不是三个独立的故事,而是同一组驱动的三种取值。LedgerLite 的低值对应续费变差、扩张放缓;高值对应 NRR 维持甚至提升。Tidecast 的低值对应销量回落、平台分成或退款恶化;高值对应长尾销售和续作预期。Nord 的低值对应服务收入无法转移、合伙人离开;高值对应客户关系和交付能力顺利交接。谈判时,双方应该争论的是「哪个情景更可能」,而不是在基准值上反复砍价。
LedgerLite:区间如何覆盖 earn-out 与托管
LedgerLite 的 headline 区间是 $672,672–$864,864。但案例给出的交易结构不是一次性现金支付,而是资产收购:现金 70%、earn-out 30%、托管 10%,再加上营运资金调整。结构条款见附录 B 第 7 节 ,这里只做净得折算。
以基准值 $768,768 为例:
| 项 | 比例或金额 | 说明 |
|---|---|---|
| Headline 价格 | $768,768 | 3.2× ARR |
| 现金部分 | 70% = $538,138 | 成交时支付 |
| 减:托管 | 10% = $76,877 | 从现金中扣留 12 个月 |
| 成交时现金 | $461,261 | 538,138 − 76,877 |
| Earn-out | 30% = $230,630 | 12 个月,ARR ≥95% 全额 |
| 营运资金调整 | −$3,000 | 目标 $15,000,实际 $12,000 |
| 最好情况净得 | $765,768 | 现金 + 托管 + earn-out − 调整 |
| 最差情况净得 | $458,261 | 只有成交时现金 − 调整 |
这张表说明了两件事。第一,headline 和净得之间隔着 $3,000 到 $310,507 的差额:最好情况只比 headline 少 $3,000,最差情况少 $310,507。第二,earn-out 和托管不是「额外的钱」,而是 headline 里可能拿不到的部分。买方用这两个工具把未来的不确定性转回给卖方。
把三档 headline 都按同一结构折算,可以得到一个净得区间:
| Headline 情景 | Headline | 成交时现金 | Earn-out | 托管 | 最好净得 | 最差净得 |
|---|---|---|---|---|---|---|
| 悲观 | $672,672 | $403,603 | $201,802 | $67,267 | $669,672 | $400,603 |
| 基准 | $768,768 | $461,261 | $230,630 | $76,877 | $765,768 | $458,261 |
| 乐观 | $864,864 | $518,919 | $259,459 | $86,486 | $861,864 | $515,919 |
表中的金额按整数美元四舍五入,比例仍是 70/30/10,营运资金调整固定为 −$3,000。可以看到,净得区间的下限 $400,603 和上限 $861,864 相差一倍多。这个差距不是估值模型算错了,而是交易结构把 headline 区间放大了:headline 越低,earn-out 和托管也按比例缩小,但「拿不到」的比例不变。
谈判时,卖方应该把「headline 区间」和「净得区间」分开谈。只谈 headline,容易接受一个看起来很高、但 earn-out 条件很严的价格;只谈净得,又可能忽略 earn-out 兑现后的上行空间。正确的做法是把三个情景都折算成净得,再看自己能不能接受最差情况。如果最差净得低于自己的底线,就应该调整结构,而不是继续在 headline 上争。
把区间与结构写成代码
下面的 range.ts 把三档情景和交易结构固化成两组函数。scenarioMultiples 围绕基准倍数生成悲观、基准、乐观三个倍数;scenarioRange 把三个倍数乘到同一个分母上,并返回区间宽度;netProceeds 把 headline 拆成成交时现金、托管、earn-out 和营运资金调整,分别计算最差、期望和最好净得。期望值使用显式概率,不使用「拍脑袋的折扣」。
// file: src/value/range.ts
// tier: L1
// verify: node --test test/value/range.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 ScenarioMultiples = {
pessimisticBps: Bps;
baseBps: Bps;
optimisticBps: Bps;
};
export type ScenarioRange = {
lowCents: Cents;
baseCents: Cents;
highCents: Cents;
spreadBps: Bps;
widthBps: Bps;
};
export type DealTerms = {
cashShareBps: Bps;
earnOutShareBps: Bps;
escrowShareBps: Bps;
earnOutProbabilityBps: Bps;
escrowReleaseProbabilityBps: Bps;
workingCapitalAdjustmentCents: Cents;
};
export type NetProceeds = {
headlineCents: Cents;
upfrontCents: Cents;
escrowExpectedCents: Cents;
earnOutExpectedCents: Cents;
adjustmentCents: Cents;
worstCents: Cents;
expectedCents: Cents;
bestCents: Cents;
haircutBps: Bps;
};
function checkAmount(amount: Cents, label: string): string | null {
if (!Number.isInteger(amount) || amount <= 0) {
return `${label} must be a positive 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;
}
export function scenarioMultiples(
baseBps: Bps,
spreadBps: Bps,
): Result<ScenarioMultiples> {
if (!Number.isInteger(baseBps) || baseBps <= 0) {
return fail("base multiple must be a positive integer in basis points");
}
if (!Number.isInteger(spreadBps) || spreadBps < 0) {
return fail("spread must be a non-negative integer in basis points");
}
if (baseBps - spreadBps <= 0) {
return fail("pessimistic multiple must remain positive");
}
return ok({
pessimisticBps: baseBps - spreadBps,
baseBps,
optimisticBps: baseBps + spreadBps,
});
}
/**
* Build a three-point band around a base multiple. Width is measured against
* the base-case value so a wider band reads directly as a larger risk signal.
*/
export function scenarioRange(
baseCents: Cents,
baseMultipleBps: Bps,
spreadBps: Bps,
): Result<ScenarioRange> {
const amountError = checkAmount(baseCents, "base");
if (amountError) {
return fail(amountError);
}
const multiples = scenarioMultiples(baseMultipleBps, spreadBps);
if (!multiples.ok) {
return multiples;
}
const lowCents = applyRate(baseCents, multiples.value.pessimisticBps);
const baseValueCents = applyRate(baseCents, multiples.value.baseBps);
const highCents = applyRate(baseCents, multiples.value.optimisticBps);
return ok({
lowCents,
baseCents: baseValueCents,
highCents,
spreadBps,
widthBps: divide(highCents - lowCents, baseValueCents),
});
}
function checkShares(terms: DealTerms): string | null {
const cashError = checkBps(terms.cashShareBps, "cash share");
if (cashError) {
return cashError;
}
const earnOutError = checkBps(terms.earnOutShareBps, "earn-out share");
if (earnOutError) {
return earnOutError;
}
const escrowError = checkBps(terms.escrowShareBps, "escrow share");
if (escrowError) {
return escrowError;
}
if (terms.cashShareBps + terms.earnOutShareBps !== 10000) {
return "cash plus earn-out shares must add up to 10000 bps";
}
if (terms.escrowShareBps > terms.cashShareBps) {
return "escrow share cannot exceed the cash share";
}
const earnOutProbabilityError = checkBps(
terms.earnOutProbabilityBps,
"earn-out probability",
);
if (earnOutProbabilityError) {
return earnOutProbabilityError;
}
const escrowProbabilityError = checkBps(
terms.escrowReleaseProbabilityBps,
"escrow release probability",
);
if (escrowProbabilityError) {
return escrowProbabilityError;
}
if (!Number.isInteger(terms.workingCapitalAdjustmentCents)) {
return "working capital adjustment must be an integer amount in cents";
}
return null;
}
/**
* Earn-out and escrow are not additional money on top of the headline price;
* they are parts of the headline that may not be paid. The expected value uses
* explicit probabilities, while worst and best bracket the structural range.
*/
export function netProceeds(
headlineCents: Cents,
terms: DealTerms,
): Result<NetProceeds> {
const amountError = checkAmount(headlineCents, "headline");
if (amountError) {
return fail(amountError);
}
const shareError = checkShares(terms);
if (shareError) {
return fail(shareError);
}
const cashCents = applyRate(headlineCents, terms.cashShareBps);
const earnOutCents = applyRate(headlineCents, terms.earnOutShareBps);
const escrowCents = applyRate(headlineCents, terms.escrowShareBps);
const upfrontCents = cashCents - escrowCents;
const escrowExpectedCents = applyRate(
escrowCents,
terms.escrowReleaseProbabilityBps,
);
const earnOutExpectedCents = applyRate(
earnOutCents,
terms.earnOutProbabilityBps,
);
const adjustmentCents = terms.workingCapitalAdjustmentCents;
const worstCents = upfrontCents - adjustmentCents;
const expectedCents =
upfrontCents +
escrowExpectedCents +
earnOutExpectedCents -
adjustmentCents;
const bestCents = upfrontCents + escrowCents + earnOutCents - adjustmentCents;
return ok({
headlineCents,
upfrontCents,
escrowExpectedCents,
earnOutExpectedCents,
adjustmentCents,
worstCents,
expectedCents,
bestCents,
haircutBps: divide(headlineCents - expectedCents, headlineCents),
});
}
代码里有两个口径需要注意。第一,scenarioRange 的宽度以基准值为分母,而不是以低值或高值为分母;这样同一套公式可以横向比较不同案例的区间宽度。第二,netProceeds 把托管视为从现金中扣留,而不是在 headline 之外额外加钱;worstCents 假设 earn-out 为零、托管不释放,bestCents 假设两者全额兑现,expectedCents 则用调用方给出的概率。概率不是事实,而是谈判前必须写下来的假设。
Nord:3.0×–4.5× SDE 的折价区间
Nord 的区间不是从 3.75× 上下浮动出来的,而是案例直接给出了两个锚:标准锚 4.5× SDE = $154,440,买方折价 3.0× SDE = $102,960。中点的 3.75× = $128,700 是为了做三档情景而取的教学基准,不是案例披露的价格。
| 情景 | 倍数 | SDE | 估值 | 买方视角 |
|---|---|---|---|---|
| 悲观 | 3.0× | $34,320 | $102,960 | 服务收入无法转移,合伙人依赖未解决 |
| 基准 | 3.75× | $34,320 | $128,700 | 部分服务关系可交接,但仍需绑定合伙人 |
| 乐观 | 4.5× | $34,320 | $154,440 | 服务收入顺利转移,合伙人留任 |
Nord 的折价来源在 11.2 已经拆过:服务收入占 72.7%,咨询续约率 70%,前 3 家客户占 48%,两位合伙人各 50% 且无书面退出条款。这些因素不会因为谈判技巧而消失,只会因为交易结构而部分缓解。比如,买方可以要求两位合伙人留任 12–24 个月、签署竞业限制、把关键客户关系转成公司合同;这些安排能把估值从 3.0× 推向 3.75×,但很难直接推到 4.5×。
Nord 还有一条合伙人退出路径:一位合伙人退出,买断 50% × 4.5 × SDE = $77,220,分 24 个月支付。这是另一个交易,不是整体出售价格。它说明同一门生意在不同交易结构下会有不同的价格:整体出售给外部买方,得到的是 3.0×–4.5× 的区间;内部合伙人买断,得到的是 4.5× 的一半,而且分 24 个月支付,时间风险由退出方承担。谈判时不能把这两个价格混在一起。
| 交易 | 价格 | 支付方式 | 风险承担方 |
|---|---|---|---|
| 整体出售(悲观) | 3.0× SDE = $102,960 | 按并购协议 | 买方承担服务流失风险 |
| 整体出售(乐观) | 4.5× SDE = $154,440 | 按并购协议 | 卖方需证明可转移性 |
| 合伙人退出 | 50% × 4.5 × SDE = $77,220 | 分 24 个月 | 退出方承担时间与信用风险 |
把 Nord 的区间和 LedgerLite 的净得区间放在一起看,会发现一个反直觉的现象:Nord 的 headline 上限 $154,440 远低于 LedgerLite 的 headline 下限 $672,672,但 Nord 的区间宽度 40.0% 高于 LedgerLite 的 25.0%。绝对值小不代表风险小;区间宽度衡量的是相对不确定性,不是价格高低。
区间宽度就是风险信号
区间宽度不是模型的技术参数,而是谈判信息。宽度越大,说明买卖双方对未来的分歧越大,或者这门生意的关键假设越难验证。LedgerLite 的 25.0% 最窄,因为它的续费基数、流失率和毛利率都有可观测的数字;Tidecast 的 44.4% 最宽,因为它没有续费基数,销量又高度集中在发布期;Nord 的 40.0% 居中偏宽,因为服务收入和合伙人依赖都难以用报表验证。
| 案例 | 区间宽度 | 主要风险源 | 谈判动作 |
|---|---|---|---|
| LedgerLite | 25.0% | 续费与扩张 | 用 GRR、NRR、月流失数据缩小分歧 |
| Tidecast | 44.4% | 单部作品销量与续作 | 把下一部作品的预算和销量假设写进条款 |
| Nord Studio | 40.0% | 服务收入与合伙人 | 用留任、竞业、客户合同转移来换倍数 |
宽度本身也有陷阱。第一,窄区间可能是假精确:如果分母选错了,比如把总收入当 ARR,区间再窄也没有意义。第二,宽区间不一定是坏事:如果宽度的来源可以被结构化,比如 earn-out 和托管,卖方可以用结构换取更高的 headline。第三,宽度会随着信息增加而收窄:尽调、客户访谈、代码审查都会让某些假设变得可验证,区间应该随信息更新,而不是一次定死。
谈判区间不是估值区间的简单复制。卖方有自己的底线,买方有自己的上限,两个区间可能重叠,也可能不重叠。重叠部分就是可成交区;不重叠时,正确的动作是回到驱动和结构,而不是强行取中点。比如 LedgerLite 的净得区间是 $400,603–$861,864,如果卖方的底线是 $500,000,买方的上限是 $700,000,那么 $500,000–$700,000 就是谈判区;如果卖方的底线是 $800,000,买方的上限是 $700,000,就没有重叠,应该重新谈结构或放弃交易。
| 区间类型 | 用途 | 不应该做什么 |
|---|---|---|
| 估值区间 | 表达不同假设下的价值 | 不要把三档平均成一个点 |
| 净得区间 | 折算 earn-out、托管和调整 | 不要把 headline 当净得 |
| 谈判区间 | 找双方可接受的重叠 | 不要在没有重叠时强行成交 |
区间估值最终要回答的不是「这门生意值多少钱」,而是「在什么条件下值多少,以及这些条件能不能被验证」。11.2 的五个驱动提供了验证清单,11.3 的三档情景提供了价格语言,交易结构则提供了把价格和条件绑定在一起的工具。三者合起来,才是一个可以谈判的估值。
小结
- 点估值会把分母质量、驱动评分和交易结构压缩成一个数字,容易变成锚定。
- 三档情景用悲观、基准、乐观三个倍数乘同一个分母;LedgerLite 是 2.8×–3.6× ARR,Tidecast 是 1.4×–2.2× 收入,Nord 是 3.0×–4.5× SDE。
- 区间宽度 = (高值 − 低值) ÷ 基准值:LedgerLite 25.0%、Tidecast 44.4%、Nord 40.0%。
- LedgerLite 的 headline $768,768 经过 70/30 现金与 earn-out、10% 托管和 −$3,000 营运资金调整后,净得区间约为 $458,261–$765,768;最差情况只有成交时现金。
- Nord 的 3.0×–4.5× SDE 给出 $102,960–$154,440 的区间,折价来自服务收入 72.7%、前 3 家客户 48% 和两位合伙人的可转移性。
- 区间宽度是风险信号:宽度越大,越应该回到驱动和结构,而不是强行取中点。
阅读导航:上一节:11.2 增长、毛利、集中度对倍数的影响 · 下一节:12.1 尽调清单:财务、客户、合同 。
常见错误与边界
- 把三档情景平均成一个点。 平均会抹掉悲观和乐观背后的假设;区间的作用是保留分歧,不是消除分歧。
- 把 headline 当净得。 LedgerLite 的 $768,768 在 earn-out 和托管全部兑现时净得 $765,768,在最差情况下只有 $458,261;两者不是一回事。
- 把 earn-out 当额外收入。 Earn-out 是 headline 的一部分,不是加在价格上的奖金;它只是把支付时点推后,并把风险转给卖方。
- 把托管当买方扣款。 托管是从现金中扣留、条件满足后释放,不是永久扣款;但卖方必须把「不释放」写进最差情景。
- 忽略营运资金调整。 LedgerLite 的目标营运资金 $15,000、实际 $12,000,下调 $3,000;金额不大,但它会直接减少净得。
- 把 Nord 的 3.0×–4.5× 当成市场区间。 这是案例披露的标准锚与折价锚,不是行业统计;中点的 3.75× 是教学基准。
- 把合伙人退出价当整体出售价。 $77,220 是 50% × 4.5 × SDE,分 24 个月支付,和整体出售的 $102,960–$154,440 不是同一交易。
- 用窄区间证明估值准确。 如果分母选错,窄区间只是精确地错;先确认分母,再看宽度。
- 把区间宽度当缺点。 宽度可以被结构化管理;earn-out、托管、留任协议都是把宽度变成条款的工具。
- 在没有重叠时强行取中点。 谈判区间不重叠,说明假设或结构有问题;此时应该回到驱动和尽调,而不是用数学平均掩盖分歧。
11.3 把估值从点变成区间,也为第 12 章的尽调做了铺垫:尽调的作用,就是把区间里那些「无法验证」的假设,变成「可以验证」的事实。区间不会因为尽调而消失,但会变窄,而且窄得有理有据。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。