ATR-Based Initial Stops with Break-Even and Fixed Reward Targets
Summary
This strategy example sets an initial stop from the entry candle’s closing price minus twice the average true range. It measures the initial risk as the distance between entry price and that stop, then defines a profit threshold by multiplying the risk by a configurable reward multiple. When profit reaches a separate risk-based threshold, the stop moves to the entry price adjusted for fees; after the reward threshold, it moves to the target price to protect gains.
The strategy stores stop levels by pair and retrieves the candle associated with a trade’s opening time. Its entry and exit signal methods are placeholders: it enters on every candle and generates no ordinary exit signals, so the example does not supply a complete trading system. No backtest, market, timeframe, or performance evidence is included. The stop behavior and thresholds therefore illustrate mechanics only and require careful validation in a real strategy.
Key ideas
- The initial stop is set using twice the average true range below the candle close.
- The profit target is calculated from initial risk multiplied by a configured reward ratio.
- A fee-adjusted break-even stop activates after a separate risk-based profit threshold.
- The entry and exit rules are placeholders and provide no meaningful signal logic.
- The document supplies no backtest or evidence of profitability.
Tags
Full text
# FixedRiskRewardLoss.py
```py
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# isort: skip_file
# --- Do not remove these libs ---
import numpy as np # noqa
import pandas as pd # noqa
from pandas import DataFrame
from freqtrade.strategy import IStrategy
# --------------------------------
# Add your lib to import here
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
from datetime import datetime
from freqtrade.persistence import Trade
import logging
logger = logging.getLogger(__name__)
class FixedRiskRewardLoss(IStrategy):
"""
This strategy uses custom_stoploss() to enforce a fixed risk/reward ratio
by first calculating a dynamic initial stoploss via ATR - last negative peak
After that, we caculate that initial risk and multiply it with an risk_reward_ratio
Once this is reached, stoploss is set to it and sell signal is enabled
Also there is a break even ratio. Once this is reached, the stoploss is adjusted to minimize
losses by setting it to the buy rate + fees.
"""
INTERFACE_VERSION: int = 3
custom_info = {
'risk_reward_ratio': 3.5,
'set_to_break_even_at_profit': 1,
}
use_custom_stoploss = True
stoploss = -0.9
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> float:
"""
custom_stoploss using a risk/reward ratio
"""
result = break_even_sl = takeprofit_sl = -1
custom_info_pair = self.custom_info.get(pair)
if custom_info_pair is not None:
# using current_time/open_date directly via custom_info_pair[trade.open_daten]
# would only work in backtesting/hyperopt.
# in live/dry-run, we have to search for nearest row before it
open_date_mask = custom_info_pair.index.unique().get_loc(trade.open_date_utc, method='ffill')
open_df = custom_info_pair.iloc[open_date_mask]
# trade might be open too long for us to find opening candle
if(len(open_df) != 1):
return -1 # won't update current stoploss
initial_sl_abs = open_df['stoploss_rate']
# calculate initial stoploss at open_date
initial_sl = initial_sl_abs/current_rate-1
# calculate take profit treshold
# by using the initial risk and multiplying it
risk_distance = trade.open_rate-initial_sl_abs
reward_distance = risk_distance*self.custom_info['risk_reward_ratio']
# take_profit tries to lock in profit once price gets over
# risk/reward ratio treshold
take_profit_price_abs = trade.open_rate+reward_distance
# take_profit gets triggerd at this profit
take_profit_pct = take_profit_price_abs/trade.open_rate-1
# break_even tries to set sl at open_rate+fees (0 loss)
break_even_profit_distance = risk_distance*self.custom_info['set_to_break_even_at_profit']
# break_even gets triggerd at this profit
break_even_profit_pct = (break_even_profit_distance+current_rate)/current_rate-1
result = initial_sl
if(current_profit >= break_even_profit_pct):
break_even_sl = (trade.open_rate*(1+trade.fee_open+trade.fee_close) / current_rate)-1
result = break_even_sl
if(current_profit >= take_profit_pct):
takeprofit_sl = take_profit_price_abs/current_rate-1
result = takeprofit_sl
return result
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['atr'] = ta.ATR(dataframe)
dataframe['stoploss_rate'] = dataframe['close']-(dataframe['atr']*2)
self.custom_info[metadata['pair']] = dataframe[['date', 'stoploss_rate']].copy().set_index('date')
# all "normal" indicators:
# e.g.
# dataframe['rsi'] = ta.RSI(dataframe)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Placeholder Strategy: buys when SAR is smaller then candle before
Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
# Always buys
dataframe.loc[:, 'enter_long'] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Placeholder Strategy: does nothing
Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
# Never sells
dataframe.loc[:, 'exit_long'] = 0
return dataframe
```Shown in full with attribution under the source's licence. Licence: GPL-3.0
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.