抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

最近用 Freqtrade 实现了一个经典的 MACD 策略——金叉做多、死叉做空,配合固定比例的止盈止损。跑了一遍回测后发现收益出乎意料地不错,趁热把整个过程记录下来,方便以后复盘和迭代。

策略的核心逻辑非常简洁:

  • 入场信号:MACD 金叉(DIF 上穿 DEA)做多,死叉(DIF 下穿 DEA)做空,同时配合趋势过滤条件;
  • 止盈止损出场:固定 8% 止盈(minimal_roi = {"0": 0.08})、-3% 止损(stoploss = -0.03),不使用移动止损。注意代码中虽然定义了 populate_exit_trend(反向交叉平仓逻辑),但 use_exit_signal = False 意味着出场信号并未启用——所有仓位均由 ROI 止盈或止损触发平仓;
  • 趋势过滤:做多要求 EMA15 > EMA30(短期均线在长期均线上方),做空则相反;同时要求 ADX(14) 的 20 周期均线 > 25 且当前 ADX 高于其自身均线,确保市场处于趋势状态而非横盘震荡。

下面按照”策略代码 → 回测配置 → 结果分析 → 可视化”的顺序,完整走一遍流程。


策略代码

完整策略文件如下。几个值得注意的细节:

  • 多空双向can_short = True,同时处理做多和做空逻辑;
  • 出场方式use_exit_signal = False,出场不依赖自定义信号,完全由 ROI(+8% 止盈)和 stoploss(-3% 止损)接管。populate_exit_trend 中的反向交叉逻辑在回测中并未实际生效;
  • 入场趋势过滤:做多要求 EMA15 > EMA30(短期均线在长期均线上方),做空则相反;ADX 过滤采用双重条件——ADX(14) 当前值高于其 20 周期均线(表明趋势强度在上升),且该均线本身 > 25(表明整体处于趋势市而非震荡市),两层过滤叠加能更有效地避开横盘噪音;
  • 15 分钟K线:在信号频率和稳定性之间取了一个平衡;
  • 参数可优化:MACD 的快慢线周期和信号线周期均设为 IntParameter,方便后续用 Hyperopt 调参。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
"""
macd_strategy.py - MACD golden-cross / death-cross strategy.

Buy: MACD crosses above signal line (golden cross)
Sell: MACD crosses below signal line (death cross)

Uses talib for MACD calculation (available in freqtrade docker).
"""

from pandas import DataFrame

from freqtrade.strategy import IStrategy, IntParameter

import talib


class MACDStrategy(IStrategy):
"""MACD 金叉买入,死叉卖出"""

INTERFACE_VERSION = 3
can_short: bool = True

# ROI and stoploss for both long and short
minimal_roi = {"0": 0.08}
stoploss = -0.03
trailing_stop = False

# 15m timeframe — good balance of signal frequency and quality
timeframe = "15m"
process_only_new_candles = True

use_exit_signal = False
exit_profit_only = False

# Need enough candles for MACD slow EMA to stabilize
startup_candle_count: int = 100

# --- Hyperoptable MACD parameters ----------------------------------------
fast_period = IntParameter(low=8, high=20, default=12, space="buy")
slow_period = IntParameter(low=20, high=40, default=26, space="buy")
signal_period = IntParameter(low=5, high=15, default=9, space="buy")

def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
macd, macdsignal, macdhist = talib.MACD(
dataframe["close"],
fastperiod=self.fast_period.value,
slowperiod=self.slow_period.value,
signalperiod=self.signal_period.value,
)
dataframe["macd"] = macd
dataframe["macdsignal"] = macdsignal
dataframe["macdhist"] = macdhist

# 添加 EMA15 和 EMA30 作为趋势过滤器
dataframe["ema15"] = talib.EMA(dataframe["close"], timeperiod=15)
dataframe["ema30"] = talib.EMA(dataframe["close"], timeperiod=30)
dataframe["adx14"] = talib.ADX(dataframe["high"], dataframe["low"], dataframe["close"], timeperiod=14)
dataframe["atr14"] = talib.ATR(dataframe["high"], dataframe["low"], dataframe["close"], timeperiod=14)
dataframe["ema200"] = talib.EMA(dataframe["close"], timeperiod=200)
return dataframe

def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# ATR vs candle length
candle_move = dataframe["close"] - dataframe["open"]

# Long: MACD golden cross (DIF crosses ABOVE DEA)
dataframe.loc[
(
(dataframe["macd"] > dataframe["macdsignal"])
& (dataframe["macd"].shift(1) <= dataframe["macdsignal"].shift(1))
& (dataframe["volume"] > 0)
& (dataframe["ema15"] > dataframe["ema30"]) # 趋势过滤器:仅在 EMA15 高于 EMA30 时考虑做多
& (dataframe["adx14"] > dataframe["adx14"].rolling(20).mean())
# & (dataframe["adx14"] > 25)
& (dataframe["adx14"].rolling(20).mean() > 25)
# & (abs(candle_move) < dataframe["atr14"].rolling(5).mean()*1.5)
# & (dataframe["ema200"] > dataframe["ema200"].shift(1))
),
"enter_long",
] = 1

# Short: MACD death cross (DIF crosses BELOW DEA)
dataframe.loc[
(
(dataframe["macd"] < dataframe["macdsignal"])
& (dataframe["macd"].shift(1) >= dataframe["macdsignal"].shift(1))
& (dataframe["volume"] > 0)
& (dataframe["ema15"] < dataframe["ema30"]) # 趋势过滤器:仅在 EMA15 低于 EMA30 时考虑做空
& (dataframe["adx14"] > dataframe["adx14"].rolling(20).mean())
# & (dataframe["adx14"] > 25)
& (dataframe["adx14"].rolling(20).mean() > 25)
# & (abs(candle_move) < dataframe["atr14"].rolling(5).mean()*1.5)
# & (dataframe["ema200"] < dataframe["ema200"].shift(1))
),
"enter_short",
] = 1

return dataframe

def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Exit long: MACD death cross
dataframe.loc[
(
(dataframe["macd"] < dataframe["macdsignal"])
& (dataframe["macd"].shift(1) >= dataframe["macdsignal"].shift(1))
& (dataframe["volume"] > 0)
),
"exit_long",
] = 1

# Exit short: MACD golden cross
dataframe.loc[
(
(dataframe["macd"] > dataframe["macdsignal"])
& (dataframe["macd"].shift(1) <= dataframe["macdsignal"].shift(1))
& (dataframe["volume"] > 0)
),
"exit_short",
] = 1

return dataframe

回测

Freqtrade 的回测只需要一行命令,--strategy 参数传入策略类名即可:

1
docker exec freqtrade freqtrade backtesting --strategy MACDStrategy --timerange 20250101-20260705

回测结果分析

回测区间为 2025 年 1 月 1 日至 2026 年 7 月 5 日,共计约 550 天。以下是完整回测报告:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
2026-07-07 09:42:28,610 - freqtrade.optimize.backtesting - INFO - Backtesting with data from 2025-01-01 00:00:00 up to 2026-07-05 00:00:00 (550 days).
2026-07-07 09:43:17,851 - freqtrade.misc - INFO - dumping json to "/freqtrade/user_data/backtest_results/backtest-result-2026-07-07_09-43-17.meta.json"
Result for strategy MACDStrategy
BACKTESTING REPORT
┏━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Pair ┃ Trades ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
┡━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
│ BTC/USDT:USDT │ 67 │ 0.37 │ 186.501 │ 18.65 │ 5 days, 14:32:00 │ 21 0 46 31.3 │
│ TOTAL │ 67 │ 0.37 │ 186.501 │ 18.65 │ 5 days, 14:32:00 │ 21 0 46 31.3 │
└───────────────┴────────┴──────────────┴─────────────────┴──────────────┴──────────────────┴────────────────────────┘
LEFT OPEN TRADES REPORT
┏━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Pair ┃ Trades ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
┡━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
│ TOTAL │ 0 │ 0.0 │ 0.000 │ 0.0 │ 0:00 │ 0 0 0 0 │
└───────┴────────┴──────────────┴─────────────────┴──────────────┴──────────────┴────────────────────────┘
ENTER TAG STATS
┏━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Enter Tag ┃ Entries ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
┡━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
│ OTHER │ 67 │ 0.37 │ 186.501 │ 18.65 │ 5 days, 14:32:00 │ 21 0 46 31.3 │
│ TOTAL │ 67 │ 0.37 │ 186.501 │ 18.65 │ 5 days, 14:32:00 │ 21 0 46 31.3 │
└───────────┴─────────┴──────────────┴─────────────────┴──────────────┴──────────────────┴────────────────────────┘
EXIT REASON STATS
┏━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Exit Reason ┃ Exits ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
┡━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
│ roi │ 21 │ 8.0 │ 1543.839 │ 154.38 │ 5 days, 4:46:00 │ 21 0 0 100 │
│ stop_loss │ 46 │ -3.11 │ -1357.338 │ -135.73 │ 5 days, 19:00:00 │ 0 0 46 0 │
│ TOTAL │ 67 │ 0.37 │ 186.501 │ 18.65 │ 5 days, 14:32:00 │ 21 0 46 31.3 │
└─────────────┴───────┴──────────────┴─────────────────┴──────────────┴──────────────────┴────────────────────────┘
MIXED TAG STATS
┏━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Enter Tag ┃ Exit Reason ┃ Trades ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃
┡━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━┩
│ │ roi │ 21 │ 8.0 │ 1543.839 │ 154.38 │ 5 days, 4:46:00 │ 21 0 0 100 │
│ │ stop_loss │ 46 │ -3.11 │ -1357.338 │ -135.73 │ 5 days, 19:00:00 │ 0 0 46 0 │
│ TOTAL │ │ 67 │ 0.37 │ 186.501 │ 18.65 │ 5 days, 14:32:00 │ 21 0 46 31.3 │
└───────────┴─────────────┴────────┴──────────────┴─────────────────┴──────────────┴──────────────────┴────────────────────────┘
SUMMARY METRICS
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Metric ┃ Value ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ Backtesting from │ 2025-01-01 00:00:00 │
│ Backtesting to │ 2026-07-05 00:00:00 │
│ Trading Mode │ Isolated Futures │
│ Max open trades │ 1 │
│ │ │
│ Total/Daily Avg Trades │ 67 / 0.12 │
│ Starting balance │ 1000 USDT │
│ Final balance │ 1186.501 USDT │
│ Absolute profit │ 186.501 USDT │
│ Total profit % │ 18.65% │
│ CAGR % │ 12.02% │
│ Sharpe (closed trades) │ 0.13 │
│ Sortino (closed trades) │ 1.55 │
│ Calmar (closed trades) │ 2.86 │
│ SQN │ 0.47 │
│ Profit factor │ 1.14 │
│ Expectancy (Ratio) │ 2.78 (0.09) │
│ Avg. daily profit │ 0.339 USDT │
│ Avg. stake amount │ 940.289 USDT │
│ Market change │ -32.69% │
│ Total trade volume │ 125736.38 USDT │
│ │ │
│ Long / Short trades │ 30 / 37 │
│ Long / Short profit % │ -8.21% / 26.86% │
│ Long / Short profit USDT │ -82.142 / 268.643 │
│ │ │
│ Best Pair │ BTC/USDT:USDT 18.65% │
│ Worst Pair │ BTC/USDT:USDT 18.65% │
│ Best trade │ BTC/USDT:USDT 8.00% │
│ Worst trade │ BTC/USDT:USDT -3.43% │
│ Best day │ 150.853 USDT │
│ Worst day │ -58.42 USDT │
│ Days win/draw/lose │ 20 / 475 / 44 │
│ Min/Max/Avg. Duration Winners │ 0d 00:15 / 15d 12:00 / 5d 04:46 │
│ Min/Max/Avg. Duration Losers │ 0d 04:15 / 30d 03:30 / 5d 19:00 │
│ Max Consecutive Wins / Loss │ 3 / 9 │
│ Rejected Entry signals │ 0 │
│ Entry/Exit Timeouts │ 0 / 0 │
│ │ │
│ Min/Max balance (closed trades) │ 761.695 USDT / 1223.043 USDT │
│ Max % of account underwater │ 23.83% │
│ Absolute drawdown │ 274.376 USDT (22.66%) │
│ Drawdown duration │ 92 days 09:30:00 │
│ Profit at drawdown start │ 210.83 USDT │
│ Profit at drawdown end │ -63.546 USDT │
│ Drawdown start │ 2026-02-24 04:00:00 │
│ Drawdown end │ 2026-05-27 13:30:00 │
│ │ │
│ Wallet based Metrics │ │
│ Min/Max balance (wallet balance) │ 761.695 USDT / 1223.043 USDT │
│ Min/Max balance dates (wallet balance) │ 2025-02-18 17:45:00 / 2026-06-25 14:00:00 │
│ Max % of account underwater (balance) │ 23.83% │
│ Absolute drawdown (wallet balance) │ 274.376 USDT (22.66%) │
│ Drawdown duration │ 92 days 09:30:00 │
│ Profit at drawdown start │ 210.83 USDT │
│ Profit at drawdown end │ -63.546 USDT │
│ Drawdown start │ 2026-02-24 04:15:00 │
│ Drawdown end │ 2026-05-27 13:45:00 │
│ Sharpe (daily wallet balance) │ 0.50 │
│ Sortino (daily wallet balance) │ 1.35 │
│ Calmar (daily wallet balance) │ 2.86 │
└────────────────────────────────────────┴──────────────────────────────────────────┘

Backtested 2025-01-01 00:00:00 -> 2026-07-05 00:00:00 | Max open trades : 1
STRATEGY SUMMARY
┏━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┓
┃ Strategy ┃ Trades ┃ Avg Profit % ┃ Tot Profit USDT ┃ Tot Profit % ┃ Avg Duration ┃ Win Draw Loss Win% ┃ Drawdown ┃
┡━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━┩
│ MACDStrategy │ 67 │ 0.37 │ 186.501 │ 18.65 │ 5 days, 14:32:00 │ 21 0 46 31.3 │ 274.376 USDT 22.66% │
└──────────────┴────────┴──────────────┴─────────────────┴──────────────┴──────────────────┴────────────────────────┴──────────────────────┘

从回测数据中可以看出几个关键点:

收益概览

  • 初始资金 1000 USDT,最终资金 1186.5 USDT,总收益率 18.65%,年化收益率(CAGR)12.02%
  • 值得注意的是,同期 BTC 市场整体下跌了 32.69%,策略在熊市中依然实现了正收益,说明做空端贡献了可观的 alpha。

多空表现差异明显

  • 做多 30 笔,亏损 -8.21%;做空 37 笔,盈利 26.86%。这与同期市场下跌的大背景高度吻合——空头策略在熊市中天然占优。

盈亏比分析

  • 胜率仅 31.3%(21 胜 / 46 负),但盈利单平均收益 8.0%,亏损单平均亏损仅 -3.11%(被止损截断),盈亏比高达 2.57:1
  • 这是一个典型的”低胜率、高盈亏比”策略——靠少数大盈利覆盖多次小亏损,最终实现整体盈利。Profit Factor 为 1.14,意味着每亏 1 元能赚回 1.14 元。

回撤风险

  • 最大回撤 22.66%(约 274 USDT),回撤持续了 92 天(2026 年 2 月至 5 月),这段时间账户从盈利 210 USDT 跌至亏损 63 USDT。
  • 最大连续亏损 9 笔,对心态是不小的考验。

改进方向

  1. 降低回撤:考虑加入 ATR 动态仓位管理或波动率过滤器,在波动剧烈时减小头寸;
  2. 提升胜率:代码中已有注释掉的 EMA200 趋势过滤和 ATR 蜡烛实体过滤,可以尝试开启后对比效果;
  3. 参数优化:MACD 的三个周期参数已设为可超参优化,后续可以通过 Hyperopt 寻找更优组合;
  4. 多交易对:目前仅回测了 BTC/USDT,扩展到 ETH 等其他主流币种可能提升策略稳健性。

Web UI 可视化

除了命令行输出,Freqtrade 还支持通过 Web UI 在浏览器中查看回测结果。在 K 线图上直接标注买卖点(三角图标),对于分析策略的入场出场时机非常有帮助。

首先在 user_data/config.json 中启用 API 服务器:

1
2
3
4
5
6
7
8
9
10
11
12
"api_server": {
"enabled": true,
"listen_ip_address": "0.0.0.0",
"listen_port": 8081,
"verbosity": "error",
"enable_openapi": false,
"jwt_secret_key": "5146xxxxxxxxxxxxxxxx68fb87",
"ws_token": "BkxxxxxxxxxxxxxxxxxOg",
"CORS_origins": ["http://127.0.0.1:8081"], # 外部访问链接
"username": "freqtrade",
"password": "7xxxxxxxxxiw"
},

这里 CORS_origins 决定了允许哪些来源访问 Web UI,需要与实际访问地址一致。

对应的 docker-compose.yml 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
---
services:
freqtrade-web:
image: freqtradeorg/freqtrade:stable
restart: unless-stopped
container_name: freqtrade-web
network_mode: "host" # 打通docker的127.0.0.1与主机
volumes:
- "./user_data:/freqtrade/user_data"
command: >
trade
--logfile /dev/null
--db-url sqlite://
--config /freqtrade/user_data/config_web.json
--strategy SampleStrategy

注意 network_mode: "host" 让容器直接使用宿主机网络,省去了端口映射的麻烦。

启动命令:

1
2
# --no-deps多个服务避免重复启动
docker compose up -d --no-deps freqtrade-web

如果服务部署在远程机器上,通过 SSH 隧道将端口转发到本地即可:

1
ssh -L 8081:localhost:8081 user@remote-host

然后在浏览器访问 http://127.0.0.1:8081 就能看到 Web 界面了。K 线上的三角标记清晰标注了每一笔交易的进出场点,对于复盘策略表现、发现潜在问题非常有价值。

图片加载失败


小结

这个 MACD 策略虽然逻辑简单,但在 550 天的回测中跑出了 18.65% 的收益,同期市场下跌 32%,表现可圈可点。当前版本还有很多可以优化的地方——代码中已经预留了几条被注释掉的过滤条件,后续可以通过 Hyperopt 系统性地调参,并尝试扩展到更多交易对。策略开发是一个不断迭代的过程,先跑通再打磨,这个思路对我个人来说比较有效。

评论



Powered by Hexo | Theme keep Volantis

本站总访问量 总访客数 🌎