SmoothOperator
Tested 1 Jun 2023 – 30 May 2025
Rank 25 of 27 by return
SmoothOperator turned $1,000 into $200 (−80.0%) 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.2%, deep enough that most people would have switched it off. With a 59% win rate and a profit factor of 0.80, losses outweighed gains.
How SmoothOperator trades
SmoothOperator looks for the end of a five-candle decline near the middle Bollinger Band with oversold CCI and RSI, plus a deeper-oversold variant. Exits combine a smoothed blend of MFI, RSI and CCI turning down with candle-pattern checks.
One of the more complex strategies in the library. More rules do not automatically mean better results; compare it with simpler mean-reversion systems such as BbandRsi.
Rules at a glance
- Indicators
- CCI, RSI, MFI, Bollinger Bands, MACD
- Take-profit (minimal ROI)
- 10% from entry
- Stoploss
- −5%
- Trailing stop
- Off
- Exit signal
- Used
Results by month
Profit or loss in USDT for trades closed in each month.
Results by coin
| Pair | Trades | Win rate | Profit (USDT) | Avg. hold |
|---|---|---|---|---|
| SOL/USDT | 228 | 66.7% | +48.89 | 10h 29m |
| DOGE/USDT | 173 | 66.5% | +33.43 | 10h 4m |
| BTC/USDT | 254 | 61.8% | −72.98 | 12h 8m |
| BNB/USDT | 215 | 54.0% | −79.52 | 12h 36m |
| AVAX/USDT | 149 | 57.0% | −84.82 | 11h 48m |
| DOT/USDT | 143 | 55.2% | −98.04 | 11h 4m |
| XRP/USDT | 227 | 58.1% | −100.59 | 11h 46m |
| ETH/USDT | 268 | 57.8% | −111.18 | 11h 8m |
| LINK/USDT | 163 | 54.6% | −147.39 | 11h 27m |
| ADA/USDT | 186 | 54.8% | −187.58 | 10h 25m |
Why trades closed
| Exit reason | Trades | Win rate | Profit (USDT) |
|---|---|---|---|
| Exit signal | 1714 | 68.6% | +2,047.03 |
| Take-profit (ROI) | 6 | 100.0% | +120.21 |
| Stoploss | 286 | 0.0% | −2,967.02 |
The code
SmoothOperator comes from the community freqtrade-strategies repository (GPL-3.0). These are the parts that decide when it trades; the full file is 304 lines.
Entry rules
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe.loc[ ( # protection against pump and dump # (dataframe['volume'] < (dataframe['volume'].rolling(window=30).mean().shift(1) * 20)) # # & (dataframe['macd'] < dataframe['macdsignal']) # & (dataframe['macd'] > 0) # # spike below entry band for 3 consecutive ticks # & (dataframe['low'] < dataframe['entry_bb_lowerband']) # & (dataframe['low'].shift(1) < dataframe['bb_lowerband'].shift(1)) # & (dataframe['low'].shift(2) < dataframe['bb_lowerband'].shift(2)) # # pattern recognition # & ( # (dataframe['close'] > dataframe['open']) # | (dataframe['CDLHAMMER'] == 100) # | (dataframe['CDLINVERTEDHAMMER'] == 100) # | (dataframe['CDLDRAGONFLYDOJI'] == 100) # ) # bottom curve detection # & (dataframe['mfi_rsi_cci_smooth'] < 0) # # | ( # 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) ) | # buy in very oversold conditions ( (dataframe['low'] < dataframe['bb_middleband']) & (dataframe['cci'] < -200) & (dataframe['rsi'] < 30) & (dataframe['mfi'] < 30) ) | # etc tends to trade like this # over very long periods of slowly building up coins # does not happen often, but once in a while ( (dataframe['mfi'] < 10) & (dataframe['cci'] < -150) & (dataframe['rsi'] < dataframe['mfi']) ) ) & # ensure we have an overall uptrend (dataframe['close'] > dataframe['close'].shift()) ), 'enter_long'] = 1 return dataframe
Exit rules
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: # different strategy used for sell points, due to be able to duplicate it to 100% dataframe.loc[ ( ( # This generates very nice sale points, and mostly sit's one stop behind # the top of the peak ( (dataframe['mfi_rsi_cci_smooth'] > 100) & (dataframe['mfi_rsi_cci_smooth'].shift(1) > dataframe['mfi_rsi_cci_smooth']) & (dataframe['mfi_rsi_cci_smooth'].shift(2) < dataframe['mfi_rsi_cci_smooth'].shift(1)) & (dataframe['mfi_rsi_cci_smooth'].shift(3) < dataframe['mfi_rsi_cci_smooth'].shift(2)) ) | # This helps with very long, sideways trends, to get out of a market before # it dumps ( StrategyHelper.eight_green_candles(dataframe) ) | # in case of very overbought market, like some one pumping # sell ( (dataframe['cci'] > 200) & (dataframe['rsi'] > 70) ) ) ), 'exit_long'] = 1 return dataframe
Indicators
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: ################################################################################## # required for entry and exit # CCI dataframe['cci'] = ta.CCI(dataframe, timeperiod=20) dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) dataframe['adx'] = ta.ADX(dataframe) dataframe['mfi'] = ta.MFI(dataframe) dataframe['mfi_smooth'] = ta.EMA(dataframe, timeperiod=11, price='mfi') dataframe['cci_smooth'] = ta.EMA(dataframe, timeperiod=11, price='cci') dataframe['rsi_smooth'] = ta.EMA(dataframe, timeperiod=11, price='rsi') ################################################################################## # 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 macd = ta.MACD(dataframe) dataframe['macd'] = macd['macd'] dataframe['macdsignal'] = macd['macdsignal'] dataframe['macdhist'] = macd['macdhist'] ################################################################################## # required for entry bollinger = qtpylib.bollinger_bands(dataframe['close'], window=20, stds=1.6) dataframe['entry_bb_lowerband'] = bollinger['lower'] dataframe['entry_bb_upperband'] = bollinger['upper'] dataframe['entry_bb_middleband'] = bollinger['mid'] dataframe['bpercent'] = (dataframe['close'] - dataframe['bb_lowerband']) / ( dataframe['bb_upperband'] - dataframe['bb_lowerband']) * 100 dataframe['bsharp'] = (dataframe['bb_upperband'] - dataframe['bb_lowerband']) / ( dataframe['bb_middleband']) # these seem to be kind useful to measure when bands widen # but than they are directly based on the moving average dataframe['bsharp_slow'] = ta.SMA(dataframe, price='bsharp', timeperiod=11) dataframe['bsharp_medium'] = ta.SMA(dataframe, price='bsharp', timeperiod=8) dataframe['bsharp_fast'] = ta.SMA(dataframe, price='bsharp', timeperiod=5) ################################################################################## # rsi and mfi are slightly weighted dataframe['mfi_rsi_cci_smooth'] = (dataframe['rsi_smooth'] * 1.125 + dataframe['mfi_smooth'] * 1.125 + dataframe[ 'cci_smooth']) / 3 dataframe['mfi_rsi_cci_smooth'] = ta.TEMA(dataframe, timeperiod=21, price='mfi_rsi_cci_smooth') # playgound dataframe['candle_size'] = (dataframe['close'] - dataframe['open']) * ( dataframe['close'] - dataframe['open']) / 2 # helps with pattern recognition dataframe['average'] = (dataframe['close'] + dataframe['open'] + dataframe['high'] + dataframe['low']) / 4 dataframe['sma_slow'] = ta.SMA(dataframe, timeperiod=200, price='close') dataframe['sma_medium'] = ta.SMA(dataframe, timeperiod=100, price='close') dataframe['sma_fast'] = ta.SMA(dataframe, timeperiod=50, price='close') 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 SmoothOperator --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.