跳至主要內容

252 支策略都沒抓到的 bug

約 9 分鐘閱讀

到六月底,對齊語料庫已從 167 支參考策略擴充到 252 支,卻始終沒測到三個 bug,它們最後在社群分享的腳本上現形:保證金不足卻沒被強制平倉的部位、不看匯率的購買力檢查,以及無聲無息、從未上膛的移動停損。

本文數據量測於 2026年6月29日,並保留當時的原貌。最新結果請參閱我們如何衡量一致性。

我們五月上一次撰文介紹語料庫時,對齊語料庫有 167 支參考策略,其中 165 支與 TradingView 達到逐筆交易的嚴格對齊。之後語料庫持續擴充:到六月底,已經有 252 支策略,其中 251 支達到「excellent」等級的對齊(99.6%),1 筆有記錄在案的異常,0 筆失敗。

頭條數字其實是最無聊的部分。真正有意思的,是語料庫沒測到的部分:三個我們原本沒有建模的 TradingView 券商行為,每一個都是因為某支社群分享的腳本,走到了那 252 支策略都沒走過的程式路徑,才被抓出來。本文就逐一拆解這三個案例。

語料庫在 2026-06-29 的狀態

corpus/validation_report.md (2026-06-29)
  Total probes:    252
  Excellent:       251  (99.6%)
  Anomaly:           1
  Strong/Moderate/Weak/Minimal/Missing: 0
 
  TV trades:      389,590
  Engine trades:  389,688
  Matched:        389,468

在整個語料庫中,引擎比 TradingView 多產生了 98 筆交易,相對於 389,590 筆參考交易,差距是 0.025%。這是加總後的數字;逐支策略來看,下面那筆異常只貢獻了其中一筆。其餘來自 23 支策略:那份報告的評分標準容許交易筆數差距低於 1%,所以它們仍被評為「excellent」。這 23 支大多只差一兩筆,其中四支就占了 98 筆裡的 90 筆。

當時最新的六支探針屬於 drawing 類別。我們推出了一套 runtime,讓 Pine 繪圖物件(line.get_price、label/box 的幾何資訊)能在回測時當作資料讀取,而不只是圖表上的標註,這六支探針就是在那時加進來的。如果策略把畫出來的線當作運算用的狀態——例如把手動錨定的趨勢線當作動態支撐位——引擎就必須像 TradingView 一樣,持續提供腳本已經刪除的物件的幾何資訊。這六支全部達到「excellent」等級。

那一筆異常,老實標出來

我們沒有把唯一對不上的那支策略濾掉。anomaly-equity-mirror-strategy-equity-01 剛好卡在保證金核准的 1 倍權益邊界上。我們當時的分析是:TradingView 的券商模擬器在這個邊界上是非單調的,它放行了某些超出權益的進場、又拒絕了某些低於權益的進場,而且每根 K 線的結果都不一致;語料庫的備註把這項分析的信心度評為 95% 以上。我們的引擎是確定性的:權益相同,核准結果就相同,每次都一樣。這符合 Pine 文件記載的保證金語意;而根據上述分析,TradingView 券商當時在那個邊界上並不是這樣做的。要對上它,就得自己引入非確定性,所以我們沒有這麼做。這個落差有記錄在案,沒有藏起來,而且是當時 252 支裡唯一的一筆。(此後,在維護者 2026-09-29 的基準結果中,這支探針已被評為「excellent」;語料庫的 README 也註明,當初的診斷仍在重新檢視中。)

Bug 一:換作 TV 早就強制平倉的部位

TradingView 的券商不會等你的策略邏輯來決定何時出場:部位一旦虧損到保證金不足,它就直接強制平倉。如果槓桿或放空部位的權益跌到所需保證金超過帳戶權益,TradingView 不管 strategy.exit() 的條件當下是否成立,都會在發生的那根 K 線上強制平倉:成交價取那根 K 線上對部位最不利的價格,平倉量是補足保證金所需數量的四倍(最多平掉整個部位)。

引擎原本沒有建模這個行為。它會繼續持有部位,讓策略自己的邏輯之後再去平倉——對一支停損設得不緊的策略來說,這可能代表 TradingView 早就強制平倉的部位,引擎還多扛了幾十根 K 線。在 24 支社群分享的腳本上,槓桿或放空部位一路抱到觸發保證金追繳,TradingView 的交易清單裡出現了「Margin call」出場,引擎卻一筆都沒產生。

修正方式是在帳戶權益跌破保證金要求時加入強制平倉,用和 TradingView 券商相同的方式計算強平價格,再把強平數量量化到該標的的 lot step——TradingView 不會平掉比標的 lot step 還小的零碎部位;只要回測時有提供 lot step,現在我們也不會。

對在永續合約上回測槓桿或放空策略的人來說,這個影響最大。如果你的權益曲線在回撤期間看起來「太平滑」,這或許就是原因。

Bug 二:不知道有 FX 這回事的購買力檢查

Pine 策略可以宣告一個和標的報價貨幣不同的帳戶貨幣,例如用 currency.INR 交易 USDT 交易對。在檢查你是否買得起一筆訂單之前,TradingView 的券商會先用當時的 FX 匯率,把訂單的名目價值換算成你的帳戶貨幣,再和同一貨幣計價的權益比較。

引擎的購買力檢查跳過了這一步換算。它直接拿 USDT 名目價值和 INR 計價的權益相比,彷彿 FX 匯率是 1:1——也就是說,一支宣告 currency.INR 的策略,看起來比實際情況「買得起」好幾十倍(一個 USDT 就值幾十盧比)。在受影響的腳本上,引擎產生的交易筆數大約是 TradingView 的兩倍:它放行了 TradingView 券商以買不起為由拒絕的進場。

修正方式是在購買力檢查執行之前,先對所需保證金套用帳戶貨幣的 FX 匯率,也就是 TradingView 在訂單流程中套用匯率的同一個環節;匯率由每次回測自行提供(引擎預設是 1:1)。如果你之前用非美元的帳戶貨幣,回測以美元或 USDT 報價的標的,悄悄灌水你交易筆數的,就是這個 bug。

Bug 三:從來沒上膛的移動停損

這是我們最希望使用者比我們先抓到的一個,因為它是無聲無息地失效。strategy.exit() 支援兩種指定移動停損的方式:trail_points,相對於進場價的距離;以及 trail_price,一個絕對的啟動價位。

// armed correctly today; trail_price-only calls used to never arm at all
strategy.exit("TS", from_entry = "Long", trail_price = close * 1.05, trail_offset = 10)

引擎的上膛條件只檢查了 trail_points。只用 trail_price 指定的移動出場,編譯不會出錯、執行不會出錯,卻從來沒有上膛——部位就這樣靜靜地掛著,整段回測期間都沒有任何移動停損在運作。沒有例外、沒有任何一行 log,也沒有任何交易。策略看起來有防護,其實沒有。

我們之所以抓到它,是因為有一支社群分享的腳本只用 trail_price 設定移動停損,結果只對上 TradingView 一半的交易;修正之後,吻合率來到 99.7%。開放語料庫裡沒有任何一支探針用到 trail_price,所以語料庫的數字根本看不出這個缺口。validation 類別正是為了這種缺口而存在,就像語料庫拆解那篇文章說的:收的是範圍狹窄的合成策略,不求賺錢、不求真實,只為了單獨測試某一個 Pine 功能,讓這類迴歸無法躲在「整體數字看起來沒問題」後面。

修正方式是只要 trail_points 或 trail_price 其中之一存在,就讓移動停損上膛。如果你曾經寫過使用 trail_price 的 Pine 策略,而權益曲線看起來疑似完全沒有移動停損——那是真的沒有:在第一個納入這項修正的引擎版本 v0.10.14(2026-07-02)之前,每一個版本都是如此。

為什麼這才是語料庫真正的意義

這三個 bug 沒有一個是靠 code review 找到的。它們都是因為某支形狀剛好對的策略跑過引擎,吐出一個和 TradingView 不一樣的數字,才被揪出來。這正是我們堅持把167 支、然後 252 支,而且還在增加的策略,當成一套預留、有版本控管的語料庫,而不是隨便幾支 smoke test 的核心理由,也是我們拿社群分享的腳本跑同一套 diff 的原因(這三個 bug 正是在那裡現形的):頭條數字再漂亮,底下也可能藏著會改變策略盈虧的 bug,而要抓到這種 bug,唯一的辦法就是不斷拿更多形狀的策略,對同一份 ground truth 做同樣的 diff。

我們會在發現缺口時持續加入新的探針,也會照實公開這些數字——不管是好的結果,還是那唯一一筆異常。

接下來可以做什麼