本文目錄
如果你也是自學量化交易、一路跌跌撞撞走過來的人,一定聽過「均線交叉」這個詞被講到爛。但講歸講,真正動手用 Backtrader 雙均線策略 跑過一次完整回測的人,其實沒有想像中多。常見的情況是:教學文章看了一堆,程式碼複製貼上卻報錯,或是跑出來的績效好到不合理——後來才發現是參數設定或資料處理出了問題。
這篇文章會帶你從零開始建立一套能真正運作的 Python 雙均線交易系統,並教你怎麼看懂 Backtrader 均線交叉回測結果背後的意義。文中所有程式碼都是可直接執行的版本,我也會把常見的錯誤與雷區整理出來,讓你在除錯時少走冤枉路。
免責聲明:本文為程式教學與研究用途,不構成任何投資建議。文中所有數字若標示為「假設範例」,僅用於說明計算邏輯,不代表真實市場績效。
什麼是雙均線策略?黃金交叉死亡交叉的核心邏輯
雙均線策略的原理其實不複雜,就是用兩條不同天期的移動平均線來判斷趨勢方向。當短期均線(例如 5 日線)由下往上穿過長期均線(例如 20 日線)時,稱為「黃金交叉」,通常視為買進訊號;反之短期均線由上往下穿過長期均線,就是「死亡交叉」,作為賣出或放空的訊號。
用數學語言來說,假設快線為 $MA_f(t)$、慢線為 $MA_s(t)$:
- 黃金交叉:$MA_f(t-1) < MA_s(t-1)$ 且 $MA_f(t) > MA_s(t)$
- 死亡交叉:$MA_f(t-1) > MA_s(t-1)$ 且 $MA_f(t) < MA_s(t)$
這套邏輯之所以歷久不衰,是因為它符合「順勢而為」的交易哲學——不預測轉折點,而是等趨勢確立後才進場,犧牲一點進場時機換取較高的勝率穩定度。
為什麼趨勢跟隨在理論上可能有效? 學術上常引用動能效應(Momentum Effect)與行為財務學的解釋:投資人對新資訊的反應往往不足(Underreaction),導致價格趨勢會延續一段時間。但這不代表任何一組均線參數都能賺錢,市場環境(趨勢盤 vs 盤整盤)對策略表現的影響極大。
提醒:光看勝率是不夠的。趨勢跟隨策略的勝率通常低於 50%,靠的是「讓利潤奔跑、快速停損」來創造正期望值。評估時必須搭配盈虧比、最大回撤等指標一起看。
環境安裝與資料準備(照抄就能跑)
在開始寫策略之前,先把環境搞定。
1. 安裝套件
pip install backtrader yfinance pandas matplotlib
backtrader:回測框架本體yfinance:從 Yahoo Finance 抓取歷史資料pandas:資料處理matplotlib:繪圖(Backtrader 的plot()依賴它)
本文範例以 Python 3.10+ 與 Backtrader 1.9.78.123 為基準。Backtrader 已多年未更新,若你使用較新的 Python 版本遇到相容性問題,可考慮改用 backtrader2 等社群維護分支,或改用虛擬環境安裝 Python 3.10。
2. 用 yfinance 取得資料(建議作法)
早期教學常直接讀 CSV,但 Yahoo Finance 的 CSV 格式欄位多、容易對不上。比較穩定的做法是用 yfinance 抓資料,再轉成 Backtrader 的 PandasData:
import yfinance as yf
import pandas as pd
下載台積電 2330.TW 日線資料
df = yf.download("2330.TW", start="2019-01-01″, end="2023-12-31″, auto_adjust=True)
yfinance 新版可能回傳 MultiIndex 欄位,這裡先攤平
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.get_level_values(0)
確保欄位名稱符合 Backtrader 預期
df = df.rename(columns={
"Open": "open",
"High": "high",
"Low": "low",
"Close": "close",
"Volume": "volume",
})
移除 NaN,避免指標計算出錯
df = df.dropna()
print(df.head())
重點說明:
auto_adjust=True會回傳還原股價(已處理除權息),這對均線策略非常重要。若用未調整股價,除息日會出現假跳空,導致假訊號。dropna()是必要的,否則 SMA 計算初期會產生 NaN,某些版本的 Backtrader 會直接報錯。- 資料來源與時間戳記請自行記錄,方便日後重現。
3. 如果你堅持用 CSV:YahooFinanceCSVData 的欄位格式
bt.feeds.YahooFinanceCSVData 需要 CSV 符合特定欄位順序,格式大致如下(第一列為標題):
Date,Open,High,Low,Close,Adj Close,Volume
2019-01-02,220.0,222.5,218.0,221.0,221.0,25000000
2019-01-03,221.5,223.0,219.5,220.5,220.5,18000000
載入時務必指定 fromdate 與 todate,否則 Backtrader 會嘗試解析整份檔案,遇到格式不符的列就會出錯:
import datetime
data = bt.feeds.YahooFinanceCSVData(
dataname="2330.csv",
fromdate=datetime.datetime(2019, 1, 1),
todate=datetime.datetime(2023, 12, 31),
reverse=False,
)
若你的 CSV 欄位順序不同,建議改用 PandasData 自行對應欄位,彈性更高。
Backtrader 雙均線策略完整程式碼實作
以下是一份可直接執行的完整程式碼,包含策略、資料載入、手續費設定、Analyzer 與繪圖。
import datetime
import backtrader as bt
import yfinance as yf
import pandas as pd
class DualMAStrategy(bt.Strategy):
params = (
("fast_period", 5),
("slow_period", 20),
)
def __init__(self):
self.fast_ma = bt.indicators.SMA(
self.data.close, period=self.params.fast_period
)
self.slow_ma = bt.indicators.SMA(
self.data.close, period=self.params.slow_period
)
self.crossover = bt.indicators.CrossOver(self.fast_ma, self.slow_ma)
def next(self):
if not self.position:
if self.crossover > 0: # 黃金交叉
self.buy()
elif self.crossover < 0: # 死亡交叉
self.close()
def load_data(symbol="2330.TW", start="2019-01-01″, end="2023-12-31″):
df = yf.download(symbol, start=start, end=end, auto_adjust=True)
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.get_level_values(0)
df = df.rename(columns={
"Open": "open", "High": "high", "Low": "low",
"Close": "close", "Volume": "volume",
})
df = df.dropna()
return bt.feeds.PandasData(dataname=df)
def run_backtest():
cerebro = bt.Cerebro()
cerebro.addstrategy(DualMAStrategy)
data = load_data()
cerebro.adddata(data)
cerebro.broker.setcash(100000)
# 台股來回成本:手續費 0.1425% × 2(買賣各一次)+ 證交稅 0.3%
# 這裡以單邊 0.001425 近似,讀者可依需求調整
cerebro.broker.setcommission(commission=0.001425)
# 加入 Analyzer
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name="sharpe",
timeframe=bt.TimeFrame.Days, riskfreerate=0.0)
cerebro.addanalyzer(bt.analyzers.DrawDown, _name="drawdown")
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name="trades")
cerebro.addanalyzer(bt.analyzers.Returns, _name="returns")
print("初始資金: %.2f" % cerebro.broker.getvalue())
results = cerebro.run()
strat = results[0]
print("最終資金: %.2f" % cerebro.broker.getvalue())
print("夏普比率:", strat.analyzers.sharpe.get_analysis())
print("最大回撤:", strat.analyzers.drawdown.get_analysis())
print("交易統計:", strat.analyzers.trades.get_analysis())
# 避免在無圖形介面環境(如 Colab、伺服器)報錯
try:
cerebro.plot(style="candlestick")
except Exception as e:
print("繪圖失敗(可能為無圖形介面環境):", e)
if __name__ == "__main__":
run_backtest()
程式碼關鍵細節
1. CrossOver 指標:會自動回傳 1(黃金交叉)或 -1(死亡交叉),不用自己寫繁瑣的條件判斷。
2. setcommission(commission=0.001425):模擬台股手續費(0.1425%)。注意:台股賣出時另收證交稅 0.3%,實務上想更貼近真實成本,可改寫成自訂 CommissionInfo 分別處理買賣兩邊,或直接把單邊成本調高到約 0.0029 來近似。
3. try/except 包住 plot():在 Colab、雲端伺服器等無 GUI 環境,cerebro.plot() 常會直接丟錯。這樣寫可避免程式整段崩潰。
4. auto_adjust=True:確保使用還原股價,避免除權息造成的假訊號。
我沒有在本文提供「實測績效數字」,因為那需要你自己用可查證的資料跑出來。以下章節會用假設範例說明如何解讀結果。
Backtrader 策略優化教學:參數調整與樣本外驗證
寫出能跑的程式碼只是第一步,真正決定策略成敗的往往是參數選擇。Backtrader 內建 optstrategy 可以做網格搜尋:
cerebro.optstrategy(
DualMAStrategy,
fast_period=range(5, 25, 5),
slow_period=range(20, 65, 10),
)
參數掃描結果應該長什麼樣?(假設範例)
以下表格是用於說明解讀方式的假設範例,並非真實回測數據,請勿當成任何標的的績效:
| 均線組合(快/慢) | 交易次數 | 勝率 | 總報酬率 | 最大回撤 |
|---|---|---|---|---|
| 5 / 10 | 68 次 | 38.2% | 12.4% | -18.6% |
| 5 / 20 | 47 次 | 46.8% | 34.2% | -14.2% |
| 10 / 30 | 29 次 | 51.7% | 38.9% | -11.5% |
| 10 / 60 | 18 次 | 55.6% | 29.3% | -9.8% |
| 20 / 60 | 12 次 | 58.3% | 22.1% | -8.2% |
| 20 / 120 | 7 次 | 42.9% | 15.6% | -13.4% |
從這張表可以看出一個常見趨勢:均線週期拉長,交易次數減少、勝率提升,但總報酬率不一定跟著上升——因為交易次數變少,錯過的獲利機會也變多。
做 Backtrader 策略優化時最重要的原則:不要只追求最高報酬率的參數,要找最穩定、最能承受不同市場環境的組合。 具體做法是:
1. 把資料切成「訓練集」與「測試集」(例如前 70% / 後 30%)。
2. 在訓練集上做網格搜尋,挑出候選參數。
3. 用測試集驗證候選參數,若表現落差過大(例如報酬率從 +40% 掉到 -10%),就是過度擬合的警訊。
避雷防坑指南:雙均線策略常見錯誤與雷區分析
以下這些坑是量化新手最常遇到的,整理出來讓你少花時間除錯。
雷區一:忘記設定滑價與手續費,回測績效嚴重失真
很多人跑出來的回測績效漂亮到不真實,十之八九是因為沒設定手續費或滑價(Slippage)。假設範例:若某策略在零成本假設下總報酬率為 89%,加入台股來回成本(手續費 0.1425% × 2 + 證交稅 0.3% ≈ 0.585%)後,報酬率會被明顯壓縮——交易次數越多,壓縮幅度越大。
防範建議:用 cerebro.broker.setcommission() 設定實際交易成本,台股建議至少抓 0.15%~0.2% 的單邊成本。若想模擬滑價,可自訂 bt.CommInfoBase 子類,或直接在 setcommission 中加上額外百分比。
雷區二:參數過度優化,實盤表現與回測落差極大
常見的錯誤是:在同一段歷史資料上反覆調參數,直到找到一組「完美參數」,結果換到另一段時間就完全失靈。這就是典型的過度擬合(Overfitting)——參數是「量身訂做」給那段特定歷史資料的。
防範建議:一定要做樣本外測試(Out-of-Sample Testing)。把資料切成訓練集跟測試集,訓練集找參數、測試集驗證,兩邊表現落差太大就要警惕。更嚴謹的做法是 Walk-Forward Analysis(滾動式前進驗證)。
雷區三:忽略資料的除權息調整
如果你抓的股票歷史資料沒有做除權息調整,回測結果會出現詭異的跳空缺口,均線交叉訊號也會因此失真。常見的錯誤是:用未調整股價測試高配息標的,策略在除息日附近頻繁出現假訊號,卻誤以為是策略邏輯問題。
防範建議:使用還原股價(Adjusted Price)資料源。若使用 yfinance,記得設 auto_adjust=True;若使用券商或第三方資料,請確認已處理除權息。
雷區四:只看總報酬率,忽略最大回撤與資金曲線平滑度
常見的錯誤是:被漂亮的總報酬率沖昏頭,卻沒注意資金曲線中間有一段長達數個月的橫盤加下跌期,最大回撤可能超過 -20%。如果實際拿真錢去操作,這段時間的心理壓力可能會讓人提前砍倉出場。
防範建議:務必同時檢視夏普比率(Sharpe Ratio)和最大回撤,並實際畫出資金曲線,看看自己能不能承受那樣的波動。
Backtrader 回測績效分析:關鍵指標怎麼看?
回測跑完之後,Backtrader 提供的 Analyzer 可以幫你算出許多重要指標。以下是最該優先檢視的幾個:
- 夏普比率(Sharpe Ratio):衡量每承擔一單位風險能換到多少超額報酬,一般認為 1.0 以上算是不錯的策略。計算公式為(策略年化報酬 − 無風險利率)÷ 年化報酬標準差。
- 最大回撤(Max Drawdown):資金曲線從高點回落的最大幅度,這個數字直接反映策略的「痛苦指數」。
- 盈虧比(Profit Factor):總獲利金額除以總虧損金額,數值大於 1.5 通常被視為體質較健康。
- 交易次數與平均持倉天數:雙均線策略屬於中長線趨勢策略,平均持倉天數通常偏長。如果你的目標是短線當沖,這套邏輯可能不適合。
加入這些 Analyzer 的程式碼如下(已整合進前面的完整範例):
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name="sharpe",
timeframe=bt.TimeFrame.Days, riskfreerate=0.0)
cerebro.addanalyzer(bt.analyzers.DrawDown, _name="drawdown")
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name="trades")
跑完之後呼叫 strat.analyzers.sharpe.get_analysis() 就能拿到完整數據。注意:SharpeRatio 的 timeframe 與 riskfreerate 參數會顯著影響結果,解讀時務必確認設定。
光是把這幾個指標搞懂,會比你亂調 100 組參數更有意義。
工具比較:Backtrader vs 其他回測工具
寫這篇文章之前,也考慮過是否該用其他工具做雙均線策略回測,這邊做個簡單比較:
| 工具 | 學習曲線 | 客製化彈性 | 視覺化能力 | 適合對象 |
|---|---|---|---|---|
| Backtrader (Python) | 中等偏高 | 非常高 | 需自行設定 | 有程式基礎的量化愛好者 |
| TradingView Pine Script | 低 | 中等 | 極佳 | 想快速驗證想法、視覺化為主 |
| Excel VBA | 低 | 低 | 差 | 極簡單策略、入門練習 |
| Zipline | 高 | 高 | 中等 | 學術研究、機構級應用 |
一個實用的工作流是:用 TradingView 快速目視觀察均線行為、抓靈感,確定方向後再用 Backtrader 寫程式碼做嚴謹的量化回測。TradingView 的均線視覺化與多時間週期切換做得相當順手,很適合策略發想階段使用。如果你也想試試這類工具,可以參考 TradingView。
常見問題 FAQ
Q1:Backtrader 雙均線策略適合用在哪種市場?
雙均線策略本質上是趨勢跟隨策略,比較適合有明顯趨勢的市場,例如波動較大的個股或期貨商品。如果是長期橫盤整理的標的,均線交叉訊號會非常頻繁但大多是假訊號,容易被「巴來巴去」,建議搭配 ADX 等趨勢強度指標過濾。
Q2:均線週期參數要怎麼選才不會過度優化?
建議至少用兩段不同時期的資料做測試:一段用來尋找參數(訓練集),另一段完全沒用過的資料驗證(測試集)。如果同一組參數在兩段資料上表現都穩定,才算是相對可靠的參數。具體判準可自行設定,例如兩段報酬率落差、回撤落差等。
Q3:Backtrader 回測結果跟實盤交易會有落差嗎?
一定會有,主要來自滑價、成交延遲、流動性不足等因素。建議回測時故意調高手續費與滑價假設(比實際成本高一些),這樣算出來的績效會比較保守,實盤落差也會比較小。
Q4:如何處理除權息對回測的影響?
最簡單的方式是使用還原股價。以 yfinance 為例,設定 auto_adjust=True 即可取得已調整的 OHLC。若使用自備 CSV,請確認資料源已做還原處理。未還原的股價在除息日會出現跳空,導致均線交叉訊號失真。
Q5:如何在 Backtrader 中設定滑價?
Backtrader 沒有內建獨立的滑價參數,常見做法有兩種:
1. 自訂 bt.CommInfoBase 子類,在 getcommission 或 getoperationcost 中加入固定百分比。
2. 直接在 setcommission 傳入較高的 commission 值(例如把單邊成本從 0.001425 提高到 0.002),把滑價與稅費一起近似進去。
class TaiwanStockCommission(bt.CommInfoBase):
params = (
("stocklike", True),
("commtype", bt.CommInfoBase.COMM_PERC),
("percabs", True),
("commission", 0.001425), # 手續費率
("slippage_pct", 0.001), # 額外滑價
)
def _getcommission(self, size, price, pseudoexec):
base = abs(size) price self.p.commission
slip = abs(size) price self.p.slippage_pct
return base + slip
cerebro.broker.addcommissioninfo(TaiwanStockCommission())
Q6:如何在 Backtrader 中加入停損停利?
最簡單的方式是在 next() 中搭配 self.position.size 與進場價判斷,或使用 bt.Order 的 bracket order。範例:
def next(self):
if not self.position:
if self.crossover > 0:
self.buy()
else:
entry = self.position.price
current = self.data.close[0]
# 停損 5%、停利 15%
if current <= entry 0.95 or current >= entry 1.15:
self.close()
結論:從回測到實盤,你準備好了嗎?
Backtrader 雙均線策略雖然邏輯簡單,但要做出真正有參考價值的回測,還是需要注意手續費設定、樣本外驗證、資料除權息處理這些細節——這些正是大多數教學文章不會特別強調的地方。本文把程式碼、環境設定、資料格式、常見雷區都攤開來講,希望能讓你少走一些冤枉路。
你的下一步行動:
1. 複製本文程式碼,用 yfinance 抓一段你有興趣的標的資料,先讓它跑起來。
2. 加入本文示範的 Analyzer,觀察夏普比率與最大回撤。
3. 把資料切成訓練集與測試集,做一次完整的樣本外驗證。
4. 試著調整參數,但切記:穩定性比最高報酬率重要。
如果你想在策略發想階段有更順手的圖表工具,可以參考 TradingView,用視覺化工具搭配紮實的 Backtrader 回測驗證,會讓你的策略開發流程更有效率。
再次提醒:本文所有內容僅供程式教學與研究,不構成投資建議。任何策略在不同市場環境下的表現都可能差異極大,請務必自行驗證並做好風險控管。