「把整個板塊的可比公司跑一遍」聽起來是個體力活,於是很多人把希望寄託在 AI 上:丟一串股票代碼進去,讓它吐出倍數表。結果往往是一張看起來專業、實際上每家公司口徑都不一樣的表格——A 家用的是上一個財政年度的數據,B 家幣別沒換,C 家的 EBITDA 含著一筆處置收益。這種表格比沒有表格更危險,因為它把「不可比」偽裝成了「可比」。
這篇文章的目標是把可比公司分析(Comparable Company Analysis,簡稱 comps)改造成一條可重跑、可審計、口徑統一的自動化流水線。讀完你會得到:同業挑選的準入與排除標準、六類必須統一的口徑問題與處理規則、倍數選擇的適用情境與陷阱、五段式流水線的 schema 設計與 Python 骨架,以及一套「AI 會在哪裡污染這張表」的機械化驗證清單。文中出現的任何數字都是教學用假設值,公司均為虛構,與真實市場數據無關。
一、先把問題定義清楚:難的是分母,不是算術
倍數的算術只有一行:
EV = 市值 + 淨負債(口徑見第二節)
P/E = 每股股價 ÷ 每股盈餘
P/B = 每股股價 ÷ 每股淨值
EV/EBITDA = 企業價值 ÷ EBITDA
EV/Sales = 企業價值 ÷ 收入
真正的工作量與風險全在分母的口徑上:EBITDA 是哪個期間的?調整過一次性項目沒有?租賃怎麼處理?幣別是什麼?淨負債含不含非控股權益?這些問題不解決,倍數之間的比較就是噪音。所以自動化的第一性原則不是「算得快」,而是:每一個進入表格的數字,都帶著期間、幣別、調整記錄與來源指針。沒有這四樣 metadata 的數字,禁止進入比較表。
「一次跑完一整個板塊」的正確含義也因此改變了:不是讓 AI 一口氣生成 20 家公司的倍數,而是把單家公司的「抽取→標準化→計算→驗證」固化成可批量重跑的技能,再對板塊內每家公司獨立執行、獨立驗證,最後才彙總。批量是結果,標準化才是原因。
二、同業挑選:五條準入標準與一張負面清單
挑選同業的錯誤通常是「用行業分類代碼代替業務判斷」。兩家公司掛同一個行業代碼,可能一家是品牌商、一家是代工廠,利潤結構完全不同。準入標準建議固定為五條,每條都要能在數據中找到證據:
| 標準 | 具體問題 | 證據來源 |
|---|---|---|
| 業務重疊 | 收入構成是否相近?主要產品線、客戶類型、商業模式(品牌/代工/平台)是否同類? | 年報分部資料、收入構成附註 |
| 規模帶 | 收入、資產、市值是否落在可比區間? | 損益表、資產負債表、市場數據 |
| 地區與監管 | 主要營運地、適用的會計準則與監管環境是否相近? | 年報公司資料、會計政策附註 |
| 成長與盈利階段 | 成長期、成熟期還是衰退期?毛利率與利潤率量級是否相近? | 多年損益表趨勢 |
| 會計口徑 | 財政年度截止日、報告幣別、會計準則(IFRS/US GAAP/當地準則)是否可統一? | 年報基本資料 |
同樣重要的是負面清單——以下情況直接排除或降級,不進入主比較表:
□ 僅因行業代碼相同、但收入構成明顯不同的公司
□ 財報期間或幣別無法可靠統一的公司(缺披露、缺匯率口徑)
□ 處於重大特殊狀態的公司:停牌、破產程序、剛完成重大重組、借殼
(不是永遠排除,而是「本期不可比」,留檔並註明原因)
□ 樣本不足時的硬湊:可比公司湊不夠,寧可縮小結論範圍,
也不要放寬標準把不可比的公司塞進來充數
挑選結果本身要產出一張「樣本表」:入選公司、入選理由(對應五條標準)、排除公司、排除理由。這張表是整份 comps 可被挑戰的第一層——挑戰樣本比挑戰倍數容易得多,先把樣本論證做扎實,後面的工作才有意義。
三、六類必須先統一的口徑問題
以下是批次比較中最常見的六種污染,每一種都對應一條明確的統一規則。規則要寫成流水線中的代碼,而不是寫在備忘錄裡。
| 口徑問題 | 不統一時的症狀 | 統一規則 |
|---|---|---|
| 財政年度不同 | A 家是 12 月結、B 家是 3 月結,直接拿「年度數據」相比 | 統一滾算至最近可得的 LTM(最近四季加總);無法滾算的標記為「期間不對齊」並降級 |
| 幣別不同 | 報告幣別、上市地幣別、收入幣別三者混用 | 明確登記每家的報告幣別;換算統一用同一時點或同一期間平均匯率,匯率來源與日期入檔 |
| 一次性項目 | 某家 EBITDA 含大額處置收益或減值,倍數突然「很便宜」 | 建立調整規則:處置損益、減值、重大訴訟和解等非經常項目逐項調整,每筆調整記錄金額與附註出處 |
| EV 構成差異 | 淨負債定義各算各的 | 固定 EV 公式並逐項登記口徑:是否含租賃負債、非控股權益、權益法投資、退休金缺口;受限現金不得計入可動用現金 |
| 股數口徑 | 基本股數與攤薄股數混用,市值差出一截 | 統一用攤薄股數(庫藏股法處理期權與認股權證),註明截止日期;可轉債的處理方式入檔 |
| 市場數據時點 | 股價是今天的、財報是半年前的 | 全表統一 as-of 日期;股價與財報期間的錯配寫進輸出備註 |
其中最容易出事的是租賃。現行租賃會計準則(如 IFRS 16)把多數經營租賃資本化上表,EBITDA 與淨負債同時被墊高;若樣本中有公司仍按舊準則或不同準則編製,EV/EBITDA 就系統性失真。處理方式不是「都調整」或「都不調整」二選一,而是登記每家的租賃口徑,調整到統一口徑,並把調整過程留檔。
統一後的 EV 公式建議固定成這樣(逐項可開關、口徑入檔):
EV = 市值(攤薄股數 × as-of 股價)
+ 總債務(含/不含租賃負債:登記選擇)
+ 非控股權益(按帳面或公允:登記選擇)
− 現金及約當現金(排除受限現金)
+ 退休金淨缺口(是否計入:登記選擇)
− 權益法投資(是否扣除:登記選擇,需與 EBITDA 口徑一致——
若 EBITDA 不含聯營損益,EV 也應扣除對應投資)
分子分母口徑必須成對一致:EV/Sales 的分母若含合併收入,分子的 EV 也應含對應子公司;EBITDA 若剔除了某分部,EV 也要剔除該分部對應的價值。這條「成對原則」是 comps 自動化裡最值得寫成斷言的規則。
四、倍數選擇:適用情境與各自的陷阱
沒有一個倍數適用於所有公司。選擇邏輯是「分母必須為正、必須與分子口徑匹配、必須反映你真正想比較的經濟變量」:
| 倍數 | 適用情境 | 主要陷阱 |
|---|---|---|
| P/E | 盈利穩定、資本結構相近的公司 | 虧損時失效;盈餘可被會計政策影響;高槓桿公司 P/E 偏低是風險不是便宜 |
| P/B | 金融業、重資產行業 | 負權益時失效;商譽與無形資產佔比高時淨值失真;不同會計準則下帳面不可比 |
| EV/EBITDA | 資本結構差異大、折舊攤提重的業務 | EBITDA 忽略維持業務所需的資本支出;租賃口徑污染(見上節);一次性項目未調整 |
| EV/Sales | 尚未盈利的成長期公司 | 完全忽略利潤率差異——高毛利與低毛利業務的 Sales 不可直接比 |
| 行業特定倍數 | 有明確計量單位的行業(每用戶、每單位產能、每儲量等) | 計量單位定義本身可能不統一;需行業知識才能審計 |
統計口徑上還有三條紀律:
1. 樣本含極端值時用中位數,不用平均數;同時報告四分位距
2. 樣本數太少時(例如個位數),不報「平均倍數」,逐家列示
3. 任何「行業平均倍數」都必須是當場從樣本算出來的,
禁止引用記憶中或模型輸出的「行業慣例值」——那是幻覺的高發區
第三條值得展開:LLM 被問「這個行業的 EV/EBITDA 通常是多少倍」時,幾乎總會給出一個流暢的數字,而且語氣篤定。這個數字沒有任何可追溯來源,是典型的編造。流水線層面的對策是:倍數只能由「抽取的原始數據 ÷ 計算」產生,模型不允許直接輸出倍數。這條規則的架構含義見 AI 在金融場景的幻覺風險。
五、自動化流水線:從抽取到輸出的五段設計
整條流水線分五段,每段有明確的輸入輸出契約,段與段之間用結構化 JSON 傳遞:
┌────────────┐ ┌────────────┐ ┌────────────┐ ┌──────────┐ ┌────────────┐
│ 1 樣本與 │→ │ 2 資料抽取 │→ │ 3 口徑標準化│→ │ 4 倍數計算│→ │ 5 異常處理 │
│ metadata │ │(帶來源) │ │(帶調整記錄)│ │(帶斷言)│ │ 與輸出 │
└────────────┘ └────────────┘ └────────────┘ └──────────┘ └────────────┘
每家公司獨立走 2→4,互不共享狀態;任何一段失敗只標記該公司,不污染全表
第 1 段:樣本與 metadata。 輸出樣本表(入選/排除與理由),以及每家的靜態 metadata:代碼、上市地、報告幣別、財政年度截止日、會計準則。
第 2 段:資料抽取。 每個數值都是「值+來源指針+as-of」三元組。來源優先級:結構化數據 API > 年報 PDF 抽取 > 網頁資訊(需人工複核)。年報抽取的做法見 300 頁年報轉結構化 JSON 與 PaddleOCR 抽取港股年報;API 資訊源的選擇見 金融數據 API 調研。抽取層的輸出 schema 示例:
{
"target_id": "FAKE-001",
"target_name": "甲公司(虛構,教學用)",
"as_of": "2026-06-30",
"reporting_currency": "HKD",
"fiscal_year_end": "12-31",
"accounting_standard": "HKFRS",
"facts": [
{
"field": "ebitda_ltm",
"value": 850.0,
"unit": "million",
"period": "LTM_2026Q2",
"source": {"type": "filing", "doc": "2025 annual report", "page": 68},
"adjustments": []
},
{
"field": "net_debt",
"value": 1200.0,
"unit": "million",
"period": "2026-06-30",
"source": {"type": "filing", "doc": "2026 interim report", "page": 12},
"adjustments": [
{"rule": "exclude_restricted_cash", "amount": -150.0,
"reason": "受限現金不得計入可動用現金", "source_page": 45}
]
}
]
}
第 3 段:口徑標準化。 規則引擎逐條套用第三節的統一規則,每筆調整寫入 adjustments 陣列。關鍵紀律:調整只能由規則觸發,不能由模型「覺得」觸發;規則本身有版本號,改了規則就要全表重跑。
第 4 段:倍數計算。 純函數計算,帶斷言:
def ev_from_facts(facts: dict) -> float:
"""EV = 市值 + 總債務 + 非控股權益 - 可動用現金(口徑由 policy 固定)"""
ev = facts["market_cap"] + facts["total_debt"] + facts["minority_interest"] \
- facts["cash_and_equivalents_unrestricted"]
assert ev > 0, "EV 為負:口徑錯誤或數據異常,標記人工裁決,不得靜默輸出"
return ev
def multiples(facts: dict, ev: float) -> dict:
out = {}
if facts.get("ebitda_ltm_adjusted", 0) > 0:
out["ev_ebitda"] = ev / facts["ebitda_ltm_adjusted"]
if facts.get("net_profit_ltm_adjusted", 0) > 0:
out["pe"] = facts["market_cap"] / facts["net_profit_ltm_adjusted"]
if facts.get("net_assets", 0) > 0:
out["pb"] = facts["market_cap"] / facts["net_assets"]
# 分母為負或缺失 → 該倍數輸出 null + 原因,而不是硬算或留空不解釋
for key in ("ev_ebitda", "pe", "pb"):
out.setdefault(key, None)
return out
第 5 段:異常值處理與輸出。 異常值的紀律是「標記→查因→裁決→留檔」,永遠不要靜默刪除。落在四分位距之外的倍數,先跑查因清單:一次性項目沒調乾淨?期間沒對齊?業務真的不同類?公司處於特殊狀態?查因結果與裁決(保留/剔除/降級)全部入檔。統計時剔除的樣本,仍要出現在附錄裡。
批量執行:「跑完一整個板塊」的正確打開方式
板塊級掃描不是把二十幾家公司塞進同一次對話,而是把單公司流水線當作原子任務批量調度。三條工程紀律:
1. 每家公司獨立上下文執行,互不共享工作記憶
2. 增量重跑:以(target_id, as_of, 規則版本)為快取鍵,
只有新財報發布或標準化規則升版時才重抽重算,其餘復用存檔
3. 全表彙總發生在所有單公司任務通過驗證之後;
彙總層只做統計與排序,禁止在彙總時「順手」修改任何一家的數字
第 1 條不是紙上談兵:連續處理多家公司時,上一家的數據洩漏進下一家,是本站實測記錄過的事故模式,完整復盤與 Clean Context 設計見 子代理隔離架構。
批量輸出的檔頭固定三行 metadata:全表 as-of 日期、標準化規則版本號、樣本構成(A/B/C 級家數與剔除名單)。任何人拿到這張表,先讀這三行,再讀倍數。
六、「不可比」是輸出而不是失敗:可比性分級
流水線的最終輸出表格,每家虛擬公司(教學假設值,均為虛構)都帶可比性等級與口徑備註:
| 公司(虛構) | EV/EBITDA (LTM) | P/E (LTM) | P/B | 可比性 | 口徑備註 |
|---|---|---|---|---|---|
| 甲公司 | 7.2 | 11.5 | 1.4 | A | 12 月結;LTM 已剔除處置收益 |
| 乙公司 | 9.8 | 15.2 | 2.1 | A | 12 月結;租賃已統一資本化口徑 |
| 丙公司 | 6.1 | n/a | 0.9 | B | 3 月結,LTM 滾算;本期虧損故無 P/E |
| 丁公司 | 14.5 | 28.0 | 3.8 | B | 含高毛利新業務分部,業務重疊度僅部分達標 |
| 戊公司 | — | — | — | C(剔除) | 重大重組進行中,本期不可比,留檔待下期重評 |
| 中位數(A+B 級) | 8.5 | 15.2 | 1.8 | 樣本 4 家,逐家列示如上 |
分級定義:
A 級:五條準入標準全達標,口徑全部統一,無需重大調整
B 級:準入達標但存在口徑調整或期間滾算,調整已留檔
C 級:本期不可比(特殊狀態/無法統一),剔除出統計、保留在附錄
規則:統計量只用 A+B;C 級必須附原因;A 級不足 3 家時,
結論降級為「逐家比較」,不輸出板塊統計量
這個設計的好處是:「不可比」從流水線的失敗,變成了流水線的正式產出之一。整張表自帶論證結構——讀者可以逐家檢查調整記錄,而不是只能選擇相信中位數。
七、AI 會在這裡出錯:七種污染與機械驗證
把這條流水線交給 AI Agent 時,可預測的失敗模式與對應驗證:
| AI 的典型錯誤 | 症狀 | 機械化驗證 |
|---|---|---|
| 憑記憶給數字 | 倍數「看起來合理」但無來源指針 | schema 強制:無 source 欄位的 fact 一律拒收 |
| LTM/NTM/FY 混用 | 同表內期間口徑不一致 | period 欄位枚舉校驗;全表 as-of 一致性斷言 |
| 幣別與單位錯 | 百萬/億、HKD/USD 混用 | unit 與 currency 必填;跨公司匯算必須引用同一匯率記錄 |
| 一次性項目未剔除 | 某家倍數異常「便宜」 | 調整規則清單逐條核對;異常倍數強制觸發查因流程 |
| 股數口徑錯 | 市值系統性偏低或偏高 | 登記基本/攤薄口徑;市值=攤薄股數×股價的重算斷言 |
| 把異常值直接刪掉 | 中位數「變漂亮」但無記錄 | 剔除必須有裁決記錄;統計樣本與附錄樣本對帳 |
| 編造「行業平均」 | 輸出記憶中的倍數當共識 | 架構規則:倍數只能由計算產生,模型輸出中出現直接倍數即判失敗 |
在此之上加兩道整體性驗證。第一道是重算抽檢:隨機抽 2–3 家,從來源文件重新抽取同一字段,與流水線結果比對;不一致即整批凍結排查。第二道是雙源交叉:關鍵字段(收入、淨負債)若同時有 API 與年報兩個來源,自動比對差異,超過容差就標記人工裁決——兩個獨立來源不一致,本身就是最有價值的警報。
這套「假設產出會被污染、所以每段都設卡」的思路,與 多 Agent 投研流水線 的失敗隔離設計同源;把驗證升級成系統級門控與評測集,則見 自建投研系統。
下一步
- DCF 假設與敏感度分析:comps 給出市場口徑的區間後,用內在價值視角交叉檢查,並把兩者的假設分歧寫成可挑戰的論證
- 300 頁年報轉結構化 JSON:抽取層的具體實作,本篇流水線第 2 段的上游
- 三表聯動與現金流品質檢查:在把一家公司納入樣本之前,先確認它的賬面數字經得起現金流檢驗
- 多 Agent 投研流水線:當板塊掃描要並行處理幾十家公司時,怎麼分工與隔離失敗