策略回调¶
主要的策略函数(populate_indicators()、populate_entry_trend()、populate_exit_trend())应以向量化方式使用,在回测期间仅被调用一次,而回调函数则在“需要时”被调用。
因此,应避免在回调函数中进行复杂的计算,以防止操作过程中出现延迟。根据所使用的回调类型,它们可能在进入/退出交易时被调用,或在交易持续期间多次调用。
当前可用的回调函数:
bot_start()bot_loop_start()custom_stake_amount()custom_exit()custom_stoploss()custom_roi()custom_entry_price()和custom_exit_price()check_entry_timeout()和check_exit_timeout()confirm_trade_entry()confirm_trade_exit()adjust_trade_position()adjust_entry_price()leverage()order_filled()
回调调用顺序
您可以在 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。返回的值将被限制在支持范围内,超出范围的操作会被记录,但交易仍会成功。
提示
返回 0 或 None 将阻止交易下单。
自定义退出信号¶
每个节流周期(大约每 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 表示“不希望修改”,这是在不想更改止损时唯一安全的返回方式。NaN 和 inf 值被视为无效,将被忽略(等同于 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/BTC 和 XRP/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_profit 为 0.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_roi 和 custom_roi,则会以两者中较低的阈值触发退出。例如,若 minimal_roi 设置为 {"0": 0.10}(0 分钟时为 10%),而 custom_roi 返回 0.05,则当利润达到 5% 时交易将退出。同样,如果 custom_roi 返回 0.10 而 minimal_roi 设置为 {"0": 0.05}(0 分钟时为 5%),交易将在利润达到 5% 时关闭。
该方法必须返回一个浮点数,表示作为比率的新 ROI 阈值,或返回 None 以回退到 minimal_roi 的逻辑。返回 NaN 或 inf 被视为无效,并会被当作 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_exitstop_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 线为止。
部分退出也可能发生类似情况。
因此,请确保使用严格的逻辑,和/或检查最近成交的订单以及是否有订单已处于未成交状态。
大量仓位调整时的性能表现
仓位调整可能是提升策略收益的有效方法——但如果过度使用,也可能带来负面影响。
每个订单在交易持续期间都会附加到交易对象上,从而增加内存占用。因此,不建议持有长时间交易并进行数十次甚至上百次仓位调整的交易,应定期平仓以避免影响系统性能。
回测
回测期间,该回调函数会在每个 timeframe 或 timeframe_detail 的 K 线上被调用一次,因此会影响运行性能。这也可能导致实盘与回测结果出现差异:回测中每根 K 线最多只能调整一次仓位,而实盘中可能在单根 K 线内多次调整。
增加仓位¶
当需要进行额外建仓(即增加仓位——对多头交易为买入订单,对空头交易为卖出订单)时,策略应返回一个介于 min_stake 和 max_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_amount 是 adjust_trade_position 函数返回的值。如公式所示,该计算不考虑当前持仓的盈亏情况,仅依赖于 trade.amount 和 trade.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
条目将被验证,如果不符合预期的模式,则不会传递给用户界面,并且会记录错误。
大量注释
使用过多注释可能导致用户界面无响应,尤其是在绘制大量历史数据时。请谨慎使用注释功能。
绘图注释示例¶

上方图表所用代码
此代码仅为示例,应据此理解其用途。
# 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