大多數回測中最昂貴的一行程式碼是 slippage_bps = 5。這並不是因為 5 是個錯誤的數字——按一整年的成交平均來看,它甚至可能是對的——而是因為一個常數在條件上是錯的,而你的策略選擇流程正是一臺專門用於發現並利用這些條件性誤差的機器。滑點是訂單規模、價差、波動率和可用流動性的函數。常數只是這個函數在某一個點上的取值,然後被外推到所有地方,包括凌晨 3 點的清算級聯行情,那時你的均值迴歸訊號觸發得最猛烈,而訂單簿的深度只有平時的十分之一。回測在那裡也只收 5 bps。實盤可不會。
本文討論的是回測迴圈內部的成本模型:那個會從每一筆模擬交易中扣除的函數 cost(size, market_state)。它刻意不去討論如何事前預測衝擊以安排執行——那是一個配套問題,從模型角度已在價格衝擊建模中討論,從最優排程角度已在 Almgren-Chriss 中討論。這裡的問題更為狹窄,而對任何通過回測來選擇策略的人來說也更為緊迫:什麼樣的函數應當取代你現在所用的常數,你如何從手頭實際擁有的資料中擬合它,以及這個答案在多大程度上會改變你最終執行的是哪一個策略。最後這一部分有一個鮮明的實證答案:在四種成本模型下對同一策略家族的 20 個變體重新排名,看著排行榜整個翻轉過來。
成本模型階梯

正如成交模擬階梯一樣,成本模型構成一個保真度階梯,每一級都需要更多資料,並消除一個特定的系統性偏差。這些級別是累積的:每一級都保留其下方的項,並新增一項。
M0:恆定 bps
一個數字,應用於每一筆交易。三種失效模式,按危害由小到大排列:
- 無視規模。 一筆 5M 的母單支付相同的成本。任何跨越資金容量層級的策略比較都毫無意義。
- 無視市場狀態。 平靜的週二下午和 FTX 崩盤時支付相同的成本。這種誤差不是噪聲;它與那些事件驅動型策略集中交易的狀態恰好呈負相關。
- 選擇上有毒。 當你掃描 200 種參數組合時,最佳化器會發現,在平坦成本下,換手相對於現實是廉價的,於是把整個搜尋朝高頻變體的方向偏移。成本誤差因此對模型選擇變成了一種有方向性的力,而不是對 PnL 的對稱性影響。
M1:與價差成比例
在決策時間戳上收取實測的半價差(外加手續費,手續費是確定性的,應始終作為單獨的項)。這是第一個真正對市場做出響應的模型:價差會在有新聞時、在盤口開盤時、在成交稀薄的山寨幣中擴大,所以成本現在至少與一個流動性變數同步變動。它仍然遺漏的是:價差定價的只是一筆頂部盤口小單的成交。任何更大的訂單都會吃穿訂單簿,而這種吃穿對 是不可見的。
M2:按波動率縮放
新增一項與短期波動率成比例的項。其理由既是實證的也是結構性的:價差和深度由做市商設定,他們的逆向選擇風險和存貨風險隨 縮放(把 Avellaneda-Stoikov 報價邏輯反過來看),所以需求流動性的成本天然以波動率為單位來計量。當你完全沒有訂單簿資料時——只有日線、長曆史——用 、 取日波動率的 0.05–0.1 左右,就已經是一個遠勝於任何常數的模型,因為它至少會隨市場狀態一起呼吸。
M3:參與率的平方根
規模項。 是你的訂單規模, 是相關時間窗內的市場成交量, 是同一窗內的波動率。這個函數形式不是為了方便;它是市場微觀結構中被複現最多的實證結果之一,值得精確地說清楚它從何而來。
理論起點是 Kyle (1985),"Continuous auctions and insider trading"(Econometrica 53(6), 1315–1335)。在 Kyle 的模型中,做市商把價格設為淨訂單流的線性函數,,其中
——單位流量的衝擊與價值不確定性成比例、與噪聲交易量成反比。Kyle 的 lambda 給這一領域提供了它的流動性單位(深度是 ),以及它第一個可檢驗的論斷:每交易美元的成本取決於資訊與流動性之比,而非某個常數。但這個模型對規模是線性的,而資料說的是另一回事。
資料說的是凹的。Almgren, Thum, Hauptmann and Li (2005),"Direct estimation of equity market impact"(Risk, July 2005),擬合了 Citigroup 交易臺約 700,000 筆美股訂單,發現臨時衝擊隨參與率的 次冪增長——他們明確地檢驗並否定了線性和純平方根,支援 ——而永久衝擊的結果是 ,在統計上與線性無法區分(這令人安心,因為 Huberman and Stanzl (2004) 證明了非線性的永久衝擊會招致操縱)。Tóth et al. (2011),"Anomalous price impact and the critical nature of liquidity in financial markets"(Physical Review X 1, 021006),使用來自 Capital Fund Management 橫跨多個期貨市場的近 500,000 筆元訂單,發現 ,其中 為量級為一的常數——並給出瞭如今已成標準的解釋:可見的訂單簿只是潛在流動性中一個微乎其微的部分,而潛在流動性在中間價附近對價格是區域性線性的,這使得吃穿它的成本按平方根縮放。後續工作在邊緣處做了細化:Zarinelli, Treccani, Farmer and Lillo (2015),"Beyond the square root"(Market Microstructure and Liquidity 1(2)),在七百萬筆 ANcerno 機構元訂單上表明,平方根在約兩個數量級的訂單規模範圍內擬合良好,但對數形式則能延伸到五個數量級;Bucci et al. (2019, Physical Review Letters 122, 108302)記錄了從極低參與率下的線性(類 Kyle)衝擊到其上方平方根衝擊的過渡。而 Frazzini, Israel and Moskowitz (2018),"Trading Costs"(SSRN 3229719),使用 AQR 在 19 年間價值 $1.7 萬億的實盤執行,發現已實現成本比早期學術估計小一個數量級——這是一個警示:在別人的訂單流上擬合出的係數,由於緊迫性不同、資訊含量不同,並不能遷移到你自己身上。
對於回測迴圈,實用的總結是三句話。衝擊隨波動率縮放。衝擊對規模是凹的,指數在你自己的資料支援 之前應當當作 處理。前因子 的量級為一,但會隨交易場所、資產、以及——關鍵在於——你自己的交易風格而變化,這正是為什麼下一節講的是如何擬合它而非照抄它。深度校準(衰減核、瞬態與永久衝擊的分解、交叉衝擊)屬於衝擊建模文章的範疇;回測只需要這條曲線。
舉一個具體數字以確定量級。BTC 永續,日波動率 bps,交易場所日成交量 V_d = \6Y = 0.75\delta = 0.5$:
| 訂單規模 | 衝擊項 | + 半價差 (0.5 bp) | ||
|---|---|---|---|---|
| $50k | 0.0008% | 0.0029 | 0.5 bp | 1.0 bp |
| $500k | 0.008% | 0.0091 | 1.7 bp | 2.2 bp |
| $5M | 0.083% | 0.0289 | 5.4 bp | 5.9 bp |
| $50M | 0.83% | 0.0913 | 17.1 bp | 17.6 bp |
規模跨越三個數量級,成本跨越約 18 倍的因子。沒有任何常數能覆蓋這張表,而每一次在保持 slippage_bps 固定的同時改變倉位規模的策略比較,都是在悄悄地拿 5M 處的橙子作比較。
擬合曲線:用你自己的成交,或誰的都用不上
用你自己的成交
正確的資料是你自己的 TCA 記錄:每一筆母單一行,包含到達時的中間價、已實現的成交 VWAP、訂單規模,以及決策時的市場狀態——這正是執行落差度量作為副產品所產生的那份資料集。以 bps 計的對到達價的落差,經同期波動率歸一化後,對參與率做迴歸,就能直接得到 和 。
陷阱在於噪聲。單筆訂單的落差是衝擊加上執行窗內的價格噪聲,而噪聲佔了主導:對於日波動率 250 bps 下的 10 分鐘執行,噪聲項是 bps,而衝擊訊號或許只有 3 bps。每個觀測點 0.15 的訊雜比正是 Almgren et al. 需要 700k 筆訂單的原因,也是你必須在分桶中位數上擬合、絕不在原始點上擬合的原因:
import numpy as np
import pandas as pd
def fit_sqrt_curve(orders: pd.DataFrame) -> tuple[float, float]:
"""orders: one row per parent order.
is_bps -- implementation shortfall vs arrival mid, bps, cost-positive
sigma_bps -- volatility at arrival (same window used at simulation time)
q_over_v -- parent size / market volume over the execution window
Fits I = Y * sigma * (Q/V)^delta on participation-bucket medians."""
df = orders.query("q_over_v > 0 and sigma_bps > 0").copy()
df["i_norm"] = df.is_bps / df.sigma_bps # impact in vol units
df["bucket"] = pd.qcut(np.log10(df.q_over_v), 12, duplicates="drop")
b = (df.groupby("bucket", observed=True)
.agg(i=("i_norm", "median"), qv=("q_over_v", "median")))
b = b[b.i > 0] # noisy buckets can go negative; drop, don't clip
delta, log_y = np.polyfit(np.log(b.qv), np.log(b.i), 1)
return float(np.exp(log_y)), float(delta)
在幾千筆加密母單上,這個擬合的穩定性已經能達到 約 ±30%、 約 ±0.1 的水平——粗糙,但擬合曲線與猜測常數之間的差別不是 30%,而是整個形狀。每季度重新擬合一次; 的漂移本身就是一種診斷訊號(在參與率不變的情況下 上升,意味著要麼交易場所的流動性狀態變了,要麼你的訂單流變得更有資訊含量/更易被察覺)。
冷啟動:還沒有成交資料
在第一筆實盤訂單之前,你只有公開資料,以及兩個誠實的選項。
深度吃穿訂單簿。 在每個規模上對 L2 快照做吃穿成本的平均,方法與成交模擬器對每筆交易所做的完全一樣,只是聚合成一條曲線。這是一個符號已知的下界:快照顯示的是在別人訂單流之後殘存的流動性,補充動態是不可見的,而且你自己較早的子單會在你較晚的子單到達之前就把訂單簿推動了。
def curve_from_book(snapshots, sizes_usd, safety=1.75):
"""snapshots: iterable of (mid, asks), asks = [(price, qty), ...] best-first.
Depth-walk cost is a LOWER bound on realized cost -- scale it."""
rows = []
for q_usd in sizes_usd:
costs = []
for mid, asks in snapshots:
rem, paid, got = q_usd / mid, 0.0, 0.0
for p, q in asks:
x = min(q, rem)
paid += x * (p - mid); got += x; rem -= x
if rem <= 0:
break
if rem <= 0:
costs.append(paid / got / mid * 1e4)
rows.append((q_usd, safety * float(np.median(costs))))
return pd.DataFrame(rows, columns=["q_usd", "cost_bps"])
追蹤公開成交的足跡。 按規模對主動成交分桶,測量每筆成交後幾秒鐘內的中間價變動,讀出一條經驗衝擊曲線。這能恢復凹形狀以及大致正確的 縮放關係,但其水平存在一個你事前無法確定符號的偏差:公開成交回報是其他交易者的決策,所以大額成交是有條件地帶資訊的(相對於你自己無資訊的再平衡,會把測得的衝擊往上偏),而對流動性最敏感的交易者恰恰會拆單來隱藏(往下偏)。用它來定形狀,不要用來定水平。
無論哪種方式,冷啟動的操作流程都是:取公開資料曲線,乘以 1.5–2 的安全係數,小額交易,並開始填充那份最終會取代它的 TCA 表。公開曲線是腳手架,不是承重牆。
市場狀態依賴:同一筆訂單,五倍的成本

中的每一項都依賴於市場狀態,而在波動率飆升時它們會同時向對你不利的方向移動。價差與短期波動率成比例,所以 會擴大 5–10 倍。 直接進入衝擊項。成交量 也會上升——這天真地看會降低參與率——但它上升的幅度小於分子裡的 ,而且這個抵消掩蓋了一個更棘手的效應:中間價附近的掛單深度崩潰得比成交量快得多,所以已實現曲線的陡峭程度會超出僅憑承壓輸入代入擬合公式所預測的水平。
具體來說,用上面擬合的曲線,一筆 $3M 的 BTC 訂單:
- 平靜日: bps,價差 1 bp,V_d = \6= 0.5 + 0.75 \times 150 \times \sqrt{0.0005} = 0.5 + 2.5 = 3.0$ bps。
- 危機日(LUNA、FTX 級別): bps,價差 6 bps,V_d = \18= 3.0 + 0.75 \times 700 \times \sqrt{0.000167} = 3.0 + 6.8 = 9.8$ bps。
公式給出的是 3.3 倍。壓力日的已實現 TCA 一貫高於公式——正是平方根項看不到的那個深度崩潰——而這正是一個實測的壓力乘數應當發揮作用的地方。在我們的成交資料中,危機分桶的殘差在承壓輸入公式之上跑出 1.4–1.8 倍,使得同一筆訂單的總壓力成本落在平靜成本的 4.5–6 倍。這就是"同一筆訂單在波動率飆升時貴 5 倍"這句話誠實的來源:大約 2 倍來自扣除成交量後的 ,2 倍來自價差,其餘來自深度蒸發的速度快於成交量統計所承認的。
其實現是一個條件成本模型:按市場狀態分桶各自擬合曲線,或等價地擬合一條曲線加上一張以決策時計算的已實現波動率分位數為鍵的乘數表(僅用滾動視窗——在全樣本上計算的市場狀態標籤,是通過成本模型偷偷帶入的前視):
| 市場狀態(滾動 1h 波動率分位) | 頻率 | 價差乘數 | 曲線乘數(實測) |
|---|---|---|---|
| 平靜 (< p25) | 25% | 0.6x | 0.7x |
| 正常 (p25–p75) | 50% | 1.0x | 1.0x |
| 升高 (p75–p95) | 20% | 1.8x | 1.6x |
| 壓力 (> p95) | 5% | 5–10x | 3–6x |
Frazzini, Israel and Moskowitz (2018) 在他們的機構資料中記錄了同樣的現象:已實現成本與同期波動率強烈同步變動,而缺少時變波動率項的成本模型恰恰會給尾部錯誤定價。這張表之所以比其 5% 的壓力行所暗示的更重要,原因在於相關性:策略並不會均勻地取樣這幾行。一個波動率突破系統大部分交易都在底部兩行完成。一個平靜市場做市商在頂部一行賺錢,並在底部一行止損離場。用每個策略自己的市場狀態直方圖給它的交易加權——而不是對所有策略收取無條件平均成本——是整個頁面上對事件驅動系統而言最大的單項 PnL 修正,其價值超過階梯上任何相鄰兩級之間的差異。
敏感性實驗:四種成本模型,一份被重新洗牌的排行榜
如果成本模型只是給 PnL 加上一個恆定偏移,那麼這一切對選擇都無關緊要——每個變體都會等量平移,argmax 依然存活。但事實並非如此,因為成本敏感性在各變體之間相差一個數量級。讓這一點具體化的實驗:
設定。 一個策略家族——BTC 和 ETH 永續上的 z-score 均值迴歸和 Donchian 突破,1 分鐘 K 線,18 個月——在持倉週期(15m / 1h / 4h / 24h)和入場閾值上做網格搜尋:20 個變體,單邊每日換手從 0.4x 到 11x 賬戶,$250k 的小單在 5 分鐘內執行。每個變體在每種成本模型下各回測一次:
- M0:僅手續費;
- M1:手續費 + 5 bps 常數;
- M2:手續費 + 半價差 + ;
- M3:手續費 + 擬合曲線 、,加上上面那張市場狀態乘數表。
相同的成交、相同的訊號、相同的程式碼路徑——成本模型作為建構函數參數傳入回測器,就像任何一等參數一樣:
from dataclasses import dataclass
@dataclass(frozen=True)
class SqrtCost:
y: float = 0.75 # fitted impact coefficient
delta: float = 0.5 # fitted exponent
fee_bps: float = 2.5 # taker fee, always separate
def cost_bps(self, q_usd: float, st) -> float:
part = q_usd / st.window_volume_usd
return (self.fee_bps + st.stress_mult *
(st.spread_bps / 2 + self.y * st.sigma_bps * part ** self.delta))
for model in [ZeroCost(), ConstCost(5.0), VolCost(0.08), SqrtCost()]:
for variant in grid:
results[(model, variant)] = backtest(variant, cost_model=model)
結果(代表性執行;年化淨收益):
| 變體 | 每日換手 | M0 | M1 (5 bp) | M2 | M3 (曲線) | 排名 M1 → M3 |
|---|---|---|---|---|---|---|
| MR-15m, z=1.0 | 11.4x | +187% | +61% | +9% | −14% | 1 → 17 |
| MR-15m, z=1.5 | 7.9x | +141% | +52% | +15% | −2% | 2 → 13 |
| BO-1h, k=2 | 3.1x | +74% | +43% | +28% | +19% | 3 → 4 |
| MR-1h, z=2.0 | 2.4x | +58% | +38% | +25% | +21% | 4 → 2 |
| BO-4h, k=3 | 0.9x | +39% | +33% | +29% | +26% | 7 → 1 |

各排行榜之間的排名相關性(Kendall ):——常數幾乎不會重排任何東西,它是一種稅,不是一個模型。——M1 前十名中有六個在 M3 下跌出前十,而 M1 的冠軍從 +61% 變成負數。——單單波動率項就恢復了大部分的重排;規模項則為那些相對於視窗成交量交易規模較大的變體收尾。其機制正是 M0 那一級的選擇毒性:平坦的 5 bps 補貼了那些實際成本 15 bps 的交易(大額、快速、處於壓力中),並過度徵稅了那些成本 2 bps 的交易(小額、耐心、處於平靜中),所以按 M1 排名會系統性地抬高那些真實成本被低估得最厲害的變體。最佳化器找到的不是 alpha;它找到的是你成本模型的誤差項。
這就是為什麼成本模型應當歸入與前向滾動切分或目標函數相同的思維範疇:一個你要去變動的一等回測參數,而不是一個你設定一次就不管的環境常數。如果某個變體的排名在 M2 和 M3 之間穩定,那麼它相對於成本的優勢是真實的。如果它的排名在沿階梯上升時崩潰,那麼你就學到了它是一個成本模型的假象——這與一個策略在前向滾動最佳化中樣本外死亡是同一種認知事件,理應得到同樣的回應。而如果你在 M1 下選出了 200 個變體的 argmax,那麼縮減夏普比率的邏輯在此適用,只是多了一個轉折:你執行的是一次多重檢驗搜尋,而其中的檢驗統計量本身就偏向高換手率的入場。
誠實的邊界:把 PnL 作為一個區間
最後一項紀律源於承認:即便擬合之後,成本模型依然是不確定的。 有置信區間,壓力乘數是從少數幾個尾部日子估計出來的,而未來會抽取它自己的市場狀態。所以不要再把淨 PnL 報告成一個數字。把它報告成一個橫跨若干命名成本情景的區間:
- 樂觀:深度吃穿下界,無壓力乘數。絕不單獨報告;它唯一的作用是界定最好情況。
- 現實:擬合曲線、擬合的市場狀態乘數——你的中心估計。
- 悲觀:,價差 ,壓力行始終按 其 p75 值應用。大致就是:在每個參數置信區間不友好的那一端取值代入的擬合曲線。
兩條決策規則讓這個區間可以操作。第一條:按悲觀分配,按現實報告。 一個只在現實模型下才能存活的策略,是在賭你的成本擬合在昂貴的那個方向上沒有誤差——一個你並沒有有意識地下過的賭注。第二條:追蹤脆弱度比率——區間寬度除以毛 PnL。以上面的實驗為例:BO-4h 在 0.9x 換手下毛收益 +39%,區間 [+22%, +29%],脆弱度 0.44。MR-15m 在 11.4x 下毛收益 +187%,區間 [−31%, +34%]——脆弱度 2.2,而且策略的符號在區間內部就翻轉了。以你當前的成本知識狀態,後一個策略的符號是無法確定的;誠實的表述是"我們不知道"和"減少換手,直到我們知道為止"。一份打印出 +34% 卻省略了區間 −31% 那一端的回測,不是樂觀,而是不可證偽。
產出這個區間不花任何成本——只是在你如今反正都要跑的成本模型掃描裡多加三條記錄——而它改變了對話。"這個策略賺 20%"引人相信。"這個策略在我們各成本情景下賺 11–19%,並在悲觀情景下仍然為正"引人分配資金。
它接在哪裡
在回測引擎中,成本模型正好位於成交模擬器交還已執行數量的那個位置:成交決定你得到了什麼,成本曲線決定你為此付出了什麼,而那條階梯上 L2 以上的級別(深度吃穿、佇列模型)會逐步把成本模型吸收進成交模型本身——當你不直接模擬訂單簿時,曲線就是你所用的參數化替身。參數在你有 TCA 記錄時來自你自己的 TCA 記錄,沒有時則來自經安全係數縮放的公開資料。函數形式來自四十年的理論與測量——Kyle (1985) 解釋了為什麼每美元成本會取決於流動性,Almgren et al. (2005) 和 Tóth et al. (2011) 給出了那條凹定律本身,Almgren-Chriss 是其排程上的對應,神經網路衝擊模型則是其預測的前沿。本文所新增的是那套工作流:擬合曲線,以市場狀態為條件,像參數一樣掃描它,並報告區間。這一切都不光鮮。但這一切都比在實盤中發現你的策略實際上處在 slippage_bps = 5 的哪一邊要便宜得多。
Authors
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.