Skip to content

策略自定义

本页面介绍如何自定义您的策略,添加新的指标并设置交易规则。

请先熟悉Freqtrade基础知识,该页面提供了有关机器人操作的总体信息。

开发自己的策略

机器人包含一个默认的策略文件。 此外,策略存储库中还提供了其他几种策略。

但是,您很可能有自己的策略想法。 本文档旨在帮助您将您的策略想法转化为自己的策略。

要开始,请使用 freqtrade new-strategy --strategy AwesomeStrategy(您可以使用自己的策略名称)。 这将从模板创建一个新的策略文件,该文件位于 user_data/strategies/AwesomeStrategy.py

注意

这只是一个模板文件,很可能无法立即实现盈利。

不同的模板级别

freqtrade new-strategy 还有一个额外的参数 --template,可以控制所创建策略中预构建信息的数量。使用 --template minimal 可以得到一个没有任何指标示例的空白策略,或者使用 --template advanced 获取已定义大多数回调的模板。

策略解剖策略文件包含构建良好策略所需的所有信息:

  • 指标
  • 入场策略规则
  • 出场策略规则
  • 推荐的最小投资回报率
  • 强烈推荐使用止损

机器人还包含一个名为SampleStrategy的示例策略,您可以通过更新user_data/strategies/sample_strategy.py文件来使用它。您可以使用参数--strategy SampleStrategy对其进行测试。

此外,还有一个名为INTERFACE_VERSION的属性,它定义了机器人应该使用的策略接口版本。当前版本为3 - 当策略中没有显式设置时,默认为此版本。

将来的版本将要求设置该属性。

freqtrade trade --strategy AwesomeStrategy

对于下面的部分,我们将使用user_data/strategies/sample_strategy.py文件作为参考。

策略和回测

为了避免在回测和实盘模式之间出现问题和意外差异,请注意在回测期间将完整的时间范围一次性传递给populate_*()方法。 因此,最好使用矢量化操作(跨整个数据帧,而不是循环)并避免索引引用(df.iloc[-1]),而是使用df.shift()来获取前一个蜡烛信息。

警告:使用未来数据

由于回测将完整的时间范围传递给populate_*()方法,策略作者需要注意避免策略利用未来的数据。 本文档的常见错误部分列出了一些常见的解决方法。

Dataframe

Freqtrade使用pandas来存储/提供蜡烛图(开高低收量)数据。 Pandas是一个用于处理大量数据的强大库。

DataFrame 中的每一行对应图表上的一个蜡烛,最新的蜡烛始终是数据帧中的最后一个(按日期排序)。

> 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

Pandas提供了快速计算指标的方法。为了从中获益,建议不要使用循环,而是使用向量化的方法。

向量化操作在整个数据范围内进行计算,因此与循环遍历每一行相比,在计算指标时速度要快得多。

由于数据帧是一个表格,简单的Python比较如下所示将无法正常工作

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

上面的部分将会报错 The truth value of a Series is ambiguous. [...]

相反,必须以适用于pandas的方式编写,使操作在整个数据帧上执行。

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

通过这一部分,你的数据框中将多一个新的列,当RSI超过30时,它将被赋值为1

自定义指标

购买和卖出信号需要指标。你可以通过扩展你的策略文件中populate_indicators()方法中的列表来添加更多的指标。

你只应该在populate_entry_trend()populate_exit_trend()或者为了给其他指标填充数据时添加使用的指标,否则可能会影响性能。

重要的是,一定要始终返回不删除/修改"open"、"high"、"low"、"close"、"volume"这些列的数据框,否则这些字段会包含一些意想不到的内容。

示例:

def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
    """
    添加几种不同的TA指标到给定的数据框中

    性能注意事项: 为了获得最佳性能,在使用指标时要节俭。只取消注释你在策略或超参数调优配置中使用的指标,
    否则你将浪费内存和CPU资源。
    :param dataframe: 从交易所获取的数据框
    :param metadata: 其他附加信息,如当前交易对
    :return: 策略所需的所有必要指标的数据框
    """
    # 添加指标
    dataframe['sar'] = ta.SAR(dataframe)
    dataframe['adx'] = ta.ADX(dataframe)
    stoch = ta.STOCHF(dataframe)
    dataframe['fastd'] = stoch['fastd']
    dataframe['fastk'] = stoch['fastk']
    dataframe['blower'] = 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)
    return dataframe

想要更多的指标示例?

参考user_data/strategies/sample_strategy.py。 然后取消需要的指标的注释。

指标库

默认情况下,freqtrade安装了以下技术库:

如有需要,可以安装其他技术库,或者策略作者可以编写/发明自定义指标。

策略的启动期

大多数指标都有一个不稳定的启动期,在此期间它们要么不可用(NaN),要么计算不正确。这可能导致不一致性,因为 Freqtrade 不知道这个不稳定期应该有多长时间。 为了解决这个问题,可以给策略分配 startup_candle_count 属性。 这应该设置为策略需要计算稳定指标所需的最大蜡烛数量。如果用户使用了包含信息性货币对的更高时间框架,则 startup_candle_count 不一定会改变。该值是任何一个信息性时间框架计算稳定指标所需的最大周期(以蜡烛计)。

你可以使用 递归分析 来检查和找到要使用的正确的 startup_candle_count

在这个例子策略中,这应该设置为 400 (startup_candle_count = 400),因为 ema100 计算所需的最小历史值为 400 个蜡烛,以确保值是正确的。

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

通过让机器人知道需要多少历史数据,可以在回测和超参数优化期间从指定的时间范围开始进行交易。

使用x次调用获取OHLCV

如果收到类似 警告 - 使用3次调用来获取OHLCV。这可能导致机器人操作变慢。请检查您的策略是否真的需要1500根蜡烛的数据 的警告 - 您应该考虑一下是否真的需要这么多的历史数据来生成信号。 这会导致 Freqtrade 对同一个交易对进行多次调用,这显然比一次网络请求慢。 因此,Freqtrade 刷新蜡烛的速度会变慢 - 因此如果可能的话应该避免。 这个数量上限为5次调用,以避免过载交易所,或使 freqtrade 过于缓慢。!!! 警告 startup_candle_count 应该小于 ohlcv_candle_limit * 5(大多数交易所为 500 * 5)- 因为在 Dry-Run/Live Trade 操作期间,只有这么多的蜡烛图可用。

示例

让我们尝试使用以前面所述的 EMA100 策略,对 5m 蜡烛图的 1 个月(2019 年 1 月)进行回测。

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

假设 startup_candle_count 设置为 400,回测将知道需要 400 个蜡烛图才能生成有效的买入信号。数据将从 20190101 - (400 * 5m) 加载 - 大约是 2018-12-30 11:40:00。 如果有此数据,则会在此扩展的时间范围内计算指标。然后,在开始回测之前,不稳定的启动期间(最多到 2019-01-01 00:00:00)将被移除。

注意

如果启动期间的数据不可用,则时间范围将进行调整以考虑此启动期间 - 因此回测将从 2019-01-02 09:20:00 开始。

入场信号规则

在您的策略文件中编辑 populate_entry_trend() 方法以更新您的进入策略。

重要的是始终返回不删除/修改列 "open", "high", "low", "close", "volume" 的数据框,否则这些字段将包含意外的内容。

此方法还将定义一个新的列 "enter_long"(做空的情况下为 "enter_short"),其中必须包含 1 表示入场,0 表示"无操作"。即使策略只做空,也必须设置 enter_long 列。来自user_data/strategies/sample_strategy.py的代码示例:

def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
    """
    根据技术指标,填充给定数据框的买入信号
    :param dataframe: 填充有指标的数据框
    :param metadata: 其他信息,例如当前交易对
    :return: 带有买入列的数据框
    """
    dataframe.loc[
        (
            (qtpylib.crossed_above(dataframe['rsi'], 30)) &  # 信号:RSI上穿30
            (dataframe['tema'] <= dataframe['bb_middleband']) &  # 保卫条件
            (dataframe['tema'] > dataframe['tema'].shift(1)) &  # 保卫条件
            (dataframe['volume'] > 0)  # 确保成交量不为0
        ),
        ['enter_long', 'enter_tag']] = (1, 'rsi_cross')

    return dataframe
进入空头交易

可以通过设置enter_short(对应于多头交易的enter_long)来创建空头交易。 enter_tag列保持不变。 空头交易需要根据您的交易所和市场配置来支持! 如果您打算做空,请确保在策略中适当设置can_short

def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> 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)  # 确保成交量不为0
        ),
        ['enter_long', 'enter_tag']] = (1, 'rsi_cross')

    dataframe.loc[
        (
            (qtpylib.crossed_below(dataframe['rsi'], 70)) &  # 信号:RSI下穿70
            (dataframe['tema'] > dataframe['bb_middleband']) &  # 保卫条件
            (dataframe['tema'] < dataframe['tema'].shift(1)) &  # 保卫条件
            (dataframe['volume'] > 0)  # 确保成交量不为0
        ),
        ['enter_short', 'enter_tag']] = (1, 'rsi_cross')

    return dataframe

注意

买入需要有卖方进行买入-因此需要成交量大于0(dataframe['volume'] > 0),以确保机器人不会在没有活动的时期买入/卖出。

退出信号规则

将方法populate_exit_trend()编辑到您的策略文件中以更新退出策略。 退出信号可以通过在配置或策略中将use_exit_signal设置为false来取消。 use_exit_signal不会影响信号冲突规则-该规则仍然适用,可能会阻止进入。## 在不删除/修改 "open", "high", "low", "close", "volume" 列的情况下,始终返回 dataframe,否则这些字段将包含意外的内容。

此方法还将定义一个新的列 "exit_long"(对于做空,是 "exit_short"),该列需要包含 1 表示退出操作,0 表示 "无操作"。

user_data/strategies/sample_strategy.py 中获取样例:

def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
    """
    基于 TA 指标,填充给定 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)  # 确保 Volume 不为 0
        ),
        ['exit_long', 'exit_tag']] = (1, 'rsi_too_high')
    return dataframe
退出做空交易

可以通过设置 exit_short(对应于 exit_long)来创建做空退出交易。 exit_tag 列保持不变。 做空交易需要在您的交易所和市场配置中得到支持!

def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> 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)  # 确保 Volume 不为 0
        ),
        ['exit_long', 'exit_tag']] = (1, 'rsi_too_high')
    dataframe.loc[
        (
            (qtpylib.crossed_below(dataframe['rsi'], 30)) &  # 信号:RSI 下穿 30
            (dataframe['tema'] < dataframe['bb_middleband']) &  # 保护
            (dataframe['tema'] > dataframe['tema'].shift(1)) &  # 保护
            (dataframe['volume'] > 0)  # 确保 Volume 不为 0
        ),
        ['exit_short', 'exit_tag']] = (1, 'rsi_too_low')
    return dataframe

最小投资回报率

该字典定义了在退出之前交易应该达到的最小投资回报率(ROI),独立于退出信号。

它的格式如下,字典键(冒号左边)是开仓后经过的分钟数,值(冒号右边)是百分比。

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

上述配置将意味着:

  • 在达到4%的利润时退出
  • 在达到2%的利润时退出(实际上是在20分钟后)
  • 在达到1%的利润时退出(实际上是在30分钟后)
  • 在交易非亏损时退出(实际上是在40分钟后)

计算中包括费用。

要完全禁用ROI,请将其设置为空字典:

minimal_roi = {}

要根据蜡烛持续时间(时间框架)使用时间,可以使用以下代码片段。这将允许您更改策略的时间框架,并且ROI时间仍将设置为蜡烛(例如,在3个蜡烛后......)

from freqtrade.exchange import timeframe_to_minutes

class AwesomeStrategy(IStrategy):

    timeframe = "1d"
    timeframe_mins = timeframe_to_minutes(timeframe)
    minimal_roi = {
        "0": 0.05,                      # 前3个蜡烛的5%
        str(timeframe_mins * 3): 0.02,  # 3个蜡烛后的2%
        str(timeframe_mins * 6): 0.01,  # 6个蜡烛后的1%
    }
无法立即填充的订单

minimal_roi 将以 trade.open_date 为参考,这是交易初始化/该交易的第一个订单被下达的时间。
这也适用于无法立即填充的限价订单(通常与通过 custom_entry_price() 提供的“非标准”价格结合使用),以及通过 adjust_entry_price() 替换初始订单的情况。 使用的时间仍将是初始 trade.open_date 的时间(初始订单首次下达的时间),而不是新下达订单的时间。### 止损

设置止损是非常推荐的,可以保护你的资金免受强势的逆市波动。

以下是设置10%止损的示例:

stoploss = -0.10

要获取有关止损功能的完整文档,请查看专门的止损页

时段

这是机器人应该下载并用于分析的蜡烛图集合。 常见的取值有"1m""5m""15m""1h",但通过交易所支持的所有取值都可以使用。

请注意,相同的入场/出场信号在一个时段上可能表现良好,但在其他时段上可能不适用。

此设置可以在策略方法中通过 self.timeframe 属性进行访问。

可做空

要在期货市场上使用空头信号,请在设置can_short=True时告知我们。启用此功能的策略将无法在现货市场上加载。禁用此功能将忽略空头信号(也适用于期货市场)。

元数据字典

元数据字典(适用于populate_entry_trendpopulate_exit_trendpopulate_indicators)包含额外的信息。目前,其中包含一个名为pair的键,你可以使用metadata['pair']来访问它,它将返回一个格式为XRP/BTC的交易对。

元数据字典不应被修改,并且在多次调用之间不会持久保存信息。相反,请参考存储信息部分。

策略文件加载

默认情况下,Freqtrade会尝试从user_data/strategies目录下的所有.py文件中加载策略。

假设你的策略名为AwesomeStrategy,存储在文件user_data/strategies/AwesomeStrategy.py中,那么你可以使用freqtrade trade --strategy AwesomeStrategy命令来启动Freqtrade。 请注意,我们使用的是类名而不是文件名。

你可以使用freqtrade list-strategies命令查看Freqtrade可以加载的所有策略(正确文件夹中的所有策略)的列表。它还会包含一个用于标识潜在问题的"status"字段。

自定义策略目录

你可以使用不同的目录,只需使用--strategy-path user_data/otherPath参数。这个参数适用于所有需要策略的命令。

有益的交易对

获取非可交易货币对的数据

对于某些策略而言,获取一些额外的、信息性的货币对(参考货币对)的数据可能是有益的。 这些货币对的OHLCV数据将作为常规白名单刷新过程的一部分进行下载,并且可以通过 DataProvider 在其他货币对一样进行使用(参见下文)。 除非这些货币对也在白名单中指定了,或者被动态白名单选择了,否则这些货币对将不会被交易。

货币对需要以元组的格式指定,格式为 ("pair", "timeframe"),其中第一个参数是货币对的名称,第二个参数是时间框架。

示例:

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

完整的示例可以在 DataProvider部分 找到。

警告

由于这些货币对将作为常规白名单刷新的一部分进行更新,因此最好保持这个列表的长度较短。 所有的时间框架和货币对都可以指定,只要它们在使用的交易所上可用(并且处于活动状态)。 但是最好在可能的情况下使用长时间框架进行重采样, 以避免向交易所发送过多的请求并冒被封锁的风险。

替代的蜡烛图类型

informative_pairs 还可以提供第三个元组元素,以明确定义蜡烛图的类型。 可用的替代蜡烛图类型将取决于交易模式和交易所。一般来说,现货交易对不能用于期货市场,期货蜡烛图不能作为现货机器人的信息性货币对使用。 关于这方面的细节可能会有所不同,如果有变化,可以在交易所的文档中找到。

def informative_pairs(self):
    return [
        ("ETH/USDT", "5m", ""),   # 使用默认的蜡烛图类型,取决于交易模式(推荐)
        ("ETH/USDT", "5m", "spot"),   # 强制使用现货蜡烛图(仅适用于在现货市场运行的机器人)。
        ("BTC/TUSD", "15m", "futures"),  # 使用期货蜡烛图(仅适用于 `trading_mode=futures` 的机器人)
        ("BTC/TUSD", "15m", "mark"),  # 使用标记蜡烛图(仅适用于 `trading_mode=futures` 的机器人)
    ]

信息性货币对修饰符(@informative()

在大多数常见情况下,可以通过使用装饰器来轻松定义信息对。所有带有@informative装饰器的populate_indicators_*方法都是独立运行的,无法访问其他信息对的数据,在最后将所有的信息数据合并并传递给主要的populate_indicators()方法。在超参优化时,不支持使用超参表中的.value属性,请使用.range属性。有关更多信息,请参见优化指标参数

完整文档
def informative(timeframe: str, asset: str = '',
                fmt: Optional[Union[str, Callable[[KwArg(str)], str]]] = None,
                *,
                candle_type: Optional[CandleType] = None,
                ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]:
    """
    用于装饰`populate_indicators_Nn(self, dataframe, metadata)`的装饰器,允许这些函数定义信息指标。

    示例用法:

        @informative('1h')
        def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
            dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
            return dataframe

    :param timeframe: 信息时间框架。必须始终等于或高于策略时间框架。
    :param asset: 信息资产,例如BTC、 BTC/USDT、ETH/BTC。不指定将使用当前交易对。还支持有限的交易对格式字符串(参见下文)
    :param fmt: 列格式(str)或列格式化程序(可调用(name, asset, timeframe))。未指定时,默认为:
    * 如果指定了资产,则为{base}_{quote}_{column}_{timeframe}。
    * 如果未指定资产,则为{column}_{timeframe}。
    交易对格式支持以下格式变量:
    * {base} -小写的基础货币,例如'eth'。
    * {BASE} -与{base}相同,只是大写。
    * {quote} -小写的报价货币,例如'usdt'。
    * {QUOTE} -与{quote}相同,只是大写。
    格式字符串还支持以下变量。
    * {asset} -资产的完整名称,例如'BTC/USDT'。
    * {column} -数据帧列的名称。
    * {timeframe} -信息数据帧的时间框架。
    :param ffill: 在合并信息对后向前填充数据帧。
    :param candle_type: '', mark, index, premiumIndex, or funding_rate
    """
快速定义信息对的简单方法

大多数情况下,我们不需要merge_informative_pair()提供的强大和灵活性,因此我们可以使用装饰器快速定义信息对。

from datetime import datetime
from freqtrade.persistence import Trade
from freqtrade.strategy import IStrategy, informative

class AwesomeStrategy(IStrategy):

    # 此方法不是必需的。
    # def informative_pairs(self): ...
```# 为每个货币对定义信息性的时间框架。装饰器可以叠加在同一个方法上。在populate_indicators方法中可使用'rsi_30m'和'rsi_1h'。
    @informative('30m')
    @informative('1h')
    def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
        return dataframe

    # 定义BTC/STAKE信息性货币对。在populate_indicators和其他方法中可以使用'btc_rsi_1h'。当前的stake货币应以{stake}格式变量的方式指定,而非硬编码实际的stake货币。当stake货币为USDT时,在populate_indicators和其他方法中可以使用'btc_usdt_rsi_1h'。
    @informative('1h', 'BTC/{stake}')
    def populate_indicators_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
        return dataframe

    # 定义BTC/ETH信息性货币对。如果报价货币与stake货币不同,必须指定报价货币。在populate_indicators和其他方法中可以使用'eth_btc_rsi_1h'。
    @informative('1h', 'ETH/BTC')
    def populate_indicators_eth_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
        return dataframe

    # 定义BTC/STAKE信息性货币对。可以为列名指定自定义格式化程序。可以指定一个可调用的`fmt(**kwargs) -> str`来实现自定义格式化。在populate_indicators和其他方法中可以使用'rsi_upper'。
    @informative('1h', 'BTC/{stake}', '{column}')
    def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe['rsi_upper'] = ta.RSI(dataframe, timeperiod=14)
        return dataframe

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # 当前货币对的策略时间框架指标。
        dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
        # 在此方法中可以使用信息性货币对。
        dataframe['rsi_less'] = dataframe['rsi'] < dataframe['rsi_1h']
        return dataframe

注意

如果您需要在生成另一个信息性货币对时使用一个信息性货币对的数据,请勿使用@informative装饰器。相反,请按照DataProvider部分中的描述手动定义信息性货币对。

注意

在访问其他货币对的信息性数据框时,请使用字符串格式化。这将允许在不必调整策略代码的情况下轻松更改配置中的stake货币。

def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
    stake = self.config['stake_currency']
    dataframe.loc[
        (
            (dataframe[f'btc_{stake}_rsi_1h'] < 35)
            &
            (dataframe['volume'] > 0)
        ),
        ['enter_long', 'enter_tag']] = (1, 'buy_signal_rsi')

    return dataframe

或者也可以使用列重命名来删除列名中的stake货币:@informative('1h', 'BTC/{stake}', fmt='{base}_{column}_{timeframe}')

方法名重复

使用@informative()装饰器标记的方法必须始终具有唯一的名称!重新使用相同的名称(例如,当复制已经定义的信息性方法时)将覆盖先前定义的方法,并且不会产生任何错误,因为Python编程语言的限制。在这种情况下,您将发现早期定义的指标在数据框中不可用。仔细检查方法名称,确保它们是唯一的!

merge_informative_pair()

这个方法可以帮助你将一个信息性的数据对合并到一个常规的数据帧中,而不会产生前瞻性偏见。 它的存在是为了以一种安全和一致的方式合并数据帧。

选项:

  • 为你重命名列,以创建唯一的列名
  • 在没有前瞻性偏见的情况下合并数据帧
  • 前向填充(可选的)

完整示例请参见下面的完整数据提供者示例

信息性数据帧的所有列都将以重命名的方式在返回的数据帧中可用:

列重命名

假设inf_tf = '1d',结果的列将是:

'date', 'open', 'high', 'low', 'close', 'rsi'                     # 来自原数据帧
'date_1d', 'open_1d', 'high_1d', 'low_1d', 'close_1d', 'rsi_1d'   # 来自信息性数据帧
列重命名 - 1小时

假设inf_tf = '1h',结果的列将是:

'date', 'open', 'high', 'low', 'close', 'rsi'                     # 来自原数据帧
'date_1h', 'open_1h', 'high_1h', 'low_1h', 'close_1h', 'rsi_1h'   # 来自信息性数据帧
自定义实现

可以进行自定义实现,具体如下所示:

# 将日期后移1根K线
# 这是必要的,因为数据始终是“开盘日期”
# 一个从12:15开始的15分钟K线不应该知道从12:00到13:00的1小时K线的收盘价
minutes = timeframe_to_minutes(inf_tf)
# 只有当时间框架不同时才执行此操作:
informative['date_merge'] = informative["date"] + pd.to_timedelta(minutes, 'm')

# 重命名列以保证唯一性
informative.columns = [f"{col}_{inf_tf}" for col in informative.columns]
# 假设inf_tf = '1d' - 那么列现在将是:
# date_1d, open_1d, high_1d, low_1d, close_1d, rsi_1d

# 合并两个数据框
# 所有辅助样本上的指标必须在此处计算
dataframe = pd.merge(dataframe, informative, left_on='date', right_on=f'date_merge_{inf_tf}', how='left')
# FFill以便在整天的每一行都有1日的值。
# 如果没有这个,比较只能一天一次
dataframe = dataframe.ffill()

!!! 注意 “辅助时间框架小于时间框架” 使用比数据框时间框架更小的辅助时间框架不推荐使用此方法,因为它不会使用提供的任何额外信息。 为了正确使用更详细的信息,应该采用更高级的方法(这超出了freqtrade文档的范围,因为这将取决于相应的需求)。

附加数据(DataProvider)

该策略提供了对 DataProvider 的访问。这使您可以获取其他数据来在策略中使用。

所有方法在失败的情况下都会返回 None(不会引发异常)。

请始终检查操作模式以选择正确的获取数据方法(示例见下文)。!!! Warning "Hyperopt" 在进行超参数优化时, 可以使用DataProvier, 但只能在策略的populate_indicators()方法中使用. 在populate_buy()populate_sell()方法中以及超参数优化文件中的populate_indicators()方法中不可用.

DataProvider的可能选项

  • available_pairs - 包含缓存的交易对和时间框架的元组的属性 (pair, timeframe).
  • current_whitelist() - 返回当前的白名单交易对列表. 适用于访问动态白名单 (比如 VolumePairlist).
  • get_pair_dataframe(pair, timeframe) - 这是一个通用的方法, 可以返回历史数据 (用于回测) 或缓存的实时数据 (用于 Dry-Run 和 Live-Run 模式).
  • get_analyzed_dataframe(pair, timeframe) - 返回分析后的数据框 (在调用 populate_indicators(), populate_buy(), populate_sell() 后) 和最新分析的时间.
  • historic_ohlcv(pair, timeframe) - 返回存储在磁盘上的历史数据.
  • market(pair) - 返回交易对的市场数据: 手续费, 限制, 精度, 活动标志等. 更多详情请参考 ccxt 文档 中的市场数据结构.
  • ohlcv(pair, timeframe) - 返回交易对的当前缓存蜡烛图 (OHLCV) 数据, 返回数据框或空数据框.
  • orderbook(pair, maximum) - 返回交易对的最新订单簿数据, 包含maximum个买盘/卖盘条目的字典.
  • ticker(pair) - 返回交易对的当前标记数据. 更多详情请参考 ccxt 文档 中的标记数据结构.
  • runmode - 包含当前运行模式的属性.

用法示例

available_pairs

for pair, timeframe in self.dp.available_pairs:
    print(f"可用交易对: {pair}, 时间框架: {timeframe}")

current_whitelist()

假设您开发了一个策略, 在最高交易量的前10个交易对上, 使用从1d时间框架生成的信号来进行5m时间框架的交易.

该策略可能如下所示:

每隔五分钟扫描前10个交易对 (VolumePairList) 的交易量, 并使用14天RSI进行买卖.由于可用数据有限,将5m K线重新采样为每日K线以在14日RSI中使用非常困难。大多数交易所限制我们只能获取500-1000根K线,这实际上相当于1.74根每日K线。我们至少需要14天的数据!

因为我们不能重新采样数据,所以我们将不得不使用一个有信息量的交易对;而且由于白名单是动态的,我们不知道要使用哪些交易对。

这就是调用self.dp.current_whitelist()的便利之处。

    def informative_pairs(self):

        # 获取白名单中所有可用交易对。
        pairs = self.dp.current_whitelist()
        # 为每个交易对分配时间框架,以便在策略中进行下载和缓存。
        informative_pairs = [(pair, '1d') for pair in pairs]
        return informative_pairs
使用current_whitelist绘图

plot-dataframe不支持使用当前白名单,因为该命令通常通过提供显式的交易对列表使用 - 并且这样做会使得此方法的返回值具有误导性。

get_pair_dataframe(pair, timeframe)

# 获取第一个有信息量的交易对的实时/历史K线(OHLCV)数据
inf_pair, inf_timeframe = self.informative_pairs()[0]
informative = self.dp.get_pair_dataframe(pair=inf_pair,
                                         timeframe=inf_timeframe)

关于回测的警告

在回测中,dp.get_pair_dataframe()的行为取决于在哪里调用它。 在populate_*()方法中,dp.get_pair_dataframe()返回完整的时间范围。请确保不要"看向未来",以避免在模拟/实际交易模式下出现意外。 在callbacks中,您将获得当前(模拟的)K线的完整时间范围。

get_analyzed_dataframe(pair, timeframe)

这个方法是freqtrade内部使用的,用于确定最后一个信号。 它也可以在特定的回调中使用,以获取导致动作的信号(有关可用回调的更多详细信息,请参见高级策略文档)。

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

无可用数据

如果请求的交易对未缓存,则返回一个空的dataframe。 您可以使用 if dataframe.empty: 来检查并相应地处理这种情况。 使用白名单中的交易对时,不应该出现这种情况。

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 ], // [ float, float ]
        [ price, amount ],
        ...
    ],
    'asks': [
        [ price, amount ],
        [ 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']

警告

尽管 ticker 数据结构是 ccxt 统一接口的一部分,但该方法返回的值可能因不同交易所而异。例如,许多交易所不返回 vwap 值,一些交易所不总是填充 last 字段(因此可能为 None),等等。因此,您需要仔细验证从交易所返回的 ticker 数据,并添加适当的错误处理/默认值。

关于回测的警告

这种方法将始终返回最新的值 - 因此,在回测/超参数优化期间,在没有运行模式检查的情况下使用将导致错误的结果。

发送通知

dataprovider .send_msg() 函数允许您从您的策略发送自定义通知。相同的通知每个蜡烛只会发送一次,除非第二个参数(always_send)设置为 True。

    self.dp.send_msg(f"{metadata['pair']} 刚刚变热!")

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

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

垃圾信息

在此方法中设置 always_send=True 可以给自己发送大量垃圾信息。请谨慎使用,并且只在您知道在一个蜡烛中不会发生的条件下才使用,以避免每隔5秒收到一条消息。

完整的数据提供程序示例

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()
        # 为每对交易对分配tf,以便可以为策略下载和缓存它们。
        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:
            # 如果没有数据提供程序,则不执行任何操作。
            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安全地合并交易对
        # 自动重命名列并合并较短时间框架的数据帧和较长时间框架的信息交易对
        # 使用ffill使得1d的值在每一天的每一行都可用。
        # 否则,原始数据帧和信息交易对之间的比较只能每天进行一次。
        # 有关此方法的完整文档,请参见下文
        dataframe = merge_informative_pair(dataframe, informative, self.timeframe, inf_tf, ffill=True)

        # 计算原始数据帧(5m时间框架)的rsi值
        dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)```python

        # 做其他的事情
# ...

        return dataframe

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

        # RSI交叉超过30的信号
        # 确保每日RSI < 30
        # 确保这个蜡烛有交易量(对于回测很重要)
        # 将enter_long和enter_tag列的值设置为(1,'rsi_cross')
        dataframe.loc[
            (
                (qtpylib.crossed_above(dataframe['rsi'], 30)) &  
                (dataframe['rsi_1d'] < 30) &                     
                (dataframe['volume'] > 0)                        
            ),
            ['enter_long', 'enter_tag']] = (1, 'rsi_cross')

附加数据(钱包)

这个策略提供了对wallets对象的访问。它包含了交易所上的当前余额。

回测/超参数优化

wallets的行为取决于它被调用的函数。 在populate_*()方法中,它会返回配置的完整钱包。 在回调中,你将得到与模拟钱包在模拟过程中的实际状态相对应的钱包状态。

请始终检查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')

钱包的可能选项

  • get_free(asset) - 当前可交易余额
  • get_used(asset) - 当前已使用余额(未完成订单)
  • get_total(asset) - 总可用余额 - 以上两者之和

附加数据(交易)

在策略中,可以通过查询数据库来获取交易历史。

在文件顶部导入Trade

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对象的文档。

!!! 警告 在回测或超参优化期间,populate_*方法中无法使用交易历史,结果将为空。

阻止特定交易对的交易发生

Freqtrade在卖出交易对后会自动锁定当前蜡烛(直到该蜡烛结束),从而防止立即重新购买该交易对。

锁定的交易对将显示消息交易对 <pair> 当前已锁定.

在策略内部锁定交易对

有时候我们希望在特定事件发生后锁定交易对(例如连续多次亏损交易)。

Freqtrade提供了一种简单的方法来在策略内部实现这一点,通过调用self.lock_pair(pair, until, [reason])until必须是一个未来的datetime对象,在此之后,该交易对的交易将被重新启用,而reason是可选的字符串,详细说明了为什么锁定了该交易对。

锁定也可以通过调用self.unlock_pair(pair)self.unlock_reason(<reason>)来手动解除,后者提供了被锁定的交易对的原因。 self.unlock_reason(<reason>)将解锁所有当前使用提供的原因被锁定的交易对。

要验证交易对当前是否被锁定,请使用self.is_pair_locked(pair)。!!! 注意 锁定的时间将总是向上取整到下一根K线。所以假设时间框架为 5m,将一个锁定对的 until 设置为10:18,将锁定该对货币直到10:15-10:20 这个时间范围的K线完成为止。

警告

在回测过程中无法手动锁定货币对,只允许通过保护功能进行锁定。

货币对锁定示例

from freqtrade.persistence import Trade
from datetime import timedelta, datetime, timezone
# 将以上代码放在策略文件的顶部,与其他导入代码一起
# --------

# 在 populate indicators (或 populate_buy) 函数中:
if self.config['runmode'].value in ('live', 'dry_run'):
    # 获取最近2天的已关闭交易
    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() 中使用打印语句。 您可能还希望打印货币对,以清楚地显示当前显示的数据。

def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
    dataframe.loc[
        (
            #>> 某个条件 <<<
        ),
        ['enter_long', 'enter_tag']] = (1, '某个字符串')

    # 打印分析的货币对
    print(f"结果:{metadata['pair']}")

    # Inspect the last 5 rows
    print(dataframe.tail())

    return dataframe

也可以打印多于几行的内容(只需使用 print(dataframe) 而不是 print(dataframe.tail())),但并不推荐,因为这样会非常冗长(每对交易对每5秒大约500行)。

开发策略时常见的错误

在回测时窥探未来

由于性能原因,回测会一次性分析整个时间范围。因此,策略作者需要确保策略在回测时不会窥探未来。 这是一个常见的痛点,会导致回测和模拟/实盘运行之间存在巨大差异,因为它们都使用不在模拟/实盘运行期间可用的数据,所以这些策略在回测时表现良好,但在实际条件下会失败/表现不佳。

以下列出了一些常见的模式,应避免以防止出现问题:

  • 不要使用 shift(-1)。这会使用未来的数据,而这些数据在实际运行时不可用。
  • 不要使用 .iloc[-1] 或任何其他在数据框中的绝对位置,这在模拟运行和回测之间会有所不同。
  • 不要使用 dataframe['volume'].mean()。这会在回测中使用完整的数据框,包括未来的数据。而应使用 dataframe['volume'].rolling(<window>).mean()
  • 不要使用 .resample('1h')。这会使用间隔的左边界,将数据从一个小时移到小时的起始位置。而应使用 .resample('1h', label='right')

识别问题

您还可以查看两个辅助命令 lookahead-analysisrecursive-analysis,它们可以以不同的方式帮助您找出策略中的问题。 请将它们视为帮助您识别最常见问题的工具。每个命令的负结果并不保证排除了上述错误。

信号冲突

当冲突信号发生(例如 'enter_long''exit_long' 都为1)时,freqtrade 将不执行任何操作,并忽略进入信号。这将避免进入并立即退出的交易。显然,这可能会导致错过入场机会。以下规则适用,如果设置了3个信号中的多于一个,则会忽略入场信号:

  • enter_long -> exit_longenter_short
  • enter_short -> exit_shortenter_long

更多策略想法

要获取更多策略的想法,请转到策略存储库。请随意使用它们——但结果将取决于当前的市场情况、使用的交易对等,请务必首先为您的交易所/期望的交易对回测策略,仔细评估,自行承担风险。 请随意将它们用作您自己策略的灵感。 我们很乐意接受包含新策略的拉取请求。

下一步

现在您已经有了一个完美的策略,您可能想要对其进行回测。 您的下一步是学习如何使用回测功能