策略回測最怕測到快樂表,什麼是快樂表?簡而言之就是紙上談兵的表現嚇嚇叫,但實際操作時卻很難貼合回測,通常這類情況多是流動性風險問題,而另一種快樂表就是回測數據的解讀錯誤,像是看到超高年化報酬率就很嗨,卻沒注意到在勝率普通的情況下,高報酬是不是剛好靠幾檔交易重壓而生成?若真是如此,那運氣成分可能佔比很大。
本文會利用一些簡單的技巧去避免統計的陷阱,讓你注意到策略中因持股重壓導致的失真問題。
持股檔數設定
小資族的資金少,選股策略在初步開發完後第一個問題是選到檔數太多,以之前介紹過的創新高延續動能策略為例,可能某幾期選股在大牛市高達百檔,但實務上根本不可能買那麼多。
from finlab.backtest import sim
from finlab import data
with data.universe(market='TSE_OTC'):
close = data.get("price:收盤價")
# 近5日內有3日以上的股價創前200日新高
position = (close == close.rolling(200).max()).sustain(5,3)
report = sim(position, resample="2W", stop_loss=0.2, name="創新高延續動能策略", upload=False)
report.display()
此時我們可以針對小資族去客製化務實的條件,每期選股價前10低的標的來當持股組合。回測結果發現報酬率降低一些,但持股檔數上限控制在10檔以內,且低價的優先,讓小資族更能參與整張現股的操作,避免零股交易較高的滑價風險,讓回測更貼近現實交易貼合的可能性。
with data.universe(market='TSE_OTC'):
close = data.get("price:收盤價")
# 近5日內有3日以上的股價創前200日新高
position = (close == close.rolling(200).max()).sustain(5,3)
position *= close
position = position[position > 0].is_smallest(10)
report = sim(position, resample="2W", name="創新高延續動能策略", upload=False)
report.display()
單檔持股比例上限
以為這樣就沒問題了嗎?你若仔細看上面「低價股優先測定持股檔數上限」的策略,會發現每期的標的數量仍參差不定,牛市有時候10檔,熊市有時候1檔,由於 position 預設分配持股比例是用檔數做平均,因此若當期只選到一隻,而沒在 sim 回測函數 內特別設定 position_limit,那就是100%重壓一檔!如下圖示對整體策略波動會造成很大的風險:
如何解決此問題呢?sim 回測函數 內設定 position_limit 當單檔標的持股比例上限,控制倉位風險。預設為None,也就是不設上限。範例:0.1,代表單檔標的最多持有 10 % 部位,若加上持股檔數上限10檔,當只選到8檔時,總持股上限則為80%,剩下 20% 為現金,在選到檔數較少的時候,自然產生空手避險的效果,而不重壓標地。
position_limit 越高一般來說波動越大 ; position_limit 越低則波動低,報酬率與最大回撤普遍都會縮小,站在風控角度,一般建議都要設 position_limit < 0.2,避免個股非系統性風險影響整體策略。
「創新高延續動能策略」的 position_limit 要用多少比較好?我們可以用 ReportCollection 來檢測不同數值的回測效果,程式如下:
def run_strategy(position_limit):
with data.universe(market='TSE_OTC'):
close = data.get("price:收盤價")
# 近5日內有3日以上的股價創前200日新高
position = (close == close.rolling(200).max()).sustain(5,3)
report = sim(position, resample="2W", position_limit=position_limit, stop_loss=0.2, name="創新高延續動能策略", upload=False)
return report
# # 產生回測組合
reports = {ind/10: run_strategy(ind/10) for ind in range(1,11)}
# 放入回測報告組合比較器
collection = ReportCollection(reports)
# 產生回測數據分群柱狀圖
collection.plot_creturns().show()
import plotly.graph_objects as go
def show_index_bar(collection, item = 'max_drawdown'):
stats = collection.get_stats()
df = round(stats.loc[item],2).sort_values()
fig = go.Figure([go.Bar(x=df.index, y=df.values, text=df.values, textposition='auto',)])
fig.update_layout(title_text=item)
return fig
# 年化報酬
show_index_bar(collection, item = 'daily_mean').show()
show_index_bar(collection, item = 'max_drawdown').show()
show_index_bar(collection, item = 'daily_sharpe').show()
回測分析
可以發現 position_limit = 0.2 時 的年化報酬率、夏普率、MDD (最大回撤) 表現最好,能有效降低重壓波動風險。如果做出來發現 position_limit = 1 的數據最好,那就要留意快樂表的可能性,可能好績效靠是重壓導致的,這時最好考慮低 position_limit 的情況
結論
colab 範例檔
你的策略有驗證過持股檔數與單檔持股上限的問題嗎?
調整一個 position_limit 參數 就能降低快樂表和大波動的風險,可說是新手必學的參數設定啊!