Bollinger Real Pullback Version 2 exclusive post (Patreon)
Downloads
Missing 1 full-res photo, 1 file, as this post has not yet been imported.
Content
This is bonus content about the Bollinger with Pullback strategy based on the Nostalgia for Infinity algorithm. At first I intended to never publish this because it was actually the first draft of a video on this algorithm that I wanted to make on the BB RPB TSL V2 strategy code. But then I found the Binance version and eventually I tested and made a post and video about that version as well. I also came to the conclusion that the results I got below are way too good to be true.
And after some consideration I decided to not use the score you see below but the initial results of the Binance version instead. I explain the reasons below and in my video.
Nonetheless, I think I have some valuable information for you in the text below as well so I did not want to keep this by myself but wanted to share this with my patrons as well. I also added the output files with strategy code and optimisatons of the second version in this post for your benefit.
Please read this post with the context given above!
The BB RPB TSL version 2 strategy
At the start of the strategy, there are a whole bunch of buy and sell parameters given for the various indicators and calculations that are made in this code. And you now can already see why this algo is getting fairly complex right at the start of the code.
# Hyperopt result area
# buy space
buy_params = {
##
"buy_btc_safe": -289,
"buy_btc_safe_1d": -0.05,
##
"buy_threshold": 0.003,
"buy_bb_factor": 0.999,
"buy_bb_delta": 0.025,
"buy_bb_width": 0.095,
##
"buy_cci": -116,
"buy_cci_length": 25,
"buy_rmi": 49,
"buy_rmi_length": 17,
"buy_srsi_fk": 32,
##
"buy_closedelta": 12.148,
"buy_ema_diff": 0.022,
##
"buy_adx": 20,
"buy_fastd": 20,
"buy_fastk": 22,
"buy_ema_cofi": 0.98,
"buy_ewo_high": 4.179,
##
"buy_ema_high_2": 1.087,
"buy_ema_low_2": 0.970,
##
}
# sell space
sell_params = {
"pHSL": -0.178,
"pPF_1": 0.019,
"pPF_2": 0.065,
"pSL_1": 0.019,
"pSL_2": 0.062,
"base_nb_candles_sell": 23,
"high_offset": 1.051,
"high_offset_2": 1.02,
"sell_btc_safe": -325
}Then there are the usual strategy parameters given here, where we see that the ROI is set, the strategy is initially set for the 5 minute timeframe and the stop loss is set in the code below. 0.99 means that stop loss gets triggered at almost 100 percent loss.
# really hard to use this
minimal_roi = {
"0": 0.10,
}
# Optimal timeframe for the strategy
timeframe = '5m'
inf_1h = '1h'
# Disabled
stoploss = -0.99
# Custom stoploss
use_custom_stoploss = True
use_sell_signal = TrueThen here again a whole bunch of buy and sell spaces for optimization later in the process. At a glance that some well known indicators like rsi, cci, Bollinger, ema, Elliot wave oscillator and much more are configured.
## 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)
is_optimize_break = False
buy_bb_width = DecimalParameter(0.05, 0.2, default=0.15, optimize = is_optimize_break)
buy_bb_delta = DecimalParameter(0.025, 0.08, default=0.04, optimize = is_optimize_break)
is_optimize_local_dip = False
buy_ema_diff = DecimalParameter(0.022, 0.027, default=0.025, optimize = is_optimize_local_dip)
buy_bb_factor = DecimalParameter(0.990, 0.999, default=0.995, optimize = False)
buy_closedelta = DecimalParameter(12.0, 18.0, default=15.0, optimize = is_optimize_local_dip)
is_optimize_ewo = False
buy_rsi_fast = IntParameter(35, 50, default=45, optimize = False)
buy_rsi = IntParameter(15, 30, default=35, optimize = False)
buy_ewo = DecimalParameter(-6.0, 5, default=-5.585, optimize = is_optimize_ewo)
buy_ema_low = DecimalParameter(0.9, 0.99, default=0.942 , optimize = is_optimize_ewo)
buy_ema_high = DecimalParameter(0.95, 1.2, default=1.084 , optimize = is_optimize_ewo)
is_optimize_ewo_2 = False
buy_ema_low_2 = DecimalParameter(0.96, 0.978, default=0.96 , optimize = is_optimize_ewo_2)
buy_ema_high_2 = DecimalParameter(1.05, 1.2, default=1.09 , optimize = is_optimize_ewo_2)
is_optimize_cofi = False
buy_ema_cofi = DecimalParameter(0.96, 0.98, default=0.97 , optimize = is_optimize_cofi)
buy_fastk = IntParameter(20, 30, default=20, optimize = is_optimize_cofi)
buy_fastd = IntParameter(20, 30, default=20, optimize = is_optimize_cofi)
buy_adx = IntParameter(20, 30, default=30, optimize = is_optimize_cofi)
buy_ewo_high = DecimalParameter(2, 12, default=3.553, optimize = is_optimize_cofi)
is_optimize_btc_safe = False
buy_btc_safe = IntParameter(-300, 50, default=-200, optimize = is_optimize_btc_safe)
buy_btc_safe_1d = DecimalParameter(-0.075, -0.025, default=-0.05, optimize = is_optimize_btc_safe)
buy_threshold = DecimalParameter(0.003, 0.012, default=0.008, optimize = is_optimize_btc_safe)
# Buy params toggle
buy_is_dip_enabled = CategoricalParameter([True, False], default=True, space='buy', optimize=False, load=True)
buy_is_break_enabled = CategoricalParameter([True, False], default=True, space='buy', optimize=False, load=True)
## Sell params
sell_btc_safe = IntParameter(-400, -300, default=-365, optimize = True)
base_nb_candles_sell = IntParameter(5, 80, default=sell_params['base_nb_candles_sell'], space='sell', optimize=True)
high_offset = DecimalParameter(0.95, 1.1, default=sell_params['high_offset'], space='sell', optimize=True)
high_offset_2 = DecimalParameter(0.99, 1.5, default=sell_params['high_offset_2'], space='sell', optimize=True)Then there are some trailing stop loss settings for optimization configured here.
## Trailing params
# hard stoploss profit
pHSL = DecimalParameter(-0.200, -0.040, default=-0.08, decimals=3, space='sell', load=True)
# profit threshold 1, trigger point, SL_1 is used
pPF_1 = DecimalParameter(0.008, 0.020, default=0.016, decimals=3, space='sell', load=True)
pSL_1 = DecimalParameter(0.008, 0.020, default=0.011, decimals=3, space='sell', load=True)
# profit threshold 2, SL_2 is used
pPF_2 = DecimalParameter(0.040, 0.100, default=0.080, decimals=3, space='sell', load=True)
pSL_2 = DecimalParameter(0.020, 0.070, default=0.040, decimals=3, space='sell', load=True)Now the only part that makes me worry a bit here is that this algo depends on the BTC/BUSD pair, set over here.
def informative_pairs(self):
informative_pairs = [("BTC/BUSD", "5m")]
return informative_pairsI have no clue if this will haunt me in the testing process. I also wonder why this was set because this makes the algo depend a lot on this specific pair. And also, if you are not having this backtesting data or using an exchange that does not even have this pair listed, how will this influence the algorithm.
The next part is a custom trailing stop loss setting. Custom stop losses can also be set in the bot I use and this is one example of them.
Apparently the stop loss level is moved when the profit also rises. The initial values for this are set in the parameter optimization section. As far as I an understand the stoploss profit level has three levels as decribed in this comment:
# For profits between PF_1 and PF_2 the stoploss (sl_profit) used is linearly interpolated
# between the values of SL_1 and SL_2. For all profits above PL_2 the sl_profit value
# rises linearly with current profit, for profits below PF_1 the hard stoploss profit is used.Each time when the profit enters a new level a new trailing stop loss method is calculated so to maximise the profits when the trade reverses. Pretty ingenious I might say.
## Custom Trailing stoploss ( credit to Perkmeister for this custom stoploss to help the strategy ride a green candle )
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> float:
# hard stoploss profit
HSL = self.pHSL.value
PF_1 = self.pPF_1.value
SL_1 = self.pSL_1.value
PF_2 = self.pPF_2.value
SL_2 = self.pSL_2.value
# For profits between PF_1 and PF_2 the stoploss (sl_profit) used is linearly interpolated
# between the values of SL_1 and SL_2. For all profits above PL_2 the sl_profit value
# rises linearly with current profit, for profits below PF_1 the hard stoploss profit is used.
if (current_profit > PF_2):
sl_profit = SL_2 + (current_profit - PF_2)
elif (current_profit > PF_1):
sl_profit = SL_1 + ((current_profit - PF_1) * (SL_2 - SL_1) / (PF_2 - PF_1))
else:
sl_profit = HSL
# Only for hyperopt invalid return
if (sl_profit >= current_profit):
return -0.99
return stoploss_from_open(sl_profit, current_profit)Then the indicators are determined, which, again are a bunch of them and you can go over them when you go over the code:
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
assert self.dp, "DataProvider is required for multiple timeframes."
# Bollinger bands (hyperopt hard to implement)
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']
bollinger3 = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=3)
dataframe['bb_lowerband3'] = bollinger3['lower']
dataframe['bb_middleband3'] = bollinger3['mid']
dataframe['bb_upperband3'] = bollinger3['upper']
### BTC protection
# BTC info
inf_tf = '5m'
informative = self.dp.get_pair_dataframe('BTC/BUSD', timeframe=inf_tf)
informative_past = informative.copy().shift(1) # Get recent BTC info
# BTC 5m dump protection
informative_past_source = (informative_past['open'] + informative_past['close'] + informative_past['high'] + informative_past['low']) / 4 # Get BTC price
informative_threshold = informative_past_source * self.buy_threshold.value # BTC dump n% in 5 min
informative_past_delta = informative_past['close'].shift(1) - informative_past['close'] # should be positive if dump
informative_diff = informative_threshold - informative_past_delta # Need be larger than 0
dataframe['btc_threshold'] = informative_threshold
dataframe['btc_diff'] = informative_diff
# BTC 1d dump protection
informative_past_1d = informative.copy().shift(288)
informative_past_source_1d = (informative_past_1d['open'] + informative_past_1d['close'] + informative_past_1d['high'] + informative_past_1d['low']) / 4
dataframe['btc_5m'] = informative_past_source
dataframe['btc_1d'] = informative_past_source_1d
### Other checks
dataframe['bb_width'] = ((dataframe['bb_upperband2'] - dataframe['bb_lowerband2']) / dataframe['bb_middleband2'])
dataframe['bb_delta'] = ((dataframe['bb_lowerband2'] - dataframe['bb_lowerband3']) / dataframe['bb_lowerband2'])
dataframe['bb_bottom_cross'] = qtpylib.crossed_below(dataframe['close'], dataframe['bb_lowerband3']).astype('int')
# CCI hyperopt
for val in self.buy_cci_length.range:
dataframe[f'cci_length_{val}'] = ta.CCI(dataframe, val)
dataframe['cci'] = ta.CCI(dataframe, 26)
dataframe['cci_long'] = ta.CCI(dataframe, 170)
# RMI hyperopt
for val in self.buy_rmi_length.range:
dataframe[f'rmi_length_{val}'] = RMI(dataframe, length=val, mom=4)
#dataframe['rmi'] = RMI(dataframe, length=8, mom=4)
# SRSI hyperopt ?
stoch = ta.STOCHRSI(dataframe, 15, 20, 2, 2)
dataframe['srsi_fk'] = stoch['fastk']
dataframe['srsi_fd'] = stoch['fastd']
# BinH
dataframe['closedelta'] = (dataframe['close'] - dataframe['close'].shift()).abs()
# SMA
dataframe['sma_15'] = ta.SMA(dataframe, timeperiod=15)
dataframe['sma_30'] = ta.SMA(dataframe, timeperiod=30)
# CTI
dataframe['cti'] = pta.cti(dataframe["close"], length=20)
# EMA
dataframe['ema_8'] = ta.EMA(dataframe, timeperiod=8)
dataframe['ema_12'] = ta.EMA(dataframe, timeperiod=12)
dataframe['ema_13'] = ta.EMA(dataframe, timeperiod=13)
dataframe['ema_16'] = ta.EMA(dataframe, timeperiod=16)
dataframe['ema_26'] = ta.EMA(dataframe, timeperiod=26)
dataframe['hma_50'] = qtpylib.hull_moving_average(dataframe['close'], window=50)
dataframe['ema_100'] = ta.EMA(dataframe, timeperiod=100)
dataframe['sma_9'] = ta.SMA(dataframe, timeperiod=9)
# RSI
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
dataframe['rsi_fast'] = ta.RSI(dataframe, timeperiod=4)
dataframe['rsi_slow'] = ta.RSI(dataframe, timeperiod=20)
# Elliot
dataframe['EWO'] = EWO(dataframe, 50, 200)
# Cofi
stoch_fast = ta.STOCHF(dataframe, 5, 3, 0, 3, 0)
dataframe['fastd'] = stoch_fast['fastd']
dataframe['fastk'] = stoch_fast['fastk']
dataframe['adx'] = ta.ADX(dataframe)
# Williams %R
dataframe['r_14'] = williams_r(dataframe, period=14)
# Volume
dataframe['volume_mean_4'] = dataframe['volume'].rolling(4).mean().shift(1)
# Calculate all ma_sell values
for val in self.base_nb_candles_sell.range:
dataframe[f'ma_sell_{val}'] = ta.EMA(dataframe, timeperiod=val)
return dataframeAnd finally we come to the part where the actual buy and sell settings are determined. Here you can see again that the developer of this strategy really made some effort to determine the different market circumstances. And also that this algorithm can detect and buy under these different circumstances.
There are multiple sections that seem to also be sections from other algorithms I tested earlier and each of them determine the buy signal for a pair. As the author mentioned on his github page, it is derived from the 'Nostalgia for infinity' algorithm I tested a while ago. You can determine yourself by looking at the how these tiny different mini algo's look for a buy signal here.
is_local_uptrend = ( # from NFI next gen
(dataframe['ema_26'] > dataframe['ema_12']) &
(dataframe['ema_26'] - dataframe['ema_12'] > dataframe['open'] * self.buy_ema_diff.value) &
(dataframe['ema_26'].shift() - dataframe['ema_12'].shift() > dataframe['open'] / 100) &
(dataframe['close'] < dataframe['bb_lowerband2'] * self.buy_bb_factor.value) &
(dataframe['closedelta'] > dataframe['close'] * self.buy_closedelta.value / 1000 )
)
is_ewo = ( # from SMA offset
(dataframe['rsi_fast'] < self.buy_rsi_fast.value) &
(dataframe['close'] < dataframe['ema_8'] * self.buy_ema_low.value) &
(dataframe['EWO'] > self.buy_ewo.value) &
(dataframe['close'] < dataframe['ema_16'] * self.buy_ema_high.value) &
(dataframe['rsi'] < self.buy_rsi.value)
)
is_ewo_2 = (
(dataframe['rsi_fast'] < self.buy_rsi_fast.value) &
(dataframe['close'] < dataframe['ema_8'] * self.buy_ema_low_2.value) &
(dataframe['EWO'] > self.buy_ewo_high.value) &
(dataframe['close'] < dataframe['ema_16'] * self.buy_ema_high_2.value) &
(dataframe['rsi'] < self.buy_rsi.value)
)
conditions.append(is_local_uptrend) # ~3.84 90.2%
dataframe.loc[is_local_uptrend, 'buy_tag'] += 'local uptrend '
conditions.append(is_ewo) # ~2.26 93.5%
dataframe.loc[is_ewo, 'buy_tag'] += 'ewo '
conditions.append(is_ewo_2) # ~3.68 90.3%
dataframe.loc[is_ewo_2, 'buy_tag'] += 'ewo2 'Just by looking at the code I can really learn how the author has puzzled all these different tiny sections together. And this really inspires me to start and create some new idea's i have into algorithmic code. I am truly standing on the shoulder of a giant right now...
But let's not drift off and look at the sell section here. Which is actually pretty simple if you compare this to the buy parameters.
There are generally two major conditions under which a position must be closed here. Divided by this else statement.
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
conditions = []
conditions.append(
(
(dataframe['close'] > dataframe['sma_9'])&
(dataframe['close'] > (dataframe[f'ma_sell_{self.base_nb_candles_sell.value}'] * self.high_offset_2.value)) &
(dataframe['rsi']>50)&
(dataframe['volume'] > 0)&
(dataframe['rsi_fast'] > dataframe['rsi_slow'])
)
|
(
(dataframe['sma_9'] > (dataframe['sma_9'].shift(1) + dataframe['sma_9'].shift(1)*0.005 )) &
(dataframe['close'] < dataframe['hma_50'])&
(dataframe['close'] > (dataframe[f'ma_sell_{self.base_nb_candles_sell.value}'] * self.high_offset.value)) &
(dataframe['volume'] > 0)&
(dataframe['rsi_fast']>dataframe['rsi_slow'])
)
)
if conditions:
dataframe.loc[
reduce(lambda x, y: x | y, conditions),
'sell'
]=1So, at the moment my overall tought here is that there are multiple mini trading algorithms are combined into one single algoritm. The simplicity of the sell signal in combination with the configured trailing stop loss tells me that the author heavily depends on this TSL to exit trades and the sell trend only defined the higher level circumstances under which there can not be traded. But ofcourse that is my initial thought and if you have a better understanding of this code, I would really appreciate it if you add this to the comments section below.
Now let me do an initial test of this strategy to find out how it performs and then do an optimization run after that.
Initial backtest results
Now because this code does look a lot like the Nostalgia for infinity algorithm, I sort of expected to see a high score.
And indeed id does perform really well in its first backtest run. At the 5 minute timeframe it does an excellent job. And you might appreciate this performance even better if I told you that on the 5 minute timeframe I had to shorten the backtesting period to only 3 years of backtesting. Otherwise the backtest engine would crash.
<figure>
</figure>50 percent win rate with a profit factor of 1.56 and 84 percent pairs that like this algo is pretty high in my book. And I have never seen a drawdown this low.
Let me try to optimize the parameters of this algo to see if this can be optimized even further. The last few strategies I tried to optimize scored worse with optimized settings, so i'm curious to find out if that is the case with this code as well.
Optimization and final results
Now optimizing the buy settings, strangely enough, did not go well since the bot complained that there was no buy space to optimize.
<figure>
</figure>So I did another try and only settled for the sell, roi and stop loss parameters to optimize.
There I got a lot of fragmenting errors:
<figure>
</figure>And also the optimization engine complained that the "CustomStoploss function did not return valid stoploss".
<figure>
</figure>But after optimizing for 13 hours I got an optimization result and backtested the algorithm with the optimization settings I got back.
<figure>
</figure>The only thing that got me worried a bit is that the backtest engine did give me the errors that there was no data found on the BTC/BUSD pair.
<figure>
</figure>But once I got results back, I was simply stunned by the performance over the backtest dataset...
An almost unbeleivable score of 359. And values that go through the roof of some of the performance factors here. As mentioned I had to dial back the overall time range to make testing over the 5 minute timeframe possible. But if you see how the performance is, then you'll probably also sit up straight at this point to watch these results.
With a 54 percent win streak this algo made it possible to get a hypothetical return of almost 4000 percent. With Calmar, Sortino and Sharpe ratios very very high. Also almost all pairs respond well to this strategy and there is almost no drawdown. Only 7 percent, which is unbeleivable in my point of view.
Did I really find the Holy Grail of trading algorithms here?
<figure>
</figure>But now comes the more analytical and critical part. If I look more closely to the exit reason statistics, I can see that the ROI gains the most profits and the trailing stop loss actually makes it losing money.
<figure>
</figure>If I compare this with the original 5 minute backtest exit reasons, then I see that the TSL makes the most gains and ROI only has 65 wins. With its original 10% profit takings - as you will find in the strategy file.
<figure>
</figure>So the actual intention of this strategy has been removed and replaced by another way of making profits by the bot.
If I look at the enter reasons that are configured in the algo I still see that the
<figure>
</figure>The order is also diferent from the initial backtest. But you can see that in the top 10 there are still some similar entry reasons there.
<figure>
</figure>Now I cannot compare the profit plots here because they are larger than 1 gigabyte. And I'm not able to open these. So I'll have to make some conclusion of the information I have at this moment.
Strategy league and conclusion
A while ago I investigated the Nostalgia for infinity strategy because of high demand from my viewers/readers. That strategy was also really complex and I wanted to find out how it performed.
Back then it also claimed the first spot on the list. Even though I did not really understand its inner workings and where it would have it's edge. Nonetheless I kept it at first spot just to make the audience aware of this strategy. And it is stil one of my most watched videos.
This time I come across yet another NFI looking algorithm with all its specific market circumstance entry reasons, different indicators, rules and optimization settings.
And it also enters the first place on the Strategy League. Sorry Stray Kidz, your performance was good. But this algo scores better.
And also, this is the third time in a row that I come across such a well performing trading algo. Chrismas came early this year I guess. And as these algo's score better and better, it might be time to think about adding new levels of scoring these performances. But thats for another time perhaps.
So yet another very surprising result to say the least here. Now I get really curious to find out if this can get even better in a future strategy. Am I reaching the limits of what is possible here? Who knows.
But I have also some important points I want to mention about this strategy and it's results as well...
Negative points
Sometimes things look too good to be true. And at those moments you'll have to be critical at what's in front of you. Just to stay aware of any negative consequences.
Because this looks to be such a promising algorithm for your future trades, that does not mean I have no important negative points to mention.
To start with the first, and most obvious one here.
The results I get after backtesting with these optimized parameters.
And you can already guess where I am going with this, but I still want to warn you again that this algo will be heavily overfitted against the backtesting dataset when you optimize it.
There are almost more than 50 parameters that can be optimized against the dataset. Almost every maximum setting can be calculated and that can certainly cause a bias for the historic dataset, and most probably a problem with the expectations you get over the future performance of this algo.
Not to mention the fact that after optimization the algorithms original dependence on a good trailing stop loss was turned into a negative factor. And time based ROI settings took over the money making aspect of this algo.
So optimization of an algorithm is good but overoptimization is a huge red flag in any traders book!
The second, more personal, negative point that I have against these kinds of algorithms is that they want to think of every possible market scenario or circumstance in one single strategy. At some point you get the risk that the code get's too complicated to maintain or signals can negate each other out.
Also for other people it's hard to understand 'what's under the hood' and how things work. You'll have to be a real specialist to know what the code does. Or as an author or maintainer, without any proper documentation after a while you might not be able to understand anymore why you coded it like it is.
Complex means a system which is elegant, reasonable and beautiful but takes time to learn and comprehend. Complicated means a system which is ugly and cobbled together without any explainable justification other than 'it seems to work'.
The third thing to stay aware of are the calculations. I only show you the calculations that come from the Freqtrade bot I use for backtesting algo's. The developers have created these calculations and I assume they know what they do. Nonetheless, Sharpe ratio's of 12 or Sortino ratios of 18 are quite uncommon and these numbers must make you wary on the trustworthyness of the results. Stay critical.
The fourth thing I have is the depending on a trading pair BTC/BUSD as an informative pair. Now you can change this ofcourse to your own prefered pair. But if you are not aware of this then this could result in very strange behaviour when actual trading.
Files
Previews only
