跳至内容

策略回调

主要的策略函数(populate_indicators()populate_entry_trend()populate_exit_trend())应以向量化方式使用,在回测期间仅被调用一次,而回调函数则在“需要时”被调用。

因此,应避免在回调函数中进行复杂的计算,以防止操作过程中出现延迟。根据所使用的回调类型,它们可能在进入/退出交易时被调用,或在交易持续期间多次调用。

当前可用的回调函数:

回调调用顺序

您可以在 bot-basics 中找到回调调用顺序

策略所需导入

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

默认情况下,我们建议将以下导入作为策略的基础:这将涵盖使 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 允许您的策略使用基于信号或基于回调的函数实现不同的退出逻辑。本节旨在比较每种不同函数,帮助您选择最适合您需求的方法。

  • populate_exit_trend() - 使用主数据帧中的指标进行向量化信号式退出逻辑 ✅ 使用场景:定义基于指标或其他可向量化计算数据的退出信号。 🚫 不适用场景:为每个单独交易定制退出条件,或退出决策依赖交易数据时。
  • custom_exit() - 自定义退出逻辑,将在每次机器人循环迭代时对每个未平仓交易调用,直到交易关闭为止。该逻辑会立即完全退出交易。 ✅ 使用场景:为每个单独交易指定退出条件(包括使用 adjust_trade_position() 调整的额外订单),或退出决策依赖交易数据时(例如根据盈利数据决定退出)。 🚫 不适用场景:当您希望使用向量化指标数据退出时(应使用 populate_exit_trend() 信号),或作为 custom_stoploss() 的替代方案;请注意,在回测中基于价格的退出可能不准确。
  • custom_stoploss() - 自定义跟踪止损,在每次迭代时对每个未平仓交易调用,直到交易关闭为止。此处返回的值也将用于交易所止损单
    使用场景:根据交易数据或其他条件自定义止损逻辑,设置动态止损。 🚫 不适用场景:基于特定条件立即退出交易。此情况应使用 custom_exit()
  • custom_roi() - 自定义 ROI,在每次迭代时对每个未平仓交易调用,直到交易关闭为止。 ✅ 使用场景:指定最低 ROI 阈值(“止盈”),使交易在持续时间内达到该 ROI 水平时退出,可基于盈利或其他条件。 🚫 不适用场景:基于特定条件立即退出交易。应使用 custom_exit()。 🚫 不适用场景:静态 ROI。应使用 minimal_roi

机器人启动

一个简单的回调函数,在策略加载时仅调用一次。可用于执行只需进行一次的操作,并在数据提供器和钱包设置完成后运行。

import requests

class AwesomeStrategy(IStrategy):

    # ... populate_* methods

    def bot_start(self, **kwargs) -> None:
        """
        Called only once after bot instantiation.
        :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
        """
        if self.config["runmode"].value in ("live", "dry_run"):
            # Assign this to the class by using self.*
            # can then be used by populate_* methods
            self.custom_remote_data = requests.get("https://some_remote_source.example.com")

在超参数优化(hyperopt)期间,此函数仅在启动时运行一次。

机器人循环开始

一个简单的回调函数,在每次机器人节流循环开始时调用(实盘/模拟模式下大约每 5 秒一次,除非另有配置),或在回测/超参数优化模式下每个 K 线周期调用一次。可用于执行与交易对无关的计算(适用于所有交易对)、加载外部数据等操作。

# Default imports
import requests

class AwesomeStrategy(IStrategy):

    # ... populate_* methods

    def bot_loop_start(self, current_time: datetime, **kwargs) -> None:
        """
        Called at the start of the bot iteration (one loop).
        Might be used to perform pair-independent tasks
        (e.g. gather some remote resource for comparison)
        :param current_time: datetime object, containing the current datetime
        :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
        """
        if self.config["runmode"].value in ("live", "dry_run"):
            # Assign this to the class by using self.*
            # can then be used by populate_* methods
            self.remote_data = requests.get("https://some_remote_source.example.com")

质押金额管理

在进入交易前调用,可用于在新建交易时管理您的仓位规模。

# Default imports

class AwesomeStrategy(IStrategy):
    def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
                            proposed_stake: float, min_stake: float | None, max_stake: float,
                            leverage: float, entry_tag: str | None, side: str,
                            **kwargs) -> float:

        dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe)
        current_candle = dataframe.iloc[-1].squeeze()

        if current_candle["fastk_rsi_1h"] > current_candle["fastd_rsi_1h"]:
            if self.config["stake_amount"] == "unlimited":
                # Use entire available wallet during favorable conditions when in compounding mode.
                return max_stake
            else:
                # Compound profits during favorable conditions instead of using a static stake.
                return self.wallets.get_total_stake_amount() / self.config["max_open_trades"]

        # Use default stake amount.
        return proposed_stake

如果您的代码引发异常,Freqtrade 将回退到使用 proposed_stake 的值。异常本身会被记录下来。

提示

无需确保 min_stake <= 返回值 <= max_stake。返回的值将被限制在支持范围内,超出范围的操作会被记录,但交易仍会成功。

提示

返回 0None 将阻止交易下单。

自定义退出信号

每个节流周期(大约每 5 秒)对已开仓的交易调用一次,直到交易关闭为止。

允许定义自定义退出信号,表示应关闭指定仓位(完全退出)。当需要为每个单独交易定制退出条件,或需要使用交易数据来做出退出决策时,这非常有用。

例如,您可以使用 custom_exit() 实现 1:2 的风险回报率(ROI)。

不过,不建议使用 custom_exit() 信号来替代止损。在这方面,它不如 custom_stoploss() 方法有效——后者还能让您保持在交易所设置的止损单。

注意

从该方法返回一个(非空)字符串True,等同于在指定时间的 K 线设置退出信号。当已经设置了退出信号,或退出信号被禁用(use_exit_signal=False)时,此方法不会被调用。字符串的最大长度为 64 个字符,超过此限制的消息将被截断为 64 个字符。custom_exit() 会忽略 exit_profit_only,只要未禁用退出信号(即 use_exit_signal=True),无论是否有新的入场信号,该方法都会被调用。

以下是一个示例,展示如何根据当前盈利情况使用不同的指标,并退出持仓超过一天的交易:

# Default imports

class AwesomeStrategy(IStrategy):
    def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
                    current_profit: float, **kwargs):
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        last_candle = dataframe.iloc[-1].squeeze()

        # Above 20% profit, sell when rsi < 80
        if current_profit > 0.2:
            if last_candle["rsi"] < 80:
                return "rsi_below_80"

        # Between 2% and 10%, sell if EMA-long above EMA-short
        if 0.02 < current_profit < 0.1:
            if last_candle["emalong"] > last_candle["emashort"]:
                return "ema_long_below_80"

        # Sell any positions at a loss if they are held for more than one day.
        if current_profit < 0.0 and (current_time - trade.open_date_utc).days >= 1:
            return "unclog"

有关策略回调中使用 DataFrame 的更多信息,请参见 Dataframe 访问

自定义止损

每个周期(大约每 5 秒)对已开仓的交易调用一次,直到交易关闭为止。

必须在策略对象中设置 use_custom_stoploss=True 才能启用自定义止损方法。

止损价格只能向上移动——如果 custom_stoploss 返回的止损值会导致比之前设定更低的止损价,则该值将被忽略。传统的 stoploss 值作为绝对下限,在此方法首次被调用前会作为初始止损价生效,且仍是必填项。
由于自定义止损表现为常规的动态止损,其行为类似于 trailing_stop,因此因该机制触发的交易退出,其退出原因将显示为 "trailing_stop_loss"

该方法必须返回一个以当前价格百分比表示的止损值(浮点数/数值)。例如,若 current_rate 为 200 美元,则返回 0.02 将使止损价格设在低 2%的位置,即 196 美元。在回测期间,current_rate(以及 current_profit)基于 K 线的最高价(或做空交易的最低价)计算,而生成的止损则会与 K 线的最低价(或做空交易的最高价)进行比较判断是否触发。

使用返回值的绝对值(符号将被忽略),因此返回 0.05-0.05 效果相同,即在当前价格下方设置 5% 的止损。返回 None 表示“不希望修改”,这是在不想更改止损时唯一安全的返回方式。NaNinf 值被视为无效,将被忽略(等同于 None)。

交易所止损机制类似于 trailing_stop,止损单会按照 stoploss_on_exchange_interval 的配置进行更新(有关交易所止损的更多细节)。

如果你在期货市场交易,请注意止损与杠杆部分,因为从 custom_stoploss 返回的止损值代表该交易的风险水平,而非价格的相对变动。

日期的使用

所有基于时间的计算都应基于 current_time 进行——不建议使用 datetime.now()datetime.utcnow(),因为这会破坏回测功能的支持。

移动止损(Trailing stoploss)

当使用自定义止损值时,建议禁用 trailing_stop。虽然两者可以协同工作,但你可能会遇到移动止损将价格上调,而你的自定义函数并不希望如此,从而导致行为冲突。

持仓调整后调整止损

根据你的策略,你可能需要在持仓调整后双向调整止损位。为此,当订单成交后,Freqtrade 将额外调用一次 after_fill=True 的参数,允许策略在任何方向上调整止损(包括扩大止损与当前价格之间的距离,这在其他情况下是被禁止的)。

向后兼容性

仅当你的 custom_stoploss 函数定义中包含 after_fill 参数时,才会触发此次调用。因此,这不会影响(也不会意外改变)现有的运行中策略。

自定义止损示例

下一节将展示一些通过自定义止损函数可以实现的功能示例。当然,还有更多可能性,并且所有示例都可以随意组合使用。

通过自定义止损实现移动止损

要模拟常规的 4% 移动止损(即始终跟随最高价,保持 4% 的回落幅度),你可以使用以下非常简单的方法:

# Default imports

class AwesomeStrategy(IStrategy):

    # ... populate_* methods

    use_custom_stoploss = True

    def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
                        current_rate: float, current_profit: float, after_fill: bool, 
                        **kwargs) -> float | None:
        """
        Custom stoploss logic, returning the new distance relative to current_rate (as ratio).
        e.g. returning -0.05 would create a stoploss 5% below current_rate.
        The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss.

        For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/

        When not implemented by a strategy, returns the initial stoploss value.
        Only called when use_custom_stoploss is set to True.

        :param pair: Pair that's currently analyzed
        :param trade: trade object.
        :param current_time: datetime object, containing the current datetime
        :param current_rate: Rate, calculated based on pricing settings in exit_pricing.
        :param current_profit: Current profit (as ratio), calculated based on current_rate.
        :param after_fill: True if the stoploss is called after the order was filled.
        :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
        :return float: New stoploss value, relative to the current_rate
        """
        return -0.04 * trade.leverage

基于时间的移动止损

前 60 分钟使用初始止损,之后切换为 10% 的移动止损;2 小时(120 分钟)后,改为 5% 的移动止损。

# Default imports

class AwesomeStrategy(IStrategy):

    # ... populate_* methods

    use_custom_stoploss = True

    def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
                        current_rate: float, current_profit: float, after_fill: bool, 
                        **kwargs) -> float | None:

        # Make sure you have the longest interval first - these conditions are evaluated from top to bottom.
        if current_time - timedelta(minutes=120) > trade.open_date_utc:
            return -0.05 * trade.leverage
        elif current_time - timedelta(minutes=60) > trade.open_date_utc:
            return -0.10 * trade.leverage
        return None

带成交后调整的基于时间的移动止损

前 60 分钟使用初始止损,之后切换为 10% 的移动止损;2 小时(120 分钟)后,改为 5% 的移动止损。如果新增订单成交,则将止损设置为新 open_rate 下方 -10% 的位置(所有入场价的平均值)。

# Default imports

class AwesomeStrategy(IStrategy):

    # ... populate_* methods

    use_custom_stoploss = True

    def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
                        current_rate: float, current_profit: float, after_fill: bool, 
                        **kwargs) -> float | None:

        if after_fill: 
            # After an additional order, start with a stoploss of 10% below the new open rate
            return stoploss_from_open(0.10, current_profit, is_short=trade.is_short, leverage=trade.leverage)
        # Make sure you have the longest interval first - these conditions are evaluated from top to bottom.
        if current_time - timedelta(minutes=120) > trade.open_date_utc:
            return -0.05 * trade.leverage
        elif current_time - timedelta(minutes=60) > trade.open_date_utc:
            return -0.10 * trade.leverage
        return None

不同交易对使用不同止损

根据交易对设置不同的止损。在此示例中,ETH/BTCXRP/BTC 使用 10% 的移动止损跟踪最高价,LTC/BTC 使用 5% 的移动止损,其余所有交易对使用 15% 的移动止损。

# Default imports

class AwesomeStrategy(IStrategy):

    # ... populate_* methods

    use_custom_stoploss = True

    def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
                        current_rate: float, current_profit: float, after_fill: bool,
                        **kwargs) -> float | None:

        if pair in ("ETH/BTC", "XRP/BTC"):
            return -0.10 * trade.leverage
        elif pair in ("LTC/BTC"):
            return -0.05 * trade.leverage
        return -0.15 * trade.leverage

带正向偏移的移动止损

在利润达到 4%之前使用初始止损;当利润超过 4%后,启用当前利润 50%的移动止损,最低不低于 2.5%,最高不超过 5%。

请注意,止损位只能上升,低于当前止损位的值将被忽略。

# Default imports

class AwesomeStrategy(IStrategy):

    # ... populate_* methods

    use_custom_stoploss = True

    def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
                        current_rate: float, current_profit: float, after_fill: bool,
                        **kwargs) -> float | None:

        if current_profit < 0.04:
            return None # return None to keep using the initial stoploss

        # After reaching the desired offset, allow the stoploss to trail by half the profit
        desired_stoploss = current_profit / 2

        # Use a minimum of 2.5% and a maximum of 5%
        return max(min(desired_stoploss, 0.05), 0.025) * trade.leverage

分级止损

此示例不采用持续跟随当前价格的方式,而是根据当前利润设定固定的止损价格层级。

  • 在利润达到 20%之前,使用常规止损。
  • 一旦利润 > 20%,将止损设为开仓价以上 7%。
  • 一旦利润 > 25%,将止损设为开仓价以上 15%。
  • 一旦利润 > 40%,将止损设为开仓价以上 25%。
# Default imports

class AwesomeStrategy(IStrategy):

    # ... populate_* methods

    use_custom_stoploss = True

    def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
                        current_rate: float, current_profit: float, after_fill: bool,
                        **kwargs) -> float | None:

        # evaluate highest to lowest, so that highest possible stop is used
        if current_profit > 0.40:
            return stoploss_from_open(0.25, current_profit, is_short=trade.is_short, leverage=trade.leverage)
        elif current_profit > 0.25:
            return stoploss_from_open(0.15, current_profit, is_short=trade.is_short, leverage=trade.leverage)
        elif current_profit > 0.20:
            return stoploss_from_open(0.07, current_profit, is_short=trade.is_short, leverage=trade.leverage)

        # return maximum stoploss value, keeping current stoploss price unchanged
        return None

使用 DataFrame 中指标的自定义止损示例

绝对止损值可由存储在 DataFrame 中的指标推导得出。例如,本例使用位于价格下方的抛物线 SAR 作为止损。

# Default imports

class AwesomeStrategy(IStrategy):

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # <...>
        dataframe["sar"] = ta.SAR(dataframe)

    use_custom_stoploss = True

    def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
                        current_rate: float, current_profit: float, after_fill: bool,
                        **kwargs) -> float | None:

        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        last_candle = dataframe.iloc[-1].squeeze()

        # Use parabolic sar as absolute stoploss price
        stoploss_price = last_candle["sar"]

        # Convert absolute price to percentage relative to current_rate
        if stoploss_price < current_rate:
            return stoploss_from_absolute(stoploss_price, current_rate, is_short=trade.is_short)

        # return maximum stoploss value, keeping current stoploss price unchanged
        return None

有关策略回调中使用 DataFrame 的更多信息,请参见 Dataframe 访问

止损计算常用辅助函数

相对于开仓价的止损

custom_stoploss() 返回的止损值必须是相对于 current_rate 的百分比,但有时你可能希望指定相对于入场价的止损。stoploss_from_open() 是一个辅助函数,用于计算一个可以从 custom_stoploss 返回的止损值,使其等效于在入场点之上设定的目标盈利对应的止损位置。

从自定义止损函数返回相对于开仓价的止损值

假设开仓价为 100 美元,current_price 为 121 美元(此时 current_profit0.21)。

如果我们希望止损价位于开仓价上方 7%,可以调用 stoploss_from_open(0.07, current_profit, False),该函数将返回 0.1157024793。121 美元下方 11.57%的位置是 107 美元,正好等于 100 美元上方 7%。

此函数会考虑杠杆因素——例如在 10 倍杠杆下,实际止损将为 100 美元上方 0.7%(0.7% × 10 倍 = 7%)。

# Default imports

class AwesomeStrategy(IStrategy):

    # ... populate_* methods

    use_custom_stoploss = True

    def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
                        current_rate: float, current_profit: float, after_fill: bool,
                        **kwargs) -> float | None:

        # once the profit has risen above 10%, keep the stoploss at 7% above the open price
        if current_profit > 0.10:
            return stoploss_from_open(0.07, current_profit, is_short=trade.is_short, leverage=trade.leverage)

        return 1

完整示例可在文档的自定义止损章节中找到。

注意

stoploss_from_open() 提供无效输入可能导致出现“自定义止损函数未返回有效止损”警告。当 current_profit 参数小于指定的 open_relative_stop 时可能发生这种情况。此类情况可能由 confirm_trade_exit() 方法阻止交易退出所引发。可通过在 confirm_trade_exit() 中检查 exit_reason 避免阻止止损卖出,或使用 return stoploss_from_open(...) or 1 的写法来解决警告问题——当 current_profit < open_relative_stop 时,表示不更改止损。

从绝对价格计算止损百分比

custom_stoploss() 返回的止损值始终是相对于 current_rate 的百分比。若要将止损设置在某个特定的绝对价格水平,我们需要利用 stop_rate 计算出一个相对于当前价格的百分比,使得其效果等同于从开仓价计算该百分比。

可以使用辅助函数 stoploss_from_absolute() 将绝对价格转换为相对于当前价格的止损比例,以便从 custom_stoploss() 中返回。

使用自定义止损函数返回基于绝对价格的止损点

如果我们希望以当前价格下方 2 倍 ATR 的位置追踪止损价,可以调用 stoploss_from_absolute(current_rate + (side * candle["atr"] * 2), current_rate=current_rate, is_short=trade.is_short, leverage=trade.leverage)。对于期货交易,我们需要调整方向(向上或向下),并考虑杠杆因素,因为custom_stoploss回调返回的是“该交易的风险”,而不是相对价格变动。

# Default imports

class AwesomeStrategy(IStrategy):

    use_custom_stoploss = True

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
        return dataframe

    def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
                        current_rate: float, current_profit: float, after_fill: bool,
                        **kwargs) -> float | None:
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        trade_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc)
        candle = dataframe.iloc[-1].squeeze()
        side = 1 if trade.is_short else -1
        return stoploss_from_absolute(current_rate + (side * candle["atr"] * 2), 
                                      current_rate=current_rate, 
                                      is_short=trade.is_short,
                                      leverage=trade.leverage)

自定义 ROI

每个周期(大约每 5 秒)对已开仓的交易调用一次,直到交易关闭为止。

必须在策略对象中设置 use_custom_roi=True 才能启用自定义 ROI 方法。

该方法允许你定义一个自定义的最小 ROI 阈值来退出交易,以比率形式表示(例如,0.05 表示 5% 的利润)。如果同时定义了 minimal_roicustom_roi,则会以两者中较低的阈值触发退出。例如,若 minimal_roi 设置为 {"0": 0.10}(0 分钟时为 10%),而 custom_roi 返回 0.05,则当利润达到 5% 时交易将退出。同样,如果 custom_roi 返回 0.10minimal_roi 设置为 {"0": 0.05}(0 分钟时为 5%),交易将在利润达到 5% 时关闭。

该方法必须返回一个浮点数,表示作为比率的新 ROI 阈值,或返回 None 以回退到 minimal_roi 的逻辑。返回 NaNinf 被视为无效,并会被当作 None 处理,导致机器人使用 minimal_roi 配置。

自定义 ROI 示例

以下示例说明了如何使用 custom_roi 函数实现不同的 ROI 逻辑。

按交易方向设置不同的 ROI

根据 side 使用不同的 ROI 阈值。在此示例中,做多入场为 5%,做空入场为 2%。

# Default imports

class AwesomeStrategy(IStrategy):

    use_custom_roi = True

    # ... populate_* methods

    def custom_roi(self, pair: str, trade: Trade, current_time: datetime, trade_duration: int,
                   entry_tag: str | None, side: str, **kwargs) -> float | None:
        """
        Custom ROI logic, returns a new minimum ROI threshold (as a ratio, e.g., 0.05 for +5%).
        Only called when use_custom_roi is set to True.

        If used at the same time as minimal_roi, an exit will be triggered when the lower
        threshold is reached. Example: If minimal_roi = {"0": 0.01} and custom_roi returns 0.05,
        an exit will be triggered if profit reaches 5%.

        :param pair: Pair that's currently analyzed.
        :param trade: trade object.
        :param current_time: datetime object, containing the current datetime.
        :param trade_duration: Current trade duration in minutes.
        :param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
        :param side: 'long' or 'short' - indicating the direction of the current trade.
        :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
        :return float: New ROI value as a ratio, or None to fall back to minimal_roi logic.
        """
        return 0.05 if side == "long" else 0.02

按交易对设置不同的 ROI

根据 pair 使用不同的 ROI 阈值。

# Default imports

class AwesomeStrategy(IStrategy):

    use_custom_roi = True

    # ... populate_* methods

    def custom_roi(self, pair: str, trade: Trade, current_time: datetime, trade_duration: int,
                   entry_tag: str | None, side: str, **kwargs) -> float | None:

        stake = trade.stake_currency
        roi_map = {
            f"BTC/{stake}": 0.02, # 2% for BTC
            f"ETH/{stake}": 0.03, # 3% for ETH
            f"XRP/{stake}": 0.04, # 4% for XRP
        }

        return roi_map.get(pair, 0.01) # 1% for any other pair

按入场标签设置不同的 ROI

根据买入信号提供的 entry_tag 使用不同的 ROI 阈值。

# Default imports

class AwesomeStrategy(IStrategy):

    use_custom_roi = True

    # ... populate_* methods

    def custom_roi(self, pair: str, trade: Trade, current_time: datetime, trade_duration: int,
                   entry_tag: str | None, side: str, **kwargs) -> float | None:

        roi_by_tag = {
            "breakout": 0.08,       # 8% if tag is "breakout"
            "rsi_overbought": 0.05, # 5% if tag is "rsi_overbought"
            "mean_reversion": 0.03, # 3% if tag is "mean_reversion"
        }

        return roi_by_tag.get(entry_tag, 0.01)  # 1% if tag is unknown

基于 ATR 的自定义 ROI

ROI 值可以从存储在 dataframe 中的技术指标推导得出。此示例使用 ATR 比率作为 ROI。

# Default imports
# <...>
import talib.abstract as ta

class AwesomeStrategy(IStrategy):

    use_custom_roi = True

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # <...>
        dataframe["atr"] = ta.ATR(dataframe, timeperiod=10)

    def custom_roi(self, pair: str, trade: Trade, current_time: datetime, trade_duration: int,
                   entry_tag: str | None, side: str, **kwargs) -> float | None:

        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        last_candle = dataframe.iloc[-1].squeeze()
        atr_ratio = last_candle["atr"] / last_candle["close"]

        return atr_ratio # Returns the ATR value as ratio

自定义订单价格规则

默认情况下,Freqtrade 使用订单簿自动设置订单价格(相关文档),但你也能够基于你的策略创建自定义的订单价格。

你可以在策略文件中创建 custom_entry_price() 函数来自定义入场价格,以及使用 custom_exit_price() 来定制出场价格。

这些方法会在向交易所下单前立即调用。

注意

如果你的自定义定价函数返回 None 或无效值,则价格将回退至 proposed_rate,该值基于常规的定价配置。

注意

使用 custom_entry_price 时,一旦与交易相关的首个入场订单被创建,Trade 对象即可使用;对于首次入场,trade 参数的值将为 None

自定义订单入场和出场价格示例

# Default imports

class AwesomeStrategy(IStrategy):

    # ... populate_* methods

    def custom_entry_price(self, pair: str, trade: Trade | None, current_time: datetime, proposed_rate: float,
                           entry_tag: str | None, side: str, **kwargs) -> float:

        dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
                                                                timeframe=self.timeframe)
        new_entryprice = dataframe["bollinger_10_lowerband"].iat[-1]

        return new_entryprice

    def custom_exit_price(self, pair: str, trade: Trade,
                          current_time: datetime, proposed_rate: float,
                          current_profit: float, exit_tag: str | None, **kwargs) -> float:

        dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
                                                                timeframe=self.timeframe)
        new_exitprice = dataframe["bollinger_10_upperband"].iat[-1]

        return new_exitprice

警告

修改开仓和平仓价格仅对限价单有效。根据所选价格的不同,这可能导致大量未成交的订单。默认情况下,当前价格与自定义价格之间的最大允许距离为 2%,该值可以在配置中通过 custom_price_max_distance_ratio 参数进行调整。示例:如果新设置的开仓价为 97,建议价格为 100,且 custom_price_max_distance_ratio 设置为 2%,则最终保留的有效自定义开仓价将是 98,即比当前(建议)价格低 2%。

回测

回测支持自定义价格(从 2021.12 版本开始),只要价格落在 K 线的最低价/最高价范围内,订单就会成交。未能立即成交的订单将受到常规超时处理机制的影响,该机制每个(详细)K 线执行一次。custom_exit_price() 仅在卖出类型为 exit_signal、自定义退出或部分退出时被调用。其他所有退出类型都将使用标准的回测价格。

自定义订单超时规则

简单的时间型订单超时可以通过策略或配置文件中的 unfilledtimeout 部分进行设置。

然而,Freqtrade 还为两种订单类型提供了自定义回调函数,允许你基于自定义条件判断订单是否应被视为超时。

注意

回测中,若订单价格落在 K 线的最低价/最高价范围内,则视为成交。对于未立即成交的订单(使用了自定义定价),以下回调函数将在每个(详细)K 线上调用一次。

自定义订单超时示例

该回调会在每个未完成订单上持续调用,直到该订单被成交或取消。check_entry_timeout() 用于交易开仓,而 check_exit_timeout() 用于交易平仓订单。

下面是一个简单的示例,根据资产价格的不同应用不同的未成交超时时间:对高价资产采用较短的超时时间,而对低价币种则给予更长的成交时间。

该函数必须返回 True(取消订单)或 False(保留订单继续有效)。

    # Default imports

class AwesomeStrategy(IStrategy):

    # ... populate_* methods

    # Set unfilledtimeout to 25 hours, since the maximum timeout from below is 24 hours.
    unfilledtimeout = {
        "entry": 60 * 25,
        "exit": 60 * 25
    }

    def check_entry_timeout(self, pair: str, trade: Trade, order: Order,
                            current_time: datetime, **kwargs) -> bool:
        if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5):
            return True
        elif trade.open_rate > 10 and trade.open_date_utc < current_time - timedelta(minutes=3):
            return True
        elif trade.open_rate < 1 and trade.open_date_utc < current_time - timedelta(hours=24):
           return True
        return False


    def check_exit_timeout(self, pair: str, trade: Trade, order: Order,
                           current_time: datetime, **kwargs) -> bool:
        if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5):
            return True
        elif trade.open_rate > 10 and trade.open_date_utc < current_time - timedelta(minutes=3):
            return True
        elif trade.open_rate < 1 and trade.open_date_utc < current_time - timedelta(hours=24):
           return True
        return False

注意

对于上述示例,unfilledtimeout 必须设置为大于 24 小时的值,否则该类型的超时将优先触发。

自定义订单超时示例(使用附加数据)

    # Default imports

class AwesomeStrategy(IStrategy):

    # ... populate_* methods

    # Set unfilledtimeout to 25 hours, since the maximum timeout from below is 24 hours.
    unfilledtimeout = {
        "entry": 60 * 25,
        "exit": 60 * 25
    }

    def check_entry_timeout(self, pair: str, trade: Trade, order: Order,
                            current_time: datetime, **kwargs) -> bool:
        ob = self.dp.orderbook(pair, 1)
        current_price = ob["bids"][0][0]
        # Cancel buy order if price is more than 2% above the order.
        if current_price > order.price * 1.02:
            return True
        return False


    def check_exit_timeout(self, pair: str, trade: Trade, order: Order,
                           current_time: datetime, **kwargs) -> bool:
        ob = self.dp.orderbook(pair, 1)
        current_price = ob["asks"][0][0]
        # Cancel sell order if price is more than 2% below the order.
        if current_price < order.price * 0.98:
            return True
        return False

机器人订单确认

确认交易开仓/平仓。这些方法是在下单前最后被调用的函数。

交易开仓(买入订单)确认

confirm_trade_entry() 可用于在最后一刻中止交易开仓(例如因为当前价格不符合我们的预期)。

# Default imports

class AwesomeStrategy(IStrategy):

    # ... populate_* methods

    def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
                            time_in_force: str, current_time: datetime, entry_tag: str | None,
                            side: str, **kwargs) -> bool:
        """
        Called right before placing a entry order.
        Timing for this function is critical, so avoid doing heavy computations or
        network requests in this method.

        For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/

        When not implemented by a strategy, returns True (always confirming).

        :param pair: Pair that's about to be bought/shorted.
        :param order_type: Order type (as configured in order_types). usually limit or market.
        :param amount: Amount in target (base) currency that's going to be traded.
        :param rate: Rate that's going to be used when using limit orders 
                     or current rate for market orders.
        :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
        :param current_time: datetime object, containing the current datetime
        :param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
        :param side: "long" or "short" - indicating the direction of the proposed trade
        :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
        :return bool: When True is returned, then the buy-order is placed on the exchange.
            False aborts the process
        """
        return True

交易退出(卖出订单)确认

confirm_trade_exit() 可用于在最后一刻中止交易退出(卖出)操作(例如因为当前价格不符合预期)。

confirm_trade_exit() 在一次迭代中可能会被多次调用,即使针对同一笔交易,只要适用不同的退出原因。退出原因(如适用)的调用顺序如下:

  • exit_signal / custom_exit
  • stop_loss
  • 投资回报率(roi)
  • trailing_stop_loss
# Default imports

class AwesomeStrategy(IStrategy):

    # ... populate_* methods

    def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
                           rate: float, time_in_force: str, exit_reason: str,
                           current_time: datetime, **kwargs) -> bool:
        """
        Called right before placing a regular exit order.
        Timing for this function is critical, so avoid doing heavy computations or
        network requests in this method.

        For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/

        When not implemented by a strategy, returns True (always confirming).

        :param pair: Pair for trade that's about to be exited.
        :param trade: trade object.
        :param order_type: Order type (as configured in order_types). usually limit or market.
        :param amount: Amount in base currency.
        :param rate: Rate that's going to be used when using limit orders
                     or current rate for market orders.
        :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
        :param exit_reason: Exit reason.
            Can be any of ["roi", "stop_loss", "stoploss_on_exchange", "trailing_stop_loss",
                           "exit_signal", "force_exit", "emergency_exit"]
        :param current_time: datetime object, containing the current datetime
        :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
        :return bool: When True, then the exit-order is placed on the exchange.
            False aborts the process
        """
        if exit_reason == "force_exit" and trade.calc_profit_ratio(rate) < 0:
            # Reject force-sells with negative profit
            # This is just a sample, please adjust to your needs
            # (this does not necessarily make sense, assuming you know when you're force-selling)
            return False
        return True

警告

confirm_trade_exit() 可以阻止止损退出,但这可能导致重大损失,因为它会忽略止损指令。confirm_trade_exit() 不会被用于强平场景——因为强平是由交易所强制执行的,无法拒绝。

调整交易仓位

策略属性 position_adjustment_enable 用于启用策略中的 adjust_trade_position() 回调函数。出于性能考虑,默认情况下该功能是关闭的,如果启用,Freqtrade 在启动时会显示警告信息。adjust_trade_position() 可用于执行额外订单,例如通过 DCA(定投平均成本法)管理风险,或增加或减少仓位。

额外订单会产生额外费用,并且这些订单不计入 max_open_trades 限制。

当存在等待执行的未成交订单(买入或卖出)时,该回调函数也会被调用——如果新订单的金额、价格或方向不同,将取消现有的未成交订单并提交新订单。此外,对于部分成交的订单也会被取消,并根据回调函数返回的新金额替换为新的订单。

adjust_trade_position() 在交易持续期间会被频繁调用,因此你必须尽可能保证其实现的高性能。

仓位调整始终沿当前交易方向进行,因此无论做多还是做空,正值始终会增加你的仓位(负值则减少仓位)。可以通过返回一个包含两个元素的元组来为调整订单分配标签,其中第一个元素为调整金额,第二个元素为标签(例如:return 250, "increase_favorable_conditions")。

无法修改杠杆,返回的投注金额(stake-amount)视为应用杠杆前的金额。

当前分配给该仓位的总投注金额存储在 trade.stake_amount 中。因此,每次通过 adjust_trade_position() 进行加仓或部分减仓时,trade.stake_amount 都会相应更新。

宽松的逻辑

在模拟运行和实盘运行中,此函数将每隔 throttle_process_secs(默认为 5 秒)调用一次。如果你使用了宽松的逻辑(例如:若上一根 K 线的 RSI 低于 30 则加仓),机器人将每 5 秒重复加仓,直到资金耗尽、达到 max_position_adjustment 限制,或出现 RSI 高于 30 的新 K 线为止。

部分退出也可能发生类似情况。
因此,请确保使用严格的逻辑,和/或检查最近成交的订单以及是否有订单已处于未成交状态。

大量仓位调整时的性能表现

仓位调整可能是提升策略收益的有效方法——但如果过度使用,也可能带来负面影响。
每个订单在交易持续期间都会附加到交易对象上,从而增加内存占用。因此,不建议持有长时间交易并进行数十次甚至上百次仓位调整的交易,应定期平仓以避免影响系统性能。

回测

回测期间,该回调函数会在每个 timeframetimeframe_detail 的 K 线上被调用一次,因此会影响运行性能。这也可能导致实盘与回测结果出现差异:回测中每根 K 线最多只能调整一次仓位,而实盘中可能在单根 K 线内多次调整。

增加仓位

当需要进行额外建仓(即增加仓位——对多头交易为买入订单,对空头交易为卖出订单)时,策略应返回一个介于 min_stakemax_stake 之间的正数 stake_amount(以投注货币计)

如果钱包中的资金不足(返回值超过 max_stake),则该信号将被忽略。max_entry_position_adjustment 属性用于限制每笔交易可执行的追加建仓次数(不包括首次开仓订单)。默认值为 -1,表示机器人对追加建仓的次数没有限制。

一旦追加建仓次数达到你在 max_entry_position_adjustment 中设置的最大值,后续的追加建仓信号将被忽略,但回调函数仍会被调用以检查是否存在部分平仓信号。

关于投注金额

使用固定投注金额时,该金额将作为首笔订单的投注金额,与不启用仓位调整时相同。如果你希望通过 DCA 进行多次加仓,请确保钱包中留有足够的资金。当使用 "unlimited" 投注金额并配合 DCA 订单时,你必须实现 custom_stake_amount() 回调函数,以避免将所有资金都分配给初始订单。

减仓

策略应返回一个负的 stake_amount(以投注货币计)来表示部分平仓。在该点返回全部持仓金额(-trade.stake_amount)将导致完全平仓。
如果返回的数值超过当前持仓总额(即剩余 stake_amount 变为负数),机器人将忽略该信号。

对于部分平仓,重要的是要了解用于计算卖出币种数量的公式为:部分平仓数量 = 负的 stake_amount * trade.amount / trade.stake_amount,其中 负的 stake_amountadjust_trade_position 函数返回的值。如公式所示,该计算不考虑当前持仓的盈亏情况,仅依赖于 trade.amounttrade.stake_amount,而这二者完全不受价格波动影响。

例如,假设你以 50 的开仓价买入 2 个 SHITCOIN/USDT,这意味着交易的投注金额为 100 USDT。现在价格上涨至 200,你想卖出一半。此时,你需要返回 trade.stake_amount 的 -50%(即 0.5 * 100 USDT),也就是 -50。机器人将计算需要卖出的数量:50 * 2 / 100,结果为 1 SHITCOIN/USDT。如果你返回 -200(即 50% × 2 × 200),机器人将忽略该信号,因为 trade.stake_amount 只有 100 USDT,而你要求卖出 200 USDT,相当于要求卖出 4 个 SHITCOIN/USDT。

回到上面的例子,由于当前价格为 200,你持仓的当前 USDT 价值为 400 USDT。假设你想部分卖出 100 USDT,以收回初始投资并保留利润继续持仓,希望价格继续上涨。这时你需要采用不同的方法。首先,计算需要卖出的确切数量:你想基于当前价格卖出价值 100 USDT 的代币,因此需要卖出的数量为 100 * 2 / 400,即 0.5 SHITCOIN/USDT。既然已知要卖出的数量(0.5),那么你在 adjust_trade_position 函数中应返回的值为 -部分平仓数量 * trade.stake_amount / trade.amount,即 -25。机器人将卖出 0.5 SHITCOIN/USDT,保留 1.5 在持仓中,你将从此次部分平仓中获得 100 USDT。

止损计算

止损仍然基于初始开仓价格计算,而不是基于平均价格。常规的止损规则依然适用(不能向下移动止损位)。

/stopentry 命令虽然会阻止机器人开立新交易,但仓位调整功能仍会在现有交易上继续买入新订单。

# Default imports

class DigDeeperStrategy(IStrategy):

    position_adjustment_enable = True

    # Attempts to handle large drops with DCA. High stoploss is required.
    stoploss = -0.30

    # ... populate_* methods

    # Example specific variables
    max_entry_position_adjustment = 3
    # This number is explained a bit further down
    max_dca_multiplier = 5.5

    # This is called when placing the initial order (opening trade)
    def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
                            proposed_stake: float, min_stake: float | None, max_stake: float,
                            leverage: float, entry_tag: str | None, side: str,
                            **kwargs) -> float:

        # We need to leave most of the funds for possible further DCA orders
        # This also applies to fixed stakes
        return proposed_stake / self.max_dca_multiplier

    def adjust_trade_position(self, trade: Trade, current_time: datetime,
                              current_rate: float, current_profit: float,
                              min_stake: float | None, max_stake: float,
                              current_entry_rate: float, current_exit_rate: float,
                              current_entry_profit: float, current_exit_profit: float,
                              **kwargs
                              ) -> float | None | tuple[float | None, str | None]:
        """
        Custom trade adjustment logic, returning the stake amount that a trade should be
        increased or decreased.
        This means extra entry or exit orders with additional fees.
        Only called when `position_adjustment_enable` is set to True.

        For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/

        When not implemented by a strategy, returns None

        :param trade: trade object.
        :param current_time: datetime object, containing the current datetime
        :param current_rate: Current entry rate (same as current_entry_profit)
        :param current_profit: Current profit (as ratio), calculated based on current_rate 
                               (same as current_entry_profit).
        :param min_stake: Minimal stake size allowed by exchange (for both entries and exits)
        :param max_stake: Maximum stake allowed (either through balance, or by exchange limits).
        :param current_entry_rate: Current rate using entry pricing.
        :param current_exit_rate: Current rate using exit pricing.
        :param current_entry_profit: Current profit using entry pricing.
        :param current_exit_profit: Current profit using exit pricing.
        :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
        :return float: Stake amount to adjust your trade,
                       Positive values to increase position, Negative values to decrease position.
                       Return None for no action.
                       Optionally, return a tuple with a 2nd element with an order reason
        """
        if trade.has_open_orders:
            # Only act if no orders are open
            return

        if current_profit > 0.05 and trade.nr_of_successful_exits == 0:
            # Take half of the profit at +5%
            return -(trade.stake_amount / 2), "half_profit_5%"

        if current_profit > -0.05:
            return None

        # Obtain pair dataframe (just to show how to access it)
        dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe)
        # Only buy when not actively falling price.
        last_candle = dataframe.iloc[-1].squeeze()
        previous_candle = dataframe.iloc[-2].squeeze()
        if last_candle["close"] < previous_candle["close"]:
            return None

        filled_entries = trade.select_filled_orders(trade.entry_side)
        count_of_entries = trade.nr_of_successful_entries
        # Allow up to 3 additional increasingly larger buys (4 in total)
        # Initial buy is 1x
        # If that falls to -5% profit, we buy 1.25x more, average profit should increase to roughly -2.2%
        # If that falls down to -5% again, we buy 1.5x more
        # If that falls once again down to -5%, we buy 1.75x more
        # Total stake for this trade would be 1 + 1.25 + 1.5 + 1.75 = 5.5x of the initial allowed stake.
        # That is why max_dca_multiplier is 5.5
        # Hope you have a deep wallet!
        try:
            # This returns first order stake size
            stake_amount = filled_entries[0].stake_amount_filled
            # This then calculates current safety order size
            stake_amount = stake_amount * (1 + (count_of_entries * 0.25))
            return stake_amount, "1/3rd_increase"
        except Exception as exception:
            return None

        return None

仓位调整计算

  • 入场价格通过加权平均法计算。
  • 退出操作不会影响平均入场价格。
  • 部分退出的相对收益是相对于当前的平均入场价格而言的。
  • 最终退出的相对收益基于总投资金额计算。(见下文示例)
计算示例

本示例为简化起见假设手续费为 0,并针对一种虚构币种建立多头仓位。

  • 买入 100@8$
  • 买入 100@9$ -> 平均价格:8.5$
  • 卖出 100@10$ -> 平均价格:8.5$,已实现利润 150$,收益率 17.65%
  • 买入 150@11$ -> 平均价格:10$,已实现利润 150$,收益率 17.65%
  • 卖出 100@12$ -> 平均价格:10$,累计已实现利润 350$,收益率 20%
  • 卖出 150@14$ -> 平均价格:10$,累计已实现利润 950$,收益率 40% <- 这将是最后一条“退出”消息

该笔交易总利润为 950$,总投资额为 3350$(100@8$ + 100@9$ + 150@11$)。因此,最终相对收益率为 28.35%(950 / 3350)。

调整订单价格

策略开发者可使用 adjust_order_price() 回调函数,在新 K 线到来时刷新或替换限价订单。
每次迭代都会调用此回调,除非当前 K 线内订单已被(重新)下单——从而限制每个订单每根 K 线最多(重新)下单一次。这也意味着首次调用将在初始订单下达后的下一根 K 线开始时进行。

请注意,custom_entry_price()/custom_exit_price() 仍是在发出信号时决定初始限价订单目标价格的主要依据。

通过返回 None 可在此回调中取消订单。

返回 current_order_rate 将使订单在交易所保持原样;返回其他任何价格将取消原有订单并以新价格重新下单。

如果原始订单取消失败,则不会进行替换——尽管该订单很可能已在交易所被取消。若发生在初始建仓阶段,会导致订单被删除;而在仓位调整阶段,则会导致仓位规模维持不变。
若订单已被部分成交,则不会被替换。如有必要或需要,您可使用 adjust_trade_position() 来将仓位规模调整至预期水平。

常规超时

入场 unfilledtimeout 机制(以及 check_entry_timeout()/check_exit_timeout())优先于该回调。通过上述方法取消的订单不会触发此回调。请确保更新超时设置以符合您的预期。

# Default imports

class AwesomeStrategy(IStrategy):

    # ... populate_* methods

    def adjust_order_price(
        self,
        trade: Trade,
        order: Order | None,
        pair: str,
        current_time: datetime,
        proposed_rate: float,
        current_order_rate: float,
        entry_tag: str | None,
        side: str,
        is_entry: bool,
        **kwargs,
    ) -> float | None:
        """
        Exit and entry order price re-adjustment logic, returning the user desired limit price.
        This only executes when a order was already placed, still open (unfilled fully or partially)
        and not timed out on subsequent candles after entry trigger.

        For full documentation please go to https://www.freqtrade.io/en/latest/strategy-callbacks/

        When not implemented by a strategy, returns current_order_rate as default.
        If current_order_rate is returned then the existing order is maintained.
        If None is returned then order gets canceled but not replaced by a new one.

        :param pair: Pair that's currently analyzed
        :param trade: Trade object.
        :param order: Order object
        :param current_time: datetime object, containing the current datetime
        :param proposed_rate: Rate, calculated based on pricing settings in entry_pricing.
        :param current_order_rate: Rate of the existing order in place.
        :param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
        :param side: 'long' or 'short' - indicating the direction of the proposed trade
        :param is_entry: True if the order is an entry order, False if it's an exit order.
        :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
        :return float or None: New entry price value if provided
        """

        # Limit entry orders to use and follow SMA200 as price target for the first 10 minutes since entry trigger for BTC/USDT pair.
        if (
            is_entry
            and pair == "BTC/USDT" 
            and entry_tag == "long_sma200" 
            and side == "long" 
            and (current_time - timedelta(minutes=10)) <= trade.open_date_utc
        ):
            # just cancel the order if it has been filled more than half of the amount
            if order.filled > order.remaining:
                return None
            else:
                dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe)
                current_candle = dataframe.iloc[-1].squeeze()
                # desired price
                return current_candle["sma_200"]
        # default: maintain existing order
        return current_order_rate

adjust_*_price() 的不兼容性

如果你同时实现了 adjust_order_price()adjust_entry_price()/adjust_exit_price(),则只会使用 adjust_order_price()。如果你需要调整入场/出场价格,可以将逻辑实现在 adjust_order_price() 中,或者使用分开的 adjust_entry_price() / adjust_exit_price() 回调,但不能同时使用两者。混合使用这些方法是不被支持的,并会在机器人启动时引发错误。

调整入场价格

adjust_entry_price() 回调可由策略开发者用于在订单到达时刷新或替换入场限价单。它是 adjust_order_price() 的子集,仅对入场订单调用。其余所有行为与 adjust_order_price() 相同。

交易的开仓时间(trade.open_date_utc)将保持为第一个订单下单时的时间。请注意这一点,并在其他回调中相应地调整你的逻辑,考虑使用第一个成交订单的时间。

调整出场价格

adjust_exit_price() 回调可由策略开发者用于在订单到达时刷新或替换出场限价单。它是 adjust_order_price() 的子集,仅对出场订单调用。其余所有行为与 adjust_order_price() 相同。

杠杆回调

在支持杠杆交易的市场中,此方法必须返回期望的杠杆倍数(默认值为 1,表示无杠杆)。

假设本金为 500 USDT,以杠杆=3 进行交易,将形成一个价值 500 × 3 = 1500 USDT 的仓位。

超过 max_leverage 的值将被调整为 max_leverage。对于不支持杠杆的市场或交易所,此方法将被忽略。

# Default imports

class AwesomeStrategy(IStrategy):
    def leverage(self, pair: str, current_time: datetime, current_rate: float,
                 proposed_leverage: float, max_leverage: float, entry_tag: str | None, side: str,
                 **kwargs) -> float:
        """
        Customize leverage for each new trade. This method is only called in futures mode.

        :param pair: Pair that's currently analyzed
        :param current_time: datetime object, containing the current datetime
        :param current_rate: Rate, calculated based on pricing settings in exit_pricing.
        :param proposed_leverage: A leverage proposed by the bot.
        :param max_leverage: Max leverage allowed on this pair
        :param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
        :param side: "long" or "short" - indicating the direction of the proposed trade
        :return: A leverage amount, which is between 1.0 and max_leverage.
        """
        return 1.0

所有利润计算均包含杠杆效应。止损/回报率(ROI)的计算也包含杠杆。例如,在 10 倍杠杆下设置 10%的止损,实际价格下跌 1%时就会触发止损。

订单成交回调

order_filled() 回调可用于在订单成交后,根据当前交易状态执行特定操作。无论订单类型(入场、出场、止损或仓位调整),该回调都会被调用。

假设你的策略需要存储交易入场时 K 线的最高价,可以通过此回调实现,如下例所示。

# Default imports

class AwesomeStrategy(IStrategy):
    def order_filled(self, pair: str, trade: Trade, order: Order, current_time: datetime, **kwargs) -> None:
        """
        Called right after an order fills. 
        Will be called for all order types (entry, exit, stoploss, position adjustment).
        :param pair: Pair for trade
        :param trade: trade object.
        :param order: Order object.
        :param current_time: datetime object, containing the current datetime
        :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
        """
        # Obtain pair dataframe (just to show how to access it)
        dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe)
        last_candle = dataframe.iloc[-1].squeeze()

        if (trade.nr_of_successful_entries == 1) and (order.ft_order_side == trade.entry_side):
            trade.set_custom_data(key="entry_candle_high", value=last_candle["high"])

        return None

了解更多关于数据存储的信息

你可以在 存储自定义交易数据 章节中了解更多关于数据存储的内容。请注意,这属于高级用法,应谨慎使用。

图表注释回调

每当 freqUI 请求数据以显示图表时,就会调用图表注释回调。该回调在交易周期上下文中没有实际意义,仅用于图表展示。

策略可以返回一个 AnnotationType 对象列表,用于在图表上显示。根据返回内容的不同,图表可以显示水平区域、垂直区域或矩形框。

完整的对象结构如下:

{
    "type": "area", // Type of the annotation, currently only "area" is supported
    "start": "2024-01-01 15:00:00", // Start date of the area
    "end": "2024-01-01 16:00:00",  // End date of the area
    "y_start": 94000.2,  // Price / y axis value
    "y_end": 98000, // Price / y axis value
    "color": "",
    "z_level": 5, // z-level, higher values are drawn on top of lower values. Positions relative to the Chart elements need to be set in freqUI.
    "label": "some label"
}

以下示例将在图表上标记出 8 点和 15 点这两个时间段,使用灰色突出显示市场开盘和收盘时间。这显然只是一个非常基础的示例。

# Default imports

class AwesomeStrategy(IStrategy):
    def plot_annotations(
        self, pair: str, start_date: datetime, end_date: datetime, dataframe: DataFrame, **kwargs
    ) -> list[AnnotationType]:
        """
        Retrieve area annotations for a chart.
        Must be returned as array, with type, label, color, start, end, y_start, y_end.
        All settings except for type are optional - though it usually makes sense to include either
        "start and end" or "y_start and y_end" for either horizontal or vertical plots
        (or all 4 for boxes).
        :param pair: Pair that's currently analyzed
        :param start_date: Start date of the chart data being requested
        :param end_date: End date of the chart data being requested
        :param dataframe: DataFrame with the analyzed data for the chart
        :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
        :return: List of AnnotationType objects
        """
        annotations = []
        while start_dt < end_date:
            start_dt += timedelta(hours=1)
            if start_dt.hour in (8, 15):
                annotations.append(
                    {
                        "type": "area",
                        "label": "Trade open and close hours",
                        "start": start_dt,
                        "end": start_dt + timedelta(hours=1),
                        # Omitting y_start and y_end will result in a vertical area spanning the whole height of the main Chart
                        "color": "rgba(133, 133, 133, 0.4)",
                    }
                )

        return annotations

条目将被验证,如果不符合预期的模式,则不会传递给用户界面,并且会记录错误。

大量注释

使用过多注释可能导致用户界面无响应,尤其是在绘制大量历史数据时。请谨慎使用注释功能。

绘图注释示例

FreqUI - plot Annotations FreqUI - plot Annotations

上方图表所用代码

此代码仅为示例,应据此理解其用途。

# Default imports

class AwesomeStrategy(IStrategy):
    def plot_annotations(
        self, pair: str, start_date: datetime, end_date: datetime, dataframe: DataFrame, **kwargs
    ) -> list[AnnotationType]:
        annotations = []
        while start_dt < end_date:
            start_dt += timedelta(hours=1)
            if (start_dt.hour % 4) == 0:
                mark_areas.append(
                    {
                        "type": "area",
                        "label": "4h",
                        "start": start_dt,
                        "end": start_dt + timedelta(hours=1),
                        "color": "rgba(133, 133, 133, 0.4)",
                    }
                )
            elif (start_dt.hour % 2) == 0:
            price = dataframe.loc[dataframe["date"] == start_dt, ["close"]].mean()
                mark_areas.append(
                    {
                        "type": "area",
                        "label": "2h",
                        "start": start_dt,
                        "end": start_dt + timedelta(hours=1),
                        "y_end": price * 1.01,
                        "y_start": price * 0.99,
                        "color": "rgba(0, 255, 0, 0.4)",
                        "z_level": 5,
                    }
                )

        return annotations