Home Creators Posts Import Register Favorites Logout

Content

Hello there, and welcome to another video/post about an algorithmic trading strategy that I found on the internet and reverse engineer, test and give my opinion of.

As you might already know I am currently testing all the strategies that are available on the Freqtrade site. This way we can find out if there are profitable strategies that we can use for our own crypto algo trading bot.

This time I will test the "Bandtastic" strategy from that repository. So let’s open the code and find out what the intentions are of the provided code are. And hopefully after some rigorous testing we will know if it has a probability to make us some money.

If you followed my earlier posts or videos, then you’ll already know that all these strategies are available on the Github site of the Freqtrade developers. They have a separate repo over there, which contains all the strategies that I have tested so far. This Bandtastic code is available in that repo.

The strategy code

After opening the file we can see the following code appear.

Let’s take a closer look at the code. Hopefully I can make some common sense out of this code to find out what the intentions are.

I’ll go over the code section by section and line by line if needed.

After the initial loading of the libraries there is a section that tells us who the author is of this code.

__author__ = "Robert Roman"
__copyright__ = "Free For Use"
__license__ = "MIT"
__version__ = "1.0"
__maintainer__ = "Robert Roman"
__email__ = "robertroman7@gmail.com"
__BTC_donation__ = "3FgFaG15yntZYSUzfEpxr5mDt1RArvcQrK"

And that is a good thing to see because I always like to give credits to the original author of the code.

This time the author is Robert Roman. So, thank you Robert for providing us with your strategy. Let’s hope it will help us make some serious money with our bot.

And for all the people who are using his code, or get a lot of inspiration of it, Robert still has 0 account balance on his bitcoin address. So if you appreciate his efforts, then please support him.
But better make contact with him before you deposit some on this address, because maybe he lost his private key or something like that.

He also shows some output of the performance of this strategy here. And in this video we are going to find out if these results also apply to my setup.

# Optimized With Sharpe Ratio and 1 year data
# 199/40000:  30918 trades. 18982/3408/8528 Wins/Draws/Losses. Avg profit   0.39%. Median profit   0.65%. Total profit  119934.26007495 USDT ( 119.93%). Avg duration 8:12:00 min. Objective: -127.60220
Then the familiar Freqtrade basic settings appear in the strategy class.
class Bandtastic(IStrategy):
INTERFACE_VERSION = 2
timeframe = '15m'
# ROI table:
minimal_roi = {
"0": 0.162,
"69": 0.097,
"229": 0.061,
"566": 0
}
# Stoploss:
stoploss = -0.345
# Trailing stop:
trailing_stop = True
trailing_stop_positive = 0.01
trailing_stop_positive_offset = 0.058
trailing_only_offset_is_reached = False

Let’s go over these settings before we explore the more strategy specific settings.

The strategy is made for the 15 minute timeframe.

The developer has probably optimized this strategy and got this set of roi take profit levels and different time periods.

Then the stoploss for this strategy is set. Which is quite a large stoploss. You lose almost 35% if the trade does not go as planned and hits the stoploss. And that on a pretty low timeframe.

And then finally the author has activated the trailing stop loss with these settings.

With the trailing stop loss in mind I now get a little bit weary because the last time I used that I got some very unrealistic results back. Due to an issue with the way trailing stop losses are handled in backtests.

So, keeping this in the back of my head. Let’s continue…

# Hyperopt Buy Parameters
buy_fastema = IntParameter(low=1, high=236, default=211, space='buy', optimize=True, load=True)
buy_slowema = IntParameter(low=1, high=126, default=364, space='buy', optimize=True, load=True)
buy_rsi = IntParameter(low=15, high=70, default=52, space='buy', optimize=True, load=True)
buy_mfi = IntParameter(low=15, high=70, default=30, space='buy', optimize=True, load=True)
buy_rsi_enabled = CategoricalParameter([True, False], space='buy', optimize=True, default=False)
buy_mfi_enabled = CategoricalParameter([True, False], space='buy', optimize=True, default=False)
buy_ema_enabled = CategoricalParameter([True, False], space='buy', optimize=True, default=False)
buy_trigger = CategoricalParameter(["bb_lower1", "bb_lower2", "bb_lower3", "bb_lower4"], default="bb_lower1", space="buy")
# Hyperopt Sell Parameters
sell_fastema = IntParameter(low=1, high=365, default=7, space='sell', optimize=True, load=True)
sell_slowema = IntParameter(low=1, high=365, default=6, space='sell', optimize=True, load=True)
sell_rsi = IntParameter(low=30, high=100, default=57, space='sell', optimize=True, load=True)
sell_mfi = IntParameter(low=30, high=100, default=46, space='sell', optimize=True, load=True)
sell_rsi_enabled = CategoricalParameter([True, False], space='sell', optimize=True, default=False)
sell_mfi_enabled = CategoricalParameter([True, False], space='sell', optimize=True, default=True)
sell_ema_enabled = CategoricalParameter([True, False], space='sell', optimize=True, default=False)
sell_trigger = CategoricalParameter(["sell-bb_upper1", "sell-bb_upper2", "sell-bb_upper3", "sell-bb_upper4"], default="sell-bb_upper2", space="sell")

This next large block of code is created for the hyperparameter optimization function.

There are sections that help me determine the buy and sell settings. And in each section the optimal parameter will be found for the ema, rsi, mfi and some triggers.

I will not go in depth on how to interpret these lines. But if you want to know more on hyperparameter optimization, you can always watch my video on that subject.

These optimizations are besides the default roi and stoploss I always optimize. And because I do not really trust the trailing stop loss I will probably not take this into consideration. But more on that later.

def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# RSI
dataframe['rsi'] = ta.RSI(dataframe)
dataframe['mfi'] = ta.MFI(dataframe)
# Bollinger Bands 1,2,3 and 4
bollinger1 = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=1)
dataframe['bb_lowerband1'] = bollinger1['lower']
dataframe['bb_middleband1'] = bollinger1['mid']
dataframe['bb_upperband1'] = bollinger1['upper']
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']
bollinger4 = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=4)
dataframe['bb_lowerband4'] = bollinger4['lower']
dataframe['bb_middleband4'] = bollinger4['mid']
dataframe['bb_upperband4'] = bollinger4['upper']
# Build EMA rows - combine all ranges to a single set to avoid duplicate calculations.
for period in set(
list(self.buy_fastema.range)
+ list(self.buy_slowema.range)
+ list(self.sell_fastema.range)
+ list(self.sell_slowema.range)
):
dataframe[f'EMA_{period}'] = ta.EMA(dataframe, timeperiod=period)
return dataframe

The indicator section contains the following:

RSI and MFI are used.

And the bollinger bands are used here. However the author has done something interesting here. He created a bollinger bands indicator which has multiple bands at multiple distances from the standard moving average. Each band is determined by the standard deviation set. So in this case there are bands that are at a distance of 1, 2, 3 and 4 standard deviations from the middle line.

On a chart the indicator will look something like this

And then finally the two buy ema’s and two sell ema’s will be combined to create an EMA for each timeperiod.

Buy settings

def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
conditions = []
# GUARDS
if self.buy_rsi_enabled.value:
conditions.append(dataframe["rsi"] < self.buy_rsi.value)
if self.buy_mfi_enabled.value:
conditions.append(dataframe["mfi"] < self.buy_mfi.value)
if self.buy_ema_enabled.value:
conditions.append(
dataframe[f"EMA_{self.buy_fastema.value}"]
> dataframe[f"EMA_{self.buy_slowema.value}"]
)
# TRIGGERS
if self.buy_trigger.value == "bb_lower1":
conditions.append(dataframe["close"] < dataframe["bb_lowerband1"])
if self.buy_trigger.value == "bb_lower2":
conditions.append(dataframe["close"] < dataframe["bb_lowerband2"])
if self.buy_trigger.value == "bb_lower3":
conditions.append(dataframe["close"] < dataframe["bb_lowerband3"])
if self.buy_trigger.value == "bb_lower4":
conditions.append(dataframe["close"] < dataframe["bb_lowerband4"])
# Check that volume is not 0
conditions.append(dataframe["volume"] > 0)
if conditions:
dataframe.loc[reduce(lambda x, y: x & y, conditions), "buy"] = 1

Now the buy settings are also interesting to see how the idea of combining buy ema’s and bollinger with multiple bands will be used.

The guards section here determines the primary conditions before the signal or trigger is valid.
Here it says that the rsi and mfi value should both be below a certain value, which was defined as a default value in the hyperopt spaces section.

Also the fast ema should be bigger than the slow ema, which says to me that there is an indication that the price is going upward but that the asset is bought during corrections or dips during the upward movement.

Then in the triggers section the close price should be below all the earlier defined Bollinger lower bands.

It is not an if – elif statement so all these conditions should be met before there is an actual buy trigger.

and finally the volume should be above zero.

So in short, the buy signal is triggered when the asset is oversold but is gaining momentum again to rise upwards again.

sell signals

The sell signals are roughly the opposite of the buy signals.

def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
conditions = []
# GUARDS
if self.sell_rsi_enabled.value:
conditions.append(dataframe['rsi'] > self.sell_rsi.value)
if self.sell_mfi_enabled.value:
conditions.append(dataframe['mfi'] > self.sell_mfi.value)
if self.sell_ema_enabled.value:
conditions.append(dataframe[f'EMA_{self.sell_fastema.value}'] < dataframe[f'EMA_{self.sell_slowema.value}'])
# TRIGGERS
if self.sell_trigger.value == 'sell-bb_upper1':
conditions.append(dataframe["close"] > dataframe['bb_upperband1'])
if self.sell_trigger.value == 'sell-bb_upper2':
conditions.append(dataframe["close"] > dataframe['bb_upperband2'])
if self.sell_trigger.value == 'sell-bb_upper3':
conditions.append(dataframe["close"] > dataframe['bb_upperband3'])
if self.sell_trigger.value == 'sell-bb_upper4':
conditions.append(dataframe["close"] > dataframe['bb_upperband4'])
# Check that volume is not 0
conditions.append(dataframe['volume'] > 0)
if conditions:
dataframe.loc[
reduce(lambda x, y: x & y, conditions),
'sell'] = 1
return dataframe

All these exit signals are the opposite of the buy signals. So I will not go over each and every line again. You get the point I hope.

In total I really like this setup and it reflects my opinion about how a strategy should look like. There have to be some overall larger timespan conditions under which the strategy performs well, which is indicated by the ema’s, rsi and mfi. And the actual buy and sell signals are triggered by the specific conditions that indicate that there are buying opportunities within this larger trend. This is the way how I learned to trade and this strategy reflects my paradigm.

The only thing which gets me worried for this specific strategy is that the author has a lot of conditions that should be met and also uses the trailing stoploss. This might be too complicated to get enough triggers for buying opportunities. But I might be wrong though.

Let’s see how it performs. I am very curious.

Initial backtest results

I am a little bit dissapointed by the results of this strategy. As I said, it’s principles are good but it lacks in the execution.

What it it does correctly is that the 15 minute timeframe looks like the best one indeed. Exactly what the author had in mind. However, negative results and drawdowns of almost 90 percent are a no go in my book.

Looking at the trade statistics, we can see that the exit reasons are the main cause for losses. These exit signals can be either from the triggers or the guards that provide losing exit signals.

It is only 40% of these exit signals that create such big losses and further investigation is needed to know which exit signal should be disposed of to make it a better strategy.

The profit plot of the initial backtest shows us something remarkable. From the highest point of the last bull market, up until the last day of the backtest, this strategy has a drawdown. This is very strange because I thought that the ema crossover should act like a guard and should prevent trading under these negative conditions.

Also the degree of parallelism is remarkable. There are trades all of the time. over the complete backtesting periods there is the maximum amount of trades active. I get the feeling that something is happening which should not happen at all here.

However, as we know by now we can also optimize the buy and sell signals of this strategy so I think we should give it a try to see of it will become more profitable, and shows less strange behavior, with other parameter settings. We’ve seen some spectacular improvements before in earlier videos.

Hyperparameter optimization

Now to say that everything went smoothly when doing hyperparameter optimization is a big understatement. It is already confirmed that my workstation is not good enough to optimize on these smaller timeframes with the time period and epochs I normally want to use for optimizing.

But let’s start first with a bug in the TA-library which causes hyperopt to error out. After some debugging it seemed that the hyperopt parameters for the ema calculation cannot use 1, so I had to change it to a 2 to make it work.

# Hyperopt Buy Parameters
buy_fastema = IntParameter(low=1, high=236, default=211, space='buy', optimize=True, load=True)
buy_slowema = IntParameter(low=1, high=126, default=364, space='buy', optimize=True, load=True)
# Hyperopt Sell Parameters
sell_fastema = IntParameter(low=1, high=365, default=7, space='sell', optimize=True, load=True)
sell_slowema = IntParameter(low=1, high=365, default=6, space='sell', optimize=True, load=True)

The solution was coincidentally found in an answer between the Freqtrade issues on github.

But then I still had the problem of a highly fragmented dataframe in combinations with the capacity issues I have.

So again I had to revert to a smaller time period and a lower epoch amount to get me some optimal results for roi, stoploss and buy and sell parameters.

The last time I did this, it was during the Clucmay strategy tests. And they proved to give some spectacular results. So maybe this time it will happen again.

Eventually I managed to get 50 epochs working over a backtesting period of a month.

Then I decided it was enough to do some backtests. I did a backtest with and without the trailing stoploss to see the differences this made. But in all cases I had some disappointing results. Even after the optimization I did to manage to get any profits from this strategy.

I also tested without the trailing stop loss setting to see if the performance got any better or not.

But unfortunately I only got some minor improvements. It still was losing money.

Looking at the backtest results I got from the optimized strategy we can see that only the ROI manages to make some profits. But they are all undone by all the other exit signals this code has.

This is typically a case of having to many signals and making the strategy too complicated to work under different circumstances. I still think the idea and setup behind this strategy is good. But it should be made less complex.

For example there are 4 Bollinger bands with 4 different deviations calculated but the one that triggers the final buy decision is the one that has 4 standard deviations. All lower ones are automatically true, so there is no need for them I think.

The way this could be improved is to test which standard deviation gives the best signals. Maybe it is not 4 but 3 deviations that gives the best signals. But that idea is for somebody else to find out and maybe he or she can provide us with their findings in the comments section.

For me this is out of my scope at the moment. Maybe another time perhaps.

After optimization the chart finally seems to look ‘normal’ again. Although I am not happy with the way the profit fluctuates. You can clearly see that it sort of fluctuates with the general market. Again I thought that the guards should take care of this but apparently I was wrong here…

Strategy league

So what happens with this strategy in the overall league?

As I said, the principles of this strategy are correct according to my preferred algorithmic trading principles. It has guards that prevent overtrading and triggers that sound logical.

Why it does not perform the way I thought should be investigated further. I should expect that the guards would prevent overtrading. But perhaps the complicated way the buy and sell signals are constructed, is its Achilles heel. There are just way to many reasons a trade could be exited. Simplification is key here to analyze what’s going on exactly.

Another factor that can greatly influence this result is the fact that I could not really complete my process of optimizing the parameters used in this strategy. And maybe I should have put more effort in trying to get the best parameters by optimizing them individually.

But wouldn’t I then be running into the trap of overfitting this strategy too much? I’ll leave the answer up to you.

Nonetheless, I think this is a pretty interesting way to look at the way to use Bollinger bands and hopefully you got some inspiration from the authors idea here for your own trading algorithm.

Files and output

I left all the output I got on my Patreon site  in this post: https://www.patreon.com/posts/80732654

so that my supporters can check and analyze my results by themselves. I also include the strategy with the improved and working hyperopt settings over there.

I think I have done enough research for this algo and will continue testing another one. I will be back with another video and blog post later.

Thank you for your attention and support and I’ll see you in the next one.

Goodbye!

Comments

No comments found for this post.