mcpField
O
officecli-financial-model

officecli-financial-model

github.com/iofficeai数据与数据库32.7k97 次安装今天更新
代码仓库

关于 officecli-financial-model

officecli-financial-model 是基于 officecli-xlsx 构建公式驱动财务模型的技能,面向制作三表、DCF 或 LBO 模型的分析人员。它规定输入、计算和输出分区,并提供情景分析与敏感性分析工作流。

主要功能

  1. 01输入、计算、输出三区架构
  2. 02三表、DCF 与 LBO 模型配方
  3. 03情景与敏感性分析模式
  4. 04财务函数和循环引用指导
  5. 05模型专用验证关卡

适用场景

  1. 01构建相互关联的财务报表预测
  2. 02制作带敏感性网格的 DCF 估值
  3. 03模拟 LBO 债务计划与投资回报

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/iofficeai/officecli --skill officecli-financial-model

安装后在 Claude Code 中输入/officecli-financial-model确认已加载。

详细介绍

OfficeCLI 财务模型 Skill

本 Skill 是建立在 officecli-xlsx 之上的场景层。 所有 xlsx 硬性规则(Shell 引号、增量执行、Help-First 规则、视觉交付底线、CFO 四色编码(蓝色输入 / 黑色公式 / 绿色跨表 / 黄色底纹假设)、数字格式标准(年份用文本、零值显示为 -、百分比保留一位小数、负数用括号)、假设单元格规范、CSV 批量导入、图表数据源形式(a/b/c)、5 道关卡交付循环、缓存漂移指引、已知问题(跨表 ! 陷阱、批量与常驻模式处理公式、渲染器注意事项))均直接继承,不再重复说明。本文件仅补充财务模型额外需要的内容:三区架构、3 种模型类型配方(三大报表 / DCF / LBO)、敏感性与情景协议、财务函数模式、循环引用规范,以及模型专属的第 4–6 道交付关卡。

凡 xlsx 基础规则已覆盖的内容,本文用 → see xlsx v2 §X 标注。若尚未阅读,请先阅读 skills/officecli-xlsx/SKILL.md。

安装

若未安装 officecli:

  • macOS / Linux:curl -fsSL https://d.officecli.ai/install.sh | bash
  • Windows(PowerShell):irm https://d.officecli.ai/install.ps1 | iex

使用 officecli --version 验证(若 PATH 尚未生效,请打开新终端)。若安装失败,请从 https://github.com/iOfficeAI/OfficeCLI/releases 下载二进制文件。

Help-First 规则

本 Skill 讲解的是财务模型的需求,而非每一个命令行参数。当属性名 / 别名 / 枚举值不确定时,请先查阅帮助再尝试:officecli help xlsx [element] [--json]。帮助信息以已安装版本为准,若本 Skill 与帮助内容有出入,以帮助为准。下文中每个 --prop X= 均已通过 officecli help xlsx <element> 验证。

思维模型与继承关系

继承 xlsx v2。 请先阅读 skills/officecli-xlsx/SKILL.md。本 Skill 假定你已熟悉 create / open / close、set 设置值与公式、用于跨表公式的 batch heredoc、/SheetName/A1 路径、命名区域、5 道关卡交付循环、跨表 ! 陷阱,并且跨表公式必须以非常驻方式写入(单次 batch 或逐个 set),绝不能在常驻模式下使用 batch。

Shell 与执行规范

Shell 引号、增量执行、$FILE 约定 → 见 xlsx v2 §Shell & Execution Discipline。同样的规则:每个 [N] 路径都要加引号;凡包含 $ 的属性都必须用单引号括起(此处所有数字格式——$#,##0;($#,##0);"-"——都需要单引号);不要手写 \$/\t/\n;每次只执行一条命令。下文示例使用 $FILE(FILE="model.xlsx")。

核心原则(身份定位)

财务模型是一份 xlsx,其核心是可用于决策、由公式驱动的数据层:每个输出都能追溯到蓝色字体假设值的完整链条,每张报表在每个期间都保持平衡,每项估值都可以重新审计。在通用 xlsx 基础上,本 Skill 有八项额外要求:

  1. 强制三区架构: 输入 → 计算 → 输出。合并区域将导致无法审计。
  2. 假设值存放在单元格中,绝不写在公式内。 应为 =B5*(1+Assumptions!GrowthRate),而非 =B5*1.05。
  3. 报表每期平衡。 资产 − 负债 − 权益 = 0,CF.EndingCash = BS.Cash。出现 IMBALANCED 时,Gate 4 判定失败。
  4. 硬编码需审计。 计算工作表不得包含硬编码数字;Gate 6 负责统计。
  5. 敏感性与情景分析为一级功能。 可采用双轴网格、下拉框 INDEX/MATCH 切换,或基准/乐观/悲观列。Excel 数据表并不能被稳定支持,仅使用手动网格。
  6. 估值单元格的缓存值至关重要。 若估值单元格未附带缓存结果(或为 #OCLI_NOTEVAL! 哨兵值),会向不重新计算的读取方展示空白或错误数字。评估器现已支持计算 NPV / XNPV / IRR / XIRR;请通过回读验证缓存值。Gate 5 进行抽查。
  7. 循环引用是一种设计选择。 合理的循环(利息 ↔ 现金、循环信贷补位 ↔ 期末现金)使用 calc.iterate=true。意外的循环则是代数错误,绝不能用 iterate 掩盖。
  8. 被引用三次及以上的假设值使用命名区域。 例如 WACC、TaxRate、TerminalGrowth、ExitMultiple、ChurnRate。声明但未使用的名称属于无用装饰,Gate 6 会予以标记。
反向交接——何时回退到 xlsx 基础

以下场景应留在 xlsx 基础:预算跟踪表、CSV 转报表的数据导出、运营 KPI 表、简单模板、不含预测逻辑的股权结构表。仅当需求涉及以下内容时才使用本 Skill:三大报表 / DCF / WACC / NPV / TV / LBO / 债务计划 / MOIC / IRR / 单位经济模型 / ARR 滚动预测 / 敏感性网格 / 情景切换 / 备考财务报表(pro forma)。

三区架构(硬性规则)

本 Skill 中的每个模型都基于三个区域构建。为每个区域命名、设置标签页颜色,并通过可执行的审计加以强制执行。 违反分区规则是导致模型无法审计的最常见原因。

区域工作表命名(约定)标签页颜色内容硬编码公式
输入Assumptions、Inputs、Drivers黄色 FFC000原始驱动因素:增长率、利润率、税率、WACC、FTE、定价、营运资本天数每个单元格均为蓝色 0000FF仅允许用于派生假设值(例如 =MonthlyARPU*12)
计算P&L、Balance Sheet、Cash Flow、DCF、Debt、ARR蓝色 4472C4所有推导过程与报表零(由 Gate 6 强制执行)同表引用为黑色 000000,跨表引用为绿色 008000
输出Summary、Dashboard、Sensitivity、Returns绿色 70AD47KPI、敏感性网格、图表、回报瀑布图仅用于标签(非数值);Gate 6 统计数值硬编码 → 应为 0按上述规则使用黑色 / 绿色

构建顺序需考虑跨区依赖。 先构建假设值,再按依赖链自下而上构建计算区(三大报表为 IS → BS → CF;DCF 为 FCF → WACC → NPV),最后构建输出区。若先构建输出区,所有单元格都会缓存 0,并被下游继承。

可执行的分区审计(在 Gate 4 之前运行):

# Calc zone: zero numeric hardcodes allowed. `cell:not(:has(formula))` selects the literal (non-formula) cells; `cell:has(formula)` selects the formula cells.
HARDCODE=$(officecli query "$FILE" 'cell[type=Number]' --json | jq '[.data.results[] | select(.format.formula == null) | select(.path | test("/(P&L|Balance Sheet|Cash Flow|DCF|Debt|ARR)/"))] | length')
[ "$HARDCODE" -eq 0 ] && echo "Zone audit OK" || { echo "REJECT: $HARDCODE hardcoded numeric cells on Calc sheets — move to Assumptions"; exit 1; }
# Assumptions zone: should be non-zero.
INPUTS=$(officecli query "$FILE" '/Assumptions/cell[type=Number]' --json | jq '[.data.results[] | select(.format.formula == null)] | length')
[ "$INPUTS" -ge 5 ] && echo "Assumptions has $INPUTS hardcoded drivers" || echo "WARN: Assumptions has only $INPUTS inputs"

打印交付(董事会 / 投资委员会 / LP)

当需求中包含“打印”/“一页”/“董事会”/“投资人”/“IC memo”/“LP update”时,打印流程仅应输出输出区。需生成两份产物:

# 1. Print_Area scoped to the Outputs sheet (Summary or Dashboard).
officecli add "$FILE" / --type namedrange --prop name=_xlnm.Print_Area --prop scope=Summary --prop 'refersTo=Summary!$A$1:$H$40'
# 2. Hide every non-Outputs sheet — Print_Area scope alone does NOT stop the print pipeline from emitting every visible sheet.
for S in Assumptions 'P&L' 'Balance Sheet' 'Cash Flow' DCF WACC Debt FCF 'S&U' Exit Returns; do
  officecli raw-set "$FILE" /workbook --xpath "//x:sheet[@name='$S']" --action setattr --xml "state=hidden" || true
done
# 3. fit-to-page landscape on Outputs sheet.
officecli raw-set "$FILE" /Summary --xpath "//x:worksheet" --action prepend --xml '<sheetPr xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><pageSetUpPr fitToPage="1"/></sheetPr>'

删除计算工作表上设置的任何 Print_Area——相互冲突的打印范围会导致多页输出,并使假设表和报表工作表泄露到结果中。

构建顺序与缓存漂移规则(3 表模型的关键)

有三个因素会导致静默的错误数字:(1) 新公式在交付时没有缓存值。Excel 打开时会重新计算,而 HTML 预览和旧版查看器不会;(2) 若下游与上游按相同顺序写入,下游会基于上游写入前的状态缓存 0;(3) 在常驻会话打开时进行跨表 batch,会在 3–5 次操作时死锁。

纪律(适用于每个方案):

  • 构建顺序遵循数据链:P&L → BS → CF(3 表);FCF → WACC → NPV → Sensitivity(DCF);S&U → Debt → P&L → CF → Returns(LBO)。
  • 跨表链完成后,执行缓存刷新步骤: 对每个汇总、估值和平衡检查单元格重新发出 set(非常驻方式)。
  • 抽查:officecli get "$FILE" /Summary/B2 --json | jq '.data.results[0].format.cachedValue' 应返回合理的非空值。null 表示 Excel 会在打开时计算,交付可以接受。若单元格显示 #OCLI_NOTEVAL! 哨兵值:关闭常驻会话并重新 set;若仍未求值,则使用缓存回退(见§财务函数模式)。

方案——三类模型

以下每个方案都是可直接运行的骨架,而非财务理论。替换数字即可,不要重构。所有方案都假设已设置 FILE="model.xlsx",并已运行 officecli create "$FILE" 和 officecli open "$FILE"。最后用 officecli close "$FILE" 关闭。

方案 A——3 表模型(利润表 + 资产负债表 + 现金流量表)

本方案产出内容。 4 个工作表:Assumptions、P&L、Balance Sheet、Cash Flow,以及 Summary。年份列为 2024A · 2025E · 2026E · 2027E。BS 上有平衡检查行,CF 上有现金核对行。每个报表行都是引用 Assumptions 的公式。

构建顺序(必须遵守)。 Assumptions → P&L → Balance Sheet → Cash Flow → Summary。不要在 P&L 之前构建 BS,因为 RetainedEarnings 依赖 NI。不要在 BS 之前构建 CF,因为 CF.OpeningCash = 上一期 CF.EndingCash 的自引用链要求 Y1 的 BS 现金已锚定。若顺序错误,技能的 Gate 4 平衡检查会静默失败。

第 1 步——工作表、标签颜色与冻结窗格。

officecli add "$FILE" / --type sheet --prop name=Assumptions --prop tabColor=FFC000
officecli add "$FILE" / --type sheet --prop name=P&L --prop tabColor=4472C4
officecli add "$FILE" / --type sheet --prop name='Balance Sheet' --prop tabColor=4472C4
officecli add "$FILE" / --type sheet --prop name='Cash Flow' --prop tabColor=4472C4
officecli add "$FILE" / --type sheet --prop name=Summary --prop tabColor=70AD47
officecli set "$FILE" /Assumptions --prop freeze=B2
officecli set "$FILE" /P&L --prop freeze=B3
officecli set "$FILE" "/Balance Sheet" --prop freeze=B3
officecli set "$FILE" "/Cash Flow" --prop freeze=B3

第 2 步——假设(蓝色字体,关键驱动项使用黄色填充)。 年份表头位于第 2 行,标签在 A 列向下排列,B:E 为蓝色数字输入。驱动项包括:RevenueGrowth、GrossMargin、OpExRatio、TaxRate、DaysReceivable/Inventory/Payable、CapExRatio、DepreciationYears。在 B:E 上设置 font.color=0000FF。将 3–5 个情景切换驱动项用黄色填充(fill=FFFF00)。

为使用次数 ≥3 的驱动项声明命名区域并引用它们(StartingARR、TaxRate、OpeningCash、GrowthRate、GrossMargin)。公式写法:=StartingARR 而非 =Assumptions!B4;=EBT*TaxRate 而非 =EBT*Assumptions!B8。已声明但未使用的名称属于无用装饰,Gate 6 会拒绝。

第 3 步——利润表行(全部为公式)。 行包括:Revenue / COGS / Gross Profit / OpEx / EBITDA / D&A / EBIT / Interest / EBT / Tax / Net Income。每一行都是引用 Assumptions 或前一行单元格的公式。以下为收入侧示例代码块,请替换为你自己的行号。本示例的行映射:B3=Revenue, B4=COGS, B5=Gross Profit, B7=OpEx, B9=EBITDA, B10=EBIT, B15=Net Income。以单个非常驻 batch 提交:

cat <<'EOF' | officecli batch "$FILE"
[
  {"command":"set","path":"/P&L/B3","props":{"formula":"Assumptions!B5","font.color":"008000"}},
  {"command":"set","path":"/P&L/C3","props":{"formula":"B3*(1+Assumptions!C6)"}},
  {"command":"set","path":"/P&L/D3","props":{"formula":"C3*(1+Assumptions!D6)"}},
  {"command":"set","path":"/P&L/E3","props":{"formula":"D3*(1+Assumptions!E6)"}},
  {"command":"set","path":"/P&L/B4","props":{"formula":"-B3*(1-Assumptions!B7)"}},
  {"command":"set","path":"/P&L/B5","props":{"formula":"B3+B4"}}
]
EOF

Assumptions 引用(B5、C6、B7)同样是占位行。更好的做法是:为每个驱动项定义命名区域(第 2 步),这样公式即可写成 =StartingRevenue*(1+RevenueGrowth_Y2),与行布局无关。对 OpEx / D&A / Interest / Tax / NI 重复此操作。每个跨表引用单元格设置 font.color=008000;同表单元格默认 000000。所有 $ 行设置 numFmt='$#,##0;($#,##0);"-"'。

第 4 步——资产负债表行(全部为公式)。 资产 = Cash + AR + Inventory + Net PP&E。负债 = AP + Debt。权益 = OpeningEquity + RetainedEarnings。营运资本行使用 Days 假设:AR = Revenue × DaysReceivable / 365。Net PP&E 滚动计算:Beg + CapEx − Depreciation。BS.Cash 不是独立的平衡项,它必须等于 'Cash Flow'!B<ending-cash-row>(在第 5 步中填充)。 留存收益:每期实时公式。 BS.RE(t) = BS.RE(t-1) + 'P&L'!NI(t) − Dividends(t)。如果留存收益硬编码,并按整数美元取整,资产负债表每期都会出现 ±$1 的偏差(CFO 会认为“模型不平”)。对于第 1 年的历史留存收益(没有前期净利润),应通过资产负债表恒等式以实时公式计算:BS!RE_Y1 = TotalAssets − TotalLiabilities − PaidInCapital。第 1 年单元格使用蓝色字体并添加经典批注;第 2 至第 5 年继续由净利润驱动。

第 5 步:现金流量表各行(全部使用公式)。 经营活动:NI + D&A − ΔWorkingCapital。投资活动:−CapEx。筹资活动:ΔDebt − Dividends。期末现金 = 期初 + 经营 + 投资 + 筹资。第 2 年及以后的期初现金 = 上期期末现金,在同一工作表内自我衔接:C17 = B19、D17 = C19、E17 = D19。第 1 年的 OpeningCash 是 Assumptions 中的输入项。

第 6 步:平衡检查与现金核对行(硬性交付检查)。 本示例的行映射如下:Balance Sheet: B10=Total Assets, B15=Total Liab, B17=Total Equity, B18=Balance Check;Cash Flow: B5=BS.Cash(跨表锚点), B19=CF.Ending Cash, B21=CF-BS Cash Recon。请根据你所用的版式替换行号,关键在于检查逻辑本身,而不是单元格地址。

cat <<'EOF' | officecli batch "$FILE"
[
  {"command":"set","path":"/Balance Sheet/B18","props":{"formula":"IF(ABS(B10-B15-B17)<0.01,\"OK\",\"IMBALANCED: \"&ROUND(B10-B15-B17,0))","bold":"true","font.color":"000000"}},
  {"command":"set","path":"/Cash Flow/B21","props":{"formula":"IF(ABS(B19-'Balance Sheet'!B5)<0.01,\"OK\",\"CF != BS CASH: \"&ROUND(B19-'Balance Sheet'!B5,0))","bold":"true"}}
]
EOF

将公式复制到 C/D/E 列。使用 type=containsText --prop text=IMBALANCED 或 text="CF !=" 条件设置红色填充(fill=FFC7CE)。Gate 4 会查询这些行,并在出现任何 IMBALANCED 时拒绝交付。

第 7 步:缓存刷新与格式处理。 重新设置 Summary 上的每个汇总单元格、每个平衡检查/核对单元格,以及 BS / CF 上的每个跨表引用(非驻留,每个工作表单批次处理)。设置列宽(col[A]=28、col[B:E]=15),对所有美元行应用 numberformat='$#,##0;($#,##0);"-"',并对分节标题行(REVENUE / COGS / ASSETS / LIABILITIES)设置标题填充(fill=1F3864、font.color=FFFFFF、bold=true)。标题填充必须覆盖 A:E 整行,而不仅仅是标签单元格(→ xlsx v2 §visual floor)。

第 8 步:Summary / Dashboard 关键指标与图表。 至少 4 个关键指标:Revenue 27E、EBITDA Margin 27E、Ending Cash 27E、Net Income CAGR,每个都是引用报表单元格的公式,并使用绿色字体。

面向董事会或高管受众交付的任何 Dashboard,至少需要 3 张图表——一张图表只是草稿,三张才算可交付成果。在添加利润率图表之前,先在 Summary!A10:E13 中预先填入毛利率 / EBITDA 利润率 / 净利润率比率行(引用 P&L 的公式)。

# (1) Top-line trend (Revenue + EBITDA).
officecli add "$FILE" /Summary --type chart --prop chartType=column --prop dataRange='P&L!A2:E5' --prop title='Revenue & EBITDA' --prop width=14cm --prop height=8cm
# (2) Margin trend (Gross / EBITDA / NI margin).
officecli add "$FILE" /Summary --type chart --prop chartType=line --prop dataRange='Summary!A10:E13' --prop title='Margin trend' --prop width=14cm --prop height=8cm
# (3) Cash trajectory (Ending Cash ± Runway).
officecli add "$FILE" /Summary --type chart --prop chartType=area --prop dataRange='Cash Flow!A19:E19' --prop title='Ending cash' --prop width=14cm --prop height=8cm

验证(三项全部执行):

# Balance check every period must say OK (range get → cells under .data.results[0].children[])
officecli get "$FILE" "/Balance Sheet/B18:E18" --json | jq '.data.results[0].children[] | .format.cachedValue // .text'
# Cash recon every period must say OK
officecli get "$FILE" "/Cash Flow/B21:E21" --json | jq '.data.results[0].children[] | .format.cachedValue // .text'
# Summary KPIs are plausible numbers, not null
officecli get "$FILE" "/Summary/B2:B5" --json | jq '.data.results[0].children[].format.cachedValue'
Recipe B — DCF 估值

本 Recipe 的产出。 工作表:Assumptions、FCF(10 年预测)、WACC(面板)、DCF(NPV + TV + 股权桥)、Sensitivity(双轴网格)。输出:Implied Equity Value 与 Implied Per-Share,并附带 WACC × g 敏感性分析。

构建顺序。 Assumptions → FCF → WACC → DCF → Sensitivity。

第 1 步 — 关键驱动因素的命名区域。 DCF 的可读性依赖于命名。下方每个公式都使用 WACC、TaxRate、g,而不是 $B$6:

cat <<'EOF' | officecli batch "$FILE"
[
  {"command":"add","parent":"/","type":"namedrange","props":{"name":"WACC","ref":"WACC!$B$12"}},
  {"command":"add","parent":"/","type":"namedrange","props":{"name":"TaxRate","ref":"Assumptions!$B$8"}},
  {"command":"add","parent":"/","type":"namedrange","props":{"name":"TerminalGrowth","ref":"Assumptions!$B$15"}},
  {"command":"add","parent":"/","type":"namedrange","props":{"name":"NetDebt","ref":"Assumptions!$B$20"}},
  {"command":"add","parent":"/","type":"namedrange","props":{"name":"SharesOut","ref":"Assumptions!$B$21"}}
]
EOF

第 2 步 — FCF 构建(10 年)。 B:K 列 = Y1..Y10。行包括:Revenue(由增长率推算)/ EBIT(收入 × 利润率)/ EBIT × (1 − TaxRate)(NOPAT)/ + D&A / − CapEx / − ΔNWC / = FCF。使用由 Assumptions 驱动的比率(CapEx = Revenue × CapExRatio)。所有单元格均为公式,黑色字体,numFmt='$#,##0;($#,##0);"-"'。

第 3 步 — WACC 面板。 在 WACC 工作表中建立一个 8 行面板:Risk-free rate / Equity risk premium / Beta / Cost of equity(=Rf + β×ERP)/ Pre-tax debt cost / After-tax debt cost(=×(1−TaxRate))/ Equity weight / Debt weight / WACC(=We×Re + Wd×Rd_after_tax)。输入项用蓝色,派生行用黑色。

第 4 步 — 终值 + NPV + 股权桥。 行映射:DCF: B/C 3=TV, 4=PV explicit FCF, 5=PV terminal, 6=EV, 7=Net Debt, 8=Equity Value, 9=Per-Share;FCF: 第 2 行 = 期数(1..10),第 11 行 = FCF,B:K = Y1..Y10。请替换为你自己的行号。备注列单元格使用 {"value":"text"},切勿使用 {"formula":"..."}——以公式形式书写的说明文字会在打开时产生 #NAME?(参见 Recipe C 第 5 步之后的提示框)。在 DCF 工作表中:

cat <<'EOF' | officecli batch "$FILE"
[
  {"command":"set","path":"/DCF/B3","props":{"value":"Terminal value (Gordon growth)"}},
  {"command":"set","path":"/DCF/C3","props":{"formula":"FCF!K11*(1+TerminalGrowth)/(WACC-TerminalGrowth)","font.color":"008000","numberformat":"$#,##0;($#,##0);\"-\""}},
  {"command":"set","path":"/DCF/B4","props":{"value":"PV of explicit-period FCF (10 yr)"}},
  {"command":"set","path":"/DCF/C4","props":{"formula":"SUMPRODUCT(FCF!B11:K11/(1+WACC)^FCF!B2:K2)","font.color":"008000","numberformat":"$#,##0;($#,##0);\"-\""}},
  {"command":"set","path":"/DCF/B5","props":{"value":"PV of terminal value"}},
  {"command":"set","path":"/DCF/C5","props":{"formula":"C3/(1+WACC)^10","numberformat":"$#,##0;($#,##0);\"-\""}},
  {"command":"set","path":"/DCF/B6","props":{"value":"Enterprise value"}},
  {"command":"set","path":"/DCF/C6","props":{"formula":"C4+C5","bold":"true","numberformat":"$#,##0;($#,##0);\"-\""}},
  {"command":"set","path":"/DCF/B7","props":{"value":"Less: Net debt"}},
  {"command":"set","path":"/DCF/C7","props":{"formula":"-NetDebt","font.color":"008000","numberformat":"$#,##0;($#,##0);\"-\""}},
  {"command":"set","path":"/DCF/B8","props":{"value":"Equity value"}},
  {"command":"set","path":"/DCF/C8","props":{"formula":"C6+C7","bold":"true","font.color":"000000","numberformat":"$#,##0;($#,##0);\"-\""}},
  {"command":"set","path":"/DCF/B9","props":{"value":"Implied per-share"}},
  {"command":"set","path":"/DCF/C9","props":{"formula":"C8/SharesOut","bold":"true","numberformat":"$0.00"}}
]
EOF

NPV 与 SUMPRODUCT——两者都能正确缓存。 计算引擎会计算 NPV(rate, cross_sheet_range) 并正确缓存结果(可通过回读验证)。SUMPRODUCT(values/(1+rate)^periods) 是其代数等价形式(期数行 FCF!B2:K2 = 1..10 只需一次性设置);这里展示它仅是一个可选的审计可读性选择——显式折现序列有时更便于审阅者追溯,而并非缓存的变通方法。两种形式均可。对于不规则日期,XNPV(rate, values, dates) 同样可以缓存;其 SUMPRODUCT 等价形式为 SUMPRODUCT(values/(1+rate)^((dates-base_date)/365))。

第 5 步 — 双轴敏感性网格(WACC × g)。 5×5 网格。行 = WACC 值 7.5% ... 11.5%,列 = g 值 1.5% ... 3.5%。每个单元格都是一个自包含公式,使用网格中的 WACC 和 g 替换后重新运行 DCF。模板如下:

# Cell D14 (first data cell, grid anchor at C14 = WACC label, C15 = first WACC value)
# Substitute $D$13 (this cell's g) and $C15 (this cell's WACC) into a replicated EV + equity formula.
cat <<'EOF' | officecli batch "$FILE"
[
  {"command":"set","path":"/Sensitivity/D15","props":{"formula":"(NPV($C15,FCF!$B$11:$K$11)+(FCF!$K$11*(1+D$14)/($C15-D$14))/(1+$C15)^10+(-NetDebt))/SharesOut","numberformat":"$0.00"}}
]
EOF

将公式复制到 D15:H19(5×5 网格)。第 14 行承载 g 值(蓝色输入);C 列承载 WACC 值(蓝色输入)。第 13 行与 B 列为标签。应用三色渐变条件格式以便快速阅读(绿色 = 上行,红色 = 下行):

officecli add "$FILE" /Sensitivity --type conditionalformatting \
  --prop type=colorScale --prop ref=D15:H19

不要使用 Excel 数据表。 Excel 原生的 /Data/Table 双变量表无法通过命令行可靠支持——每个网格单元格都必须是显式公式。请复制模板,不要尝试使用 Data Table 输入单元格。

验证。

officecli get "$FILE" "/DCF/C8" --json | jq '.data.results[0].format.cachedValue'   # equity value, plausible $
officecli get "$FILE" "/DCF/C9" --json | jq '.data.results[0].format.cachedValue'   # per-share, in $XX.XX range
officecli get "$FILE" "/Sensitivity/F17" --json | jq '.data.results[0].format.cachedValue'   # grid center cell, plausible

如果 C8 或 C9 返回 null 或显示 #OCLI_NOTEVAL! 哨兵值,请重新设置它们(非常驻)——参见 §Build-order & cache-drift。

Recipe C — LBO 模型

本 Recipe 的产出。 工作表:Assumptions、S&U(资金来源与运用)、Debt(多档债务计划表)、P&L(5 年)、CF、Exit / Returns。输出:MOIC、IRR,以及一个 4 级回报瀑布。LBO 是压力测试——预计会出现循环引用(利息 ↔ 现金)、最深的跨工作表依赖链,以及对命名区域的最大量使用。

构建顺序。 Assumptions → S&U → P&L → Debt → CF → Exit → Returns。P&L 应先于 Debt(债务利息依赖 P&L 的 EBIT 进行覆盖率检查);Debt 应先于 CF(CF 使用利息与本金摊还)。在第 5 步之前启用 calc.iterate。

第 1 步 — 资金来源与运用(必须平衡,每项费用逐项列出)。

Uses    = Purchase_EV (EntryEBITDA × EntryMultiple) + Transaction_fees (Purchase_EV × TxnFeePct, typ 1.5–2.5%)
        + Financing_fees ((Senior + Mezz) × FinFeePct, typ 1–3%) + Refinanced_debt
Sources = Senior_TLB + Mezz + Revolver_drawn + Sponsor_equity

发起人股权——二选一,绝不能两者并用。 (a) 设定值: Sponsor_equity = Assumptions!SponsorEquity,然后按比例调整高级债与夹层债,使 Sources = Uses(费用由债务承担,而非静默平衡项)。(b) 求解值: Sponsor_equity = Uses − Senior − Mezz − Revolver − Refinanced,标签为「Sponsor Equity (solved)」,不单独引用 Assumptions。硬编码的 SponsorEquity 加上 =Uses − Senior − Mezz 平衡项,必然导致费用被静默吸收——设定值 1.40 亿美元与平衡值 1.9467 亿美元之间相差 5467 万美元的未计入费用,CFO 一眼就会驳回。

# Sources = Uses hard check.
officecli set "$FILE" /S&U/B12 --prop formula='IF(ABS(SUM(B4:B7)-SUM(B9:B11))<1,"BALANCED","S&U IMBALANCE: "&ROUND(SUM(B4:B7)-SUM(B9:B11),0))' --prop bold=true

# Stated-vs-plug consistency (Gate 4 addendum; only run if you chose pattern (a)).
STATED=$(officecli get "$FILE" /Assumptions/B12 --json | jq -r '.data.results[0].format.cachedValue // "null"')
PLUGGED=$(officecli get "$FILE" /S&U/B10 --json | jq -r '.data.results[0].format.cachedValue // "null"')   # B10 = sponsor-equity row on S&U
DELTA=$(python3 -c "print(abs(float('$STATED') - float('$PLUGGED')))" 2>/dev/null || echo 99999)
python3 -c "import sys; sys.exit(0 if float('$DELTA') <= 1 else 1)" && echo "S&U sponsor OK (stated=$STATED plug=$PLUGGED)" || { echo "REJECT Gate 4 S&U: stated $STATED ≠ plug $PLUGGED (Δ=$DELTA) — fees silently absorbed"; exit 1; }

S&U 上每一条非发起人项目,要么是蓝色的 Assumptions 输入项(目标 EBITDA、入场倍数、费用百分比),要么是派生公式。Uses / Sources 中不得出现硬编码数字。

第 2 步——债务进度表(多档债务)。 每个档次每年一行。列为:BeginningBalance / Mandatory amortization / Cash sweep / EndingBalance / AverageBalance / InterestExpense。高级 TLB:1% 强制摊销,全部超额现金用于现金清扫。夹层债:0% 摊销,仅按期支付现金利息。本示例的行映射(高级 TLB 档次,第 2 年列 C):C4=Beginning Balance, C5=Mandatory Amort, C6=Ending Balance, C7=Cash Sweep, C8=Average Balance, C9=Interest Expense。CF!C20 为可用于清扫的自由现金(CF 表中清扫前的第 2 年期末现金)。请根据您的布局替换为相应档次的行区块。

# year 2 senior TLB
cat <<'EOF' | officecli batch "$FILE"
[
  {"command":"set","path":"/Debt/C4","props":{"formula":"B6"}},
  {"command":"set","path":"/Debt/C5","props":{"formula":"-C4*Assumptions!$B$30","numberformat":"$#,##0;($#,##0);\"-\""}},
  {"command":"set","path":"/Debt/C6","props":{"formula":"C4+C5+C7","numberformat":"$#,##0;($#,##0);\"-\""}},
  {"command":"set","path":"/Debt/C7","props":{"formula":"-MIN(-CF!C20,C4+C5)"}},
  {"command":"set","path":"/Debt/C8","props":{"formula":"(C4+C6)/2","numberformat":"$#,##0;($#,##0);\"-\""}},
  {"command":"set","path":"/Debt/C9","props":{"formula":"-C8*Assumptions!$B$31","numberformat":"$#,##0;($#,##0);\"-\""}}
]
EOF
# Add the sweep-rule comment as a classic comment (comment is NOT a cell prop — separate --type comment).
officecli add "$FILE" /Debt --type comment --prop ref=C7 --prop text='cash sweep capped at available cash and remaining tranche balance'

循环信贷额度上限。 若交易使用循环信贷档次,则每期循环信贷余额受承诺额度上限约束:

Revolver_Balance = MIN(Assumptions!RevolverCapacity, MAX(0, prior_revolver + draw − paydown))

若缺少外层 MIN(capacity, ...),资金短缺的季度将静默地超额提取该融资额度。

请根据您的布局调整行索引。对每个档次(高级 / 夹层 / 循环信贷)及每一年重复此操作。

第 3 步——损益表(5 年)及来自债务表的利息。 损益表利息行从债务表取值:Interest = 'Debt'!TotalInterestRowY<N>。这将形成循环引用:利息 → 净利润 → 现金流 → 现金清扫 → 债务余额 → 利息。

写入顺序警告。 calc.iterate=true 控制的是重新计算,而非写入阶段。若将跨表循环的闭合部分追加到已包含该循环的文件中,无论 iterate 如何设置,引擎都会死锁并占满 100% CPU。对于复杂循环(多档 LBO、循环信贷 + TLB + 夹层债),请使用下文的 §写入顺序手术(先解除循环 → 写入下游 → 重新闭合循环)。在写入循环公式之前,先启用 calc.iterate=true:

officecli set "$FILE" / --prop calc.iterate=true --prop calc.iterateCount=100 --prop calc.iterateDelta=0.001

对于自然阻尼的循环,iterate 通过逐次逼近收敛(利率越高 → 现金越少 → 清扫越少 → 余额越高,并以 EBIT 为界)。若出现 #REF! 或数值发散,应暂停;请修正代数,不要将 iterateCount 提高到 1000。

第 4 步——CF 与现金清扫。 期末现金 = 期初现金 + CFO − CapEx − 强制摊销 − 现金清扫。现金清扫 = MIN(freeCashAfterCapEx, seniorDebtBalance + seniorMandatoryAmort)。MIN 上限可防止清扫金额为负。

第 5 步——退出与回报。 行映射:Exit: B3=Exit EV, B4=Less: remaining debt, B5=Exit equity to sponsor;Returns: B3=MOIC, B4=IRR。

# Values/formulas — single non-resident batch.
cat <<'EOF' | officecli batch "$FILE"
[
  {"command":"set","path":"/Exit/B3","props":{"formula":"'P&L'!F8*Assumptions!$B$25","numberformat":"$#,##0;($#,##0);\"-\""}},
  {"command":"set","path":"/Exit/B4","props":{"formula":"-('Debt'!F6+'Debt'!F13)","font.color":"008000","numberformat":"$#,##0;($#,##0);\"-\""}},
  {"command":"set","path":"/Exit/B5","props":{"formula":"B3+B4","bold":"true","numberformat":"$#,##0;($#,##0);\"-\""}},
  {"command":"set","path":"/Returns/B3","props":{"formula":"'Exit'!B5/('S&U'!B9)","numberformat":"0.00\"x\""}},
  {"command":"set","path":"/Returns/B4","props":{"formula":"IRR({-'S&U'!B9,0,0,0,0,'Exit'!B5})","numberformat":"0.0%"}}
]
EOF
# Classic comments — one --type comment per anchor cell.
officecli add "$FILE" /Exit --type comment --prop ref=B3 --prop text='Exit EV = Y5 EBITDA × exit multiple'
officecli add "$FILE" /Returns --type comment --prop ref=B3 --prop text='MOIC = exit equity / sponsor equity'
officecli add "$FILE" /Returns --type comment --prop ref=B4 --prop text='IRR — 5-yr, entry + exit only; use XIRR for mid-year dividends'

说明文字:标签的三种不同机制——comment 元素、备注列与 formula。

  • 悬停提示 → officecli add ... --type comment --prop ref=<cell> --prop text='...'。comment 键不是 set cell 的有效属性(officecli help xlsx cell 中不存在),若嵌入 set cell 的属性字典中会被静默丢弃。请使用专用元素。
  • 相邻备注列中的可见文字 → {"command":"set","path":"/DCF/D3","props":{"value":"TV = FCF × (1+g) / (WACC−g)"}} ——使用 value,而非 formula,即普通的带引号字符串。
  • 以真实公式形式书写的公式式说明文字 → 绝对不要这样做。{"formula":"FCF10*(1+g)/(WACC-g)"} 会在 Excel 中产生 #NAME?(在该单元格上下文中,FCF10、g、WACC 都是未绑定的标识符)。

对于期中分红或部分退出,请使用 XIRR({cashflows}, {dates}) 代替 IRR。 第 6 步 — 收益瀑布(可选,4 层 LP/GP)。 各层级:(1)LP 优先回报 8%;(2)GP 追赶至 20%;(3)超过门槛后按 80/20 分配;(4)亏损时 100% 归 LP。每一层都是一个 MAX(0, MIN(...)) 限幅。通用网格模式参见 §敏感性与情景。

验证。

officecli get "$FILE" /S&U/B12 --json | jq '.data.results[0].format.cachedValue // .data.results[0].text'   # must say BALANCED
officecli get "$FILE" /Returns/B3 --json | jq '.data.results[0].format.cachedValue'                # MOIC, expect 2.0x-4.0x typical
officecli get "$FILE" /Returns/B4 --json | jq '.data.results[0].format.cachedValue'                # IRR, expect 0.15-0.30 typical
# Iterate converged?
officecli query "$FILE" 'cell:contains("#REF!")' --json | jq '.data.results | length'   # must be 0

敏感性与情景

三种模式,任选其一:

  • (a) 在 Assumptions 中设置基准 / 乐观 / 悲观列:并列展示各情景,通过"Active"列加 INDEX/MATCH 实现无下拉框切换。
  • (b) 下拉框 + INDEX/MATCH 切换:在 Summary 上设置一个数据验证下拉框,通过 INDEX(Base:Downside, MATCH(Dropdown, ScenLabels, 0)) 驱动所有驱动因子。
  • (c) 双轴敏感性网格:5×5 或 7×7,每个单元格使用一个自包含公式,行列表头即两个驱动因子。WACC × g 示例参见 Recipe B 第 5 步。

混用 (a) 和 (b) 会产生循环输入(情景既由下拉框选取,又被 Active 列覆盖),请二选一。

网格规则: 每个单元格将行驱动因子和列驱动因子代入一份自包含的输出公式副本中。不能引用 WACC 命名区域(那是面板本身),应引用网格的坐标轴单元格。

下拉框情景切换。 在 Summary 上设置一个 validation 下拉框,驱动每个 Assumptions 行:

cat <<'EOF' | officecli batch "$FILE"
[
  {"command":"add","parent":"/Summary","type":"validation","props":{"sqref":"B1","type":"list","formula1":"Base,Upside,Downside"}},
  {"command":"set","path":"/Assumptions/B5","props":{"formula":"INDEX(C5:E5,MATCH(Summary!$B$1,$C$4:$E$4,0))"}}
]
EOF
# If you want a hover tooltip on B5, add it separately:
officecli add "$FILE" /Assumptions --type comment --prop ref=B5 --prop text='Revenue growth — picked by Summary!B1 scenario dropdown'

每个 Assumptions 驱动行都使用相同的 INDEX/MATCH。C:E 列上的基准 / 乐观 / 悲观列保持蓝色(硬编码的情景输入)。

龙卷风图(football field)图表模式(DCF 估值汇总)。 为 3–5 种估值方法(DCF 基准、DCF 悲观、可比公司交易、先例交易、LBO 底价)横向绘制从低到高的条形,纵向堆叠。在 Football 工作表上:A 列为方法名称,B 列为低值($),C 列为高值($),D 列为 =C−B(宽度)。图表为堆叠条形图,B 列作为不可见的第一系列(白色/无填充),D 列作为可见系列——dataRange=Football!A3:D7,chartType=bar。Excel 会将其视为每种方法对应的浮动条。

财务函数模式

简明参考,并非金融教科书。如果你不了解这些函数的作用,请暂停并询问用户。

函数优先于原因
XNPV(rate, values, dates)NPV现金流日期不规则(如年中完成并购、分批交割的份额)
XIRR(values, dates)IRR日期不规则;能更好地处理多次正负号变化
INDEX(range, MATCH(lookup, key, 0))VLOOKUP插入安全(源区域中插入列时 VLOOKUP 会失效)
IFERROR(x/y, 0) 或 IF(y=0, 0, x/y)直接除法财务模型中每个 / 都要加保护——交付出现 #DIV/0! 即视为交付失败
MIRR(values, financeRate, reinvestRate)通过翻转符号使用 IRR现金流模式存在 2 次及以上正负号变化时
SUMIFS(sumRange, criteriaRange1, criterion1, ...)SUMPRODUCT((...)) 数组条件求和意图更清晰;两者都能正确计算

计算引擎覆盖范围——先验证,不要预先硬编码。 CLI 计算引擎可以计算本技能使用的财务函数,包括 NPV / XNPV / IRR / XIRR、数组常量公式(IRR({...}))以及 SUMPRODUCT(1/COUNTIF(range,range)) 去重计数,并会缓存正确结果且标记 evaluated:true。不存在"将 NPV 改写为 0"的义务,也不存在去重计数中 1/N 的陷阱。诚实的原则是:先写出你真正想要的公式,然后通过回读验证缓存值(get ... --json | jq '.data.results[0].format.cachedValue')。只有当某个单元格确实返回 #OCLI_NOTEVAL! 哨兵值(公式先于其输入写入,或使用了不支持的函数)时,才应退而求其次——先在 close 之后重新设置非驻留状态;如果仍无法计算,则将计算出的值硬编码为蓝色字体,并通过 officecli add "$FILE" /Sheet --type comment --prop ref=<cell> --prop text='cached valuation; refreshes on open in Excel — do not edit' 添加经典批注,并在交付说明中披露。

循环引用与迭代计算

仅当循环引用在代数上有依据时才启用 calc.iterate: 利息 ↔ 现金(LBO 循环信贷 / 现金清扫)、税盾 ↔ 净利润(少见,大多数三表模型会先计算利息再计算税,从而避免循环)、循环信贷平衡项 ↔ 期末现金(带最低现金余额的企业现金瀑布)。

officecli set "$FILE" / --prop calc.iterate=true --prop calc.iterateCount=100 --prop calc.iterateDelta=0.001

iterateCount=100 / iterateDelta=0.001 是 Excel 默认值,适用于自然收敛的循环。

写入顺序调整(解环 → 写入下游 → 重新成环)

calc.iterate 只控制重算,不控制写入阶段。若在已经连接好的跨表环(Debt.Interest ↔ CF.Cash ↔ Debt.CashSweep)上追加闭合环节,会导致 CPU 占用 100% 而死锁;对于未收敛的环,view html / get 也会挂起。

三步操作流程:

  1. 解环 — 写入 Debt 时,将 10–20 个环内单元格设为字面量 0(例如 C7=0,而不是 =-MIN(...))。这样就移除了环。
  2. 写入下游 — 以非常驻方式构建所有非循环链路(P&L、CF、Exit、Returns、Summary、网格),每个工作表一个 heredoc。所有结果都基于已置零的单元格进行缓存。
  3. 重新成环 — 关闭所有常驻进程,使用一次 set 为每个循环单元格重新写入其真实公式,以非常驻方式执行。

验收。 get /Debt/C7 --json | jq '.data.results[0].format.cachedValue' 应返回非零且非空的值。如果某个单元格仍然死锁,则保留 =0 并添加经典注释 "circular; recalculates in Excel on F9",并在交付时予以标注。切勿用 iterateCount=1000 掩盖问题。

不要把 iterate 当作 #REF! / 发散值的补丁。 将 iterateCount 提高到 1000 会掩盖问题,并交付一个看似合理实则错误的值;validate 也无法发现此类问题。应当从代数上打破循环(例如仅按期初余额计算利息,而非平均余额)。

验证收敛性。 读取循环单元格,调整一个驱动假设再调回,然后重新读取——数值必须一致:

V1=$(officecli get "$FILE" /Debt/C9 --json | jq '.data.results[0].format.cachedValue')
officecli set "$FILE" /Assumptions/B31 --prop value=0.085
officecli set "$FILE" /Assumptions/B31 --prop value=0.0845
V2=$(officecli get "$FILE" /Debt/C9 --json | jq '.data.results[0].format.cachedValue')
[ "$V1" = "$V2" ] && echo "Iterate converged" || echo "WARN: drift V1=$V1 V2=$V2 — tighten iterateDelta or check algebra"

审计与交付关卡

假设存在问题。 第一版几乎不可能完全正确。运行以下每一道关卡,每项检查都必须输出成功信息。validate 通过并不等于可以交付——模型可能通过了结构校验,却仍然在数量级上错误十倍。

关卡 1–3 — 直接沿用 xlsx v2

→ 参见 xlsx v2 的 §QA 最小周期(关卡 1–3 涵盖 view issues、错误单元格查询,以及关闭后的 validate)。请先按 xlsx v2 中的写法执行这些关卡,不做任何针对财务模型的调整。

关卡 4 — 报表完整性(三表与 LBO)

由 Recipe A / C 生成的平衡检查行和现金核对行,每期都必须显示 OK / BALANCED。对检查行执行 query,遇到任何 IMBALANCED / CF != 即拒绝交付:

BS_FAIL=$(officecli query "$FILE" 'cell:contains("IMBALANCED")' --json | jq '.data.results | length')
CF_FAIL=$(officecli query "$FILE" 'cell:contains("CF !=")' --json | jq '.data.results | length')
SU_FAIL=$(officecli query "$FILE" 'cell:contains("S&U IMBALANCE")' --json | jq '.data.results | length')
if [ "$BS_FAIL" -eq 0 ] && [ "$CF_FAIL" -eq 0 ] && [ "$SU_FAIL" -eq 0 ]; then
  echo "Gate 4 OK (balance + recon + S&U all pass)"
else
  echo "REJECT Gate 4: BS=$BS_FAIL CF=$CF_FAIL S&U=$SU_FAIL"; exit 1
fi

如果有任何检查失败,说明模型存在静默错误——交付前请修复上游链路。最常见的原因是跨表公式被存储为 \!(被 shell 转义破坏)——运行 officecli query "$FILE" 'cell:contains("\\\\!")',然后通过批量 heredoc 重新输入。

关卡 5 — 估值单元格缓存值检查

NPV / IRR / XIRR / 股权桥 / MOIC / 汇总 KPI 单元格如果以未计算状态交付(#OCLI_NOTEVAL! 哨兵值,通常是因为公式写入时其输入尚不存在),会向不重新计算的读者展示空白或错误数字。列出所有估值单元格,并确认已存在缓存值:

# Customize the path list per recipe — this is the DCF example
for P in "/DCF/C4" "/DCF/C5" "/DCF/C6" "/DCF/C8" "/DCF/C9"; do
  V=$(officecli get "$FILE" "$P" --json | jq -r '.data.results[0].format.cachedValue // "null"')
  if [ "$V" = "null" ] || [ "${V#\#OCLI_NOTEVAL}" != "$V" ]; then
    echo "REJECT Gate 5: $P unevaluated (cached=$V) — re-set after close (see §Build-order & cache-drift)"; exit 1
  fi
  echo "Gate 5 $P: cached=$V OK"
done

对于 LBO,请扩展该列表:/Exit/B5、/Returns/B3、/Returns/B4。对于三表模型,请补充 /Summary/B2:B5。

关卡 6 — 硬编码 / 区域规范

每个计算工作表中的数值硬编码数量必须为零。可执行检查:

# `cell:not(:has(formula))` selects the literal cells (and `cell:has(formula)` the formula cells).
HARDCODE=$(officecli query "$FILE" 'cell[type=Number]' --json \
  | jq '[.data.results[] | select(.format.formula == null) | select(.path | test("/(P&L|Balance Sheet|Cash Flow|DCF|Debt|FCF|WACC|Exit|Returns)/"))] | length')
[ "$HARDCODE" -eq 0 ] && echo "Gate 6 OK (no hardcodes on Calc sheets)" || { echo "REJECT Gate 6: $HARDCODE hardcoded numeric cells on Calc zone — move to Assumptions"; exit 1; }

# Named-range coverage + dead-decoration audit: ≥3 ranges declared AND each referenced by ≥1 formula.
NR=$(officecli query "$FILE" namedrange --json | jq '.data.results | length')
[ "$NR" -ge 3 ] && echo "Gate 6 OK ($NR named ranges)" || echo "WARN Gate 6: only $NR named ranges"
DEAD=0
for NR_NAME in $(officecli query "$FILE" namedrange --json | jq -r '.data.results[].format.name'); do
  # Match the formula SOURCE (`formula~=`), not the cached/displayed RESULT — `:contains` would scan
  # the computed value and report every name as dead (false reject).
  USES=$(officecli query "$FILE" "cell[formula~=$NR_NAME]" --json | jq '.data.results | length')
  [ "$USES" -ge 1 ] && echo "  $NR_NAME: $USES uses OK" || { echo "  WARN: $NR_NAME unused"; DEAD=$((DEAD+1)); }
done
[ "$DEAD" -eq 0 ] && echo "Gate 6 named-range audit OK" || { echo "REJECT Gate 6: $DEAD dead-decoration name(s)"; exit 1; }
Gate 5b — 通过 HTML 预览进行视觉审计(必做)

Gates 1–4/6 是 grep 防线,无法看到渲染后的表格。运行 officecli view "$FILE" html 并读取返回的 HTML。逐一检查每个工作表(继承 xlsx v2 视觉底线):

  • 任何数字单元格中不得出现 ###(加宽列)。
  • 标签或分节标题不得被截断(加宽列或设置 alignment.wrapText=true)。
  • 不得出现占位符标记(TBD、{var}、xxxx),见下方 Gate 6.1 的 grep 检查。
  • 平衡检查 / 对账行在每个期间列都须显示 OK / BALANCED。
  • 仪表板图表须正常渲染:ARR/收入折线的 y 轴从 0 开始,图表数据与报表工作表一致。
  • 敏感性表格颜色须呈现从绿色(上行)到红色(下行)的渐变,即已应用色阶条件格式。
  • 汇总 KPI 不得残留过期的缓存值 0;如存在,运行缓存刷新步骤。

发现任何缺陷均须驳回。人工预览: 运行 officecli watch "$FILE",或在 Excel / WPS / Numbers 中打开。最终颜色与图表保真度只有在目标查看器中才能完整呈现。

Gate 6.1 — 标记 / 占位符扫描
LEAK=$(officecli view "$FILE" text | grep -niE 'TBD|\(fill in\)|xxxx|lorem|\{\{|placeholder|coming soon')
[ -z "$LEAK" ] && echo "Gate 6.1 OK (no placeholder tokens)" || { echo "REJECT Gate 6.1:"; echo "$LEAK"; exit 1; }
诚实的局限

validate 只能发现结构错误,无法发现财务错误。一个模型可以通过 validate,但其中 BS.Cash 被硬编码以强制平衡,NPV 缓存值为 0,敏感性表格因在 FCF 之前构建而全为零,或者 P&L 命名的工作表中引用未加引号,导致运行时出现 #NAME?。Gates 4 / 5 / 6 / 5b 的存在,正是因为仅靠结构层面的 validate 无法发现这些问题。

已知问题与常见陷阱

→ 基础陷阱(跨表 ! 陷阱、批量 JSON 点号名称规则、渲染器注意事项):参见 xlsx v2 §Known Issues & Pitfalls,全部适用。

金融模型专属:

  • COGS 上的 AP 符号。 应付账款(AP):若利润表中 COGS 以负数存储,AP 公式必须取反,即 =-COGS*DaysPayable/365。符号错误会虚增净营运资本并使现金流方向反转。该问题不会报错,也能通过 validate。
  • query / validate 无法捕获 #NAME?。 跨表公式若引用 P&L!B3 而未给表名加引号(因为 & 是特殊字符),运行时会得到 #NAME?。跨表引用必须写成 'P&L'!B3,即只要表名包含 &、空格、(、) 等字符,就需要加单引号。只有第 5b 步的视觉检查才能发现此问题。
  • 迭代计算的静默不收敛。 calc.iterate=true iterateCount=100 会在迭代上限处停止并返回结果,即使真实答案是该值的 2 倍。务必执行收敛验证(参见 §Circular references)。复杂的 LBO 循环(多层级债务 + 现金流清扫 + 税盾)可能无法收敛;当循环单元格的 cachedValue=0 时,请使用 §Write-order surgery。
  • 循环写入时的批量驻留死锁。 通过 batch 写入带有未关闭驻留会话的跨表循环的闭合环节,会导致 CPU 占用率达到 100% 并死锁。即使对循环单元格进行单次 set 也可能挂起。解决方法:先关闭驻留会话,然后按 §Write-order surgery 分两步写入该循环。只有非驻留的单次 heredoc 形式是安全的。
  • view html 中跨表缓存值过时。 若下游单元格与上游按相同顺序写入,会缓存为 0。Excel 打开时会重新计算,但 HTML 预览不会。请在整个链条写完后,对每个非驻留的下游单元格重新 set(参见 §Build-order & cache-drift)。
  • NPV() / XNPV() 可正确计算并缓存。 求值器可计算两者(同表和跨表),无需改写。SUMPRODUCT(values/(1+rate)^periods) 仅是可选的审计可读性替代写法,并非缓存的变通方案。
  • 敏感性表格的构建顺序仍然重要。 若表格单元格的写入早于其 FCF/WACC 输入,它会基于空输入求值,并可能缓存错误或空白的值。应先构建 FCF、WACC 和 DCF,再在单独的非驻留批次中写入敏感性表格,并进行验证(jq '.data.results[0].format.cachedValue')。这是一种顺序纪律,而非求值器的限制。
  • BS.Cash 始终等于现金流量表的期末现金。 包括第 1 年在内(BS.Cash = 'Cash Flow'!B19)。绝不能将其作为独立的平衡项或引用 Assumptions,否则人为平衡的 BS.Cash 会掩盖资产负债表错误。
  • 第 2 年及以后的 Opening Cash 等于上一期的 Ending Cash(C17=B19、D17=C19)。若第 2 年及以后的期初现金作为独立输入,会悄无声息地与资产负债表产生偏差。
  • 瀑布图的“合计”柱。 chartType=waterfall 无法通过程序标记合计项,请使用 colors= 约定(深色 = 合计,中色 = 正值,红色 = 负值)。参见 help xlsx chart。
  • 当 SharesOut 为公式时的 DCF 每股价值。 =BasicShares + OptionPool × ExerciseAssumption:应添加一个蓝色字体的假设单元格,并将 SharesOut 命名区域指向计算结果单元格,而非原始输入。

帮助指引

存有疑问时,执行:officecli help xlsx [element] [--json]。帮助信息是权威的架构定义;本技能则是金融建模差异部分的决策指南。

评论

支持 Markdown