雙軸參數空間:為什麼你的大部分參數掃描幾乎是免費的
選自"沒有幻覺的回測"系列。
維度災難通常被當作一句警告來講述:你每增加一個參數,都會成倍放大搜索空間,所以一個 18 維的策略是無望掃描的。這種敘述方式悄悄地假定了每一個維度的評估成本都相同。事實並非如此。在我們的雙/三時間框架引擎中,三分之二的參數掃描起來幾乎是免費的,而三分之一的參數幾乎承擔了全部的計算開銷。一旦你看清了這種分裂,"搜尋空間太大"就不再是恰當的抱怨。恰當的問題是:你到底在為哪些維度付費?
那個本不需要重新計算的維度
我們的參數搜尋基準執行的是一個多時間框架動量策略:在兩個或三個時間框架上使用 Hull 移動平均線及其三重平滑變體(HMA3),並配以一個方向分離門控,由它決定一次交叉何時"乾淨"到足以據此行動。三時間框架變體的完整搜尋空間是十八維——每個時間框架的週期和 HMA 長度,再加上每個時間框架上用於入場和出場的一組分離閾值。
掃描這個空間的樸素做法是單層迴圈:選一個參數向量,從頭構建每個指標,執行模擬,打分,重複。我們就是從這裡起步的。它很慢,而且它慢的原因一旦被我們說出口就顯得很尷尬:在絕大多數相鄰試驗之間,我們都在重新計算那些根本沒有變化的指標。
把入場分離閾值從 0.03 微調到 0.035 再重新執行。一整年 1 分鐘 K 線上的 Hull MA 與上一次試驗逐位相同——閾值根本不出現在它的定義中的任何地方。然而樸素迴圈還是照樣把它重建一遍,跨越五十多萬根 K 線,深入三個時間框架,每一次都如此。我們幾乎把全部的計算預算都花在了重新推導那些對我們實際正在改變的參數保持不變的量上。
這個觀察就是整篇文章的核心。一旦你注意到有些參數會改變指標,而有些參數只改變應用在固定指標上的決策規則,參數空間就不再是一團扁平的維度雲,而變成了兩層巢狀、價籤迥異的結構。
是兩條軸,而非一條

按照重新評估某個參數會迫使你重新計算什麼來劃分參數:
-
昂貴軸——指標參數。 時間框架週期和 HMA 長度。改變其中任何一個,你都必須在整個價格序列上重建指標:對更高的時間框架重取樣,執行 Hull 加權,重新計算交叉序列以及每次交叉處的分離度。這是一次針對每一根歷史 K 線的 O(n) 掃描,而且對每一個不同的組合你都要完整地付出這份代價。在三時間框架策略中,這條軸是六個參數:高、中、低三個時間框架各自的
(period, hma_length)。 -
廉價軸——決策閾值。 方向分離門控,它基於已經計算好的交叉序列和分離序列,來決定入場還是出場。改變一個閾值,任何指標都不會動。你只需對預計算訊號重新執行一次 O(n) 遍歷,檢查門控並記賬成交。在三時間框架策略中,這條軸是十二個參數(每個時間框架上有四個分離閾值——入場買、入場賣、出場買、出場賣,共三個時間框架)。雙時間框架變體則有八個(兩個時間框架各四個閾值)。
所以這個空間真正的形狀是:三時間框架為 6 個昂貴 + 12 個廉價 = 18,雙時間框架為 4 個昂貴 + 8 個廉價 = 12。三分之二的維度都住在廉價軸上。而且這兩條軸不只是數量不同——它們的單位成本相差超過三個數量級,而這個數字正是本文餘下部分要講的東西。
這個結構是嚴格巢狀的。昂貴軸上的每一個點——每個時間框架一組具體的 (period, hma_length)——定義了一組固定的訊號陣列。在那個固定的地基之上,坐落著廉價軸的一整張薄片:成千上萬個閾值向量,每一個都是對同一批陣列的一次快速遍歷。你只為地基付一次費,然後把它攤銷到整張薄片上。
為什麼指標對閾值不變
這套快取之所以奏效,靠的是一個數學事實,而非僥倖的實現細節,而且它值得被精確地陳述,因為它是那個承重的假設。指標陣列僅僅是 (period, hma_length) 的函數。分離閾值根本不出現在它們的定義中。
具體來說,對每個時間框架,引擎會在基準索引上預計算四個對齊的陣列:Hull MA hma、其三重平滑值 hma3、交叉序列 cross(+1 買 / −1 賣 / 0),以及每次交叉處的 separation 百分比。這些陣列中的每一個都是由價格和兩個指標參數推導而來。閾值——那個你拿來與 separation 比較的數字——是稍後在決策時才應用的。它從不觸碰這些陣列。
這並不是一次天然安全的重排。它之所以安全,只是因為我們對資訊何時變得可用這件事非常小心,而這正是本系列別處那篇前視偏差研究所關注的問題。基準 K 線 i 上的更高時間框架指標,是由已收盤的更高 TF K 線,加上一根正在形成的 K 線計算而來的,這根 K 線的即時收盤價等於當前基準收盤價 close[i]——一個在第 i 根 K 線時已知的值。沒有任何來自未來的東西洩露進來。所以第 i 根 K 線處的預計算訊號,恰恰就是一個實盤機器人在第 i 根 K 線時會看到的東西,而且無論你稍後用哪個閾值去檢驗它,它都保持有效。快取一個洩露的訊號只會把洩露一併快取下來;快取一個因果的訊號,快取的則是你真正可以拿去交易的東西。
這種不變性給了我們一個因式分解。把對一個完整參數向量 θ = (indicator_params, threshold_params) 的評估寫成:
signals = build_indicators(indicator_params) # EXPENSIVE, depends only on indicator_params
score = simulate(signals, threshold_params) # CHEAP, reuses signals across all thresholds
build_indicators 不讀取 threshold_params。就是這一個事實,授權了快取:固定住 indicator_params,自由地變動 threshold_params,而 signals 是一個你只需計算一次的常量。
架構:計算一次,掃描多次

這個引擎(scripts/engine_multitf.py,我們回測器中的提交 bfc8aaa)用兩個部件實現了這一因式分解,而這兩個部件的名字恰好說明了它們的作用。
SignalCache——被記憶化的昂貴軸。 它是一個以 (period_bars, hma_length) 為鍵的字典。向它索取某個時間框架的訊號,如果那個指標組合此前已經構建過,它就返回快取的 TFSignals,否則就構建一次並存起來。因為鍵只是指標參數,每一個共享同一指標組合的閾值配置——而在一次密集的閾值掃描裡,那是成千上萬個——都會命中同一個快取條目。尤其是更高的時間框架,對此回報豐厚:比如說,四個候選週期乘以少數幾個 HMA 長度組成的粗網格,只是一個很小數量的不同的昂貴構建,而每一個都被堆疊在它之上的整張閾值薄片所複用。
sweep_separations——被批處理的廉價軸。 它接收快取的訊號陣列和一個閾值向量矩陣(sps,形狀為 [m, 12]),並把它們全部送進一個已編譯的核心裡執行。每一行都是一次 O(n) 遍歷:走遍所有 K 線,應用門控,在 open[i+1] 處記賬無洩露的成交,累計盈虧和持倉時間。這個迴圈內部不重建任何指標——它直接從快取中讀取 cross 和 separation。內層模擬經過 JIT 編譯(Numba),所以一旦預熱完成,每個配置的成本就由對 K 線的那一次線性掃描主導,而不是由 Python 開銷主導。
這兩個部件組合成一個自然的巢狀搜尋:外層迴圈走遍昂貴軸(每次迭代構建並快取一個指標組合),而內層迴圈在那些快取陣列上扇出一個跨越廉價軸的寬批次。在程式碼中,其形狀恰好如此——一個很小的昂貴迴圈包裹著一個很寬的廉價批次:
cache = SignalCache(base_close, base_ts) # keyed by (period, hma_length)
for htf_p, htf_h, mtf_p, mtf_h, ltf_p, ltf_h in indicator_grid: # EXPENSIVE axis (coarse)
htf = cache.get(htf_p, htf_h) # built once, then a cache hit forever
mtf = cache.get(mtf_p, mtf_h)
ltf = cache.get(ltf_p, ltf_h)
sps = sample_thresholds(m=4000) # CHEAP axis: [m, 12] threshold vectors
pnl, n_trades, bars_in_pos = sweep_separations( # one compiled batch, no indicator work
base_close, base_open, htf, ltf, sps, mtf=mtf)
你只在指標網格所允許的次數內觸碰那個昂貴的構建器,而讓廉價掃描去承擔體量——成千上萬行 sps,對著永不移動的陣列。這就是全部的最佳化——沒有近似,沒有損失保真度,只是拒絕重新計算那些沒有變化的東西。
"免費"實際上要花多少錢:數字

我們在演示工作負載上測量了這兩條軸:一整年的 1 分鐘 ETHUSDT K 線(約 527k 根),三時間框架,指標已預熱且 JIT 編譯已排除在計時之外。
在廉價軸上,sweep_separations 穩定維持在每秒約 5,600 個閾值配置。這是對每個配置的一次完整模擬——門控、成交、盈虧、敞口——跨越五十多萬根 K 線,大約每個配置 180 微秒。它之所以能這麼快,是因為它做零指標工作:每個配置都讀取同一批快取的 cross 和 separation 陣列。
現在給替代方案定個價。構建一次三時間框架的指標集大約需要幾百毫秒的量級(約 0.3 秒)——對三個時間框架重取樣、Hull 加權、在整整一年上提取交叉和分離度。如果你在配置迴圈內部重新計算指標——也就是樸素的單層迴圈設計——那麼每秒那 5,600 個配置中的每一個,都會轉而付出完整的指標構建代價。每個配置的成本從約 180 微秒膨脹到約 0.3 秒:
| 每個配置的成本 | 配置/秒 | |
|---|---|---|
| 廉價軸(快取訊號) | 約 180 µs | 約 5,600 |
| 每個配置重新計算指標 | 約 0.3 s | 約 3.4 |
這個比值是約 1,600 倍。在快取訊號上掃描廉價軸,比那種為每個閾值向量都重建指標的樸素設計,大約便宜三個數量級。說得具體些:一批幾千個閾值配置,在快取路徑上不到一秒就能跑完,而如果每個配置都要重建它的指標,則要花掉將近一個小時。相同的結果,相同的保真度,數學上沒有任何捷徑——唯一的區別在於,一個重新計算了一個不變數,而另一個沒有。
這不是你在最後隨手撒上去的一點微最佳化。它改變了哪些搜尋是可行的。在 5,600 cfg/s 的速度下,閾值軸足夠密集,可以被恰當地探索——你負擔得起為每個指標組合做一個精細網格或一次長時間的隨機/QMC 取樣——而昂貴軸則刻意保持為一小撮構建。計算預算流向了參數真正有成本的地方。
維度災難,重新定價

回到本文開篇的那個敘述框架。維度災難說的是,搜尋空間隨參數數量呈指數增長,所以維度越多就絕對越糟。這一點在網格的規模上是對的。但它在覆蓋網格的成本上具有誤導性,因為它把每個維度都定了相同的價。
一旦你把這兩條軸分開,這個數字讀起來就不一樣了。三時間框架策略是 18 維的,但其中只有 6 個維度是昂貴的。另外 12 個是廉價軸維度,它們擴張了網格,卻沒有以任何有意義的方式擴張計算開銷——你可以用幾個小錢就往上面砸成千上萬個配置。雙時間框架策略是 12 維的,只有 4 個昂貴維度和 8 個廉價維度。在這兩種情況下,"災難"的大部分都集中在那條几乎不花成本就能掃描的軸上。
所以,誠實地推理搜尋成本,靠的不是"有多少個參數",而是"有多少個昂貴參數,以及它們的網格能有多粗"。昂貴軸才是指數真正傷人的地方,也是你想要一個小而精心挑選的網格的地方——幾個候選週期,一段適度的 HMA 長度範圍——可能還會從粗到細地細化,秉承我們在別處使用的自適應解析度逐層下鑽的精神。廉價軸則是你可以揮霍的地方,因為每增加一個閾值配置只需 180 微秒。
這次重新定價的適用範圍遠遠超出我們的 HMA 策略。這個模式——有些參數改變特徵,大多數參數改變應用在固定特徵上的規則——在系統化交易中處處復現。指標長度、重取樣頻率和回看視窗是昂貴的;入場/出場閾值、止損距離、倉位規模乘數以及確認門控是廉價的。任何時候,只要一個參數只是在已計算好的訊號之上重塑決策邊界,它就屬於廉價軸,而且就應該在那裡被掃描。來自同一族的相關快取收益也出現在多時間框架 parquet 快取以及更宏觀的回測引擎速度階梯中。
免費午餐的賬單在哪裡
廉價軸在計算上是廉價的。它在統計上並不廉價,而把這兩者混為一談,正是你如何把一次效能勝利變成一臺過擬合機器的方式。
你評估的每一個閾值配置都是一次試驗,而當你在一個數據集上執行成千上萬次試驗時,其中最好的那個之所以看起來不錯,有一部分是靠運氣。把這些試驗變得便宜 1,600 倍並不會讓運氣消失——它只會讓運氣更容易被累積起來。密集的閾值掃描恰恰是多重檢驗膨脹咬得最狠的情形:候選者眾多,歷史卻只有一段,而選擇規則報告的是最大值。世界上最快的引擎也會樂呵呵地遞給你一個閾值向量,它把你測試窗口裡的噪聲擬合得漂漂亮亮,卻在樣本外敗下陣來。
所以紀律必須隨速度一起擴張。一旦一次試驗的計算成本降向零,那麼統計核算就成了那個約束性的約束,而你必須明確地為它付賬:
- 為試驗次數做貶值(deflate)。 用你嘗試過多少個配置來給贏家打分,而不是拿零來打分。縮減夏普比率(Deflated Sharpe Ratio)和回測過擬合機率(PBO)的存在正是為了這個——它們把"我們嘗試了 4,000 個閾值向量"變成對所報告優勢的一次折扣。
- 逐折(per fold)做樣本外驗證。 一次廉價的掃描仍然必須在一次誠實的前向最佳化(walk-forward)劃分內部執行;一個只在樣本內取勝的閾值毫無價值,無論你找到它的速度有多快。我們的引擎讓成交保持無洩露(
open[i+1]),正是為了讓廉價軸無法靠偷看來買到效能。 - 偏好高原而非尖峰。 因為閾值軸既密集又快速,你可以繪製它的響應曲面,而不只是它的 argmax。一大片全都奏效的閾值區域是一個真實的優勢;一個孤零零的尖銳最大值則是一個擬合出來的假象——高原與尖峰之分,正是被我們所描述的這份速度變得負擔得起。
用正確的方式來理解這個雙軸結構:它並不為你買來更多的信心,它為你買來的是在每次試驗相同的信心成本下做更多次試驗。這確實很有價值——對廉價軸的密集覆蓋,正是讓你能夠找到高原並刻畫曲面的東西——但前提是你要讓統計賬本保持誠實。速度消除了不去徹底搜尋的計算藉口;它並沒有消除對徹底搜尋所得結果打折扣的義務。
如何構建你自己的搜尋
要把這套方法應用到你自己的策略上,工作主要是分類——把你的參數分揀到正確的軸上——隨後是一個巢狀迴圈:
- 按每個參數迫使你重新計算什麼來給它貼標籤。 如果改變它會改變某個指標/特徵陣列,它就是昂貴的。如果它只改變應用在固定陣列上的某個比較、閾值或規模規則,它就是廉價的。拿不準的時候,問一句:這個參數是否出現在指標定義內部的任何地方?如果沒有,它就是廉價的。
- 只用昂貴軸給快取做鍵。 用指標參數來記憶化特徵構建(就像
SignalCache用(period, hma_length)那樣)。共享同一指標組合的相鄰試驗,便可免費複用同一批陣列。 - 在快取特徵上批次處理廉價軸。 把閾值配置作為一個對預計算訊號的緊湊已編譯迴圈來執行,而不是作為完整的重新評估。這就是你的吞吐量——在我們的例子裡是約 5,600 cfg/s——的來源。
- 巢狀迴圈:昂貴在外,廉價在內。 讓昂貴網格保持又小又刻意(粗,或從粗到細);讓廉價掃描保持密集。把預算花在參數有成本的地方。
- 用過擬合來給試驗計數做預算,而不是用時鐘。 既然時鐘不再是極限,就讓縮減夏普 / PBO 來當極限。決定你在統計上負擔得起多少次廉價軸試驗,並把贏家拿到樣本外去驗證。
工程上的回報和統計上的護欄是同一個想法的兩面:把兩條軸分開,讓你能夠窮盡地搜尋廉價維度,而這恰恰是你隨後必須為你搜索得有多窮盡而打折扣的原因。
要點
- 並非所有維度成本相同。 一個策略的參數分裂為一條昂貴軸(指標——在整個序列上重新計算)和一條廉價軸(閾值——對預計算訊號做一次 O(n) 遍歷)。在我們的引擎裡,三時間框架是 6 昂貴 + 12 廉價,雙時間框架是 4 + 8。
- 指標對閾值不變,而這種不變性就是整個最佳化。 特徵構建只依賴於指標參數,所以你構建一次,以
(period, hma_length)為鍵快取,然後在每一個共享該組合的閾值配置之間複用。 - 廉價軸執行起來便宜約 1,600 倍。 在快取訊號上是每秒約 5,600 個閾值配置(每個約 180 µs),而如果你每次都重建指標則是每個配置約 0.3 秒。相同的保真度——唯一的區別是拒絕重新計算一個不變數。
- 維度災難其實是一場昂貴維度的災難。 大部分參數數量都住在那條几乎免費就能掃描的軸上。讓昂貴網格保持粗糙;在廉價的那條上盡情揮霍。
- 速度把約束性約束從計算轉移到了統計。 一次密集而快速的掃描就是一臺多重檢驗機器。為試驗次數做貶值,逐折驗證,並偏好高原而非尖峰——免費午餐是真的,但那份統計賬單並非可選。
完整的引擎——SignalCache、sweep_separations、無洩露的多 TF 模擬,以及把它釘死在實盤即時 K 線語義上的對拍測試——都在我們回測器的 scripts/engine_multitf.py(提交 bfc8aaa)中。下次有人告訴你一個 18 參數的策略太大而無法搜尋時,問問他們這些參數裡有多少個真正會動到指標。通常只有三分之一,而其餘的幾乎都是免費的。
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.