Home Creators Posts Import Register Favorites Logout
haven't archived this post yet. have a subscription? use the importer!

Downloads

  • Obelisk_Ichimoku_Slow_v1_3.7z

Missing 1 file, as this post has not yet been imported.

Content

Introduction

Hi there and welcome to yet another postwhere I try to find the best trading strategy or algorithm for manual or bot trading.

This time I will test a second strategy from a repository, owned by  Brook Miles. And I hope that this one performs as well as the previous  strategy I tested from this repo.

In my previous video I tested an experimental strategy called the  "Obelisk Ichimoku Zema" from this repo which proved to be the best  performing algorithm I found until that moment. In this video I will  test the Obelisk Ichimoku Slow version 1.3 algorithm and hope that it  will show a similar or even better hypothetical performance.

And I sey hypothetical because I test these algo's in a controlled  backtesting environment with historical data. So future performances are  not guaranteed as you might already realised.

Anyway, let's take a quick and high level look at the code to find  out what the filosophy is behind this algo before I show you the  testresults.

The strategy

The code starts with the definition of an ssl_atr indicator that will be used in the algo later on.

def ssl_atr(dataframe, length = 7):
   df = dataframe.copy()
   df['smaHigh'] = df['high'].rolling(length).mean() + df['atr']
   df['smaLow'] = df['low'].rolling(length).mean() - df['atr']
   df['hlv'] = np.where(df['close'] > df['smaHigh'], 1, np.where(df['close'] < df['smaLow'], -1, np.NAN))
   df['hlv'] = df['hlv'].ffill()
   df['sslDown'] = np.where(df['hlv'] < 0, df['smaHigh'], df['smaLow'])
   df['sslUp'] = np.where(df['hlv'] < 0, df['smaLow'], df['smaHigh'])
   return df['sslDown'], df['sslUp']

The strategy is made with the 1 hour timeframe in mind. Probably because the author had the best testresults at this timeframe.

It uses a startup time of 180 candles to let it create the indicators in a correct way.

Also the minimal ROI has been set to take profit at 1000 percent.  Effectively disabling the ROI, except when there is an actual 100  percent profits to be taken.

Stoploss has been set to -99 percent stop loss. Which also means that  an automatic stop loss has been disabled. Unless the pair actuallu  suffers a loss of 99 percent.

class Obelisk_Ichimoku_Slow_v1_3(IStrategy):

   # Optimal timeframe for the strategy
   timeframe = '1h'

   # generate signals from the 1h timeframe
   informative_timeframe = '1h'

   # WARNING: ichimoku is a long indicator, if you remove or use a
   # shorter startup_candle_count your results will be unstable/invalid
   # for up to a week from the start of your backtest or dry/live run
   # (180 candles = 7.5 days)
   startup_candle_count = 180

   # NOTE: this strat only uses candle information, so processing between
   # new candles is a waste of resources as nothing will change
   process_only_new_candles = True

   minimal_roi = {
       "0": 10,
   }

   # WARNING setting a stoploss for this strategy doesn't make much sense, as it will buy
   # back into the trend at the next available opportunity, unless the trend has ended,
   # in which case it would sell anyway.

   # Stoploss:
   stoploss = -0.99

Then there is a section where informative pairs are defined. In this case all the pairs configured in the config settings.

And the slow timeframe indicator function is defined. In that functon  the Ichimoku is used for determining the signals that will be used for  the buy and sell moments for the trading bot that uses this code.

def informative_pairs(self):
       pairs = self.dp.current_whitelist()
       informative_pairs = [(pair, self.informative_timeframe) for pair in pairs]
       return informative_pairs

   def slow_tf_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:

       displacement = 30
       ichimoku = ftt.ichimoku(dataframe,
           conversion_line_period=20,
           base_line_periods=60,
           laggin_span=120,
           displacement=displacement
           )

       dataframe['chikou_span'] = ichimoku['chikou_span']

       # cross indicators
       dataframe['tenkan_sen'] = ichimoku['tenkan_sen']
       dataframe['kijun_sen'] = ichimoku['kijun_sen']

       # cloud, green a > b, red a < b
       dataframe['senkou_a'] = ichimoku['senkou_span_a']
       dataframe['senkou_b'] = ichimoku['senkou_span_b']
       dataframe['leading_senkou_span_a'] = ichimoku['leading_senkou_span_a']
       dataframe['leading_senkou_span_b'] = ichimoku['leading_senkou_span_b']
       dataframe['cloud_green'] = ichimoku['cloud_green'] * 1
       dataframe['cloud_red'] = ichimoku['cloud_red'] * -1

       dataframe.loc[:, 'cloud_top'] = dataframe.loc[:, ['senkou_a', 'senkou_b']].max(axis=1)
       dataframe.loc[:, 'cloud_bottom'] = dataframe.loc[:, ['senkou_a', 'senkou_b']].min(axis=1)

       # DANGER ZONE START

       # NOTE: Not actually the future, present data that is normally shifted forward for display as the cloud
       dataframe['future_green'] = (dataframe['leading_senkou_span_a'] > dataframe['leading_senkou_span_b']).astype('int') * 2

       # The chikou_span is shifted into the past, so we need to be careful not to read the
       # current value.  But if we shift it forward again by displacement it should be safe to use.
       # We're effectively "looking back" at where it normally appears on the chart.
       dataframe['chikou_high'] = (
               (dataframe['chikou_span'] > dataframe['senkou_a']) &
               (dataframe['chikou_span'] > dataframe['senkou_b'])
           ).shift(displacement).fillna(0).astype('int')

       # DANGER ZONE END

In this section the EMA's are defined that will be used in the  strategy. And it looks like a sort of golden & death cross condition  over here. Where the close price is above the ema50 and ema50 is above  the ema200.

dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
       dataframe['ema200'] = ta.EMA(dataframe, timeperiod=200)
       dataframe['ema_ok'] = (
               (dataframe['close'] > dataframe['ema50'])
               & (dataframe['ema50'] > dataframe['ema200'])
           ).astype('int') * 2

It goes a little too far to explain each and every line here. But the  overall idea of this section is that it detects the moments where there  is a trend found or where it is stopped. This will then be used to  determine the actual buy and sell moments of this algorithm.

dataframe['efi_base'] = ((dataframe['close'] - dataframe['close'].shift()) * dataframe['volume'])
       dataframe['efi'] = ta.EMA(dataframe['efi_base'], 13)
       dataframe['efi_ok'] = (dataframe['efi'] > 0).astype('int')

       dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)
       ssl_down, ssl_up = ssl_atr(dataframe, 10)
       dataframe['ssl_down'] = ssl_down
       dataframe['ssl_up'] = ssl_up
       dataframe['ssl_ok'] = (
               (ssl_up > ssl_down)
           ).astype('int') * 3

       dataframe['ichimoku_ok'] = (
               (dataframe['tenkan_sen'] > dataframe['kijun_sen'])
               & (dataframe['close'] > dataframe['cloud_top'])
               & (dataframe['future_green'] > 0)
               & (dataframe['chikou_high'] > 0)
           ).astype('int') * 4

       dataframe['entry_ok'] = (
               (dataframe['efi_ok'] > 0)
               & (dataframe['open'] < dataframe['ssl_up'])
               & (dataframe['close'] < dataframe['ssl_up'])
           ).astype('int') * 1

       dataframe['trend_pulse'] = (
               (dataframe['ichimoku_ok'] > 0)
               & (dataframe['ssl_ok'] > 0)
               & (dataframe['ema_ok'] > 0)
           ).astype('int') * 2

       dataframe['trend_over'] = (
               (dataframe['ssl_ok'] == 0)
           ).astype('int') * 1

       dataframe.loc[ (dataframe['trend_pulse'] > 0), 'trending'] = 3
       dataframe.loc[ (dataframe['trend_over'] > 0) , 'trending'] = 0
       dataframe['trending'].fillna(method='ffill', inplace=True)

       return dataframe

No fast indicators are defined here at the moment.

def fast_tf_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
       # none atm
       return dataframe

The actual indicators used in the strategy are normally defined in  this section. However because the actual indicators were already defined  earlier, they are defined as the dataframe the backtest engine will use  to determine the buy and sell settings.

def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:

       # Obelisk_Ichimoku_Slow does not use trailing stop or roi and should be safe to backtest at 1h
       # if self.config['runmode'].value in ('backtest', 'hyperopt'):
       #     assert (timeframe_to_minutes(self.timeframe) <= 5), "Backtest this strategy in 5m or 1m timeframe."

       if self.timeframe == self.informative_timeframe:
           dataframe = self.slow_tf_indicators(dataframe, metadata)
       else:
           assert self.dp, "DataProvider is required for multiple timeframes."

           informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=self.informative_timeframe)
           informative = self.slow_tf_indicators(informative.copy(), metadata)

           dataframe = merge_informative_pair(dataframe, informative, self.timeframe, self.informative_timeframe, ffill=True)
           # don't overwrite the base dataframe's OHLCV information
           skip_columns = [(s + "_" + self.informative_timeframe) for s in ['date', 'open', 'high', 'low', 'close', 'volume']]
           dataframe.rename(columns=lambda s: s.replace("_{}".format(self.informative_timeframe), "") if (not s in skip_columns) else s, inplace=True)

       dataframe = self.fast_tf_indicators(dataframe, metadata)

       return dataframe

In these sections, the buy and sell signals are defined. For example:  if the trending column in the dataframe is larger than 0, and if the  entry OK column also contains a value lager than 0 and the timeframe has  been set to 0 then there will be a buy signal.

def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
       dataframe.loc[
           (dataframe['trending'] > 0)
           & (dataframe['entry_ok'] > 0)
           & (dataframe['date'].dt.minute == 0) # when backtesting at 5m/1m only set signal on the hour
       , 'buy'] = 1
       return dataframe

For the sell signal, this only sells if the trend is over and returns to a value of 0.

def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
       dataframe.loc[
           (dataframe['trending'] == 0)
           & (dataframe['date'].dt.minute == 0) # when backtesting at 5m/1m only set signal on the hour
           , 'sell'] = 1
       return dataframe

Again the author of the code has given all the information to create  plot output of the strategies workings, signals and indicators. So I can  use that to give more insights to the inner workings here.

plot_config = {
       # Main plot indicators (Moving averages, ...)
       'main_plot': {
           'senkou_a': {
               'color': 'green',
               'fill_to': 'senkou_b',
               'fill_label': 'Ichimoku Cloud',
               'fill_color': 'rgba(0,0,0,0.2)',
           },
           # plot senkou_b, too. Not only the area to it.
           'senkou_b': {
               'color': 'red',
           },
           'tenkan_sen': { 'color': 'blue' },
           'kijun_sen': { 'color': 'orange' },

           # 'chikou_span': { 'color': 'lightgreen' },

           # 'ssl_up': { 'color': 'green' },
           # 'ssl_down': { 'color': 'red' },

           # 'ema50': { 'color': 'violet' },
           # 'ema200': { 'color': 'magenta' },
       },
       'subplots': {
           "Trend": {
               'trend_pulse': {'color': 'blue'},
               'trending': {'color': 'orange'},
               'trend_over': {'color': 'red'},
           },
           "Signals": {
               'ichimoku_ok': {'color': 'green'},
               'ssl_ok': {'color': 'red'},
               'ema_ok': {'color': 'orange'},
               'entry_ok': {'color': 'blue'},
           },
       }
   }

Let's quickly start by backtesting this code over different  timeframes and find out if this algorithm has the potential to make us  money in the crypto trading market here.

I created one of these plots from a random backtest to show you  vidually how this strategy uses combinations of signals that determine  the buy and sell settings.

Based on the conditions in the slow_tf_indicators function, there are  indicators calculated that, together, form the base for a buy signal.  To give you one example from the trend subplot: If the orange trending  line is 3 and the blue trend pulse equals 2 (or is at lease higher than  0), then this forms a buy signal with the blue entry ok signal from the  signals subplot.

So as you can see, a nifty way of generating signals based on indicator conditions here.

Now lets close this chart and see how this trading strategy performs on my backtest setup.

Backtest results

So after I have backtested the code, I just presented, on my setup, I got the following results back.

I could test this algo on the 1 hour and lower timeframes. And the 30  minute timeframe seems to have the best fit for this strategy,  considering it's results.

All the backtests have profitable outcomes, only the performance  indicators have somewhat dissapointing numbers here. Overall the winrate  is lower than 40%, and a 40 percent win rate is the minimum I  personally expect from a trend following strategy.

This also has its influence on other performance and risk indicators  like CAGR, Calmar, Sortino and Sharpe ratio. They lag a little big  behind of great performing strategies. Also this algo suffers from a  high drawdown, higher lose streaks than win streaks and the pairs I use  for backtesting are responding less well too here.

I know that nowadays I am a little bit spoiled by all the great  performing algo's I found until now. But this also means that I set the  bar higher for this current and future tested algorithms. Which on  itself I think is a good thing here because the quality also gets higher  as time progresses.

In this next plot you can clearly see that the strategy has a  potential to attain great wealth. But the way it holds the gains is not  great at all. The fact that it gives its gains to the market again is  not preferable and there are other algorithms that have better rules to  keep the attained profits in your account balance.

You can see this more clearly if I compare this performance with other top 10 strategies I tested.

The Obelisk Zema, Godstra, Beastbot and SMA Offset algorithms have  much better ways to hold their gains. They also have a more even profit  curve that has a steady incline and almost no clearly visible drawdowns.  At least, in this overview. If I would only consider the end amount of  hypothetical gains, then this strategy would end up as the fifth best.

But since I also consider applicability and risk factors like  Sortino, Sharpe, winrate, drawdown and more, then the endscore ends up  totally different.

Comparing the individual performance indicators it is also clear to  see that the strategy lags somewhat behind the top 10 algos I already  found so far. The most significant plot here is the drawdown, which is  way too high. No bonuspoints there.

Furthermore the CAGR is comparable with other strategies, but this  does not make enough difference and I give more weight to factors like  Sortino and Sharpe to determine if a strategy will be used by me.

This all results in a endscore of 274 points and the 15th spot.  Ending with the same points as the Bollinger real pullback strategy. But  that one ends up one place higher because of the sortino and sharpe  ratios that are better.

Which automatically brings us to the end of yet another postwhere I test a trading strategy algorithm.

If you like these posts, then give a thumbs up. And if you have a suggestion for me to test another algo then leave it as a comment or send a direct message. Then I'll see what I can do with it.

Thanks for reading,

And I'll see you in the next post.

Good luck and goodbye!


Comments

No comments found for this post.