FSD – How to use ATR for exits (ROI and stoploss)

Update 2024-04-18: If you are on latest freqtrade, please use this method instead. It stores the info into database, so you have no risk of losing the info in case of bot restart

The main point of this method is each trades will have their own ROI and stoploss level based of their ATR at the signal candle instead of fixed roi and stoploss percentages. To do this, first of all, we need to create a dict to store the ATR levels. Adjust risk_reward_ratio and atr_distance to suit your preferances.

from freqtrade.exchange import timeframe_to_prev_date, timeframe_to_minutes
from typing import Optional, Union

class ATR (IStrategy):

	risk_reward_ratio = 1
	atr_distance = 3

	custom_info_fixed_rr = dict()

	init_fixed_rr_dict = {
		'roi': 0,
		'sl': 0,
	}
	
	timeframe = '5m' #change this to your preferences
	timeframe_minutes = timeframe_to_minutes(timeframe)

Then we calculate the ATR inside populate_indicators

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

	return dataframe

After that, we will calculate the exit levels at confirm_trade_entry

def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, time_in_force: str, current_time: datetime, entry_tag: Optional[str], side: str, **kwargs) -> bool:
	
	if (pair not in self.custom_info_fixed_rr):
		self.custom_info_fixed_rr[pair] = self.init_fixed_rr_dict.copy()
	
	dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
	current_candle = dataframe.iloc[-1].squeeze()

	self.custom_info_fixed_rr[pair]['roi'] = (current_candle['close'] + (self.atr_distance * self.risk_reward_ratio * current_candle['atr']))
	self.custom_info_fixed_rr[pair]['sl'] = (current_candle['close'] - (self.atr_distance * current_candle['atr']))

	return True

We will reset the atr levels for that pair at confirm_trade_exit

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:

	if (pair in self.custom_info_fixed_rr):
		self.custom_info_fixed_rr[pair] = self.init_fixed_rr_dict.copy()

	return True

Then we move to the exit logics. To use the ATR levels, we will use custom_exit. Before we move into the code, you will need to read about the backtest trap if you haven’t read it. To make sure our dry/live in line with backtest, I won’t use current_rate and use last closed candle’s close value instead. And the first if check is there to make sure we don’t check exit when the last closed candle is the same as the signal candle.

def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> Optional[Union[str, bool]]:
  
  entry_time = timeframe_to_prev_date(self.timeframe, trade.open_date_utc)
  
	if (current_time - timedelta(minutes=int(self.timeframe_minutes)) >= entry_time):
		dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
		current_candle = dataframe.iloc[-1].squeeze()

		if (current_candle['close'] >= self.custom_info_fixed_rr[pair]['roi']):
			return "atr_roi"

		if (current_candle['close'] <= self.custom_info_fixed_rr[pair]['sl']):
			return "atr_sl"

This still isn’t finished though. The major weakness of this approach is that the dict is gonna lose the contents when the bot restarted. So in that case, we need to add some lines to get the signal candle and re-calculate the ATR levels in case the bot restarted.

def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> Optional[Union[str, bool]]:

	entry_time = timeframe_to_prev_date(self.timeframe, trade.open_date_utc)

	if (pair not in self.custom_info_fixed_rr):
		self.custom_info_fixed_rr[pair] = self.init_fixed_rr_dict.copy()
		signal_time = entry_time - timedelta(minutes=int(self.timeframe_minutes))
		signal_candle = dataframe.loc[dataframe['date'] == signal_time]
		# Signal candle found
		if not signal_candle.empty:
			self.custom_info_fixed_rr[pair]['roi'] = (signal_candle['close'] + (self.atr_distance * self.risk_reward_ratio * signal_candle['atr']))
			self.custom_info_fixed_rr[pair]['sl'] = (signal_candle['close'] - (self.atr_distance * signal_candle['atr']))

	if (current_time - timedelta(minutes=int(self.timeframe_minutes)) >= entry_time):
		dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
		current_candle = dataframe.iloc[-1].squeeze()

		if (current_candle['close'] >= self.custom_info_fixed_rr[pair]['roi']):
			return "atr_roi"

		if (current_candle['close'] <= self.custom_info_fixed_rr[pair]['sl']):
			return "atr_sl"

But what if the bot can’t find the signal candle, for example when the trade has been opened for too long that the signal candle already goes past the amount of candles retrieved? Then we need to have emergency roi and stoploss in this case. We will re-calculate current_profit (reason can be read at the backtest trap post). Please change the emergency roi and sl’s percentage to suit your preferences.

def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> Optional[Union[str, bool]]:

	entry_time = timeframe_to_prev_date(self.timeframe, trade.open_date_utc)

	if (pair not in self.custom_info_fixed_rr):
		self.custom_info_fixed_rr[pair] = self.init_fixed_rr_dict.copy()
		signal_time = entry_time - timedelta(minutes=int(self.timeframe_minutes))
		signal_candle = dataframe.loc[dataframe['date'] == signal_time]
		# Signal candle found
		if not signal_candle.empty:
			self.custom_info_fixed_rr[pair]['roi'] = (signal_candle['close'] + (self.atr_distance * self.risk_reward_ratio * signal_candle['atr']))
			self.custom_info_fixed_rr[pair]['sl'] = (signal_candle['close'] - (self.atr_distance * signal_candle['atr']))

	if (current_time - timedelta(minutes=int(self.timeframe_minutes)) >= entry_time):
		dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
		current_candle = dataframe.iloc[-1].squeeze()

		# Use ATR
		if (self.custom_info_fixed_rr[pair]['roi'] > 0):
			if (current_candle['close'] >= self.custom_info_fixed_rr[pair]['roi']):
				return "atr_roi"

			if (current_candle['close'] <= self.custom_info_fixed_rr[pair]['sl']):
				return "atr_sl"
		else:
			current_profit = trade.calc_profit_ratio(current_candle['close'])
			if (current_profit > 0.01):
				return "emergency_roi"
			elif (current_profit < -0.04):
				return "emergency_sl"

4 Comments

  1. When using this code timeframe_minutes those a no attribute error.

    Am I missing an import somewhere that isn’t mentioned?

Leave a Reply

Your email address will not be published. Required fields are marked *