跳轉到

歷史回測

以每日 position 訊號搭配 finlab.backtest.sim,即可快速完成回測並產生選股清單與風險/報酬分析。

快速上手

以下示範以「股價 < 6 元」為條件,每月調整部位:

from finlab import data
from finlab import backtest

# 只買入價格小於 6 元的股票
close = data.get('price:收盤價')
position = close < 6

# 回測,每月底(M)重新調整股票權重
report = backtest.sim(position, resample='M', name="價格小於6的股票")

上傳回測至雲端後,可於「選股清單」檢視持股與異動,並手動跟單。

顯示策略回測結果

report.display()
display

取得策略交易紀錄

交易紀錄重點欄位

report.get_trades()
* exit_sig_date:出場訊號產生日。 * entry_sig_date:進場訊號產生日。 * entry_date:進場日。 * exit_date:出場日。 * period:持有天數。 * position: 持有佔比。 * return:報酬率。 * mdd:持有期間最大回撤。 * mae:持有期間最大不利幅度。 * g_mfe:持有期間最大有利幅度。 * b_mfe:mae發生前的最大有利幅度。

顯示波動分析圖

report.display_mae_mfe_analysis()
display_mae_mfe

策略流動性風險檢測

from finlab.analysis.liquidityAnalysis import LiquidityAnalysis

# 交易紀錄進出場成交張數大於1000張的比例, 成交金額大於1000000元的比例,檢測資金部位胃納量
report.run_analysis(LiquidityAnalysis(required_volume=100000, required_turnover=1000000))
策略流動性檢測

更多範例

創新高策略

以 250 日高點判斷創新高:

from finlab import data
from finlab.backtest import sim

# 創 250 天新高的股票
close = data.get('price:收盤價')
position = (close == close.rolling(250).max())

# 回測,每月(M)調整一次,選出當天創新高股票
sim(position, resample='M', name="創年新高策略")

高 RSI 技術指標策略

取 RSI 最大的 20 檔並持有一週:

from finlab import data
from finlab.backtest import sim

# 選出 RSI 最大的 20 檔股票
rsi = data.indicator('RSI')
position = rsi.is_largest(20)

# 回測,每月(M)調整一次
report = sim(position, resample='W', name="高RSI策略")

乖離率 + 財報濾網

以 60 日乖離率取前 30 名,再用 ROE > 0 濾網:

from finlab import data
from finlab.backtest import sim

# 下載 ROE 跟收盤價
roe = data.get("fundamental_features:ROE稅後")
close = data.get('price:收盤價')

position = (
      (close / close.shift(60)).is_largest(30) # 選出乖離率前 30 名的股票
    & (roe > 0) # 選出 ROE 大於 0 的股票
)

# 回測,每月(M)調整一次
report = sim(position, resample='M', name="乖離率和ROE濾網策略")

常見錯誤與解決方法

錯誤 1:策略無任何交易記錄

現象:回測完成但無任何交易,get_trades() 回傳空 DataFrame

close = data.get('price:收盤價')
position = (close > 100) & (close < 110)  # 條件過嚴
report = sim(position, resample='M')

trades = report.get_trades()
print(len(trades))  # 0

原因: - 進場條件過於嚴格,導致 position 幾乎全為 False - 資料日期範圍過短,無足夠交易日觸發進場 - 篩選後的股票池過小或不存在

解決方法

from finlab import data
from finlab.backtest import sim

close = data.get('price:收盤價')
position = (close > 100) & (close < 110)

# 檢查進場訊號統計
entry_stats = position.sum(axis=1)  # 每日進場股票數
print("=== 進場訊號統計 ===")
print(f"平均每日進場股票數:{entry_stats.mean():.2f}")
print(f"最大進場股票數:{entry_stats.max()}")
print(f"最小進場股票數:{entry_stats.min()}")
print(f"有進場訊號的交易日數:{(entry_stats > 0).sum()} / {len(entry_stats)}")

# 如果平均進場數 < 1,表示條件過嚴
if entry_stats.mean() < 1:
    print("\n⚠️ 警告:進場條件過嚴,平均每日不到 1 檔股票")
    print("建議:")
    print("1. 放寬價格區間(如 80-120 元)")
    print("2. 使用 .is_largest(N) 確保固定檔數")
    print("3. 檢查資料完整性(data.get() 是否正常)")

    # 修正範例:使用 is_largest() 確保固定檔數
    ma20 = close.average(20)
    position_fixed = (close > ma20).is_largest(30)  # 固定選出 30 檔
    print("\n✅ 改用固定檔數策略:")
    print(f"   平均每日進場股票數:{position_fixed.sum(axis=1).mean():.2f}")

# 執行回測並檢查結果
report = sim(position, resample='M', name="測試策略")
trades = report.get_trades()

if len(trades) == 0:
    print("\n❌ 錯誤:策略無任何交易記錄")
    print("請檢查上方的進場訊號統計,確認條件是否過嚴")
else:
    print(f"\n✅ 回測成功,共 {len(trades)} 筆交易")
    print(f"   持有期間:{trades['entry_date'].min()} ~ {trades['exit_date'].max()}")

進階診斷

# 檢查特定時間點的持倉狀態
import pandas as pd

# 隨機抽樣 10 個交易日檢查
sample_dates = position.index[::len(position)//10][:10]
for date in sample_dates:
    stocks = position.loc[date][position.loc[date]].index.tolist()
    print(f"{date}: {len(stocks)} 檔股票 - {stocks[:5]}...")

# 檢查是否有股票「從未」進場
never_entered = (position.sum(axis=0) == 0)
print(f"\n從未進場的股票數量:{never_entered.sum()} / {len(position.columns)}")

錯誤 2:KeyError - 日期索引錯誤

現象:執行回測時拋出 KeyError,錯誤訊息顯示某個日期不存在

report = sim(position, resample='M')
# KeyError: Timestamp('2023-05-01 00:00:00')

原因: - 使用多個資料源時,日期索引範圍不一致 - position 的日期範圍超出資料可用範圍 - 使用 shift()rolling() 導致前期資料缺失

解決方法

from finlab import data
from finlab.backtest import sim
import finlab

# 方法 1:使用 truncate_start 對齊起始日期
finlab.truncate_start = '2020-01-01'  # 只回測 2020 年後的資料

close = data.get('price:收盤價')
volume = data.get('price:成交股數')

# 檢查日期範圍
print(f"收盤價日期範圍:{close.index[0]} ~ {close.index[-1]}")
print(f"成交量日期範圍:{volume.index[0]} ~ {volume.index[-1]}")

# 方法 2:手動對齊日期索引
common_dates = close.index.intersection(volume.index)
close_aligned = close.loc[common_dates]
volume_aligned = volume.loc[common_dates]

print(f"對齊後日期範圍:{common_dates[0]} ~ {common_dates[-1]}")

# 方法 3:使用 try-except 捕捉錯誤
try:
    position = (close > close.average(20)) & (volume > 1000)
    report = sim(position, resample='M')
    print("✅ 回測成功")

except KeyError as e:
    print(f"❌ 日期索引錯誤:{e}")
    print("\n可能原因:")
    print("1. 使用 rolling() 或 average() 導致前期資料缺失")
    print("2. 多個資料源的日期範圍不一致")
    print("\n解決方法:")
    print("1. 設定 finlab.truncate_start 統一起始日期")
    print("2. 使用 .dropna() 移除缺失值")

    # 自動修正:移除缺失值
    position_cleaned = position.dropna(how='all', axis=0)  # 移除全為 NaN 的列
    position_cleaned = position_cleaned.fillna(False)       # 將 NaN 填充為 False

    print(f"\n✅ 已自動移除缺失值,重新回測...")
    report = sim(position_cleaned, resample='M')
    print(f"   回測期間:{position_cleaned.index[0]} ~ {position_cleaned.index[-1]}")

進階診斷:檢查 position 完整性

import pandas as pd

# 檢查 position 的缺失值
missing_info = position.isna()
print("=== Position 缺失值統計 ===")
print(f"總缺失值數量:{missing_info.sum().sum()}")
print(f"全部為 NaN 的列(日期)數量:{missing_info.all(axis=1).sum()}")
print(f"全部為 NaN 的欄(股票)數量:{missing_info.all(axis=0).sum()}")

# 顯示前幾個有缺失值的日期
missing_dates = missing_info.any(axis=1)
if missing_dates.sum() > 0:
    print(f"\n前 5 個有缺失值的日期:")
    for date in position.index[missing_dates][:5]:
        na_count = position.loc[date].isna().sum()
        print(f"  {date}: {na_count} 檔股票缺失")

錯誤 3:忘記設定 resample 導致每日調倉

現象:回測結果報酬率極低,交易次數異常多

report = sim(position)  # ❌ 未設定 resample
trades = report.get_trades()
print(len(trades))  # 數千筆交易

原因: - 未設定 resample 參數,預設為每日調倉 - 每日調倉會產生大量交易成本(手續費 + 證交稅) - 實際上大多數策略不應該每日頻繁調倉

解決方法

from finlab import data
from finlab.backtest import sim

close = data.get('price:收盤價')
position = close > close.average(20)

# ❌ 錯誤:未設定 resample(每日調倉)
report_daily = sim(position, name="每日調倉")
trades_daily = report_daily.get_trades()

# ✅ 正確:設定合理的調倉頻率
report_monthly = sim(position, resample='M', name="每月調倉")
trades_monthly = report_monthly.get_trades()

# 對比交易次數與績效
print("=== 調倉頻率對比 ===")
print(f"每日調倉:{len(trades_daily)} 筆交易,年化報酬 {report_daily.stats['annual_return']:.2%}")
print(f"每月調倉:{len(trades_monthly)} 筆交易,年化報酬 {report_monthly.stats['annual_return']:.2%}")

# 通常每月調倉的績效會比每日調倉好(交易成本較低)
if report_monthly.stats['annual_return'] > report_daily.stats['annual_return']:
    print("\n✅ 建議使用「每月調倉」,可大幅降低交易成本")

常用調倉頻率

# 週線策略(適合短線)
report = sim(position, resample='W', name="每週調倉")

# 月線策略(最常用,適合上班族)
report = sim(position, resample='M', name="每月調倉")

# 季線策略(適合長線)
report = sim(position, resample='Q', name="每季調倉")

# 自訂調倉頻率(如每 20 個交易日)
report = sim(position, resample='20D', name="每 20 日調倉")

參考資源