30m BB filter short-only — more trades, same quality
Timeframe
30m
Direction
Long & Short
Stoploss
-1.5%
Trailing Stop
Yes
ROI
0m: 2.5%, 180m: 1.8%, 480m: 1.0%, 960m: 0.0%
Interface Version
3
Startup Candles
120
Indicators
6
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
from pandas import DataFrame
import talib.abstract as ta
import numpy as np
from freqtrade.strategy import IStrategy
# ============================================================
# WINNER: BB filter short-only 1h → Sortino 4.46, Sharpe 0.24
# Scale up: 30m timeframe for more trades
# ============================================================
class BtcEth30mBB(IStrategy):
"""30m BB filter short-only — more trades, same quality"""
INTERFACE_VERSION = 3
timeframe = '30m'
can_short = True
stoploss = -0.015
trailing_stop = True
trailing_stop_positive = 0.006
trailing_stop_positive_offset = 0.01
trailing_only_offset_is_reached = True
minimal_roi = {"0": 0.025, "180": 0.018, "480": 0.01, "960": 0}
max_open_trades = 4
startup_candle_count = 120
process_only_new_candles = True
use_exit_signal = False
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['ema9'] = ta.EMA(dataframe, timeperiod=9)
dataframe['ema21'] = ta.EMA(dataframe, timeperiod=21)
dataframe['ema55'] = ta.EMA(dataframe, timeperiod=55)
macd = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9)
dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal']
dataframe['adx'] = ta.ADX(dataframe, timeperiod=14)
dataframe['volume_sma'] = ta.SMA(dataframe['volume'], timeperiod=20)
dataframe['volume_ratio'] = dataframe['volume'] / dataframe['volume_sma']
bb = ta.BBANDS(dataframe, timeperiod=20, nbdevup=2.0, nbdevdn=2.0)
dataframe['bb_pct'] = (dataframe['close'] - bb['lowerband']) / (bb['upperband'] - bb['lowerband'])
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
((dataframe['ema9'] < dataframe['ema21']) &
(dataframe['ema21'] < dataframe['ema55']) &
(dataframe['macd'] < dataframe['macdsignal']) &
(dataframe['adx'] > 22) &
(dataframe['volume_ratio'] > 1.15) &
(dataframe['bb_pct'] > 0.25) &
(dataframe['rsi'] > 35) &
(dataframe['volume'] > 0)),
['enter_short', 'enter_tag']
] = (1, 's_30m')
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
return dataframe
# ============================================================
# V2: 1h BB filter short-only — slightly looser BB for more trades
# ============================================================
class BtcEth1hBB2(IStrategy):
"""1h BB filter — looser bb_pct > 0.15 for more trades"""
INTERFACE_VERSION = 3
timeframe = '1h'
can_short = True
stoploss = -0.02
trailing_stop = True
trailing_stop_positive = 0.008
trailing_stop_positive_offset = 0.012
trailing_only_offset_is_reached = True
minimal_roi = {"0": 0.035, "240": 0.022, "720": 0.012, "1440": 0}
max_open_trades = 4
startup_candle_count = 120
process_only_new_candles = True
use_exit_signal = False
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
dataframe['ema30'] = ta.EMA(dataframe, timeperiod=30)
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
macd = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9)
dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal']
dataframe['adx'] = ta.ADX(dataframe, timeperiod=14)
dataframe['volume_sma'] = ta.SMA(dataframe['volume'], timeperiod=20)
dataframe['volume_ratio'] = dataframe['volume'] / dataframe['volume_sma']
bb = ta.BBANDS(dataframe, timeperiod=20, nbdevup=2.0, nbdevdn=2.0)
dataframe['bb_pct'] = (dataframe['close'] - bb['lowerband']) / (bb['upperband'] - bb['lowerband'])
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
((dataframe['ema10'] < dataframe['ema30']) & (dataframe['ema30'] < dataframe['ema100']) &
(dataframe['macd'] < dataframe['macdsignal']) & (dataframe['adx'] > 25) &
(dataframe['volume_ratio'] > 1.2) & (dataframe['bb_pct'] > 0.15) &
(dataframe['rsi'] > 35) & (dataframe['volume'] > 0)),
['enter_short', 'enter_tag']
] = (1, 's_1h2')
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
return dataframe
# ============================================================
# V3: 1h BB filter — both longs and shorts, loose filter
# Longs were bad before. Try with BB filter.
# ============================================================
class BtcEth1hDualBB(IStrategy):
"""1h dual with BB filter"""
INTERFACE_VERSION = 3
timeframe = '1h'
can_short = True
stoploss = -0.02
trailing_stop = True
trailing_stop_positive = 0.008
trailing_stop_positive_offset = 0.012
trailing_only_offset_is_reached = True
minimal_roi = {"0": 0.035, "240": 0.022, "720": 0.012, "1440": 0}
max_open_trades = 4
startup_candle_count = 120
process_only_new_candles = True
use_exit_signal = False
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
dataframe['ema30'] = ta.EMA(dataframe, timeperiod=30)
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
macd = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9)
dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal']
dataframe['adx'] = ta.ADX(dataframe, timeperiod=14)
dataframe['volume_sma'] = ta.SMA(dataframe['volume'], timeperiod=20)
dataframe['volume_ratio'] = dataframe['volume'] / dataframe['volume_sma']
bb = ta.BBANDS(dataframe, timeperiod=20, nbdevup=2.0, nbdevdn=2.0)
dataframe['bb_pct'] = (dataframe['close'] - bb['lowerband']) / (bb['upperband'] - bb['lowerband'])
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Long: BB not overbought + trend
dataframe.loc[
((dataframe['ema10'] > dataframe['ema30']) & (dataframe['ema30'] > dataframe['ema100']) &
(dataframe['macd'] > dataframe['macdsignal']) & (dataframe['adx'] > 25) &
(dataframe['volume_ratio'] > 1.2) & (dataframe['bb_pct'] < 0.80) &
(dataframe['rsi'] < 65) & (dataframe['volume'] > 0)),
['enter_long', 'enter_tag']
] = (1, 'l_dualbb')
# Short
dataframe.loc[
((dataframe['ema10'] < dataframe['ema30']) & (dataframe['ema30'] < dataframe['ema100']) &
(dataframe['macd'] < dataframe['macdsignal']) & (dataframe['adx'] > 25) &
(dataframe['volume_ratio'] > 1.2) & (dataframe['bb_pct'] > 0.15) &
(dataframe['rsi'] > 35) & (dataframe['volume'] > 0)),
['enter_short', 'enter_tag']
] = (1, 's_dualbb')
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
return dataframe