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
Backtest simulation orchestrator.
This is the main entry point for running backtests via sim().
Configuration, validation, report building, and notifications are
delegated to focused package modules:
finlab.backtest.config-- SimConfig, input validationfinlab.backtest.report-- trades, MAE/MFE, report assemblyfinlab.backtest.notify-- Line notification delivery
SimConfig
dataclass
SimConfig(
resample=None,
data_readiness=None,
planned_rebalance_date=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,
trail_stop_activation=None,
touched_exit=False,
retain_cost_when_rebalance=False,
stop_trading_next_period=True,
live_performance_start=None,
end_date=None,
mae_mfe_window=0,
mae_mfe_window_step=1,
market=None,
upload=None,
notification_enable=False,
line_access_token='',
)
Configuration for backtest simulation parameters.
Groups the many sim() parameters into a single config object for cleaner internal passing between phases.
columns_to_numpy
Convert DataFrame columns to numpy array for pandas 3.0+ compatibility.
sim
sim(
position: DataFrame | Series,
resample: str | None = None,
resample_offset: str | None = None,
trade_at_price: str | DataFrame = 'close',
position_limit: float = 1,
fee_ratio: float | None = None,
tax_ratio: float | None = None,
name: str = '未命名',
stop_loss: float | DataFrame | None = None,
take_profit: float | DataFrame | None = None,
trail_stop: float | None = None,
trail_stop_activation: float | None = None,
touched_exit: bool = False,
retain_cost_when_rebalance: bool = False,
stop_trading_next_period: bool = True,
live_performance_start: str | None = None,
mae_mfe_window: int = 0,
mae_mfe_window_step: int = 1,
market: Market | None = None,
upload: bool | None = None,
*,
end_date: str | date | Timestamp | None = None,
metrics_only: Literal[True],
notification_enable: bool = False,
line_access_token: str = '',
) -> Metrics
sim(
position: DataFrame | Series,
resample: str | None = None,
resample_offset: str | None = None,
trade_at_price: str | DataFrame = 'close',
position_limit: float = 1,
fee_ratio: float | None = None,
tax_ratio: float | None = None,
name: str = '未命名',
stop_loss: float | DataFrame | None = None,
take_profit: float | DataFrame | None = None,
trail_stop: float | None = None,
trail_stop_activation: float | None = None,
touched_exit: bool = False,
retain_cost_when_rebalance: bool = False,
stop_trading_next_period: bool = True,
live_performance_start: str | None = None,
mae_mfe_window: int = 0,
mae_mfe_window_step: int = 1,
market: Market | None = None,
upload: bool | None = None,
*,
end_date: str | date | Timestamp | None = None,
metrics_only: Literal[False] = False,
notification_enable: bool = False,
line_access_token: str = '',
) -> report.Report
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,
trail_stop_activation=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=None,
*,
end_date=None,
metrics_only=False,
notification_enable=False,
line_access_token='',
)
Simulate the equity given the stock position history.
See the module-level docstring and the parameter docs below for full details.
| PARAMETER | DESCRIPTION |
|---|---|
position
|
Buy/sell signal history. True = hold, False = flat.
Can also be a
TYPE:
|
resample
|
Trading period frequency (e.g. 'W', 'M', 'Q'), or a DataFrame/Series, DatetimeIndex or datetime list supplying rebalance dates. FinlabDataFrame period labels are converted to disclosure dates.
TYPE:
|
resample_offset
|
Pandas offset string (e.g. '7D') added to each rebalance date, for both frequency strings and supplied dates. Applied after disclosure-date conversion and before filtering the backtest period.
TYPE:
|
trade_at_price
|
Price type for trade execution ('close', 'open', etc.).
TYPE:
|
position_limit
|
Maximum single-stock weight (0-1).
TYPE:
|
fee_ratio
|
Trading fee ratio.
TYPE:
|
tax_ratio
|
Trading tax ratio.
TYPE:
|
name
|
Strategy name.
TYPE:
|
stop_loss
|
Stop-loss threshold, or a DataFrame of per-date, per-stock
thresholds indexed by the date each value becomes available. Each
bar uses the latest value dated on or before it (strictly before
it with touched_exit=True, since intraday touches happen before
that day's value exists). NaN, 0 and dates before the first value
mean no stop. With a DataFrame,
TYPE:
|
take_profit
|
Take-profit threshold, or a DataFrame with the same rules
as
TYPE:
|
trail_stop
|
Drop from peak return, measured relative to entry cost. For a long position with close-price execution and touched_exit=False, signal an exit when (peak_price - close) / entry_cost > trail_stop. Entry 100, peak 150 and trail_stop=0.2 give a 130 threshold. The signal is executed on the next bar, not at a guaranteed threshold price.
TYPE:
|
trail_stop_activation
|
Profit threshold before trailing stop activates.
TYPE:
|
touched_exit
|
Use intraday touched stop-loss/take-profit.
TYPE:
|
retain_cost_when_rebalance
|
Keep original cost basis on rebalance.
TYPE:
|
stop_trading_next_period
|
Skip next period after stop-loss/take-profit.
TYPE:
|
live_performance_start
|
ISO date string marking live trading start.
TYPE:
|
mae_mfe_window
|
Window size for MAE/MFE calculation.
TYPE:
|
mae_mfe_window_step
|
Step size for MAE/MFE window.
TYPE:
|
market
|
Market instance or name string ('TW_STOCK', 'US_STOCK').
TYPE:
|
upload
|
Whether to upload the strategy report to FinLab cloud. Defaults
to
TYPE:
|
end_date
|
Last date of the backtest (inclusive; a bare date covers the
whole day). Position, prices and the report (creturn, trades,
stats) stop there, as if the backtest ran on that day. Positions
still open on
TYPE:
|
metrics_only
|
Return the same
TYPE:
|
notification_enable
|
Send Line notifications on completion.
TYPE:
|
line_access_token
|
Line Notify access token.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Report | Metrics
|
A Report, or a Metrics object when |
upload default (since 2.0.24)
upload defaults to None: the report is uploaded only in cloud-scheduled runs (the FINLAB_FORCED_STRATEGY_NAME environment variable is set). FinLab Studio, local, Jupyter and Colab runs do not upload by default; pass upload=True or call report.upload() after the backtest to upload.
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 (local runs do not upload by default)
)
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,
resample='M',
stop_loss=0.1, # 10% stop-loss
take_profit=0.2, # 20% take-profit
)
# 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
metrics_from_nav()
finlab.backtest.metrics_from_nav
Measure any engine's daily NAV with FinLab's curve-only Metrics.
Supply costs-adjusted NAV neutralized for deposits/withdrawals. Dates must increase with one observation per day; invalid rows are never dropped or sorted. This computes caller-provided performance, not audited returns. Measurement uses local calendar dates without a timezone, so midnight DST transitions do not invalidate otherwise valid daily observations. No report, holdings, upload or orders are inferred from the curve.
metrics_from_nav() accepts daily NAV from any engine or custom ledger and measures curve performance using the same definitions as sim(). Research/strategy management tools can register these results without rewriting the backtest:
import pandas as pd
from finlab.backtest import metrics_from_nav
from finlab.backtest.metrics import metrics_to_dict
nav = pd.Series([100, 102, 101], index=pd.date_range("2026-01-01", periods=3))
metrics = metrics_from_nav(nav)
print(metrics.annual_return(), metrics.sharpe_ratio(), metrics.max_drawdown())
print(metrics_to_dict(metrics)) # Non-finite measurements become None
print(metrics.provenance) # Backend, period and observation count
Supply a numeric Series with a DatetimeIndex, at least two increasing dates, at most one observation per day, and finite positive values. Measurement uses the input’s local calendar dates. The first value is normalized to 1; invalid rows are never sorted or dropped. NAV must include costs and neutralize deposits/withdrawals. Performance is not independently audited brokerage performance. The return value is standalone Metrics with curve-derived measurements; no Report, trade records, holdings or orders are created.
Deploy any engine's program and required files with the existing ZIP deployment API. Put third-party dependencies in requirements.txt at the ZIP root and call metrics_from_nav after running the engine. Cloud Python, dependency and resource limits still apply:
python -m finlab cloud deploy external_nav --zip strategy.zip --entry-script main.py --time 09:30 --no-contest
This function does not create/upload a cloud Report or establish contest eligibility.
line_notify()
finlab.backtest.line_notify
Send backtest position and recent trade signals to a Line chat room.
| PARAMETER | DESCRIPTION |
|---|---|
report
|
The backtest result report.
TYPE:
|
line_access_token
|
Line Notify access token.
TYPE:
|
test
|
If True, send a test message instead of report data.
TYPE:
|
name
|
Strategy name for the notification header.
TYPE:
|
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 - In-depth walkthrough of 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