Home Creators Posts Import Register Favorites Logout

Content

Introduction

It is rare to test trading algorithms on their potential profitability and then discover a whole bunch of really promising ones back to back. In this blogpost I will present you yet another a well performing strategy which is called “Bollinger with real pullback”. So let’s quickly find out how well it performs!

The strategy

The creator of this algorithm is called “jilv220” and this time the author of the strategy has build a complete Github repository around it. You can find it on the following location: https://github.com/jilv220/BB_RPB_TSL

That is really nice of the author to do and more strategy developers should take note of that.

The readme states that the strategy should not be used in live trading since it is still in dry run mode. Although the repo hasn’t been updated in 2 years. So it could be that this strategy has been abandoned or developed further elsewhere.

There is also a warning to not use it on Kucoin not to use volume list and finally a thank you to the authors of the Nostalgia for Infinity strategy and real pullback.

Furthermore in the code itself is more information about the origins of this algo. Apparently the inspiration came from this blogpost: https://hacks-for-life.blogspot.com/2020/12/freqtrade-notes.html. By another “Real Pull Back” strategy in this Github repository: https://github.com/GeorgeMurAlkh/freqtrade-tuff/blob/main/user_data/strategies/TheRealPullbackV2.py. And finally also inspired by the trailing custom stop loss taken from the “BigZ04_TSL” algorithm from Perkmeister (modded by ilya).

So please, if you like this trading algorithm and you have great success with, please give a thank you to the author and other sources of this endresult to show you your appreciation. This is not my work, I’m just the guy tot test on it’s performance.

Also , this is a great example that there is nothing holding you back from being inspired by other strategies and then take the best parts to create synergy and and maybe an even more profitable trading algorithm.

The strategy

Now about the code itself. If you can remember the Nostalgia for infinity post/video you know that the algorithm was pretty large and difficult to understand at first sight. It’s the same thing here. Although not as large, I will still go over the code more high level and try to tell you the gist of this trading algorithm.

The code starts ofcourse with the importing of different modules that is uses further down.

Everywhere that says <snip> I removed the code for two reasons. I do not want to upset the original author by adding the complite code here; and otherwise this post gets way to long…

# --- Do not remove these libs ---
import freqtrade.vendor.qtpylib.indicators as qtpylib
import numpy as np
import talib.abstract as ta
import pandas_ta as pta
from freqtrade.persistence import Trade
from freqtrade.strategy.interface import IStrategy
from pandas import DataFrame, Series, DatetimeIndex, merge
from datetime import datetime, timedelta
from freqtrade.strategy import merge_informative_pair, CategoricalParameter, DecimalParameter, IntParameter, stoploss_from_open
from freqtrade.exchange import timeframe_to_prev_date
from functools import reduce
from technical.indicators import RMI, zema, ichimoku

Then some custom indicators are defined here that will probably be used later on. Like the Elliot wave oscillator, I think this is a smoothed rate of change here, Williams R, Chaikin money flow and some other custom ones.

def ha_typical_price(bars):
res = (bars['ha_high'] + bars['ha_low'] + bars['ha_close']) / 3.
return Series(index=bars.index, data=res)
def EWO(dataframe, ema_length=5, ema2_length=35):
df = dataframe.copy()
ema1 = ta.EMA(df, timeperiod=ema_length)
ema2 = ta.EMA(df, timeperiod=ema2_length)
emadif = (ema1 - ema2) / df['low'] * 100
return emadif
<snip>
# Chaikin Money Flow
def chaikin_money_flow(dataframe, n=20, fillna=False) -> Series:
"""Chaikin Money Flow (CMF)
It measures the amount of Money Flow Volume over a specific period.
http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:chaikin_money_flow_cmf
Args:
dataframe(pandas.Dataframe): dataframe containing ohlcv
n(int): n period.
fillna(bool): if fill nan values.
Returns:
pandas.Series: New feature generated.
"""
mfv = ((dataframe['close'] - dataframe['low']) - (dataframe['high'] - dataframe['close'])) / (dataframe['high'] - dataframe['low'])
mfv = mfv.fillna(0.0)  # float division by zero
mfv *= dataframe['volume']
cmf = (mfv.rolling(n, min_periods=0).sum()
/ dataframe['volume'].rolling(n, min_periods=0).sum())
if fillna:
cmf = cmf.replace([np.inf, -np.inf], np.nan).fillna(0)
return Series(cmf, name='cmf')

Then there is the actual strategy class itself. And that starts with a whole bunch of buy and sell parameters that can be optimized with hyperopt. And you can also see immediately that this also will be pretty complex.

# buy space
buy_params = {
"max_slip": 0.668,
##
"buy_bb_width_1h": 1.074,
"buy_roc_1h": 4,
##
"buy_threshold": 0.003,
"buy_bb_factor": 0.999,
#
<snip>
##
"buy_adx": 13,
"buy_cofi_39_r14": -85.016,
"buy_cofi_cti": -0.892,
"buy_ema_cofi": 1.147,
"buy_ewo_high": 8.594,
"buy_fastd": 28,
"buy_fastk": 39,
##
"buy_nfix_39_cti": -0.105,
"buy_nfix_39_r14": -81.827,
}
# sell space
sell_params = {
##
"sell_cmf": -0.046,
"sell_ema": 0.988,
<snip>
"sell_deadfish_bb_width": 0.043,
"sell_deadfish_volume_factor": 2.37
}

Then ofcourse the mandatory strategy settings like ROI, Timeframe and stoploss. Stop loss has been set to 0.99 to disable it and let the strategy exit trades and ROI has been set to 20 percent profits.

Also the algo has an informative timeframe set here so I cannot test timeframes higher than 1 hour this time.

minimal_roi = {
"0": 0.205,
}
# Optimal timeframe for the strategy
timeframe = '5m'
inf_1h = '1h'
# Run "populate_indicators()" only for new candle.
process_only_new_candles = True
# Disabled
stoploss = -0.99
# Custom stoploss
use_custom_stoploss = True
use_sell_signal = True

Again there are a whole bunch of optimization settings here for the buy and sell parameters. And here the default indicator settings are set as well as the spaces where Hyperoptimization should search for the optimal setting.

## Buy params
is_optimize_dip = False
buy_rmi = IntParameter(30, 50, default=35, optimize= is_optimize_dip)
buy_cci = IntParameter(-135, -90, default=-133, optimize= is_optimize_dip)
buy_srsi_fk = IntParameter(30, 50, default=25, optimize= is_optimize_dip)
buy_cci_length = IntParameter(25, 45, default=25, optimize = is_optimize_dip)
buy_rmi_length = IntParameter(8, 20, default=8, optimize = is_optimize_dip)
<snip>
is_optimize_deadfish = False
sell_deadfish_bb_width = DecimalParameter(0.03, 0.75, default=0.05 , optimize = is_optimize_deadfish)
sell_deadfish_profit = DecimalParameter(-0.15, -0.05, default=-0.05 , optimize = is_optimize_deadfish)
sell_deadfish_bb_factor = DecimalParameter(0.90, 1.20, default=1.0 , optimize = is_optimize_deadfish)
sell_deadfish_volume_factor = DecimalParameter(1, 2.5, default=1.0 , optimize = is_optimize_deadfish)
############################################################################
Then the code has a section where the informative pairs and timeframe settings are determined. And again you can see by yourself that this is pretty complex.
def informative_pairs(self):
pairs = self.dp.current_whitelist()
informative_pairs = [(pair, '1h') for pair in pairs]
return informative_pairs
def informative_1h_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
assert self.dp, "DataProvider is required for multiple timeframes."
# Get the informative pair
informative_1h = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=self.inf_1h)
<snip>
# Heikin Ashi
inf_heikinashi = qtpylib.heikinashi(informative_1h)
informative_1h['ha_close'] = inf_heikinashi['close']
informative_1h['rocr'] = ta.ROCR(informative_1h['ha_close'], timeperiod=168)
# Pump protections
#informative_1h['hl_pct_change_48'] = range_percent_change(informative_1h, 'HL', length=48)
#informative_1h['hl_pct_change_36'] = range_percent_change(informative_1h, 'HL', length=36)
#informative_1h['hl_pct_change_24'] = range_percent_change(informative_1h, 'HL', length=24)
#informative_1h['hl_pct_change_12'] = range_percent_change(informative_1h, 'HL', length=12)
#informative_1h['hl_pct_change_6'] = range_percent_change(informative_1h, 'HL', length=6)
return informative_1h

Followed by a custom stoploss setting and a section there NFIX code is used to determine the custom sell settings.

def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> float:
sl_new = 1
if (current_profit > 0.2):
sl_new = 0.05
elif (current_profit > 0.1):
sl_new = 0.03
elif (current_profit > 0.06):
sl_new = 0.02
elif (current_profit > 0.03):
sl_new = 0.015
return sl_new
# From NFIX
def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
current_profit: float, **kwargs):
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_candle = dataframe.iloc[-1]
previous_candle_1 = dataframe.iloc[-2]
max_profit = ((trade.max_rate - trade.open_rate) / trade.open_rate)
max_loss = ((trade.open_rate - trade.min_rate) / trade.min_rate)
# sell trail
if 0.012 > current_profit >= 0.0:
if (max_profit > (current_profit + 0.045)) and (last_candle['rsi'] < 46.0):
return 'sell_profit_t_0_1'
elif (max_profit > (current_profit + 0.025)) and (last_candle['rsi'] < 32.0):
return 'sell_profit_t_0_2'
elif (max_profit > (current_profit + 0.05)) and (last_candle['rsi'] < 48.0):
return 'sell_profit_t_0_3'
elif 0.02 > current_profit >= 0.012:
if (max_profit > (current_profit + 0.01)) and (last_candle['rsi'] < 39.0):
return 'sell_profit_t_1_1'
elif (max_profit > (current_profit + 0.035)) and (last_candle['rsi'] < 45.0) and (last_candle['cmf'] < -0.0) and (last_candle['cmf_1h'] < -0.0):
return 'sell_profit_t_1_2'
elif (max_profit > (current_profit + 0.02)) and (last_candle['rsi'] < 40.0) and (last_candle['cmf'] < -0.0) and (last_candle['cti_1h'] > 0.8):
return 'sell_profit_t_1_4'
<snip>
max_slip = self.max_slip.value
if(len(dataframe) < 1):
return False
dataframe = dataframe.iloc[-1].squeeze()
if ((rate > dataframe['close'])) :
slippage = ( (rate / dataframe['close']) - 1 ) * 100
#print("open rate is : " + str(rate))
#print("last candle close is : " + str(dataframe['close']))
#print("slippage is : " + str(slippage) )
#print("############################################################################")
if slippage < max_slip:
return True
else:
return False
return True
Then there is a section where the normal or current timeframe indicator settings are determined.
############################################################################
def normal_tf_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Bollinger bands
bollinger2 = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
dataframe['bb_lowerband2'] = bollinger2['lower']
dataframe['bb_middleband2'] = bollinger2['mid']
dataframe['bb_upperband2'] = bollinger2['upper']
<snip>
# MOMDIV
mom = momdiv(dataframe)
dataframe['momdiv_buy'] = mom['momdiv_buy']
dataframe['momdiv_sell'] = mom['momdiv_sell']
dataframe['momdiv_coh'] = mom['momdiv_coh']
dataframe['momdiv_col'] = mom['momdiv_col']
return dataframe
############################################################################
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# The indicators for the 1h informative timeframe
informative_1h = self.informative_1h_indicators(dataframe, metadata)
dataframe = merge_informative_pair(dataframe, informative_1h, self.timeframe, self.inf_1h, ffill=True)
# The indicators for the normal (5m) timeframe
dataframe = self.normal_tf_indicators(dataframe, metadata)
return dataframe

And then finally we come to the actual buy and sell rules.

And as you can see, there are different rules under which to buy here. Like if it’s a dip, local uptrend, wave oscillator, deadfish and more…

def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
conditions = []
dataframe.loc[:, 'buy_tag'] = ''
is_dip = (
(dataframe[f'rmi_length_{self.buy_rmi_length.value}'] < self.buy_rmi.value) &
(dataframe[f'cci_length_{self.buy_cci_length.value}'] <= self.buy_cci.value) &
(dataframe['srsi_fk'] < self.buy_srsi_fk.value)
)
is_break = (
(dataframe['bb_delta'] > self.buy_bb_delta.value) &
(dataframe['bb_width'] > self.buy_bb_width.value) &
(dataframe['closedelta'] > dataframe['close'] * self.buy_closedelta.value / 1000 ) &    # from BinH
(dataframe['close'] < dataframe['bb_lowerband3'] * self.buy_bb_factor.value)
)
<snip>
conditions.append(is_nfix_49)                                              # ~0.33 / 100% / 0%           D
dataframe.loc[is_nfix_49, 'buy_tag'] += 'nfix_49 '
if conditions:
dataframe.loc[
is_additional_check
&
reduce(lambda x, y: x | y, conditions)
, 'buy' ] = 1
return dataframe
Funny enough, the sell trend seems to be empty
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[ (dataframe['volume'] > 0), 'sell' ] = 0
return dataframe
But you in this algo the custom sell settings were determined in the NFIX part which I showed you above.
# From NFIX
def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
current_profit: float, **kwargs):
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_candle = dataframe.iloc[-1]
previous_candle_1 = dataframe.iloc[-2]
max_profit = ((trade.max_rate - trade.open_rate) / trade.open_rate)
max_loss = ((trade.open_rate - trade.min_rate) / trade.min_rate)
# sell trail
if 0.012 > current_profit >= 0.0:
if (max_profit > (current_profit + 0.045)) and (last_candle['rsi'] < 46.0):
return 'sell_profit_t_0_1'
elif (max_profit > (current_profit + 0.025)) and (last_candle['rsi'] < 32.0):
return 'sell_profit_t_0_2'
elif (max_profit > (current_profit + 0.05)) and (last_candle['rsi'] < 48.0):
return 'sell_profit_t_0_3'

I’m sorry if I maybe glanced over this algo code a little bit too quickly. But I did this for time sake. If I had to reverse engineer what this algo all did I would take me probably a couple of days to find out. That’s why I have such admiration of people like this author to put so much effort in this code.

It really makes me curious to find out how well it will perform over my backtest setup. So let’s get right ahead and do it.

Initial backtest results

First a thing I have to mention here I had to shorten the date range of the 5 minute timeframe backtest to actually get results. Otherwise the backtest would error out on me. So I have set the 5 minute range to 3 years of backtest data for the pairs.

But even with this shorter time range, this algorithm sure did not dissapoint me. It’s initial run over my backtest setup has an endscore of 293. And the numbers I got back from the backtest engine do not lie. This seems to be a pretty good algo.

219 percent profits with a 73% win rate and high calmar, sharpe and sortino ratios, a great profit factor. And most coins are responding well to this strategy. For me this is already an algorithm that I will put in the waiting room to dry run it and see how it performs under real market circumstances.

As with all other 5 minute strategies I tested in the past, the plot I made is over a Gigabyte large and I am not able to open it in my browser.

Now I can get a feeling on which internal sub strategy this algo get’s the most profits from and as it seem now it is from the local uptrend. With the most entries and also cumulative profits here.

nfi_38, of which I do not know what it exactly does, comes second with less than half of the local uptrends performance.

But as it stands now I am very curious how it would hypothetically perform if I would optimize its parameters. So let’s do that and see how far I get.

Hyperopt and backtest results

Optimizing the parameters was actually simpler that I had imagined at first. Both the buy and the sell parameters were not discovered in the actual strategy so unfortunately I could not optimize these.

The only parameters I always can optimize however are the ROI and stop loss parameters. But I did not do this. And there is a small story behind this.

Before I tested this Bollinger with real pull back on Binance exchange version, I actually tested two other versions I got. An original BB_RPB_TSL_RNG version and a BB_RPB_TSL_RNG_2 version. The version 2 also scored pretty well and I decided to investigate that one further.

This second version could be optimized, but only on the sell parameter, so I decided to give it a try and optimized the sell, roi and stoploss. Then I did another test with the optimized settings. And I got the following unbelievable results back:

You can see that this optimized version would crush all other strategies with these numbers. And I almost made a video about these results to inform you about it.

But then I quickly realized that these numbers looked to good to be true. A Sharpe ratio of 12 and Sortino of 18, Calmar of almost 34% and a mere 7% drawdown and 90% pairs that are happy with this algo. This started to look unreal.

I was investigating all the output to the terminal and investigated the code and found some unexplainable errors or just settings that I could not ever use with my setup.

I’ll run over them quickly here:

  1. In that code the “BTC/BUSD” pair was used as an informative pair, and in my setup I do not use this, so this could lead to unexplainable trade entries or exits
  2. No data was found for this pair in the backtests, which is logical if you do not have this data at all
  3. Also If you use an exchange that does not have this pair at all, then the strategy has to be adjusted I guess
  4. I got loads of “CustomStoploss function did not return valid stoploss” errors in the output
  5. Also a lot of fragmentation errors
  6. And most importantly of all, the custom trailing stoploss functionality was totally negated by the ROI take profit settings and was actually making most of the losses in that optimized version. Which goes against the philosophy of the algo I think.

I did another run to only optimize the sell parameters, but I got similar results.

So to get back to my original story. The experiences I got with the other versions of this algo made me decide not to optimize this. But instead keep the original results here.

This also prevents me from overoptimizing this strategy (of which I am certain I did in the earlier one). You have to know that there are over 50 parameters that theoretically can be optimized and that would probably lead to a huge bias for the backtest dataset. Giving absolutely no guarantees for future performances.

So maybe its better that I could not optimize this Binance adjusted version otherwise I would have fallen into the same trap.

Strategy League and conclusion

Now, there is nothing to be ashamed about with this strategy. Without any adjustments it performs really well straight out of the box on my dataset, without any adjustments. The funny thing is that this is the second strategy in a row that enters my strategy league on the second spot. Moving last weeks strategy, that also entered the league in second spot, to the third spot.

The only reason why it does not score that much higher is because of the profit percentage. But you also have to remember that this 5 minute backtest was done over a shorter date range. And still it performed that well.

The only things I have against this algorithm is that it, just like the NFIX strategy, pretty complex to learn. Also there are a lot of moving parts in this strategy that can be adjusted. Therefore this algo is a bit complicated to maintain and adjust to meet more of your specific circumstances.

Another negative aspect is the amount of adjustable parameters and the intention to make these all hyperoptable. If I would make the effort to optimize all these parameters, then the risk of overfitting gets very high.

Nonetheless, don’t let me keep you from trying out this code and I hope you get similar results as me. Just don’t forget to do an extensive dry run here to find out if this also performs good in reality as well.

Files

If you like this strategy and want to use this on your own system, then please pay the original author’s Github page a visit. And send him a message or thank you or something like that to show your appreciation.

All the files were posted in the following Patreon post at an earlier date, since this was a repost: https://www.patreon.com/posts/86284671

Thank you for reading this post and I’ll see you in the next one…

Goodbye!

Comments

Peter

Hi sir, is this the one you're running dry run and live streaming on YouTube?