finlab.backtest
Backtesting engine module for evaluating strategy performance on historical data.
Use Cases
- Validate strategy logic: Confirm that entry/exit logic works as expected, inspect actual trade records
- Evaluate risk/reward: Calculate annualized return, Sharpe ratio, maximum drawdown, and other key performance metrics
- Optimize strategy parameters: Combine with
finlab.optimizeto test different parameter combinations (rebalancing frequency, stop-loss/take-profit, etc.) - Compare multiple strategies: Compare performance across different strategies to find the best trading approach
Quick Examples
from finlab import data
from finlab.backtest import sim
# Load data
close = data.get('price:收盤價')
# Strategy logic: price breaks above 20-day moving average
ma20 = close.average(20)
position = close > ma20
# Backtest (monthly rebalancing, 10% max per stock)
report = sim(
position,
resample='M', # Monthly rebalancing
position_limit=0.1 # 10% max weight per stock
)
# Display performance report
report.display()
Detailed Guide
See Complete Backtesting Guide for:
- Full backtest parameter documentation (resample, position_limit, stop_loss, etc.)
- Detailed performance metric interpretation (annualized return, Sharpe ratio, win rate, etc.)
- Advanced backtesting techniques (multi-market, custom fees, slippage settings)
- In-depth backtest report analysis methods
API Reference
sim()
finlab.backtest.sim
sim(position, resample=None, resample_offset=None, trade_at_price='close', position_limit=1, fee_ratio=None, tax_ratio=None, 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:
Common Parameter Combinations
Monthly strategy (suitable for passive investors):
report = sim(
position,
resample='M', # Monthly rebalancing
position_limit=0.1, # 10% max per stock
upload=True # Upload to cloud
)
Weekly strategy (suitable for active investors):
report = sim(
position,
resample='W', # Weekly rebalancing
position_limit=0.05, # 5% max per stock (diversified)
trade_at_price='open' # Trade at open price (more conservative)
)
Stop-loss/take-profit strategy:
# Method 1: Set in sim()
report = sim(
position.hold_until(stop_loss=0.1, take_profit=0.2),
resample='M'
)
# Method 2: Use hold_until() for more flexibility
exit_condition = close < ma20 # Exit when price drops below MA
position_with_exit = position.hold_until(
exit=exit_condition,
stop_loss=0.1, # 10% stop-loss
take_profit=0.2 # 20% take-profit
)
report = sim(position_with_exit, resample='M')
Common Errors and Solutions
1. Strategy has no trade records
# Problem: Entry conditions are too strict, position is all False
report = sim(position, resample='M')
trades = report.get_trades()
if len(trades) == 0:
print("Warning: Strategy has no trade records!")
# Check entry frequency
print(f"Average daily stocks in position: {position.sum(axis=1).mean():.1f}")
print(f"Maximum stocks in position: {position.sum(axis=1).max()}")
# Solution: Relax conditions or check data range
2. KeyError: Date does not exist
# Problem: Data date ranges don't match
# Solution: Use truncate_start to align start dates
import finlab
finlab.truncate_start = '2020-01-01' # Only backtest data after 2020
report = sim(position, resample='M')
3. Forgot to set resample, causing daily rebalancing
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 Notification Setup
- Go to LINE Notify to get a personal token
- Set the environment variable:
- Send notification:
FAQ
Q: What if backtest results differ from live trading?
Common causes and solutions:
-
Backtest does not account for slippage:
-
Insufficient trading volume:
-
Trade price vs actual execution price gap:
sim()signals are always "generated today, executed tomorrow".trade_at_pricedetermines which price is used for execution the next day:# trade_at_price='close' (default) -> next day's close price # Difficult to execute precisely at close price in practice, prone to slippage report = sim(position, trade_at_price='close') # Next day close, hard to match in practice # trade_at_price='open' -> next day's open price (recommended) # Closer to real trading: review signals after market close, place orders at next day's open report = sim(position, trade_at_price='open') # Next day open, closer to live trading
Q: How do I set custom transaction fees?
report = sim(
position,
resample='M',
fee_ratio=0.001425, # 0.1425% broker commission (buy and sell)
tax_ratio=0.003 # 0.3% securities transaction tax (sell only)
)
Q: How do I limit the maximum number of holdings?
# Method 1: Use is_largest() to limit stocks in position
position = (close > ma20).is_largest(30) # Hold at most 30 stocks
# Method 2: Limit in sim()
report = sim(
position,
resample='M',
position_limit=0.1, # 10% max per stock
# If position has 50 stocks, effectively holds at most 10 (100% / 10% = 10)
)
Q: Backtest is too slow, what can I do?
# 1. Use a longer rebalancing period
report = sim(position, resample='Q') # Quarterly rebalancing (3x faster than monthly)
# 2. Shorten backtest period
import finlab
finlab.truncate_start = '2018-01-01' # Only test recent 5 years
# 3. Reduce the number of stocks
position = (close > ma20).is_largest(50) # Only test 50 stocks
Resources
- Complete Backtesting Tutorial - Deep dive into the backtest engine
- Complete Strategy Development Workflow - From research to live trading
- Strategy Optimization Guide - Finding optimal parameters
- Risk Management Guide - Stop-loss/take-profit in practice
- GitHub Source Code
- Example Strategies - Learn from practical examples