跳轉到

回測:技術指標與參數尋優

所屬章節:回測

self.Ind — 技術指標工具箱

self.Ind 是 Core 的類別屬性(所有策略共用同一份),採增量計算: 每根新 K 棒呼叫一次,回傳目前累積的完整序列(list),取 [-1] 就是最新值。

同一條序列若有多種用途,用 name= 參數區分狀態快取; 不指定時以 window 加資料物件 id 自動區分。

函式 簽名 備註
rolling_mean (ls, window, min_period=None, name=None) 移動平均
rolling_max (ls, window, min_period=None, name=None) 移動最大
rolling_min (ls, window, min_period=None, name=None) 移動最小
rolling_std (ls, window, k=1, min_period=None, name=None) ta-lib 相容 STDDEV:母體標準差(ddof=0)× k
ema (ls, window, alpha=None, name=None) alpha=None → α=2/(N+1) 標準 EMA;alpha=1/N → Wilder 平滑(ATR/RSI 用)
macd (ls, short_window=12, long_window=26, macd_window=9, name=None) 回傳 macd_dif, macd_ema, macd_hist
atr (high, low, close, window=14, name=None) ta-lib 相容,Wilder 平滑
rsi (ls, window=14, name=None) ta-lib 相容
stochastic (high, low, close, rsv_window=9, k_window=3, d_window=3, name=None) KD,與 pandas ewm(adjust=False) 對齊
rolling_sum (ls, window, min_period=None, name=None) 移動加總
skew / kurt (ls, window, name=None) 移動偏度/峰度
zscore / minmax (ls, window, eps=1e-9, name=None) 移動標準化 /區間正規化
median / tsrank (ls, window, name=None) 移動中位數/時序排名
argmax / argmin (ls, window, norm=True, name=None) 窗口內極值的位置
decay / slope / delta (ls, window, name=None) 線性衰減加權/斜率/差分
corr / cov (a, b, window, name=None) 兩條序列的移動相關/共變異
katz / shannon (ls, window[, bins=8], name=None) 碎形維度/熵
def my_indicators(self, row):
    self.close_list.append(row['Close'])
    ma5  = self.Ind.rolling_mean(self.close_list, 5)
    ma20 = self.Ind.rolling_mean(self.close_list, 20)
    self.char['ma5']  = ma5[-1]      # 存進 self.char,show_monitor() 與 dashboard 的 K 線就會畫
    self.char['ma20'] = ma20[-1]
    return ma5[-1], ma20[-1]

兩個容易踩的地方

每根 bar 都要呼叫一次。 增量計算的前提是「每來一根就餵一次」, 包在 if 裡條件性呼叫會讓內部狀態跟資料脫節。

同一根 K 棒呼叫兩次會前進兩次。 例如逐欄位算特徵時三個欄位都要 ema(c, 15),就推進了三次,窗口全部錯位(實測 MACD 因此反號)。用當根根數當快取鍵,保證一根只算一次。

多個 core 共用 Ind。 Ind 是類別屬性,同一個 kernel 跑多個 core 時,給相同 name 會互相污染。在 on_init 寫 self.Ind = type(self.Ind)() 讓每個實例各用一份。

同一條序列不同用途要給 name。 例如同一份 close_list 同時要 5 日與 5 日 (但不同 min_period)的平均,不給 name 會共用同一份快取狀態。


bt.Tools.optimize — 參數尋優

bt.Tools.optimize(my_strategy, search_range, times=50, random_ratio=0.5)
參數 說明 預設
my_strategy 自訂策略函式,需回傳有 report 屬性的物件(即 generate_report 的結果) 必填
search_range 參數範圍字典,格式 {'名稱': [最小, 最大, 步長]} 必填
times 優化總次數 50
random_ratio 前面隨機探索的比例,之後用智能(TPE)尋優 0.5

回傳最佳參數 dict(例 {'percent_in': 2.3, 'percent_out': 4})。評分是 rp.report 的 CalmarRatio(CAGR / abs(MddRatio),NaN 當 0)。全部結果另存目前資料夾的 optimize.xlsx(CalmarRatio 在最前面),跑完會自動開檔。

def opt(percent_in=2, percent_out=3):
    core = MyStrategy()
    core.percent_in  = percent_in       # 覆寫 on_init 設的 self.xxx
    core.percent_out = percent_out
    core.feed_data(df)
    return bt.generate_report({cfg: core})

arg = {'percent_in': [1, 5, 0.1], 'percent_out': [1, 10, 0.1]}
best = bt.Tools.optimize(opt, arg, times=50)

rp_best = opt(**best)                   # 用最佳參數重跑一次
print(rp_best.report)

尋優要注意的事

參數必須是 on_init 裡設的 self.xxx,optimize 才能從外面覆寫。 寫死在 on_bar 裡的常數改不到。

每次呼叫都要建新的 core。 core 是有狀態的,重複用同一個會累積前一次的部位與統計。

評分是 CalmarRatio(CAGR / abs(MddRatio))。 這個目標偏好高報酬低回撤,但完全不看交易次數—— 可能挑出只交易 3 次剛好都對的參數。自己看 rp.report 的 TotalTrades 檢查。

參數型別會變。 取樣值剛好是整數時傳進來是 int(1.0 會變 1),否則是 float。 window 這類參數在函式開頭自己 int(x);字串參數用索引搜尋('metric_i': [0, len(METRICS)-1, 1]); 要固定某個參數不搜,用 functools.partial(run, metric='YoY')。

成功: 0組 接著 UnboundLocalError: best_params 是症狀不是原因:每一組都拋了例外、被吞掉沒印。 自己呼叫一次 run(**某組參數) 真正的錯誤就會出現(最常見:參數名收不到、回傳的不是 rp)。

連續跑兩輪先關 Excel。 上一輪自動開啟的 optimize.xlsx 會鎖住檔案,下一輪整輪跑完才在寫檔時 PermissionError。

尋優本身就是過度配適的來源。 times=50 在兩個參數上已經掃過相當比例的空間, 挑出的最佳值在樣本外不見得成立。至少把資料切成前後兩段分別驗。


相關頁