Freqtrade Strategy

BinHV27

5m candlesDip buyingSpot, long only

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

BinHV27 turned $1,000 into $406 (−59.4%) 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 60.3%, deep enough that most people would have switched it off. With a 56% win rate and a profit factor of 0.80, losses outweighed gains.

BinHV27 account balance compared with holding Bitcoin$0$1,000$2,000$3,000$4,000JulOctJan 2024AprJulOctJan 2025AprBinHV27 $406BTC hold $3,880
Account balance from $1,000 (closed trades, daily). Dashed line: $1,000 held in BTC over the same days.
Total return−59.4%$406 final balance
Max drawdown60.3%5 Jun 2023 to 20 May 2025
Win rate55.7%1,380 wins / 1,097 losses
Profit factor0.80Gross wins ÷ gross losses
Trades2,4773.4 per day
Average trade−0.12%after fees
Average hold4h 53mwinners 3h 18m, losers 6h 52m
Annualised (CAGR)−36.3%Sharpe -4.36 · Sortino -4.22

How BinHV27 trades

BinHV27 looks for sharp drops on the 5-minute chart: price below its 60 and 120 EMAs, a fast RSI that has fallen to 20 or less and stopped falling, and a strong directional move to the downside. Several alternative entry branches cover different trend states. Exits are driven by price recovering above those averages.

The 50% stoploss and 100% take-profit are effectively disabled, so the strategy depends entirely on its exit signal. In a falling market that can mean holding a losing coin for a long time.

Rules at a glance

Indicators
RSI 5, ADX, +DI / -DI, EMA 60 / 120 / 240
Take-profit (minimal ROI)
100% (effectively off)
Stoploss
−50% (effectively off)
Trailing stop
Off
Exit signal
Used

Results by month

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

JFMAMJJASOND2023-89-36-87-52+16+77+82024+8+11+131-202+50-59-76-117+1-30+42-122025-18-101+8-53-12

Results by coin

PairTradesWin rateProfit (USDT)Avg. hold
SOL/USDT27058.1%+60.224h 50m
DOGE/USDT23261.2%+38.814h 36m
XRP/USDT24655.7%−31.274h 40m
DOT/USDT21856.0%−46.074h 58m
LINK/USDT22856.1%−55.334h 32m
BNB/USDT28454.9%−69.215h 8m
AVAX/USDT24061.7%−79.494h 33m
BTC/USDT23749.8%−81.845h 7m
ADA/USDT25351.4%−128.955h 7m
ETH/USDT26952.8%−201.155h 11m

Why trades closed

Exit reasonTradesWin rateProfit (USDT)
Still open at end of test10.0%−1.09
Exit signal247655.7%−593.20

The code

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

Entry rules
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
    dataframe.loc[
        dataframe['slowsma'].gt(0) &
        dataframe['close'].lt(dataframe['highsma']) &
        dataframe['close'].lt(dataframe['lowsma']) &
        dataframe['minusdi'].gt(dataframe['minusdiema']) &
        dataframe['rsi'].ge(dataframe['rsi'].shift()) &
        (
          (
            ~dataframe['preparechangetrend'] &
            ~dataframe['continueup'] &
            dataframe['adx'].gt(25) &
            dataframe['bigdown'] &
            dataframe['emarsi'].le(20)
          ) |
          (
            ~dataframe['preparechangetrend'] &
            dataframe['continueup'] &
            dataframe['adx'].gt(30) &
            dataframe['bigdown'] &
            dataframe['emarsi'].le(20)
          ) |
          (
            ~dataframe['continueup'] &
            dataframe['adx'].gt(35) &
            dataframe['bigup'] &
            dataframe['emarsi'].le(20)
          ) |
          (
            dataframe['continueup'] &
            dataframe['adx'].gt(30) &
            dataframe['bigup'] &
            dataframe['emarsi'].le(25)
          )
        ),
        'enter_long'] = 1
    return dataframe
Exit rules
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
    dataframe.loc[
        (
          (
            ~dataframe['preparechangetrendconfirm'] &
            ~dataframe['continueup'] &
            (dataframe['close'].gt(dataframe['lowsma']) | dataframe['close'].gt(dataframe['highsma'])) &
            dataframe['highsma'].gt(0) &
            dataframe['bigdown']
          ) |
          (
            ~dataframe['preparechangetrendconfirm'] &
            ~dataframe['continueup'] &
            dataframe['close'].gt(dataframe['highsma']) &
            dataframe['highsma'].gt(0) &
            (dataframe['emarsi'].ge(75) | dataframe['close'].gt(dataframe['slowsma'])) &
            dataframe['bigdown']
          ) |
          (
            ~dataframe['preparechangetrendconfirm'] &
            dataframe['close'].gt(dataframe['highsma']) &
            dataframe['highsma'].gt(0) &
            dataframe['adx'].gt(30) &
            dataframe['emarsi'].ge(80) &
            dataframe['bigup']
          ) |
          (
            dataframe['preparechangetrendconfirm'] &
            ~dataframe['continueup'] &
            dataframe['slowingdown'] &
            dataframe['emarsi'].ge(75) &
            dataframe['slowsma'].gt(0)
          ) |
          (
            dataframe['preparechangetrendconfirm'] &
            dataframe['minusdi'].lt(dataframe['plusdi']) &
            dataframe['close'].gt(dataframe['lowsma']) &
            dataframe['slowsma'].gt(0)
          )
        ),
        'exit_long'] = 1
    return dataframe
Indicators
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
    dataframe['rsi'] = numpy.nan_to_num(ta.RSI(dataframe, timeperiod=5))
    rsiframe = DataFrame(dataframe['rsi']).rename(columns={'rsi': 'close'})
    dataframe['emarsi'] = numpy.nan_to_num(ta.EMA(rsiframe, timeperiod=5))
    dataframe['adx'] = numpy.nan_to_num(ta.ADX(dataframe))
    dataframe['minusdi'] = numpy.nan_to_num(ta.MINUS_DI(dataframe))
    minusdiframe = DataFrame(dataframe['minusdi']).rename(columns={'minusdi': 'close'})
    dataframe['minusdiema'] = numpy.nan_to_num(ta.EMA(minusdiframe, timeperiod=25))
    dataframe['plusdi'] = numpy.nan_to_num(ta.PLUS_DI(dataframe))
    plusdiframe = DataFrame(dataframe['plusdi']).rename(columns={'plusdi': 'close'})
    dataframe['plusdiema'] = numpy.nan_to_num(ta.EMA(plusdiframe, timeperiod=5))
    dataframe['lowsma'] = numpy.nan_to_num(ta.EMA(dataframe, timeperiod=60))
    dataframe['highsma'] = numpy.nan_to_num(ta.EMA(dataframe, timeperiod=120))
    dataframe['fastsma'] = numpy.nan_to_num(ta.SMA(dataframe, timeperiod=120))
    dataframe['slowsma'] = numpy.nan_to_num(ta.SMA(dataframe, timeperiod=240))
    dataframe['bigup'] = dataframe['fastsma'].gt(dataframe['slowsma']) & ((dataframe['fastsma'] - dataframe['slowsma']) > dataframe['close'] / 300)
    dataframe['bigdown'] = ~dataframe['bigup']
    dataframe['trend'] = dataframe['fastsma'] - dataframe['slowsma']
    dataframe['preparechangetrend'] = dataframe['trend'].gt(dataframe['trend'].shift())
    dataframe['preparechangetrendconfirm'] = dataframe['preparechangetrend'] & dataframe['trend'].shift().gt(dataframe['trend'].shift(2))
    dataframe['continueup'] = dataframe['slowsma'].gt(dataframe['slowsma'].shift()) & dataframe['slowsma'].shift().gt(dataframe['slowsma'].shift(2))
    dataframe['delta'] = dataframe['fastsma'] - dataframe['fastsma'].shift()
    dataframe['slowingdown'] = dataframe['delta'].lt(dataframe['delta'].shift())
    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 BinHV27 --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.