FSD – Better way of using ATR for exits

This is an improved version of this method, but it requires Freqtrade version 2024.3 or later.

This method is simpler than the old one, and have no weakness. Now the atr roi and stoploss will survive any bot restart. You also don’t need to write confirm_trade_entry and confirm_trade_exit functions

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)
		dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)

		atr_roi = trade.get_custom_data(key='atr_roi', default=0)
		atr_sl = trade.get_custom_data(key='atr_sl', default=0)
		if (atr_roi == 0):
			# need to set the roi and sl
			signal_time = entry_time - timedelta(minutes=int(self.timeframe_minutes))
			signal_candle = dataframe.loc[dataframe['date'] == signal_time]
			if not signal_candle.empty:
				# make sure we take only a row
				signal_candle = signal_candle.iloc[-1].squeeze()
				
				atr_roi = (signal_candle['close'] + (self.atr_distance * self.risk_reward_ratio * signal_candle['atr']))
				atr_sl = (signal_candle['close'] - (self.atr_distance * signal_candle['atr']))

				trade.set_custom_data(key='atr_roi', value=atr_roi)
				trade.set_custom_data(key='atr_sl', value=atr_sl)

		if (current_time - timedelta(minutes=int(self.timeframe_minutes)) >= entry_time):
			
			current_candle = dataframe.iloc[-1].squeeze()

			if (atr_roi > 0):			
				if (current_candle['close'] >= atr_roi):
					return "atr_roi"

				if (current_candle['close'] <= atr_sl):
					return "atr_sl"

			else:
				# Signal candle not found, use percentage as exits
				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"

		return None

If you are on Futures market, then the snippet should be modified a little bit like this

lev = 10

# ROI before leverage
roi = 0.0025

# Stoploss before leverage
stoploss = -0.001

risk_reward_ratio = 1
atr_distance = 2

timeframe = '15m'
can_short = True
timeframe_minutes = timeframe_to_minutes(timeframe)

# Disable ROI
minimal_roi = {
	"0": 1000
}


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)
	cur_time = timeframe_to_prev_date(self.timeframe, current_time)
	dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)

	atr_roi = trade.get_custom_data(key='atr_roi', default=None)
	atr_sl = trade.get_custom_data(key='atr_sl', default=None)

	if (atr_roi is None):
		signal_time = entry_time - timedelta(minutes=int(self.timeframe_minutes))
		signal_candle = dataframe.loc[dataframe['date'] == signal_time]
		if not signal_candle.empty:
			signal_candle = signal_candle.iloc[-1].squeeze()
			if trade.is_short:
				trade.set_custom_data(key='atr_roi', value=(signal_candle['close'] - (self.atr_distance * self.risk_reward_ratio * signal_candle['atr'])))
				trade.set_custom_data(key='atr_sl', value=(signal_candle['close'] + (self.atr_distance * signal_candle['atr'])))
			else:
				trade.set_custom_data(key='atr_roi', value=(signal_candle['close'] + (self.atr_distance * self.risk_reward_ratio * signal_candle['atr'])))
				trade.set_custom_data(key='atr_sl', value=(signal_candle['close'] - (self.atr_distance * signal_candle['atr'])))
		
		atr_roi = trade.get_custom_data(key='atr_roi', default=None)
		atr_sl = trade.get_custom_data(key='atr_sl', default=None)

	if (cur_time > entry_time):
		current_candle = dataframe.iloc[-1].squeeze()
		
		# use ATR
		if atr_roi:
		  if trade.is_short:
  			if (current_candle['close'] <= atr_roi):
  				return "atr_roi"
  
  			if (current_candle['close'] >= atr_sl):
  				return "atr_sl"
  		else:
  			if (current_candle['close'] >= atr_roi):
  				return "atr_roi"
  
  			if (current_candle['close'] <= atr_sl):
  				return "atr_sl"
		# Use simple % roi/SL
		else:
			current_profit = trade.calc_profit_ratio(current_candle['close'])
			if current_profit >= (self.roi * self.lev):
				return "emergency roi"
			if current_profit <= (self.stoploss * self.lev):
				return "emergency sl"
	return None

7 Comments

  1. Hey, I think there is a typo in the futures code:
    >> if (atr_roi is not None):
    atr_roi never actually gets calculated, I think it should be
    >> if (atr_roi is None):

  2. Plus with the line:
    >> if current_profit <= -(self.stoploss * self.lev):
    afaik self.stoploss is always negative, so doing another negation will make the bot exit on potentially positive profit

  3. ALSO, another suggestion for the futures code:
    if atr_roi:
    if (current_candle[‘close’] >= atr_roi):
    return “atr_roi”

    if (current_candle[‘close’] = atr_roi):
    return “atr_roi”

    if (current_candle[‘close’] <= atr_sl):
    return "atr_sl"
    if atr_roi and trade.is_short:
    if (current_candle['close'] = atr_sl):
    return “atr_sl”

    to take into account the direction of the trade.

    Sorry for spamming, just going along with the article and giving feedback

  4. Oops, code got mangled, I meant that atr_roi/atr_sl returning logic should probably look something like this in order to account for short trades:

    if atr_roi and not trade.is_short:
    if (current_candle[‘close’] >= atr_roi):
    return “atr_roi”
    if (current_candle[‘close’] <= atr_sl):
    return "atr_sl"

    if atr_roi and trade.is_short:
    if (current_candle['close'] = atr_sl):
    return “atr_sl”

Leave a Reply

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