回測:技術指標與參數尋優
所屬章節:回測
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 — 參數尋優
| 參數 | 說明 | 預設 |
|---|---|---|
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 在兩個參數上已經掃過相當比例的空間,
挑出的最佳值在樣本外不見得成立。至少把資料切成前後兩段分別驗。