保真度關卡:由粗到細的回測會更快地愚弄你——除非廉價代理的排序方式與昂貴評估一致
本文屬於"無幻覺回測"系列文章之一。
每一次認真的參數搜尋都會撞上同一堵牆:參數空間極其龐大,而每一次誠實的評估都很昂貴。一個包含十幾個閾值和三個指標週期的多時間框架策略,在多折 walk-forward 劃分下評估,每個配置可能要花費數秒。一萬個配置就是數小時。於是你會尋找捷徑,而捷徑永遠是同一個想法:**先廉價評估,只把存活者提升到昂貴的測試中。**在一折資料上篩選一萬個配置,保留排名前幾百的,再讓它們跑完所有折;在小時級K線上篩選,只對最終入圍者下鑽到1秒級別。
這就是由粗到細的搜尋——下鑽、多保真度、逐次減半、Hyperband、ASHA——它是最佳化領域裡真正好用的想法之一,能把一天的算力開銷壓縮成一小時。我們自己就在用它。但它隱藏著一個極其隱蔽的陷阱,隱蔽到大多數使用它的人從未去檢驗過,這個陷阱是:**整套方法都假設你的廉價代理對配置的排序方式,和昂貴評估的排序方式是一致的。**如果不一致,你並不是在更快地搜尋,而是在第一道關卡就把未來的勝者扔掉,轉而提拔噪聲——你只是在更快地自欺欺人。
本文要講的是唯一一項能告訴你,你的下鑽搜尋究竟是真實有效還是自我欺騙的測量。我們在自己的多時間框架搜尋上做了這項測量,結果令人不安:在最廉價的保真度下,我們的代理對配置的排序幾乎是隨機的(Spearman ρ ≈ 0.03)。如果我們在這一保真度下就大力剪枝——正如每個開箱即用的 ASHA 預設設定都在誘使你去做的那樣——我們就會在第一輪就把最終的勝者淘汰掉。解決辦法是在搜尋開始之前執行一道強制性關卡:先測量排序相關性,並且拒絕在排序尚不可信的保真度以下進行剪枝。
多保真度搜索究竟是什麼

這一整個方法家族都遵循同一種形態。你需要定義一個保真度(也叫資源或預算):一個從廉價到昂貴的旋鈕,當它調到最大值時,給出真實的目標值。然後你在低保真度下執行各個配置,對它們排序,只保留最優的一部分,再只對這些存活者在更高保真度下重新執行——如此反覆,直到少數幾個最終入圍者在完整保真度下完成評估。
逐次減半(Jamieson & Talwalkar, 2016)是其核心:以最小資源啟動 N 個配置,保留排名前 1/η 的部分,將資源乘以 η,然後重複。Hyperband(Li et al., 2018)把逐次減半包裹在一個外層迴圈裡,在不同的起始資源之間進行對沖,這樣你就不用去猜該以多激進的力度剪枝。ASHA(Li et al., 2020)是非同步、對並行友好的版本。淘汰率 η 是它們共有的唯一旋鈕;標準的預設值是 η = 3——每一檔保留前三分之一,把預算變為三倍。
保真度本身幾乎可以是任何一種從廉價到昂貴、最終收斂於真值的量度:
- **walk-forward 的折數。**先在1折上評估,再2折、3折,直到K折。這是我們下文要研究的保真度,因為它對於帶有滾動樣本外劃分的回測來說是最自然的一種。
- **K線解析度。**在1小時K線上篩選,提升到1分鐘級別,只對最終入圍者下鑽到1秒級別或原始成交資料。這是把自適應下鑽的思路應用於搜尋,而不是應用於成交撮合模擬。
- 歷史長度、訓練輪數(epoch)、資料集子取樣——這些是經典的機器學習保真度。
在回測中,折數和解析度是最重要的兩種保真度,而——這正是整篇文章的核心——**它們並不是同等安全的。**其中一種存在偏差,無論你把關卡設定得多麼謹慎,它都會悄悄地選出錯誤的參數。
一切都依賴的那一個假設

讓我們明確寫下逐次減半要想正確運作,究竟需要什麼。它並不需要廉價保真度給出正確的目標數值——沒有人在乎一折報告的夏普比率和六折不一樣。它需要的是一種更弱、但也更具體的東西:廉價保真度必須以和昂貴保真度相同的方式對配置進行排序。如果配置A在完整保真度下勝過配置B,那麼A在低保真度下也應該傾向於勝過B。僅此而已。但這也就是全部。
從形式上說,決定下鑽搜尋是否有效的量,是在整個配置空間上、低保真度目標與高保真度目標之間的排序相關性。如果這個相關性很高,廉價輪次的存活者就是本應在昂貴輪次中獲勝的那些配置,你就白白省下了算力。如果它很低,廉價輪次就是一個隨機過濾器:它會淘汰好的配置、提拔差的配置,而你則是把預算花在了加速奔向一個更差答案的路上。沒有任何中間的 η 設定能救你——一個糟糕的代理,即便溫和地剪枝也仍會漏掉勝者;劇烈剪枝的話,則會讓勝者大量流失。
這種失敗會以一貫的方式隱而不現。搜尋照常完成,報告出一個冠軍,冠軍在樣本內看起來也不錯。沒有任何東西會丟擲錯誤。你只有在冠軍在樣本外崩潰時,才會發現原來代理一直在撒謊——而到那時,你早已把失敗歸咎於策略本身,而不是選出這個策略的搜尋過程。因此,這裡的準則和本系列一再得出的準則是同一個:**在信任一個方法之前,先測量該方法所假設的那件事。**對於下鑽搜尋而言,需要測量的就是排序相關性,而這項測量的成本很低。
測量它:保真度關卡

這道關卡是一個小實驗,你在正式搜尋之前,在你即將搜尋的同一個空間上執行一次。步驟如下:
- 從參數空間中隨機抽取幾百個配置(我們使用約200個——足以給出穩定的Spearman估計,成本也足夠低,負擔得起)。
- 在每一個保真度檔位上評估每個配置:1折、2折……直到完整的K折。對於以折數計的保真度而言,這幾乎是免費的,因為你在廉價檔位上計算出的折,會在昂貴檔位中被複用——這是一種累積平均。
- 對每個檔位
r,在這約200個配置上,計算 r折排序與完整K折排序之間的Spearman 排序相關係數。 - ρ 首次跨過閾值(我們使用 ρ ≥ 0.5)的那個檔位,就是你被允許開始剪枝的最淺保真度。低於這個檔位,排序的噪聲太大,不可信任;你絕不能在那裡淘汰配置。
整個實現只有二十來行程式碼。其核心部分如下:
def fidelity_check(cache, n_probe, seed=7):
"""Spearman ρ: cumulative mean over the first r folds (in FOLD_ORDER)
vs the full K-fold objective, on n_probe random configs."""
rng = np.random.default_rng(seed)
k = len(FOLDS)
per_fold = np.empty((k, n_probe))
order_win = np.array([list(FOLDS[fi]) for fi in FOLD_ORDER], np.int64)
for j in range(n_probe):
p = _unit_to_params(rng.random(len(PNAMES))) # random config
scores, *_ = eval_group(cache, p, _sp_of(p)[None, :], order_win, False)
per_fold[:, j] = scores[0] # score on each fold
cums = np.cumsum(per_fold, axis=0) / np.arange(1, k + 1)[:, None] # r-fold mean
rhos = []
for r in range(1, k):
rho = spearmanr(cums[r - 1], cums[-1]).statistic # r folds vs all K
rhos.append(0.0 if math.isnan(rho) else rho)
return rhos
注意一個刻意設計的細節:這些折是按照 FOLD_ORDER 的順序取用的,這是一種交錯順序,在日曆上早期和晚期的切片之間交替(先是折0,然後是一箇中間的折,再是折1,然後是一個更晚的折……)。這一點極其重要,也是後文一節要討論的主題:它意味著"1折"是橫跨歷史中段的一個切片,而"2折"是一個早期加一個晚期的切片——絕不是一段連續的近期視窗。廉價保真度之所以更便宜,是因為使用了更少的折,而不是更近期的折。
結果:單折的排序幾乎是隨機的

下面是這道關卡在我們兩套多時間框架配置上執行後所報告的結果。表中數字是在約200個隨機配置上、r折排序與完整K折排序之間的 Spearman ρ——衡量廉價保真度排序對完整保真度排序的預測有多忠實。
折數 r |
多時間框架執行 | 更困難的行情 |
|---|---|---|
| 1 | +0.43 | +0.03 |
| 2 | +0.67 | +0.43 |
| 3 | +0.78 | +0.78 |
| 4 | +0.82 | — |
| 5 | +0.91 | +0.91 |
先看"更困難的行情"這一列,因為它才是令人警覺的那個。**在一折時,ρ = 0.03。**這不是一個微弱的相關性;這根本沒有相關性——兩百個配置在一折上的排序,在統計上和把它們隨機打亂是無法區分的。一個(如大多數預設設定那樣)配置為從 min_resource = 1 開始剪枝的逐次減半執行,在這種行情下,會在第一輪、也是最激進的一輪剪枝中,憑拋硬幣來決定去留。那些最終會在完整保真度下獲勝的配置,在最初這一輪中存活下來的機率只有五五開。等你到達排序真正有意義的保真度(三折時 ρ = 0.78)時,其中大多數早已被淘汰。
多時間框架這一列的情況溫和一些,但從另一個角度說明了同一個道理。即便在那裡,一折也只給出 ρ = 0.43——低於我們設定的 0.5 關卡。它看起來像是一個還不錯的相關性,而這恰恰就是危險所在:0.43 高到足以讓你放鬆警惕,又低到足以洩漏掉你最好的配置。只有到兩折(ρ = 0.67)時,排序才變得可信。
從中可以歸納出兩點。第一,一折時的 ρ 依賴於具體行情,且不可靠——我們在不同配置上測出的值從 0.03 到 0.43 不等,且沒有一個案例中單獨一折能達標。第二,ρ 單調且快速地上升:到三折時,兩套配置都達到了 0.78,到五折時,二者都收斂到 0.91。訊號確實存在;你只需要投入足夠的保真度就能看見它。這道關卡的作用,就是找到"足夠"開始出現的那個確切檔位——並禁止在此以下進行剪枝。
為什麼單折會如此嘈雜
這種噪聲並不是我們折構造方式中的一個缺陷;它是內在固有的,理解這一點能讓你不會用錯誤的方式去"修復"它。單個 walk-forward 折是一個很短的視窗——只是某一種市場行情下的幾周時間。策略在這個視窗上的得分,主要由其參數恰好多大程度上貼合了那種行情所主導,而這和參數的泛化能力只有鬆散的關係。兩個質量真正不同的配置,可能僅僅因為其中一個恰好抓住了那一折恰好包含的某個趨勢,就在單折上互換名次。單折上的目標值,是你真正關心的那個目標值的一個高方差估計量,而排序相關性恰恰是高方差最先摧毀的東西——你可以讓一個估計量的均值保持不變,同時它的排序卻純屬噪聲。
增加折數會把這種方差平均掉。每一折都是對市場狀況的一次部分獨立的抽樣;r折均值是一個方差更低的估計量,其排序也會向完整保真度的排序收斂。這正是 ρ = 0.03 → 0.43 → 0.78 → 0.91 這一路攀升的原因:不是目標值本身在變化,而是隨著行情特有的運氣被平均掉,其排序估計逐漸趨於穩定。由此得到的教訓是,對於回測而言,保真度從根本上關乎你取樣到了多少個獨立的行情——而單獨一個行情幾乎從來都不足以用來排序。
這也解釋了為什麼"更困難的行情"從 0.03 起步,而多時間框架執行則從 0.43 起步。在更困難的行情中,單折更具行情特異性——各配置在單折上的得分更多是由運氣驅動,而較少由持久的優勢驅動,因此它們的排序更接近隨機。這道關卡會自動讀出這種差異,並以要求更多折數才允許剪枝作為回應。你不必事先知道自己身處哪種行情——去測量就好。
程式碼中的關卡:自動提升最低保真度
這道關卡的輸出是一個單一整數:min_resource,即 ASHA 被允許開始剪枝的最淺保真度。規則是機械式的——遍歷各個檔位,取第一個 ρ 越過閾值的檔位:
RHO_GATE = 0.5
min_res = len(FOLDS) # default: pruning OFF (full fidelity)
rhos = fidelity_check(cache, n_probe=200) # [ρ@1, ρ@2, …, ρ@(K-1)]
passing = [r for r, rho in enumerate(rhos, 1) if rho >= RHO_GATE]
if passing:
min_res = passing[0] # first rung that clears the gate
pruner = SuccessiveHalvingPruner(min_resource=min_res, reduction_factor=3)
讓我們把這兩次執行代入其中追蹤一遍。在多時間框架配置中,ρ@1 = 0.43 未能通過關卡,但 ρ@2 = 0.67 通過了,因此 min_resource 被自動提升為 2:ASHA 會讓每個配置至少跑完兩折之後,才被允許淘汰任何配置,此後再照常剪枝。在更困難的行情中,ρ@1 = 0.03 和 ρ@2 = 0.43 都未通過;ρ@3 = 0.78 是第一個通過的,因此 min_resource 變為 3。還有一個至關重要的兜底機制:如果沒有任何檔位達到 0.5,passing 就是空的,min_resource 就會保持在 K——剪枝被徹底關閉,搜尋會優雅地退化為一次純粹的完整保真度搜索,而不是一次快速卻錯誤的搜尋。一次無法證明自己代理有效的下鑽搜尋,就乾脆拒絕剪枝。
這一整套理念,濃縮在一行控制流裡就說完了。每個逐次減半庫的預設做法都是"從 min_resource = 1 開始剪枝,信任那個廉價檔位"。這道關卡把它替換成了"從資料證明可信的第一個檔位開始剪枝,如果沒有一個檔位可信,就不剪枝"。它的代價是預先做一次約200個配置的探測,而它把下鑽搜尋從一種信仰行為,轉變為一個經過測量的決策。淘汰率 η = 3 保持不變;這道關卡不會觸碰你剪枝的力度,只觸碰你被允許多早開始剪枝。
上面程式碼裡有一條誠實的註腳:提高 min_resource 會侵蝕掉一部分加速效果。以3折而不是1折開始剪枝,意味著每一個配置——包括那些最終會被淘汰的——都要付出三折的計算代價。這就是正確性的代價,而這是一筆劃算的交易:一個能找到真正勝者、只有2倍加速的搜尋,勝過一個把勝者都淘汰掉、有6倍加速的搜尋。這道關卡把這筆交易明明白白地擺出來,而不是把它藏起來。
錯誤的廉價軸:縮短歷史長度是一個陷阱
有一種廉價保真度既誘人又顯而易見,但卻是有偏的,它值得被點名指出,因為每個人都會第一個想到它:**縮短歷史長度。**在最近一個月的資料上篩選配置,把存活者提升到完整兩年的資料上。這種做法便宜得不費吹灰之力,感覺上和減少折數是同一個想法。但其實並不是。
減少折數和縮短歷史長度之間,存在一個決定性的差異。一個折數保真度,如果構造得當,依然橫跨整個日曆——它只是取樣得更粗略而已。而一個縮短歷史長度的保真度,是對日曆的某個子區間進行密集取樣。而市場歷史的一個子區間,就是一種特定的行情。當你在最近一個月的資料上給配置排序時,你得到的並不是它們全週期排序的一個有噪聲、但無偏的估計;你得到的是一個有偏的估計,它系統性地偏袒那些為最近一個月的行情調好參的配置。無論你把樣本量提到多大——在更多配置、更多試驗上取平均——偏差都不會縮小,因為它根本不是方差問題。你會提拔那些最貼合篩選視窗的配置,而這些配置恰恰最可能是對短暫行情的過擬合。
這就是為什麼我們的保真度是按交錯順序、而不是按時間順序遍歷各折的。當K折分佈在整段歷史上時,"1折"是靠近中段的單個切片,"2折"是一個早期切片加一個晚期切片,"3折"則鋪開早期/中期/晚期——每一個保真度級別,即便是最廉價的那個,也橫跨整個日曆取樣。廉價代理是對整段週期的一種粗略檢視,而絕不是對其中某一段切片的清晰檢視。正是這種交錯,使得折數保真度只是有噪聲的(這道關卡可以治癒),而不是有偏的(任何關卡都無法修復這一點——針對一個有偏目標測出高 ρ,只能證明你會穩定地選中那個貼合特定行情的配置)。**如果你讓一個保真度變廉價的唯一辦法,是讓它變得更接近當下,那你手裡根本沒有一個有效的保真度。**應該讓它變得更粗略,而不是更近期。
解析度——誠實的廉價軸
另一條廉價的軸則完全避開了這種偏差,它是我們在成交撮合模擬中所描述的自適應下鑽的天然搭檔:**K線解析度。**在1小時K線上對整個空間進行篩選,把存活者提升到1分鐘級別,只對少數幾個最終入圍者下鑽到1秒級別或原始成交資料。在相同的完整歷史上使用更粗的K線,評估成本更低——K線數量更少、指標計算更快、模擬更快——而且,和縮短視窗不同,對整個日曆的粗略檢視是無偏的:它能看到每一種行情,只是K線內部的細節更少。
解析度和折數是兩條互補的保真度軸,而保真度關卡對兩者都適用。在信任1小時篩選之前,運行同樣的探測:取約200個配置,分別在1小時和1分鐘級別上打分,測量排序相關性。如果 ρ(1小時, 1分鐘) 很高,那麼按小時篩選是安全的,你誠實地換來了一次可觀的加速。如果它很低——這種情況發生在策略的優勢存在於K線內部結構、而小時級K線把這些結構抹平的時候——那麼小時級篩選對這個策略而言就是一個隨機過濾器,關卡會告訴你需要從更精細的粒度開始。規則永遠不變:只有在你測量出某個保真度的排序與真值一致之後,你才被允許在該保真度上剪枝。
這兩條軸的失效方式也恰好相反,這一點很有用。粗略的解析度會丟失K線內部的資訊;較少的折數會丟失跨行情的資訊。一個日線級別的動量策略,可能在小時級K線上排序完美,卻需要很多折才能把行情運氣平均掉;一個剝頭皮策略,可能在很少的折數上排序就很好,但一旦解析度粗於1秒級別就會崩潰。按軸分別執行這道關卡,會告訴你對於這個策略,你在哪條軸上可以放心地節省成本——而不是假定一個恰好方便你的答案。
節省的算力究竟從何而來
有一條注意事項能讓下鑽搜尋保持誠實:**一個保真度只能在成本實際所在的地方節省算力。**在我們的多時間框架引擎中,昂貴的部分是預先計算指標——多時間框架的 HMA 和分離訊號——這部分成本是每個配置只支付一次,在任何折執行之前就已經付出。而基於快取訊號的逐折模擬則相對便宜。所以按折數剪枝,節省的只是模擬成本,而不是佔主導地位的指標計算成本;折數保真度是真實有效的,但它的上限要比原始的折數比例所暗示的要低。
相比之下,解析度軸直接衝著佔主導地位的成本下手:更粗的K線意味著計算指標所依據的K線數量更少,於是昂貴的預計算和廉價的模擬會一起縮小。這不是一個細枝末節——它決定了哪種下鑽值得去構建。在投入構建多保真度搜索之前,先問問你的時間都花在了哪裡。如果90%都是各折共享的指標預計算,那麼折數保真度帶來的收益很有限,而解析度保真度則能帶來巨大的收益。先做效能剖析;正確的廉價軸,是那條能真正去除你實際擁有的成本的軸,而且它仍然必須通過這道關卡。
與其他內容的聯絡
保真度關卡在本系列關於回測衛生的鏈條中,處於一個特定的位置:
- 它處於**過擬合控制的上游。**一個依據糟糕代理進行剪枝的下鑽搜尋,是一種新的過擬合方式——你是在讓一個充滿噪聲的早期檔位去挑選你的最終入圍者。冠軍仍然必須經受住縮減夏普比率和PBO的檢驗,而且輸入這些關卡的試驗計數,必須包含每一個被剪掉的試驗,而不僅僅是存活者——剪枝並不能讓一次試驗從多重檢驗的賬本中被免除。
- 它和平臺期分析有著共同的敵人:一個只在某一折或某一個篩選視窗中獲勝、別處都不行的配置,正是這兩種工具都存在的目的所在——拒絕這種貼合特定行情的產物。這道關卡拒絕在這樣的一折上進行選擇;平臺期分析則拒絕信任一個僅僅立足於這樣一折的冠軍。
- 它以底層誠實的walk-forward 劃分為前提——折橫跨整個日曆,樣本外部分被單獨留出——而它是我們為成交撮合模擬所構建的下鑽搜尋,在搜尋階段的對應物:同樣的由粗到細原則,只是應用於評估哪些配置,而不是應用於以多精確的方式去撮合它們。
- 而且它完全依賴於每一個檔位的保真度都不存在洩漏。如果廉價檔位存在昂貴檔位所沒有的前視偏差,那麼 ρ 衡量的就是與一個被汙染的目標之間的一致性,而這道關卡也就在為一次洩漏背書。是的,要測量排序相關性——但必須建立在一個誠實的目標之上。
再一次強調那個貫穿全文、本系列會不斷重複的核心理念:回測是一場統計實驗,而其中的每一個捷徑,都是一個你有義務去檢驗的假設。下鑽搜尋的假設是"廉價排序 ≈ 昂貴排序"。這個假設用約200個配置就能檢驗。去檢驗它。
要點總結
- **由粗到細的搜尋建立在一個假設之上:廉價代理對配置的排序方式和昂貴評估一樣。**不是數值相同——而是排序相同。如果排序相關性很低,激進的剪枝就會淘汰掉你未來的勝者,轉而提拔噪聲。你只是在更快地朝著一個更差的答案搜尋。
- **在信任它之前先測量它。**抽取約200個隨機配置,在每個保真度檔位上打分,計算每個廉價檔位與完整保真度排序之間的 Spearman ρ。這隻需要二十來行程式碼和一次低成本的探測。
- **單折的排序幾乎是隨機的。**根據行情不同,我們測得 ρ@1 從 0.03(相當於拋硬幣)到 0.43(仍低於可信線)不等;隨著折數累積,它會攀升到 0.67、0.78、0.82 和 0.91。對回測而言,每個開箱即用的 ASHA 所預設的
min_resource = 1,通常都是錯的。 - **將最低保真度自動提升到 ρ ≥ 0.5 的第一個檔位,如果沒有一個檔位達標,就乾脆完全不剪枝。**這道關卡在一套配置中變成了
min_resource = 2,在另一套更困難的配置中變成了3;兜底機制會優雅地退化為完整保真度搜索。正確性是要付出一些加速代價的——付就是了。 - **選擇廉價軸要看它是否有偏,而不是看它是否便宜。**縮短歷史長度是有偏的——它會選出貼合特定行情的參數,而任何樣本量都無法修復這一點。應該使用橫跨整個日曆的更少折數(交錯,而不是連續),或者在完整週期上使用更粗的解析度。並且,要把廉價軸用在你實際成本所在的地方。
下鑽搜尋是回測中最好的加速手段之一,也是最容易變成一種更快自欺方式的手段之一。這兩者之間的差別,就是一個你可以在搜尋開始之前測出來的數字。如果你的代理無法證明它的排序方式和真值一致,那它就不是一個代理——它只是一個帶著看似合理成本的隨機數生成器。
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.