Freqtrade Strategy

ReinforcedQuickie

5m candlesDip buyingSpot, long only

Tested 1 Jun 2023 – 30 May 2025
Rank 23 of 27 by return

ReinforcedQuickie turned $1,000 into $202 (−79.8%) over 729 days, while holding Bitcoin returned +288.1% and an equal mix of the ten coins +194.8%. Its worst drawdown was a severe 80.0%, deep enough that most people would have switched it off. With a 66% win rate and a profit factor of 0.73, losses outweighed gains.

ReinforcedQuickie account balance compared with holding Bitcoin$0$1,000$2,000$3,000$4,000JulOctJan 2024AprJulOctJan 2025AprReinforcedQuickie $202BTC hold $3,880
Account balance from $1,000 (closed trades, daily). Dashed line: $1,000 held in BTC over the same days.
Total return−79.8%$202 final balance
Max drawdown80.0%4 Jun 2023 to 6 Apr 2025
Win rate65.9%1,384 wins / 716 losses
Profit factor0.73Gross wins ÷ gross losses
Trades2,1002.9 per day
Average trade−0.19%after fees
Average hold8h 9mwinners 5h 4m, losers 14h 7m
Annualised (CAGR)−55.1%Sharpe -5.73 · Sortino -5.21

How ReinforcedQuickie trades

ReinforcedQuickie combines two entries: a new short-term low below the EMAs and the lower Bollinger Band, or a five-candle decline that just turned up. Both are filtered by a longer resampled trend. It exits on an overbought setup or a run of green candles.

A 1% take-profit and 5% stoploss make every loss worth about five wins, so the win rate needs to stay very high.

Rules at a glance

Indicators
EMA 5 / 12 / 21, Bollinger Bands, MFI, Resampled SMA
Take-profit (minimal ROI)
1% from entry
Stoploss
−5%
Trailing stop
Off
Exit signal
Used

Results by month

Profit or loss in USDT for trades closed in each month.

JFMAMJJASOND2023-159-178-123-12+42-31-52024-42+18-88-79+9-48-6-39-7+22+17-32025-30-27-28-4

Results by coin

PairTradesWin rateProfit (USDT)Avg. hold
BTC/USDT23462.8%−19.2712h 28m
DOGE/USDT18869.1%−44.077h 7m
BNB/USDT24564.5%−51.4810h 8m
AVAX/USDT17874.2%−56.425h 32m
XRP/USDT22369.1%−59.399h 15m
ETH/USDT19859.1%−80.659h 39m
DOT/USDT17866.3%−94.007h 25m
LINK/USDT19565.1%−118.356h 40m
ADA/USDT19359.6%−122.257h 14m
SOL/USDT26869.4%−152.385h 14m

Why trades closed

Exit reasonTradesWin rateProfit (USDT)
Take-profit (ROI)893100.0%+1,787.55
Exit signal101548.4%−594.09
Stoploss1920.0%−1,991.73

The code

ReinforcedQuickie comes from the community freqtrade-strategies repository (GPL-3.0). These are the parts that decide when it trades; the full file is 195 lines.

Entry rules
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
    dataframe.loc[
        (
                (
                        (
                                (dataframe['close'] < dataframe['ema_{}'.format(self.EMA_SHORT_TERM)]) &
                                (dataframe['close'] < dataframe['ema_{}'.format(self.EMA_MEDIUM_TERM)]) &
                                (dataframe['close'] == dataframe['min']) &
                                (dataframe['close'] <= dataframe['bb_lowerband'])
                        )
                        |
                        # simple v bottom shape (lopsided to the left to increase reactivity)
                        # which has to be below a very slow average
                        # this pattern only catches a few, but normally very good buy points
                        (
                                (dataframe['average'].shift(5) > dataframe['average'].shift(4))
                                & (dataframe['average'].shift(4) > dataframe['average'].shift(3))
                                & (dataframe['average'].shift(3) > dataframe['average'].shift(2))
                                & (dataframe['average'].shift(2) > dataframe['average'].shift(1))
                                & (dataframe['average'].shift(1) < dataframe['average'].shift(0))
                                & (dataframe['low'].shift(1) < dataframe['bb_middleband'])
                                & (dataframe['cci'].shift(1) < -100)
                                & (dataframe['rsi'].shift(1) < 30)
                                & (dataframe['mfi'].shift(1) < 30)

                        )
                )
                # safeguard against down trending markets and a pump and dump
                &
                (
                        (dataframe['volume'] < (dataframe['volume'].rolling(window=30).mean().shift(1) * 20)) &
                        (dataframe['resample_sma'] < dataframe['close']) &
                        (dataframe['resample_sma'].shift(1) < dataframe['resample_sma'])
                )
        )
        ,
        'enter_long'] = 1

    return dataframe
Exit rules
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
    dataframe.loc[
        (
                (dataframe['close'] > dataframe['ema_{}'.format(self.EMA_SHORT_TERM)]) &
                (dataframe['close'] > dataframe['ema_{}'.format(self.EMA_MEDIUM_TERM)]) &
                (dataframe['close'] >= dataframe['max']) &
                (dataframe['close'] >= dataframe['bb_upperband']) &
                (dataframe['mfi'] > 80)
        ) |

        # always sell on eight green candles
        # with a high rsi
        (
                (dataframe['open'] < dataframe['close']) &
                (dataframe['open'].shift(1) < dataframe['close'].shift(1)) &
                (dataframe['open'].shift(2) < dataframe['close'].shift(2)) &
                (dataframe['open'].shift(3) < dataframe['close'].shift(3)) &
                (dataframe['open'].shift(4) < dataframe['close'].shift(4)) &
                (dataframe['open'].shift(5) < dataframe['close'].shift(5)) &
                (dataframe['open'].shift(6) < dataframe['close'].shift(6)) &
                (dataframe['open'].shift(7) < dataframe['close'].shift(7)) &
                (dataframe['rsi'] > 70)
        )
        ,
        'exit_long'
    ] = 1
    return dataframe
Indicators
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
    dataframe = self.resample(dataframe, self.timeframe, self.resample_factor)

    ##################################################################################
    # buy and sell indicators

    dataframe['ema_{}'.format(self.EMA_SHORT_TERM)] = ta.EMA(
        dataframe, timeperiod=self.EMA_SHORT_TERM
    )
    dataframe['ema_{}'.format(self.EMA_MEDIUM_TERM)] = ta.EMA(
        dataframe, timeperiod=self.EMA_MEDIUM_TERM
    )
    dataframe['ema_{}'.format(self.EMA_LONG_TERM)] = ta.EMA(
        dataframe, timeperiod=self.EMA_LONG_TERM
    )

    bollinger = qtpylib.bollinger_bands(
        qtpylib.typical_price(dataframe), window=20, stds=2
    )
    dataframe['bb_lowerband'] = bollinger['lower']
    dataframe['bb_middleband'] = bollinger['mid']
    dataframe['bb_upperband'] = bollinger['upper']

    dataframe['min'] = ta.MIN(dataframe, timeperiod=self.EMA_MEDIUM_TERM)
    dataframe['max'] = ta.MAX(dataframe, timeperiod=self.EMA_MEDIUM_TERM)

    dataframe['cci'] = ta.CCI(dataframe)
    dataframe['mfi'] = ta.MFI(dataframe)
    dataframe['rsi'] = ta.RSI(dataframe, timeperiod=7)

    dataframe['average'] = (dataframe['close'] + dataframe['open'] + dataframe['high'] + dataframe['low']) / 4

    ##################################################################################
    # required for graphing
    bollinger = qtpylib.bollinger_bands(dataframe['close'], window=20, stds=2)
    dataframe['bb_lowerband'] = bollinger['lower']
    dataframe['bb_upperband'] = bollinger['upper']
    dataframe['bb_middleband'] = bollinger['mid']

    macd = ta.MACD(dataframe)
    dataframe['macd'] = macd['macd']
    dataframe['macdsignal'] = macd['macdsignal']
    dataframe['macdhist'] = macd['macdhist']

    return dataframe

Reproduce this test

With freqtrade installed (see our Docker guide), download the data and run the same backtest:

freqtrade download-data --exchange binance --pairs ADA/USDT AVAX/USDT BNB/USDT BTC/USDT DOGE/USDT DOT/USDT ETH/USDT LINK/USDT SOL/USDT XRP/USDT --timeframes 5m --timerange 20230501-20250530
freqtrade backtesting --strategy ReinforcedQuickie --timeframe 5m \
  --timerange 20230601-20250530 --dry-run-wallet 1000 --stake-amount 200 --max-open-trades 5 --fee 0.001

Results can differ slightly with a different data source or freqtrade version. See how we test.