finlab.backtest
回測引擎模組,用於評估策略在歷史資料上的表現。
使用情境
- 驗證策略邏輯: 確認策略的進出場邏輯是否符合預期,檢視實際交易記錄
- 評估風險報酬: 計算年化報酬、夏普率、最大回撤等關鍵績效指標
- 優化策略參數: 結合
finlab.optimize模組測試不同參數組合(調倉頻率、停損停利等) - 對比多個策略: 比較不同策略的績效表現,找出最適合的交易方法
快速範例
from finlab import data
from finlab.backtest import sim
# 載入資料
close = data.get('price:收盤價')
# 策略邏輯:價格突破 20 日均線
ma20 = close.average(20)
position = close > ma20
# 回測(每月調倉,單股上限 10%)
report = sim(
position,
resample='M', # 每月調倉
position_limit=0.1 # 單股權重上限 10%
)
# 顯示績效報告
report.display()
詳細教學
參考 歷史回測完整指南,了解:
- 完整回測參數說明(resample, position_limit, stop_loss 等)
- 績效指標詳細解讀(年化報酬、夏普率、勝率等)
- 進階回測技巧(多市場、自訂手續費、滑價設定)
- 回測報告的深度分析方法
API Reference
sim()
finlab.backtest.sim
sim(position, resample=None, resample_offset=None, trade_at_price='close', position_limit=1, fee_ratio=1.425 / 1000, tax_ratio=3 / 1000, name='未命名', stop_loss=None, take_profit=None, trail_stop=None, touched_exit=False, retain_cost_when_rebalance=False, stop_trading_next_period=True, live_performance_start=None, mae_mfe_window=0, mae_mfe_window_step=1, market=None, upload=True, fast_mode=False, notification_enable=False, line_access_token='')
Simulate the equity given the stock position history. 回測模擬股票部位所產生的淨值報酬率。
| PARAMETER | DESCRIPTION |
|---|---|
position
|
買賣訊號紀錄。True 為持有, False 為空手。 若選擇做空position,只要將 sim(position) 改成負的 sim(-position.astype(float))即可做空。
TYPE:
|
resample
|
交易週期。將 position 的訊號以週期性的方式論動股票,預設為每天換股。其他常用數值為 W、 M 、 Q (每週、每月、每季換股一次),也可以使用 W-Fri 在週五的時候產生新的股票清單,並且於下週交易日下單。
Note 'D'與'None'的差別? resample='D' 的意義為每天隨股價變化做再平衡,就算當天股票清單沒變,但股票漲跌後,部位大小會變化,而 resample='D' 會強制再平衡,平均分散風險。 但是當 resample=None 的話,假如清單不變,則不會強制再平衡,只有清單改變時,才做再平衡。適用情境在較常選到大波段標的的趨勢策略,較有機會將強勢股留下,而不會汰強留弱做再平衡。 另外
TYPE:
|
resample_offset
|
交易週期的時間位移,例如。
TYPE:
|
trade_at_price
|
選擇回測之還原股價以收盤價或開盤價計算,預設為'close'。可選'close'、'open'、'open_close_avg'、'high_low_avg'或 'price_avg'。
TYPE:
|
position_limit
|
maximum amount of investing a stock. 單檔標的持股比例上限,控制倉位風險。預設為None。範例:0.2,代表單檔標的最多持有 20 % 部位。
TYPE:
|
fee_ratio
|
fee ratio of buying or selling a stock. 交易手續費率,預設為台灣無打折手續費 0.001425。可視個人使用的券商優惠調整費率。
TYPE:
|
tax_ratio
|
tax ratio of selling a stock. 交易稅率,預設為台灣普通股一般交易交易稅率 0.003。若交易策略的標的皆為ETF,記得設成 0.001。
TYPE:
|
name
|
name of the strategy. 策略名稱,預設為 未指名。策略名稱。相同名稱之策略上傳會覆寫。命名規則:全英文或開頭中文,不接受開頭英文接中文。
TYPE:
|
stop_loss
|
停損基準,預設為None,不執行停損。範例:0.1,代表從再平衡開始,虧損 10% 時產生出場訊號。
TYPE:
|
take_profit
|
停利基準,預設為None,不執行停利。範例:0.1,代表從再平衡開始, 10% 時產生出場訊號。
TYPE:
|
trail_stop
|
移動停損停利基準,預設為None,不執行。範例:0.1,代表從最高點開始下跌,跌至 10% 時產生出場訊號。
TYPE:
|
touched_exit
|
是否在回測時,使用觸價停損停利?預設為 False。
TYPE:
|
retain_cost_when_rebalance
|
當持股再平衡時,如有股票繼續持有,是否保留原本進場的成本,當成停損停利的參考?預設為 False。
TYPE:
|
stop_trading_next_period
|
當期已經停損停利,則下一期不買入,預設為 True。
TYPE:
|
live_performance_start
|
策略建構的日期,例如
TYPE:
|
mae_mfe_window
|
計算mae_mfe於進場後於不同持有天數下的數據變化,主要應用為edge_ratio (優勢比率)計算。預設為0,則Report.display_mae_mfe_analysis(...)中的edge_ratio不會顯現。
TYPE:
|
mae_mfe_window_step
|
與mae_mfe_window參數做搭配,為時間間隔設定,預設為1。若mae_mfe_window設20,mae_mfe_window_step設定為2,相當於python的range(0,20,2),以2日為間距計算mae_mfe。
TYPE:
|
market
|
可選擇
TYPE:
|
upload
|
上傳策略,預設為True,上傳策略。 範例: False,不上傳,可用 finlab.backtest.sim(position, upload=False, ...).display() 快速檢視策略績效。
TYPE:
|
fast_mode
|
預設為False,若設定為True,則會使用快速模式,快速模式會忽略所有的停利停損設定,並且只有換股日進行報酬率模擬,因此會有一些誤差,當持有較多檔股票時,可以大幅加速回測速度。
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Report
|
回測數據報告 |
Examples:
Assume the history of portfolio is construct as follows: When market close on 2021-12-31, the portfolio {B: 0.2, C: 0.4} is calculated. When market close on 2022-03-31, the portfolio {A:1} is calculated.
| Stock 2330 | Stock 1101 | Stock 2454 | |
|---|---|---|---|
| 2021-12-31 | 0% | 20% | 40% |
| 2022-03-31 | 100% | 0% | 0% |
| 2022-06-30 | 100% | 0% | 0% |
With the portfolio, one could backtest the equity history as follows:
常用參數組合
月線策略(適合上班族):
週線策略(適合積極型):
report = sim(
position,
resample='W', # 每週調倉
position_limit=0.05, # 單股上限 5%(分散風險)
trade_at_price='open' # 以開盤價交易(更保守)
)
停損停利策略:
# 方法 1: 在 sim() 中設定
report = sim(
position.hold_until(stop_loss=0.1, stop_profit=0.2),
resample='M'
)
# 方法 2: 使用 hold_until() 更靈活
exit_condition = close < ma20 # 跌破均線出場
position_with_exit = position.hold_until(
exit=exit_condition,
stop_loss=0.1, # 虧損 10% 停損
stop_profit=0.2 # 獲利 20% 停利
)
report = sim(position_with_exit, resample='M')
常見錯誤與解決方法
1. 策略無任何交易記錄
# 問題:進場條件過嚴,position 全為 False
report = sim(position, resample='M')
trades = report.get_trades()
if len(trades) == 0:
print("⚠️ 警告:策略無任何交易記錄!")
# 檢查進場次數
print(f"平均每日進場股票數:{position.sum(axis=1).mean():.1f}")
print(f"最大進場股票數:{position.sum(axis=1).max()}")
# 解決:放寬條件或檢查資料範圍
2. KeyError: 日期不存在
# 問題:資料日期範圍不匹配
# 解決:使用 truncate_start 對齊起始日期
import finlab
finlab.truncate_start = '2020-01-01' # 只回測 2020 年後的資料
report = sim(position, resample='M')
3. 忘記設定 resample 導致每日調倉
line_notify()
finlab.backtest.line_notify
傳送回測結果之目前部位、近期換股訊息至Line聊天室。
| PARAMETER | DESCRIPTION |
|---|---|
report
|
回測完的結果報告。
TYPE:
|
line_access_token
|
於Line Notify取得的access_token(權杖)。至Line Notify登入Line帳號後,點選個人頁面,點選「發行權杖」,選擇欲接收訊息的聊天室(可選擇1對1接收Line Notify通知、或是選擇其他群組聊天室),即可取得權杖。
TYPE:
|
test
|
是否進行傳送訊息測試。
TYPE:
|
name
|
策略名稱,預設為空字串。
TYPE:
|
Examples:
欲進行測試,則設定test參數為True。
from finlab import backtest
line_access_token = 'xxxxxxxxxxxx'
backtest.line_notify(line_access_token=line_access_token, test=True)
若成功收到通知,則權杖設定已完畢,可直接在sim回測模組中開啟使用,或單獨調用此函式發送回測換股訊息。
於sim中使用:
from finlab import backtest
line_access_token = 'xxxxxxxxxxxx'
position = ...
report = backtest.sim(position, notification_enable =True, line_access_token = line_access_token)
已回測完,單獨傳訊息用:
LINE 通知設定
- 前往 LINE Notify 取得個人 token
- 設定環境變數:
- 發送通知:
常見問題
Q: 回測結果與實盤有落差怎麼辦?
常見原因與解決方法:
-
回測未考慮滑價:
-
成交量不足:
-
未來函數(資料洩露):
Q: 如何設定自訂手續費?
report = sim(
position,
resample='M',
fee_ratio=0.001425, # 買賣各 0.1425%(券商手續費)
tax_ratio=0.003 # 賣出證交稅 0.3%
)
Q: 如何限制最大持股數量?
# 方法 1: 使用 is_largest() 限制進場股票數
position = (close > ma20).is_largest(30) # 最多持有 30 檔
# 方法 2: 在 sim() 中限制
report = sim(
position,
resample='M',
position_limit=0.1, # 單股上限 10%
# 如果 position 中有 50 檔,實際最多持有 10 檔(100% / 10% = 10)
)
Q: 回測速度太慢怎麼辦?
# 1. 使用更長的調倉週期
report = sim(position, resample='Q') # 季度調倉(比月線快 3 倍)
# 2. 縮短回測期間
import finlab
finlab.truncate_start = '2018-01-01' # 只測試近 5 年
# 3. 減少股票數量
position = (close > ma20).is_largest(50) # 只測試 50 檔