Skip to article frontmatterSkip to article content
Freqtrade 策略开发

策略自定义指南

如何开发自己的交易策略

策略自定义

本页将介绍如何自定义你的策略、添加新指标以及设置交易规则。

如果你还没有了解过,建议先阅读:

开发你自己的策略

机器人自带了一个默认策略文件。

此外,策略仓库中还提供了其他策略。

不过你很可能有自己的策略想法。本文档旨在帮助你将想法转化为可运行的策略。

生成策略模板

你可以通过以下命令快速开始:

freqtrade new-strategy --strategy AwesomeStrategy

这会基于模板创建一个名为 AwesomeStrategy 的新策略,文件路径为 user_data/strategies/AwesomeStrategy.py

策略结构剖析

一个策略文件包含构建策略逻辑所需的全部信息:

机器人自带一个名为 SampleStrategy 的示例策略,可作为参考:user_data/strategies/sample_strategy.py

你可以用参数 --strategy SampleStrategy 进行测试。注意这里用的是策略类名,而不是文件名。

此外,还有一个名为 INTERFACE_VERSION 的属性,用于定义策略接口的版本。当前版本为 3,如果未在策略中显式设置,则默认为 3

你可能会看到旧策略设置为接口版本 2,未来版本会要求升级到 v3

trade 命令即可启动机器人进入 drylive 模式:

freqtrade trade --strategy AwesomeStrategy

机器人运行模式

Freqtrade 策略可在 5 种主要模式下被机器人处理:

关于如何设置 drylive 模式,请查阅配置文档

测试时请始终使用 dry 模式,这样可以在不冒资金风险的情况下了解策略实际表现。

深入剖析

以下内容将以 user_data/strategies/sample_strategy.py 为参考。

DataFrame

Freqtrade 使用 pandas 存储/提供 K 线(OHLCV)数据。 Pandas 是处理表格数据的强大库。

DataFrame 的每一行对应一根K线,最新的完整K线总是排在最后(按日期排序)。

用 pandas 的 head() 查看前几行:

> dataframe.head()
                       date      open      high       low     close     volume
0 2021-11-09 23:25:00+00:00  67279.67  67321.84  67255.01  67300.97   44.62253
1 2021-11-09 23:30:00+00:00  67300.97  67301.34  67183.03  67187.01   61.38076
2 2021-11-09 23:35:00+00:00  67187.02  67187.02  67031.93  67123.81  113.42728
3 2021-11-09 23:40:00+00:00  67123.80  67222.40  67080.33  67160.48   78.96008
4 2021-11-09 23:45:00+00:00  67160.48  67160.48  66901.26  66943.37  111.39292

DataFrame 是一个表格,列不是单一值,而是一组数据。因此,像下面这样直接用 Python 比较会报错:

    if dataframe['rsi'] > 30:
        dataframe['enter_long'] = 1

上述写法会报错:The truth value of a Series is ambiguous [...]

应改为 pandas 向量化写法,对整个 dataframe 执行操作:

    dataframe.loc[
        (dataframe['rsi'] > 30)
    , 'enter_long'] = 1

这样会在 RSI 大于 30 时,为新列 enter_long 赋值 1。

Freqtrade 会用这个新列作为入场信号,假定下一根K线开盘时开仓。

Pandas 支持高效的向量化计算,建议尽量避免循环,直接用向量化方法。

向量化操作会对整列数据进行计算,比逐行循环快得多。

为什么看不到"实时"K线数据?

Freqtrade 不会在 dataframe 中存储未完成/未收盘的K线。

用未完成数据做决策叫"重绘",有些平台允许,但 Freqtrade 不支持。只有完整K线数据可用。

自定义指标

入场和出场信号需要用到指标。你可以在策略文件的 populate_indicators() 方法中添加更多指标。

只应添加在 populate_entry_trend()populate_exit_trend() 或用于生成其他指标的指标,否则会影响性能。

务必返回 dataframe,且不要删除/修改 “open”, “high”, “low”, “close”, “volume” 这几列,否则会导致异常。

示例:

def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
    """
    为给定 DataFrame 添加多种技术指标

    性能提示:为获得最佳性能,请只用你策略或超参优化用到的指标,否则会浪费内存和 CPU。
    :param dataframe: 交易所数据 DataFrame
    :param metadata: 额外信息,如当前交易对
    :return: 包含所有策略所需指标的 DataFrame
    """
    dataframe['sar'] = ta.SAR(dataframe)
    dataframe['adx'] = ta.ADX(dataframe)
    stoch = ta.STOCHF(dataframe)
    dataframe['fastd'] = stoch['fastd']
    dataframe['fastk'] = stoch['fastk']
    dataframe['bb_lower'] = ta.BBANDS(dataframe, nbdevup=2, nbdevdn=2)['lowerband']
    dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
    dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
    dataframe['mfi'] = ta.MFI(dataframe)
    dataframe['rsi'] = ta.RSI(dataframe)
    dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
    dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
    dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
    dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
    dataframe['ao'] = awesome_oscillator(dataframe)
    macd = ta.MACD(dataframe)
    dataframe['macd'] = macd['macd']
    dataframe['macdsignal'] = macd['macdsignal']
    dataframe['macdhist'] = macd['macdhist']
    hilbert = ta.HT_SINE(dataframe)
    dataframe['htsine'] = hilbert['sine']
    dataframe['htleadsine'] = hilbert['leadsine']
    dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
    dataframe['plus_di'] = ta.PLUS_DI(dataframe)
    dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
    dataframe['minus_di'] = ta.MINUS_DI(dataframe)

    # 记得始终返回 dataframe
    return dataframe
指标库

Freqtrade 默认安装了以下技术指标库:

如有需要可安装其他技术指标库,或自行编写自定义指标。

策略启动期

部分指标在启动初期因数据不足会出现 NaN 或计算不准确。Freqtrade 无法自动判断不稳定期长度,会直接用 dataframe 中的指标值。

为此,策略可设置 startup_candle_count 属性。

该值应设为策略中所有指标所需的最大历史K线数。若用到高阶时间周期 informative pair,startup_candle_count 也无需改变,只需用最大周期。

可用 recursive-analysis 检查合适的 startup_candle_count。当递归分析显示方差为 0% 时,说明历史数据已足够。

如本例策略用到 ema100,则应设为 400(startup_candle_count = 400),以保证 ema100 计算准确。

    dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)

告知机器人所需历史长度后,回测和超参优化可从指定时间点开始。

示例

假设用 EMA100 策略回测 2019 年 1 月的 5m K 线:

freqtrade backtesting --timerange 20190101-20190201 --timeframe 5m

startup_candle_count 设为 400,回测会自动加载 400 根K线的历史数据,即从 20190101 - (400 * 5m),约等于 2018-12-30 11:40:00。

若有该数据,指标会用扩展时间段计算,启动期(到 2019-01-01 00:00:00)会被剔除。

入场信号规则

编辑策略文件中的 populate_entry_trend() 方法,更新入场逻辑。

务必返回 dataframe,且不要删除/修改 “open”, “high”, “low”, “close”, “volume” 这几列,否则会导致异常。

该方法还需定义新列 enter_long(做空为 enter_short),入场时为 1,无操作为 0。即使只做空也必须设置 enter_long

可用 enter_tag 列为信号命名,便于后续调试和分析。

示例(来自 sample_strategy.py):

def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
    """
    基于技术指标,为给定 dataframe 生成买入信号
    :param dataframe: 已填充指标的 DataFrame
    :param metadata: 额外信息,如当前交易对
    :return: 包含买入信号的 DataFrame
    """
    dataframe.loc[
        (
            (qtpylib.crossed_above(dataframe['rsi'], 30)) &  # RSI 上穿 30
            (dataframe['tema'] <= dataframe['bb_middleband']) &  # 条件
            (dataframe['tema'] > dataframe['tema'].shift(1)) &  # 条件
            (dataframe['volume'] > 0)  # 确保有成交量
        ),
        ['enter_long', 'enter_tag']] = (1, 'rsi_cross')

    return dataframe

出场信号规则

编辑策略文件中的 populate_exit_trend() 方法,更新出场逻辑。

可通过在配置或策略中设置 use_exit_signal 为 false 禁用出场信号。

use_exit_signal 不影响信号冲突规则,后者仍然生效,可能阻止入场。

务必返回 dataframe,且不要删除/修改 “open”, “high”, “low”, “close”, “volume” 这几列,否则会导致异常。

该方法还需定义新列 exit_long(做空为 exit_short),出场时为 1,无操作为 0。

可用 exit_tag 列为信号命名,便于后续调试和分析。

示例(来自 sample_strategy.py):

def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
    """
    基于技术指标,为给定 dataframe 生成卖出信号
    :param dataframe: 已填充指标的 DataFrame
    :param metadata: 额外信息,如当前交易对
    :return: 包含卖出信号的 DataFrame
    """
    dataframe.loc[
        (
            (qtpylib.crossed_above(dataframe['rsi'], 70)) &  # RSI 上穿 70
            (dataframe['tema'] > dataframe['bb_middleband']) &  # 条件
            (dataframe['tema'] < dataframe['tema'].shift(1)) &  # 条件
            (dataframe['volume'] > 0)  # 确保有成交量
        ),
        ['exit_long', 'exit_tag']] = (1, 'rsi_too_high')
    return dataframe

最小收益率(ROI)

minimal_roi 策略变量定义了交易在退出前应达到的最小收益率,与出场信号无关。

格式如下,是一个 python 字典,键为开仓后经过的分钟数,值为百分比:

minimal_roi = {
    "40": 0.0,
    "30": 0.01,
    "20": 0.02,
    "0": 0.04
}

上述配置含义:

计算时包含手续费。

禁用最小 ROI

如需完全禁用 ROI,将其设为空字典:

minimal_roi = {}
ROI 中用计算表达式

如需按K线周期(timeframe)设置时间,可用如下写法:

from freqtrade.exchange import timeframe_to_minutes

class AwesomeStrategy(IStrategy):

    timeframe = "1d"
    timeframe_mins = timeframe_to_minutes(timeframe)
    minimal_roi = {
        "0": 0.05,                      # 前 3 根 K 线 5%
        str(timeframe_mins * 3): 0.02,  # 3 根 K 线后 2%
        str(timeframe_mins * 6): 0.01,  # 6 根 K 线后 1%
    }

止损

强烈建议设置止损,以保护资金免受极端行情影响。

如设置 10% 止损:

stoploss = -0.10

更多止损功能详见止损专页

时间周期(Timeframe)

即策略用的K线周期。

常见值有 “1m”、“5m”、“15m”、“1h”,也可用交易所支持的其他周期。

同一入场/出场信号在不同周期下效果可能完全不同。

在策略方法中可通过 self.timeframe 访问。

是否支持做空

如需在合约市场做空,需设置 can_short = True

启用后,策略在现货市场会加载失败。

enter_short 列有 1,但 can_short = False(默认),则即使配置了合约市场也不会做空。

Metadata 字典

metadata 字典(populate_entry_trendpopulate_exit_trendpopulate_indicators 可用)包含额外信息。 目前有 pair,可通过 metadata['pair'] 获取,如 XRP/BTC(合约市场为 XRP/BTC:BTC)。

metadata 不应被修改,也不会在策略函数间持久化。

如需持久化信息,请查阅信息存储

策略所需的导入

在创建策略时,你需要导入必要的模块和类。以下是一个策略所需的导入:

默认情况下,我们建议使用以下导入作为策略的基础: 这将涵盖 freqtrade 功能所需的所有导入。 当然,你可以根据需要添加更多导入。

# flake8: noqa: F401
# isort: skip_file
# --- Do not remove these imports ---
import numpy as np
import pandas as pd
from datetime import datetime, timedelta, timezone
from pandas import DataFrame
from typing import Dict, Optional, Union, Tuple

from freqtrade.strategy import (
    IStrategy,
    Trade, 
    Order,
    PairLocks,
    informative,  # @informative decorator
    # Hyperopt Parameters
    BooleanParameter,
    CategoricalParameter,
    DecimalParameter,
    IntParameter,
    RealParameter,
    # timeframe helpers
    timeframe_to_minutes,
    timeframe_to_next_date,
    timeframe_to_prev_date,
    # Strategy helper functions
    merge_informative_pair,
    stoploss_from_absolute,
    stoploss_from_open,
)

# --------------------------------
# Add your lib to import here
import talib.abstract as ta
from technical import qtpylib

策略文件加载

默认情况下,freqtrade 会尝试从 userdir(默认 user_data/strategies)下所有 .py 文件加载策略。

假设你的策略叫 AwesomeStrategy,文件为 user_data/strategies/AwesomeStrategy.py,可用如下命令 dry(或 live,视配置而定)运行:

freqtrade trade --strategy AwesomeStrategy

注意这里用的是类名,不是文件名。

freqtrade list-strategies 可查看所有可加载的策略(正确目录下的所有策略)。会有"状态"字段,提示潜在问题。

辅助交易对(Informative Pairs)

获取非交易对的数据

有些策略需要参考更大周期的其他交易对数据。

这些辅助对的 OHLCV 数据会在常规白名单刷新时一并下载,可通过 DataProvider 获取。

这些对不会被交易,除非也在白名单或被动态白名单(如 VolumePairlist)选中。

需用元组指定,格式为 ("pair", "timeframe"),第一个为交易对,第二个为周期。

示例:

def informative_pairs(self):
    return [("ETH/USDT", "5m"),
            ("BTC/TUSD", "15m"),
            ]

完整示例见DataProvider 部分

    def informative_pairs(self):
        return [
            ("ETH/USDT", "5m", ""),   # 默认 K 线类型,随 trading_mode 变化(推荐)
            ("ETH/USDT", "5m", "spot"),   # 强制用现货 K 线(仅现货机器人)
            ("BTC/TUSD", "15m", "futures"),  # 用合约 K 线(仅合约机器人)
            ("BTC/TUSD", "15m", "mark"),  # 用标记 K 线(仅合约机器人)
        ]

Informative pairs 装饰器(@informative()

可用 @informative 装饰器快速定义 informative pair。所有被装饰的 populate_indicators_* 方法独立运行,不能访问其他 informative pair 的数据。但所有 informative dataframe 会合并传递给主 populate_indicators()

超参优化时,hyperoptable 参数不支持 .value,请用 .range。见优化指标参数

merge_informative_pair()

该方法可安全、无前视偏差地将 informative pair 合并到主 dataframe。

功能:

完整示例见完整数据提供者示例

所有 informative dataframe 列会以新名字出现在主 dataframe:

额外数据(DataProvider)

策略可通过 DataProvider 获取更多数据。

所有方法失败时返回 None,不抛异常。

请根据运行模式选择合适方法(见下文示例)。

DataProvider 可用方法

示例用法

available_pairs

for pair, timeframe in self.dp.available_pairs:
    print(f"available {pair}, {timeframe}")

current_whitelist()

假设你开发了一个用 5m 周期、用 1d 周期信号的策略,且用 VolumePairList 动态选前 10 个交易对。

策略逻辑如下:

每 5 分钟扫描前 10 个交易对,用 14 日 RSI 生成信号。

因数据有限,无法用 5m K 线重采样成日线。大多数交易所只给 500-1000 根K线,约等于 1.74 天。我们需要至少 14 天!

因此需用 informative pair,且因白名单动态变化,不知用哪些对!

此时可用 self.dp.current_whitelist() 获取当前白名单对。

    def informative_pairs(self):

        # 获取白名单所有对
        pairs = self.dp.current_whitelist()
        # 为每个对分配周期
        informative_pairs = [(pair, '1d') for pair in pairs]
        return informative_pairs

get_pair_dataframe(pair, timeframe)

# 获取第一个 informative pair 的K线数据
inf_pair, inf_timeframe = self.informative_pairs()[0]
informative = self.dp.get_pair_dataframe(pair=inf_pair,
                                         timeframe=inf_timeframe)

get_analyzed_dataframe(pair, timeframe)

该方法供 freqtrade 内部用来判断最后信号,也可在特定回调中用来获取触发操作的信号(见高级策略文档)。

# 获取当前 dataframe
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=metadata['pair'],
                                                         timeframe=self.timeframe)

orderbook(pair, maximum)

if self.dp.runmode.value in ('live', 'dry_run'):
    ob = self.dp.orderbook(metadata['pair'], 1)
    dataframe['best_bid'] = ob['bids'][0][0]
    dataframe['best_ask'] = ob['asks'][0][0]

orderbook 结构与 ccxt 一致:

{
    'bids': [
        [ price, amount ],
        ...
    ],
    'asks': [
        [ price, amount ],
        ...
    ],
}

ob['bids'][0][0] 可取最优买价,ob['bids'][0][1] 为该价位数量。

ticker(pair)

if self.dp.runmode.value in ('live', 'dry_run'):
    ticker = self.dp.ticker(metadata['pair'])
    dataframe['last_price'] = ticker['last']
    dataframe['volume24h'] = ticker['quoteVolume']
    dataframe['vwap'] = ticker['vwap']

发送通知

dataprovider 的 .send_msg() 可在策略中发送自定义通知。 相同的通知在每个蜡烛图期间只会发送一次,除非第二个参数(always_send)设置为 True。

    self.dp.send_msg(f"{metadata['pair']} just got hot!")

    # 强制发送此通知,避免缓存(请阅读下面的警告!)
    self.dp.send_msg(f"{metadata['pair']} just got hot!", always_send=True)

通知只会在交易模式(实盘/模拟盘)中发送 - 因此可以在回测中无条件调用此方法。

完整 DataProvider 示例

from freqtrade.strategy import IStrategy, merge_informative_pair
from pandas import DataFrame

class SampleStrategy(IStrategy):
    # 策略初始化...

    timeframe = '5m'

    # ...

    def informative_pairs(self):

        # 获取白名单所有对
        pairs = self.dp.current_whitelist()
        # 为每个对分配周期
        informative_pairs = [(pair, '1d') for pair in pairs]
        # 可选:添加静态对
        informative_pairs += [("ETH/USDT", "5m"),
                              ("BTC/TUSD", "15m"),
                            ]
        return informative_pairs

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        if not self.dp:
            # DataProvider 不可用时不做任何操作
            return dataframe

        inf_tf = '1d'
        # 获取信息对
        informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=inf_tf)
        # 获取14天RSI
        informative['rsi'] = ta.RSI(informative, timeperiod=14)

        # 使用辅助函数 merge_informative_pair 安全地合并交易对
        # 自动重命名列并合并较短时间周期的 dataframe 和较长时间周期的信息对
        # 使用 ffill 使1天的值在一天中的每一行都可用
        # 如果没有这个,原始数据框和信息对的列之间的比较每天只能进行一次
        # 此方法的完整文档,见下文
        dataframe = merge_informative_pair(dataframe, informative, self.timeframe, inf_tf, ffill=True)

        # 计算原始数据框的RSI(5分钟时间周期)
        dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)

        # 其他操作
        # ...

        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:

        dataframe.loc[
            (
                (qtpylib.crossed_above(dataframe['rsi'], 30)) &  # RSI 上穿 30
                (dataframe['rsi_1d'] < 30) &                     # 日线 RSI < 30
                (dataframe['volume'] > 0)                        # 有成交量
            ),
            ['enter_long', 'enter_tag']] = (1, 'rsi_cross')

额外数据(钱包 Wallets)

策略可通过 wallets 对象获取当前钱包/账户余额。

调用前请检查 wallets 是否可用,避免回测时报错。

if self.wallets:
    free_eth = self.wallets.get_free('ETH')
    used_eth = self.wallets.get_used('ETH')
    total_eth = self.wallets.get_total('ETH')

Wallets 可用方法


额外数据(交易记录 Trades)

可在策略中通过数据库查询历史交易。

在文件顶部,导入所需对象:

from freqtrade.persistence import Trade

以下示例查询当天当前对的已平仓交易(可加其他过滤条件):

trades = Trade.get_trades_proxy(pair=metadata['pair'],
                                open_date=datetime.now(timezone.utc) - timedelta(days=1),
                                is_open=False,
            ]).order_by(Trade.close_date).all()
# 汇总该交易对的利润
curdayprofit = sum(trade.close_profit for trade in trades)

更多方法请查阅 Trade 对象 文档。

阻止特定对交易

Freqtrade 会在某对平仓后自动锁定该对至当前K线结束,防止同一K线内频繁交易。

锁定对时会提示 Pair <pair> is currently locked.

在策略中锁定对

有时希望在特定事件后锁定某对(如连续亏损)。

可用 self.lock_pair(pair, until, [reason]) 实现,until 为未来时间点,reason 为可选说明。

可用 self.unlock_pair(pair)self.unlock_reason(<reason>) 手动解锁,后者会解锁所有因该原因锁定的对。

self.is_pair_locked(pair) 检查对是否被锁定。

锁定对示例
from freqtrade.persistence import Trade
from datetime import timedelta, datetime, timezone
# 放在策略文件顶部
# --------

# 在 populate_indicators 或 populate_entry_trend 中:
if self.config['runmode'].value in ('live', 'dry_run'):
    # 查询近两天已平仓交易
    trades = Trade.get_trades_proxy(
        pair=metadata['pair'], is_open=False, 
        open_date=datetime.now(timezone.utc) - timedelta(days=2))
    # 分析是否需要锁定
    sumprofit = sum(trade.close_profit for trade in trades)
    if sumprofit < 0:
        # 锁定 12 小时
        self.lock_pair(metadata['pair'], until=datetime.now(timezone.utc) + timedelta(hours=12))

可在 populate_entry_trend()populate_exit_trend() 中打印当前主 dataframe,便于调试。

def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
    dataframe.loc[
        (
            #>> 你的条件 <<<
        ),
        ['enter_long', 'enter_tag']] = (1, 'somestring')

    # 打印分析的对
    print(f"result for {metadata['pair']}")

    # 打印最后 5 行
    print(dataframe.tail())

    return dataframe

如需打印更多行可用 print(dataframe),但不建议,否则每对每 5 秒会输出约 500 行。

开发策略时的常见错误

回测时"看未来"

回测为提升性能会一次性分析整个 dataframe。策略作者需确保不"看未来",即不使用 dry/live 时不可用的数据。

这是常见痛点,易导致回测与 dry/live 巨大差异。看未来的策略回测时表现极佳,实盘却很差。

常见错误包括:

信号冲突

当信号冲突(如 enter_longexit_long 同时为 1)时,freqtrade 会忽略入场信号,避免刚进场就立刻出场。

规则如下,若 3 个信号中有多个为 1,则忽略入场:

更多策略思路

如需更多策略思路,请访问 策略仓库。可作为学习参考,实际效果取决于市场、交易对等。请务必先回测,再 dry-run,谨慎实盘。

欢迎参考、改编,也欢迎向仓库提交新策略 PR。

下一步,回测

现在你已经有了完美的策略,下一步请学习如何回测