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:
bot_start()
bot_loop_start()
custom_stake_amount()
custom_exit()
custom_stoploss()
custom_roi()
custom_entry_price()
andcustom_exit_price()
check_entry_timeout()
andcheck_exit_timeout()
confirm_trade_entry()
confirm_trade_exit()
adjust_trade_position()
adjust_entry_price()
leverage()
order_filled()
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
- 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