精華筆記

· @aihub.tw

AI 量化交易

回測實戰:freqtrade 上手

回測實戰:freqtrade 上手

你花了幾個晚上寫好策略,在 Jupyter Notebook 裡畫出一條往右上方爬的資金曲線,參數調得剛剛好,年化報酬看起來超過 40%。結果搬進 freqtrade、接上真實資料一跑——報酬腰斬,或者根本虧損。這不是倒楣,是因為你的回測根本沒有算對:沒扣手續費、指標計算有前視偏差、回測區間只選了多頭市場。

這堂課要解決的核心問題就是這個:怎麼跑一個「有意義」的回測,而不是一個「好看」的回測。兩者差距有時候只是有沒有在指令結尾加上 --fees 0.001,但這個細節就是真實績效和數字遊戲的分水嶺。

風險警語 本課程所有內容僅供學習與研究用途,不構成任何投資建議。回測績效無論多漂亮,都不代表未來實盤結果。量化交易在實盤環境可能因策略失效、市場環境轉變或執行問題造成本金損失,且損失速度可能極快。本課程全程使用模擬盤教學;若日後決定進入實盤,風險由你自行承擔。請先確認自身風險承受能力。
這堂課適合誰 適合:已完成第 3 課、想要學習用 freqtrade 做嚴謹回測的人。本課程屬工程師應用專區。需要基礎:Python 初階(能讀懂類別繼承與 DataFrame 操作即可,不需要演算法底子)。前置課:第 1 課(freqtrade 安裝)、第 3 課(策略基礎與均線邏輯)。

這堂學什麼

  • freqtrade 策略檔結構:三個核心函式(populate_indicatorspopulate_entry_trendpopulate_exit_trend)的職責與寫法
  • 完整回測流程:下載歷史資料 → 寫策略 → 跑回測 → 讀指標
  • 五大關鍵指標解讀:夏普值、最大回撤、勝率、獲利因子哪個最重要,不能只看報酬率
  • 手續費與滑點殺傷力實算:具體算出「策略每月要賺多少才能保本」
  • 把第 3 課的雙均線策略搬進 freqtrade 框架,用真實數字驗收它

觀念一:策略檔的骨架

freqtrade 的每個策略都是一個繼承自 IStrategy 的 Python 類別。這個框架幫你把底層事務全包了——資料載入、倉位追蹤、手續費計算、報告生成——你只需要回答三個問題:「計算哪些指標?」「什麼時候進場?」「什麼時候出場?」對應到三個必須實作的方法。

freqtrade 策略檔結構圖:IStrategy 繼承出 DualMACross,三個核心方法依序被呼叫,加上 minimal_roi 與 stoploss 兩組屬性

freqtrade 官方文件(2026 年 7 月實況) 圖:freqtrade 官方文件(2026 年 7 月實況),來源:freqtrade.io

除了三個方法,還有兩組必須設定的類別屬性:

  • minimal_roi:最小獲利目標。格式是 {持倉分鐘數: 報酬率}——當達到對應的報酬目標,系統自動出場。這是除了停損之外的另一道保護機制。
  • stoploss:停損比例,填負數(例如 -0.05 代表虧損 5% 時強制停損)。

freqtrade 2026 版的進出場欄位名稱是 enter_longexit_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% 年化就已是頂尖水準了。

手續費殺傷力堆疊長條圖:每週交易次數 1/2/4/8/10 對應的年化費用,4 次/週已達 58%、超過頂尖基金 20-30% 的年化目標

結論:低頻策略才是散戶的可行路。 每次交易的費用是固定的,交易頻率越低,費用佔總報酬的比例就越小。那些高頻、每天幾十次訊號的策略,在散戶的費用結構下,光養手續費就很辛苦。

觀念四:回測漂亮 ≠ 實盤賺錢

即使回測有加手續費、有扣滑點、數字看起來合理,實盤還是可能表現不一樣。這裡有五個系統性的落差來源:

回測與實盤落差的五個來源對照表:成交、費用、資料、市場環境、幣種樣本,左欄回測假設對右欄實盤現實

  1. 前視偏差(Lookahead Bias):指標計算不小心用到當時還沒有的未來資料。最常見是忘記對信號做 shift(1),讓「明天的收盤價」影響「今天的進場決定」。freqtrade 2026.5 版已加強偵測機制並會主動發出警告,但不保證能抓到所有情況。

  2. 過擬合(Overfitting):參數是在歷史資料上「調出來的最佳解」,不是市場的普遍規律。第 3 課已詳細討論過這個問題,這裡只提醒:回測數字越漂亮、越是要懷疑是否過擬合了。

  3. 市場環境轉變(Regime Change):2021 年多頭市場調出來的動量參數,在 2022 年熊市可能整年虧損。市場的波動率、流動性結構都在改變,策略沒有「永久有效」這種東西。

  4. 流動性假設:回測假設「你想買多少就買多少」。現實中,小市值幣對或下大單量時,可能成交不到你預期的價格,實際滑點遠高於保守估計。

  5. 生存者偏差(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_fastsma_slow)必須在三個方法裡保持一致
  • enter_long/exit_long 是 freqtrade 現行版本(2026.5)的欄位名稱,不是舊版的 buy/sell
  • shift(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 統計表中 Sharpe 0.38、Max Drawdown -16.8%、Profit Factor 1.31、總報酬 6.24% 各以不同顏色標注解讀

做對照實驗:不加費用再跑一次

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:下載資料時 ReadTimeoutRate 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 搜尋——通常是已知問題。

作業

  1. 完成核心實戰:把 DualMACross.py 寫進 user_data/strategies/,分別用「有費用(--fees 0.001)」和「無費用(--fees 0)」跑一次 2023–2025 回測。記錄兩次的 Total Profit %、Sharpe Ratio、Max Drawdown,算出費用對報酬率的影響百分比。

  2. 熊市壓力測試:把 --timerange 改成 20220101-20230101(2022 年 BTC 整年崩跌),看你的策略在最慘市場的最大回撤是多少。問自己:那個數字,你真實投入資金後能接受嗎?

  3. 參數變化實驗:把策略的均線週期從 20/60 改成 10/30,再跑一次完整回測。比較交易次數、勝率、Sharpe Ratio 的變化——你正在親手體驗第 3 課講的「過擬合」是怎麼發生的。

  4. 選做:在 populate_entry_trend 裡加入 MACD 確認信號(進場條件再加上 macd > macd_signal),看複合信號有沒有提升 Sharpe Ratio。如果有提升,同時注意 Entries 是不是大幅減少了——信號越多重疊的機會越少,要確認交易次數夠,費用的統計才有意義。

下一課預告

你現在已經能跑出一個「有意義的回測」:知道怎麼寫策略檔、怎麼看指標、怎麼算費用的殺傷力。但這個策略的進出場訊號只用了技術指標,完全沒考慮市場的宏觀狀態——這條均線策略在趨勢明確的市場表現尚可,但在震盪盤裡可能反覆被假突破洗到。

第 5 課《AI 的正確位置:訊號層,不是執行層》會帶你把 LLM 引進策略系統:用 Claude 分析新聞情緒、標注當前市場所處的 Regime(趨勢/震盪/高波動),並把這個判斷輸出成可以接進 freqtrade 的額外訊號分數。更重要的是,第 5 課會把業界共識說清楚——AI 在哪個層發力、為什麼絕對不能讓 LLM 直接下單,以及怎麼安全地把 AI 整合進你已經建好的回測框架。

再次提醒:風險 本課程的所有操作都在 `dry_run: true`(模擬盤)環境下進行。回測績效再好看,都不保證實盤結果相同。量化系統在實盤環境可能因策略失效、市場環境轉變、API 故障或設定錯誤造成損失,且速度可能非常快。在你對自己的系統有充分理解並完成完整模擬盤測試之前,請勿投入無法承受損失的資金。本課程不提供任何投資建議。

#AI 量化交易#freqtrade#回測#backtesting#量化策略

← 回所有文章