回測實戰:freqtrade 上手
你花了幾個晚上寫好策略,在 Jupyter Notebook 裡畫出一條往右上方爬的資金曲線,參數調得剛剛好,年化報酬看起來超過 40%。結果搬進 freqtrade、接上真實資料一跑——報酬腰斬,或者根本虧損。這不是倒楣,是因為你的回測根本沒有算對:沒扣手續費、指標計算有前視偏差、回測區間只選了多頭市場。
這堂課要解決的核心問題就是這個:怎麼跑一個「有意義」的回測,而不是一個「好看」的回測。兩者差距有時候只是有沒有在指令結尾加上 --fees 0.001,但這個細節就是真實績效和數字遊戲的分水嶺。
這堂學什麼
- freqtrade 策略檔結構:三個核心函式(
populate_indicators、populate_entry_trend、populate_exit_trend)的職責與寫法 - 完整回測流程:下載歷史資料 → 寫策略 → 跑回測 → 讀指標
- 五大關鍵指標解讀:夏普值、最大回撤、勝率、獲利因子哪個最重要,不能只看報酬率
- 手續費與滑點殺傷力實算:具體算出「策略每月要賺多少才能保本」
- 把第 3 課的雙均線策略搬進 freqtrade 框架,用真實數字驗收它
觀念一:策略檔的骨架
freqtrade 的每個策略都是一個繼承自 IStrategy 的 Python 類別。這個框架幫你把底層事務全包了——資料載入、倉位追蹤、手續費計算、報告生成——你只需要回答三個問題:「計算哪些指標?」「什麼時候進場?」「什麼時候出場?」對應到三個必須實作的方法。

圖:freqtrade 官方文件(2026 年 7 月實況),來源:freqtrade.io
除了三個方法,還有兩組必須設定的類別屬性:
minimal_roi:最小獲利目標。格式是{持倉分鐘數: 報酬率}——當達到對應的報酬目標,系統自動出場。這是除了停損之外的另一道保護機制。stoploss:停損比例,填負數(例如-0.05代表虧損 5% 時強制停損)。
freqtrade 2026 版的進出場欄位名稱是 enter_long 和 exit_long。如果你從網路上找到的舊教學或 AI 生成的程式碼用的是 buy/sell 欄位名稱,那是 v1 時代的舊 API,現在已改名,必須更新,否則回測的 Entries 會是 0(常見坑第 2 條就是這個)。
觀念二:五大指標,哪個最重要
回測輸出的統計表有十幾個數字,初學者往往只看 Total Profit %。這是很危險的習慣。我們把五個最關鍵的指標逐一拆解。

總報酬(Total Profit %):最直覺卻最容易被誤讀。必須和同期「買著不動」(Buy & Hold)比較。策略一年報酬 15%,但同期 BTC 漲了 80%——你付出了高得多的複雜度和執行風險,卻沒得到應有的回報,不如直接買著放。
夏普值(Sharpe Ratio):衡量「每單位風險換來多少報酬」,公式是(年化報酬率 − 無風險利率)/ 年化波動率。業界一般認為長期 > 1.0 才算說得過去,> 2.0 是優秀策略的門檻。夏普值低的策略,即使報酬率看起來高,代表你是在承擔過多風險換那點報酬。
最大回撤(Max Drawdown):帳戶從最高點跌到最低點的最大幅度。這個指標最直接衝擊你的心理承受力——最大回撤 -30% 代表你要眼睜睜看著 100 萬台幣縮水到 70 萬,然後繼續讓策略跑。大多數人在這個時點會恐慌手動停損,打亂整個策略的期望值,最後虧損卻把帳算在「策略沒用」上。
勝率(Win Rate):100 次交易中賺錢的比例。勝率高不代表賺錢——如果每次獲利平均 0.5%、每次虧損平均 3%,即使勝率 80% 你最終還是虧損的。必須搭配盈虧比一起看。
獲利因子(Profit Factor):總獲利除以總虧損。這個數字同時包含勝率和盈虧比的資訊,是最綜合的單一指標。> 1.5 才有競爭力,> 2.0 屬於優秀。
**最重要的指標是哪個?回答:最大回撤。**理由:一個策略即使長期期望值為正,如果回撤太深,執行者在中途就會崩潰出場。策略有效,但你在最低點平倉了,前功盡棄——這種情況比你想像的更常發生。
觀念三:手續費與滑點的殺傷力實算
這是最多人在回測時漏掉的現實因素。
以 Binance Spot 為例(2026 年現況):掛單手續費 0.1%,吃單 0.1%,一來一回合計 0.2%。持有 BNB 享 25% 折扣可降到 0.15%,但我們用保守的 0.2% 計算。
再加滑點(Slippage)——下單的預期成交價與實際成交價之間的落差:平靜市場約 0.050.1%,波動市場 0.10.5%。以保守的 0.1%(雙邊合計)估算:
保守估計:單次 round trip 總交易成本 ≈ 0.3%
下表實算不同交易頻率下的費用負擔:
| 每週交易次數 | 每次成本(round trip) | 每月費用 | 年化費用 |
|---|---|---|---|
| 1 次 | 0.3% | 1.2% | ~15% |
| 4 次 | 0.3% | 4.8% | ~58% |
| 10 次 | 0.3% | 12% | ~144% |
每週 4 次交易,光是手續費和滑點一年就吃掉 58%——你的策略年化報酬必須超過 58% 才能保本。這個門檻高得驚人;多數機構基金能長期穩定 20~30% 年化就已是頂尖水準了。

結論:低頻策略才是散戶的可行路。 每次交易的費用是固定的,交易頻率越低,費用佔總報酬的比例就越小。那些高頻、每天幾十次訊號的策略,在散戶的費用結構下,光養手續費就很辛苦。
觀念四:回測漂亮 ≠ 實盤賺錢
即使回測有加手續費、有扣滑點、數字看起來合理,實盤還是可能表現不一樣。這裡有五個系統性的落差來源:

前視偏差(Lookahead Bias):指標計算不小心用到當時還沒有的未來資料。最常見是忘記對信號做
shift(1),讓「明天的收盤價」影響「今天的進場決定」。freqtrade 2026.5 版已加強偵測機制並會主動發出警告,但不保證能抓到所有情況。過擬合(Overfitting):參數是在歷史資料上「調出來的最佳解」,不是市場的普遍規律。第 3 課已詳細討論過這個問題,這裡只提醒:回測數字越漂亮、越是要懷疑是否過擬合了。
市場環境轉變(Regime Change):2021 年多頭市場調出來的動量參數,在 2022 年熊市可能整年虧損。市場的波動率、流動性結構都在改變,策略沒有「永久有效」這種東西。
流動性假設:回測假設「你想買多少就買多少」。現實中,小市值幣對或下大單量時,可能成交不到你預期的價格,實際滑點遠高於保守估計。
生存者偏差(Survivorship Bias):你在交易所上看到的幣對都是「撐下來的」。如果資料集裡包含現在已下架的幣,結果可能偏樂觀。
實作原則:回測只是篩選工具,不是保證。回測過不了的策略直接淘汰;回測過得了的,再拿去做 Walk-Forward 驗證,最後才上模擬盤做終極驗收。
手把手實戰:把第 3 課策略搬進 freqtrade
現在來做最關鍵的一步:把第 3 課學到的雙均線策略(SMA20 / SMA60 交叉)移植進 freqtrade 框架,跑一次完整且誠實的回測。
確認 freqtrade 環境
如果你已完成第 1 課的安裝,直接啟動虛擬環境:
source freqtrade-env/bin/activate
# Windows 用:freqtrade-env\Scripts\activate
freqtrade --version
# 應該看到 freqtrade 2026.x
沒裝過的請先回第 1 課完成安裝。確認版本後,進入你的 freqtrade 工作目錄:
cd my-quant
建立策略檔
在 user_data/strategies/ 底下建立 DualMACross.py。這個策略直接對應第 3 課討論的雙均線邏輯:快線(SMA20)上穿慢線(SMA60)進場,下穿出場,並加 RSI 過濾避免追高:
# user_data/strategies/DualMACross.py
# 雙均線交叉策略 — 第 3 課移植版
# 策略邏輯:SMA20 穿越 SMA60,RSI 過濾
from freqtrade.strategy import IStrategy
from pandas import DataFrame
import talib.abstract as ta
class DualMACross(IStrategy):
"""
雙均線交叉策略
- 進場:SMA20 上穿 SMA60,且 RSI < 70(避免追高)
- 出場:SMA20 下穿 SMA60,或 RSI > 80
- 停損:-5%
- 使用 1 小時 K 線
"""
timeframe = '1h'
# 最小獲利目標表
# 超過 120 分鐘才出場,至少要有 2% 才鬆手
# 超過 60 分鐘,3% 即出場
# 0 分鐘:只有達到 5% 才立刻出場(等同一般持有直到停損)
minimal_roi = {
"120": 0.02,
"60": 0.03,
"0": 0.05,
}
# 停損 -5%,超過就強制砍倉
stoploss = -0.05
# 用市價單確保成交(會有較高滑點,但不會因限價單未成交而漏掉訊號)
order_types = {
'entry': 'market',
'exit': 'market',
'stoploss': 'market',
}
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""第一步:計算所有技術指標,把欄位加進 dataframe"""
# 快線:20 週期 SMA
dataframe['sma_fast'] = ta.SMA(dataframe, timeperiod=20)
# 慢線:60 週期 SMA
dataframe['sma_slow'] = ta.SMA(dataframe, timeperiod=60)
# RSI:14 週期(預設標準)
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""第二步:定義進場條件,符合條件就在 enter_long 填 1"""
dataframe.loc[
(
# SMA20 上穿 SMA60:這根在上、前一根在下
(dataframe['sma_fast'] > dataframe['sma_slow']) &
(dataframe['sma_fast'].shift(1) <= dataframe['sma_slow'].shift(1)) &
# RSI 未超買(避免在高點追進)
(dataframe['rsi'] < 70) &
# 慢線已有足夠資料(60 根以上才不是 NaN)
(dataframe['sma_slow'].notna())
),
'enter_long'
] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""第三步:定義出場條件,符合條件就在 exit_long 填 1"""
dataframe.loc[
(
# SMA20 下穿 SMA60
(dataframe['sma_fast'] < dataframe['sma_slow']) &
(dataframe['sma_fast'].shift(1) >= dataframe['sma_slow'].shift(1))
) |
(
# RSI 進入極度超買就直接出場
(dataframe['rsi'] > 80)
),
'exit_long'
] = 1
return dataframe
注意幾個細節:
- 欄位命名(
sma_fast、sma_slow)必須在三個方法裡保持一致 enter_long/exit_long是 freqtrade 現行版本(2026.5)的欄位名稱,不是舊版的buy/sellshift(1)是防止前視偏差的關鍵:我們在判斷「這根K線」有沒有穿越時,要看「前一根K線」的狀態
下載 2 年歷史資料
回測越長、越包含不同市場環境(多頭/空頭/震盪盤),結果才越有參考價值。這次我們下載 2023–2025 兩年資料:
freqtrade download-data \
--exchange binance \
--pairs BTC/USDT \
--timeframe 1h \
--timerange 20230101-20250101
下載完後確認資料已存入:
freqtrade list-data --exchange binance --pairs BTC/USDT --timeframe 1h
你應該會看到這筆資料:行數大約在 17,000 筆(兩年 × 365 天 × 24 小時)。如果下載中途遇到 timeout,請參考常見坑第 4 條。
跑回測,一定要帶費用參數
freqtrade backtesting \
--config config.json \
--strategy DualMACross \
--timerange 20230101-20250101 \
--fees 0.001
--fees 0.001 代表單邊手續費 0.1%(round trip 合計 0.2%)。這個參數是區分「有意義的回測」和「數字遊戲」的最關鍵差別。漏掉它,回測等於廢紙。
回測通常跑 30 秒到 2 分鐘,結束後你會看到完整的統計表。
讀懂回測結果:實際數字範例
一個典型的雙均線策略結果長這樣:
============================== BACKTESTING REPORT ==============================
| Pair | Entries | Avg Profit % | Total Profit USDT | Win% |
|------------|---------|--------------|-------------------|-------|
| BTC/USDT | 34 | 0.61 | +62.4 | 55.9 |
============================ SUMMARY METRICS ============================
Backtesting from 2023-01-01 00:00:00 up to 2025-01-01 00:00:00 (730 days)
Initial capital: 1000.00 USDT
Final capital : 1062.4 USDT
Absolute profit: +62.4 USDT
Total profit %: 6.24%
Trades per day: 0.05
Avg. stake amount: 997.22 USDT
Total trade duration: 4 days, 2:15:00
Max Drawdown: -16.8%
Sharpe Ratio: 0.38
Sortino Ratio: 0.54
Profit Factor: 1.31
Winrate: 55.9%
Expectancy (Ratio): 0.61 (0.02)
逐一解讀:
- 總報酬 6.24%:兩年才 6.24%,同期 BTC 從約 16,000 美元漲到超過 90,000 美元(漲幅近 460%)。你的策略完全沒跑贏「買著不動」,甚至沒跑贏銀行定存。這個結果很常見——它告訴你這個策略版本需要改進。
- 最大回撤 -16.8%:帳戶最慘的時候縮水了將近 17%。問自己:如果你投入了 50 萬,你能否接受餘額掉到 41.6 萬,然後繼續讓策略跑?如果答案是否,這個策略的風險對你來說就太高了。
- Sharpe Ratio 0.38:遠低於 1.0 的及格線,代表你承擔的風險遠大於獲得的報酬。
- Profit Factor 1.31:勉強大於 1,代表確實是賺的,但沒有競爭力。
- 每天平均 0.05 筆交易:30 天才約 1.5 筆。低頻,費用負擔低,這點是優點。

做對照實驗:不加費用再跑一次
freqtrade backtesting \
--config config.json \
--strategy DualMACross \
--timerange 20230101-20250101 \
--fees 0
把這次的結果和上一次對比。典型情況是:沒有手續費的版本總報酬可能跳到 20~30%,Sharpe Ratio 也會好看很多。
**那個差距,就是網路上那些「漂亮回測」和「真實績效」之間的鴻溝。**看到有人分享誇張的回測截圖,第一個問題永遠是:「有沒有扣手續費和滑點?」沒有的話,那個截圖沒有任何參考價值。
常見坑
坑 1:回測跑完 Entries 為 0,一筆交易都沒有
BACKTESTING REPORT
| Pair | Entries | Avg Profit % | ...
|------------|---------|---
| BTC/USDT | 0 | None |
原因:九成是訊號欄位名稱用錯了。舊版 freqtrade 用 'buy' 和 'sell',現行版本(2026.5)改成 'enter_long' 和 'exit_long'。如果你的 populate_entry_trend 裡寫的是 dataframe['buy'] = 1,freqtrade 不會報錯,但直接忽略這個欄位,導致零交易。
解法:全文搜尋把 'buy' 改成 'enter_long'、'sell' 改成 'exit_long'、方法名稱 populate_buy_trend 改成 populate_entry_trend。舊教學和 AI 生成的舊版程式碼很常踩這個坑。
坑 2:AttributeError: 'DataFrame' object has no attribute 'sma_fast'
AttributeError: 'DataFrame' object has no attribute 'sma_fast'
File ".../user_data/strategies/DualMACross.py", line 62, in populate_entry_trend
(dataframe['sma_fast'] > dataframe['sma_slow']) &
KeyError: 'sma_fast'
原因:最常見的兩個情況:一是 populate_indicators() 裡的欄位名稱和 populate_entry_trend() 裡用的名稱拼字不一致(例如一個是 'sma_fast',另一個是 'fast_sma');二是 populate_indicators() 整個方法因為縮排錯誤脫離了類別定義,變成獨立函式而不是方法,所以根本沒被呼叫。
解法:先確認欄位名稱一致;再用 freqtrade list-strategies 確認策略可以被載入,若載入失敗通常會給出 Python 語法錯誤提示。
坑 3:回測選的區間全是多頭市場,數字特別好看
症狀:指定 --timerange 20231001-20241201 回測報酬超過 80%。但那段時間 BTC 恰好從約 27,000 美元漲到 98,000 美元(漲幅約 260%),任何做多策略幾乎都漂亮。換一段你不知道結果的時間測,策略立刻原形畢露。
這不是系統錯誤,而是確認偏差(Confirmation Bias)——你的潛意識在找「支持策略有效」的區間。
解法:回測期間必須包含空頭與震盪盤。建議至少測試 2 年(涵蓋多種市場環境),並額外做一段嚴峻的空頭期驗證:
# 專門測試 2022 年熊市期間(BTC 跌幅超過 70%)
freqtrade backtesting \
--strategy DualMACross \
--timerange 20220101-20230101 \
--fees 0.001
如果策略在熊市的回撤超過你的心理承受上限,這個策略在實盤裡你大概率撐不到它回血。
坑 4:下載資料時 ReadTimeout 或 Rate limit exceeded
ERROR - Got exception from download_data:
requests.exceptions.ReadTimeout: HTTPSConnectionPool(host='api.binance.com', port=443):
Read timed out. (read timeout=30)
原因:Binance API 限速,或在台灣連線速度較慢。解法:分段下載,每次縮短時間範圍:
# 分 4 段下載,每段 6 個月
freqtrade download-data --pairs BTC/USDT --timeframe 1h --timerange 20230101-20230701
freqtrade download-data --pairs BTC/USDT --timeframe 1h --timerange 20230701-20240101
freqtrade download-data --pairs BTC/USDT --timeframe 1h --timerange 20240101-20240701
freqtrade download-data --pairs BTC/USDT --timeframe 1h --timerange 20240701-20250101
如果連 Binance 穩定性就是差,可以改用 --exchange bybit 或 --exchange okx,資料品質相近,不影響策略回測。
坑 5:回測結果和 freqtrade 版本文件描述不一樣
症狀:按照某篇教學操作,但統計表的欄位名稱或格式對不起來。
原因:freqtrade 版本更新頻率高(2026 年已到 2026.5),較舊的部落格文章或課程可能對應 2024/2025 的版本。每次升級前先查 GitHub Releases 的 Changelog。如果出現未知行為,把錯誤訊息貼給 Claude 或到 freqtrade Discussions 搜尋——通常是已知問題。
作業
完成核心實戰:把
DualMACross.py寫進user_data/strategies/,分別用「有費用(--fees 0.001)」和「無費用(--fees 0)」跑一次 2023–2025 回測。記錄兩次的 Total Profit %、Sharpe Ratio、Max Drawdown,算出費用對報酬率的影響百分比。熊市壓力測試:把
--timerange改成20220101-20230101(2022 年 BTC 整年崩跌),看你的策略在最慘市場的最大回撤是多少。問自己:那個數字,你真實投入資金後能接受嗎?參數變化實驗:把策略的均線週期從
20/60改成10/30,再跑一次完整回測。比較交易次數、勝率、Sharpe Ratio 的變化——你正在親手體驗第 3 課講的「過擬合」是怎麼發生的。選做:在
populate_entry_trend裡加入 MACD 確認信號(進場條件再加上macd > macd_signal),看複合信號有沒有提升 Sharpe Ratio。如果有提升,同時注意 Entries 是不是大幅減少了——信號越多重疊的機會越少,要確認交易次數夠,費用的統計才有意義。
下一課預告
你現在已經能跑出一個「有意義的回測」:知道怎麼寫策略檔、怎麼看指標、怎麼算費用的殺傷力。但這個策略的進出場訊號只用了技術指標,完全沒考慮市場的宏觀狀態——這條均線策略在趨勢明確的市場表現尚可,但在震盪盤裡可能反覆被假突破洗到。
第 5 課《AI 的正確位置:訊號層,不是執行層》會帶你把 LLM 引進策略系統:用 Claude 分析新聞情緒、標注當前市場所處的 Regime(趨勢/震盪/高波動),並把這個判斷輸出成可以接進 freqtrade 的額外訊號分數。更重要的是,第 5 課會把業界共識說清楚——AI 在哪個層發力、為什麼絕對不能讓 LLM 直接下單,以及怎麼安全地把 AI 整合進你已經建好的回測框架。