Freqtrade Strategy

Strategy004

5m candlesOscillator reversalSpot, long only

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

Strategy004 turned $1,000 into $778 (−22.2%) 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 50.2%, deep enough that most people would have switched it off. It won 93% of its trades yet still lost money, because its few losing trades were much larger than its many small wins.

Strategy004 account balance compared with holding Bitcoin$0$1,000$2,000$3,000$4,000JulOctJan 2024AprJulOctJan 2025AprStrategy004 $778BTC hold $3,880
Account balance from $1,000 (closed trades, daily). Dashed line: $1,000 held in BTC over the same days.
Total return−22.2%$778 final balance
Max drawdown50.2%11 Apr 2024 to 7 Apr 2025
Win rate92.6%1,461 wins / 117 losses
Profit factor0.91Gross wins ÷ gross losses
Trades1,5782.2 per day
Average trade−0.07%after fees
Average hold13h 47mwinners 10h 39m, losers 2d 4h
Annualised (CAGR)−11.8%Sharpe -0.99 · Sortino -301.10

How Strategy004 trades

Strategy004 buys when a strong or established trend (ADX above 50, or slow ADX above 26) coincides with oversold CCI and both a fast and slow stochastic turning up from low levels. It exits when the trend weakens and the stochastic is overbought.

Mixes trend strength with oversold timing, which is a reasonable idea. The 10% stoploss and stepped 5%-to-1% ROI define the payoff.

Rules at a glance

Indicators
ADX, CCI, Fast and slow Stochastic, EMA 5
Take-profit (minimal ROI)
5% from entry; 4% after 20m; 3% after 30m; 1% after 1h 0m
Stoploss
−10%
Trailing stop
Off
Exit signal
Used

Results by month

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

JFMAMJJASOND2023-15+57-92-10+26+56+1262024+68+50+41-197+36-25-90-174+30-23+145+1072025-11-212-148-19+51

Results by coin

PairTradesWin rateProfit (USDT)Avg. hold
XRP/USDT14195.7%+83.9711h 29m
BTC/USDT16897.0%+30.4218h 45m
ADA/USDT16492.7%+15.337h 50m
SOL/USDT15991.8%−2.609h 4m
DOGE/USDT15991.2%−11.3913h 46m
BNB/USDT15994.3%−20.2214h 50m
AVAX/USDT16391.4%−26.119h 50m
DOT/USDT15591.6%−42.6714h 50m
LINK/USDT14390.9%−49.2310h 7m
ETH/USDT16789.2%−199.481d 2h

Why trades closed

Exit reasonTradesWin rateProfit (USDT)
Take-profit (ROI)625100.0%+1,634.65
Exit signal836100.0%+526.07
Stoploss1170.0%−2,382.69

The code

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

Entry rules
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
    dataframe.loc[
        (
            (
                (dataframe['adx'] > 50) |
                (dataframe['slowadx'] > 26)
            ) &
            (dataframe['cci'] < -100) &
            (
                (dataframe['fastk-previous'] < 20) &
                (dataframe['fastd-previous'] < 20)
            ) &
            (
                (dataframe['slowfastk-previous'] < 30) &
                (dataframe['slowfastd-previous'] < 30)
            ) &
            (dataframe['fastk-previous'] < dataframe['fastd-previous']) &
            (dataframe['fastk'] > dataframe['fastd']) &
            (dataframe['mean-volume'] > 0.75) &
            (dataframe['close'] > 0.00000100)
        ),
        'enter_long'] = 1

    return dataframe
Exit rules
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
    dataframe.loc[
        (
            (dataframe['slowadx'] < 25) &
            ((dataframe['fastk'] > 70) | (dataframe['fastd'] > 70)) &
            (dataframe['fastk-previous'] < dataframe['fastd-previous']) &
            (dataframe['close'] > dataframe['ema5'])
        ),
        'exit_long'] = 1
    return dataframe
Indicators
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:

    # ADX
    dataframe['adx'] = ta.ADX(dataframe)
    dataframe['slowadx'] = ta.ADX(dataframe, 35)

    # Commodity Channel Index: values Oversold:<-100, Overbought:>100
    dataframe['cci'] = ta.CCI(dataframe)

    # Stoch
    stoch = ta.STOCHF(dataframe, 5)
    dataframe['fastd'] = stoch['fastd']
    dataframe['fastk'] = stoch['fastk']
    dataframe['fastk-previous'] = dataframe.fastk.shift(1)
    dataframe['fastd-previous'] = dataframe.fastd.shift(1)

    # Slow Stoch
    slowstoch = ta.STOCHF(dataframe, 50)
    dataframe['slowfastd'] = slowstoch['fastd']
    dataframe['slowfastk'] = slowstoch['fastk']
    dataframe['slowfastk-previous'] = dataframe.slowfastk.shift(1)
    dataframe['slowfastd-previous'] = dataframe.slowfastd.shift(1)

    # EMA - Exponential Moving Average
    dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
    
    # get the rolling volume mean for the last hour (12x5)
    # Note: dataframe['volume'].mean() uses the whole dataframe in 
    # backtesting hence will have lookahead, but would be fine for dry/live use
    dataframe['mean-volume'] = dataframe['volume'].rolling(12).mean()

    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 Strategy004 --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.