跳到主要内容

Strategy Callbacks

While the main strategy functions (populate_indicators(), populate_entry_trend(), populate_exit_trend()) should be used in a vectorized way, and are only called once during backtesting, callbacks are called "whenever needed".

As such, you should avoid doing heavy calculations in callbacks to avoid delays during operations.

Depending on the callback used, they may be called when entering / exiting a trade, or throughout the duration of a trade.

Currently available callbacks:

Callback calling sequence

You can find the callback calling sequence in bot-basics

Bot start

A simple callback which is called once when the strategy is loaded. This can be used to perform actions that must only be performed once and runs after dataprovider and wallet are set

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")

During hyperopt, this runs only once at startup.

Bot loop start

A simple callback which is called once at the start of every bot throttling iteration in dry/live mode (roughly every 5 seconds, unless configured differently) or once per candle in backtest/hyperopt mode.

This can be used to perform calculations which are pair independent (apply to all pairs), loading of external data, etc.

# 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")

Stake size management

Called before entering a trade, makes it possible to manage your position size when placing a new trade.

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:
"""
Customize stake size for each new trade.

: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_stake: A stake amount proposed by the bot.
:param min_stake: Minimal stake size allowed by exchange (for both entries and exits)
:param max_stake: Maximum stake size allowed by exchange (for both entries and exits)
:param leverage: Leverage selected for this trade.
: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: A stake size, which is between min_stake and max_stake.
"""
return proposed_stake

For example, to use a fixed stake amount of 150 USDT:

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:
return 150.0

Custom exit signal

Called for open trade every bot iteration (roughly every 5 seconds) until a trade is closed.

Allows to define custom exit signals, indicating that specified position should be sold. This is very useful when we need to customize exit conditions for each individual trade, or if you need trade data to make an exit decision.

from freqtrade.persistence import Trade

class AwesomeStrategy(IStrategy):
def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
current_profit: float, **kwargs):
"""
Custom exit signal logic indicating that specified position should be sold.

: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 **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return: To execute exit, return a string with custom exit reason or True. Otherwise return None or False.
"""
if current_profit > 0.20:
return 'profit_target_reached'

# Exit after 3 days
if (current_time - trade.open_date_utc).days >= 3:
return 'max_holding_period'

return None

Custom stoploss

Called for open trade every bot iteration (roughly every 5 seconds) until a trade is closed.

The usage of the custom stoploss method must be enabled by setting use_custom_stoploss=True on the strategy object.

from freqtrade.persistence import Trade

class AwesomeStrategy(IStrategy):
use_custom_stoploss = True

def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> float:
"""
Custom stoploss logic, returning the new distance relative to current_rate (as ratio).

: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 **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return: New stoploss value, relative to the current_rate
"""

# Calculate time-based trailing stop
trade_duration = (current_time - trade.open_date_utc).total_seconds() / 3600 # in hours

if trade_duration < 1:
return -0.05 # 5% stop loss for first hour
elif trade_duration < 4:
return -0.03 # 3% stop loss after 1 hour
else:
return -0.02 # 2% stop loss after 4 hours

Custom ROI

Called for open trade every bot iteration (roughly every 5 seconds) until a trade is closed.

from freqtrade.persistence import Trade

class AwesomeStrategy(IStrategy):
use_custom_roi = True

def custom_roi(self, pair: str, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> float | None:
"""
Custom ROI logic, returning the new ROI relative to current_rate (as ratio).

: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 **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return: New ROI value or None to keep using the existing ROI
"""

# Dynamic ROI based on market conditions
if current_profit > 0.10: # If profit > 10%
return 0.05 # Set ROI to 5%

return None # Use default ROI

Trade entry confirmation

Called right before placing a entry order.

class AwesomeStrategy(IStrategy):
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.

:param pair: Pair that's about to be bought/shorted.
:param order_type: Order type (as configured in order_types).
: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
: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: True -> allow trade, False -> reject trade
"""

# Don't enter trades during high volatility periods
if self.is_high_volatility(pair):
return False

return True

Trade exit confirmation

Called right before placing an exit order.

from freqtrade.persistence import Trade

class AwesomeStrategy(IStrategy):
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 an exit order.

: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).
:param amount: Amount in base currency.
:param rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param exit_reason: Exit reason.
: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: True -> allow exit, False -> reject exit
"""

# Don't exit during stop loss if profit is positive
if exit_reason == 'stop_loss' and trade.calc_profit_ratio(rate) > 0:
return False

return True

Adjust trade position

Called once per bot iteration for open trades.

from freqtrade.persistence import Trade

class AwesomeStrategy(IStrategy):
position_adjustment_enable = True

def adjust_trade_position(self, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float,
min_stake: float | None, max_stake: float, **kwargs):
"""
Custom trade adjustment logic, returning the stake amount that a trade should be
increased or decreased.

:param trade: trade object.
:param current_time: datetime object, containing the current datetime
:param current_rate: Current buy rate.
:param current_profit: Current profit (as ratio), calculated based on current_rate.
:param min_stake: Minimal stake size allowed by exchange
:param max_stake: Maximum stake size allowed by exchange
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return: None or 0 to keep current position, or a value to modify the position
"""

# Add to position if losing and RSI is oversold
if current_profit < -0.05 and self.get_rsi(trade.pair) < 30:
return min_stake # Add minimum stake amount

return None

Leverage callback

Called once per pair, when a new trade is about to be placed.

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.

: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
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return: A leverage amount, which is between 1.0 and max_leverage.
"""

# Use higher leverage for high-confidence signals
if entry_tag == 'high_confidence':
return min(3.0, max_leverage)
else:
return 1.0 # No leverage for regular signals

Order filled callback

Called whenever an order is filled (entry or exit).

from freqtrade.persistence import Trade, Order

class AwesomeStrategy(IStrategy):
def order_filled(self, pair: str, trade: Trade, order: Order, current_time: datetime, **kwargs) -> None:
"""
Called whenever an order is filled.

: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.
"""

if order.ft_order_side == 'buy':
# Entry order filled
self.log_entry_filled(pair, trade, order)
else:
# Exit order filled
self.log_exit_filled(pair, trade, order)

Best Practices

Performance Considerations

  • Keep callback logic lightweight
  • Avoid heavy calculations or API calls
  • Cache expensive operations when possible
  • Use vectorized operations where applicable

Error Handling

def custom_exit(self, pair: str, trade: Trade, current_time: datetime, 
current_rate: float, current_profit: float, **kwargs):
try:
# Your custom logic here
if some_condition:
return 'custom_exit_reason'
except Exception as e:
# Log the error but don't crash the bot
logger.error(f"Error in custom_exit for {pair}: {e}")

return None

Testing Callbacks

  • Test callbacks thoroughly in dry-run mode
  • Use logging to debug callback behavior
  • Validate return values match expected types
  • Consider edge cases and error conditions
Important Notes
  • Callbacks are called frequently - keep them efficient
  • Always include **kwargs in callback signatures for future compatibility
  • Return appropriate types (bool, float, str, None) as documented
  • Test thoroughly before using in live trading