The ‘CofiBit’ strategy (Patreon)
Content
The freqtrade developers have done a tremendous job to make a trading bot and provided us with a lot of algo’s to use and to learn. In this post I am testing the CofiBit strategy to find out what it does and how it performs.
If you read my earlier posts, then you know that you can find these strategies in the github repository of the Freqtrade developers. If you did not know this, then you can follow this link to get to their repo where they keep all of their strategies. I will investigate all of these to find out if there are any good strategies we can use with our own bot. If not, it will still be a good learning opportunity so that we can use this for developing our own algo.
The CofiBit strategy is also part of the mentioned repository and collected by the berlinguyinca. But if you open the code, this file is taken from slack by a user called CofiBit. The original author is not mentioned, but as always all credits go to the author and collectors of this code. I’m just the guy that tests if it performs well.
The strategy
Let’s dive in to the code of the strategy to find out how it works. I will then do some backtests, optimizing and final conclusion to see how well the strategy performs and if it is worth our time and money to actually use it.
If you are not familiar with the way the strategy files are set up for the freqtrade bot I am using, then watch this and my earlier video’s to learn how to interpret the code.
# --- Do not remove these libs ---
import freqtrade.vendor.qtpylib.indicators as qtpylib
import talib.abstract as ta
from freqtrade.strategy import IStrategy
from freqtrade.strategy import IntParameter
from pandas import DataFrame
First of all there are import statements here that load additional modules that are used in the strategy code further down.
class CofiBitStrategy(IStrategy):
"""
taken from slack by user CofiBit
"""
INTERFACE_VERSION: int = 3
# Buy hyperspace params:
buy_params = {
"buy_fastx": 25,
"buy_adx": 25,
}
# Sell hyperspace params:
sell_params = {
"sell_fastx": 75,
}
Then the strategy class is defined and we will find some optimization parameters here. These are used in hyperparameter optimization. The actual spaces where the optimal setting should be found is a little bit more down the code.
buy_fastx = IntParameter(20, 30, default=25)
buy_adx = IntParameter(20, 30, default=25)
sell_fastx = IntParameter(70, 80, default=75)
Then the obligatory settings for each strategy. Each strategy has it’s ROI settings where profit is taken when a specific percentage is reached after a set amount of time.
# Minimal ROI designed for the strategy.
# This attribute will be overridden if the config file contains "minimal_roi"
minimal_roi = {
"40": 0.05,
"30": 0.06,
"20": 0.07,
"0": 0.10
}
Also the stoploss is set.
# Optimal stoploss designed for the strategy
# This attribute will be overridden if the config file contains "stoploss"
stoploss = -0.25
And the timeframe where the strategy is build for is determined.
# Optimal timeframe for the strategy
timeframe = '5m'
But my experience until now is that a strategy sometimes performs better on other timeframes. So let’s find out later if this is also the case here.
Then we come to the block of code where the indicators are defined that will be used.
In this case the strategy has the stochastics oscillator, ema and adx indicators configured.
The special thing here is that the author has created an ema band that consists of a higher, middle and lower band. Where the higher band is made of all the candle highs and the lower band is made from the candle lows. The middle band is made of the close price if of the candle. The lenght of the EMA indicators is 5.
Also the stochastics has different settings in comparison to the default 14, 1, 3 settings here.
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
stoch_fast = ta.STOCHF(dataframe, 5, 3, 0, 3, 0)
dataframe['fastd'] = stoch_fast['fastd']
dataframe['fastk'] = stoch_fast['fastk']
dataframe['ema_high'] = ta.EMA(dataframe, timeperiod=5, price='high')
dataframe['ema_close'] = ta.EMA(dataframe, timeperiod=5, price='close')
dataframe['ema_low'] = ta.EMA(dataframe, timeperiod=5, price='low')
dataframe['adx'] = ta.ADX(dataframe)
return dataframe
The settings above looks on the chart like this:
Buy signals
Let’s see how the buy signals are formed:
To get a buy signal the following conditions should be met:
- The open price should be below the EMA low band
- The stochastics fastk should have a cross above the fastd line
- Both the fastk and fastd lines of the stochastics should be lower then the buy_fastx line
- and finally the adx should be higher then the buy_adx line
So if i understand this correctly there should be upward momentum indicated by the adx but a pullback of the price is detected because the close is below the lower ema. Finally there should be an indication that the short term momentum changed again during the pullback because the fastk crosses above the fastd again below a certain treshold.
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
dataframe.loc[
(
(dataframe['open'] < dataframe['ema_low']) &
(qtpylib.crossed_above(dataframe['fastk'], dataframe['fastd'])) &
(dataframe['fastk'] < self.buy_fastx.value) &
(dataframe['fastd'] < self.buy_fastx.value) &
(dataframe['adx'] > self.buy_adx.value)
),
'enter_long'] = 1
return dataframe
Sell signals
The sell signal appears when the open price of the candle is higher than the upper ema band
or the fastk line gets above the sell_fastx value
or if the fastd line gets above the sell_fastx value
So if one of these three conditions are met, then there is a sell signal.
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
dataframe.loc[
(
(dataframe['open'] >= dataframe['ema_high'])
) |
(
(qtpylib.crossed_above(dataframe['fastk'], self.sell_fastx.value)) |
(qtpylib.crossed_above(dataframe['fastd'], self.sell_fastx.value))
),
'exit_long'] = 1
return dataframe
I think this is an interesting theory. Buying pullbacks if the momentum is high.
However I have some reservations because adx momentum can be both upward and downward. And I miss the DI+ and DI- lines here to determine the momentum direction. You see, If I add these lines to the example, then you see that there is a negative momentum here:
Luckily this time it seems that there was a sell with a profit. But when you catch a huge bearish trend, this might as well hit your stop loss many times before any recovery.
But this is all theory. Let’s find out by actual backtesting to see how this strategy performs.
Initial backtest result
The initial backtest shows that the 4 hour timeframe performs best over all other timeframes. With a winrate of 68.5 percent this looks optimistic, but the profit made dissapoints me to say the least.
Almost 2100 trades with only 0.2 percent profit is indeed what’s keeping this strategy back from making huge profits. As seen the ROI is the main source of profits and all other signals are only making losses.
It’s probable that these fast profit takings, that were intentionally meant for low timeframes have a negatige impact on the higher timeframes, and as you can see, the 30 minute timeframe performs a little bit better, but suffers from higher fluctuations between profits and drawdown.
It might be good to see if I can optimize these parameters and maybe widen the ROI to that it takes profit at higher percentages. Also the buy and sell lines could be improved as this strategy is build to also give us the opportunity to optimize these as well.
Let’s find out…
Hyperparameter optimizaton
It is rare to see that optimizing parameters actually makes the strategy better considering its ratios but at the same time makes it performing worse if you look at the profits, CAGR and winrate.
Sure, the Sortino ratio skyrockets and the drawdown gets much better. Also more pairs respond positive to these changes. But overall you put your money on the line to make a meager 25 profit over 5 years of hypothetical trading. According to my scoring and risk appetite, this is the better setup. But at a price.
Considering the fact that there are other strategies that perform way better with sort of the same numbers, using this strategy has high opportunity costs.
Plot comparison
Looking at the profit charts of both the before and after optimizing situation We see that the characteristics of the curves changed completely.
The first thing that strikes me is that on the initial setup the drawdown occurs at the second peak of the last bull market. also the pairs look to me that they are evenly disperced between winning and losing pairs. The parralellism is higher in this situation so the amount of trades happing is more, which means that there are more buy signals here as well. The underwater plots clearly show that this strategy also reacts to bearish circumstances. Every time the market drops, the underwater plots also show negative results.
However the optimized plot has a total different situation.
The main drawdown period occured between 2018 and 2020. Between two market cycles.
Then it shows a constant upward movement in profits. Not much but still nice to see.
The amount of pairs that have positive and negative results are also even with the exception of XEC/USDT. This is suspisous and might as well could make this result look better than in reality. Or it’s just luck that the bot caught a pump and dump here.
Anyway, seeing the amount of trades open at the same time, this looks a little bit dull and it looks like there is not much opportunity to trade. A low amount of signals could indicate that these do not appear a lot.
Finally, if you started a bot with this particular strategy at the end of 2018, I think you already left the complete market in disbelief. Just before the new bull cycle in 2020. Because you only would see your trading account dry up of all these losses.
The other drawdowns are also almost at the same time there were bearish periods in the latest cycle.
Strategy league
So where does this leave us. How does this strategy performs in comparison to the earlier tested ones…
To be honest. I am not super stoked about the performance of this strategy. If you score all the ratios and performances, it still looks to provide you with a moderate performance. However, at this moment I expect more of an algo strategy for crypto.
However I do like the Idea of the ema bands and stochastics because it reflects a little bit the same thought of the Kelter strategy.
However the usage of the ADX band only makes it difficult to catch only positive momentum because there is no usage of the DI+ and DI- line.
Another thing about the ADX is that it is kind of sluggish. It means that by the time the ADX rises above its theshold into the momentum zone, the signal has already passed and therefore not valid anymore.
One final thing is that I think that the usage of the qtpylib.crossover module is too specific. I think that the actual moment where this crossover occurs in combination with all the other signals is very rare. It is either already passed or will soon occur. Especially the signal where the open price is below the ema low band, is too rare. But when it does, the other signals are already passed.
Maybe if this could be changed to be 2 or three candles before or after the other signals occur, it would give much more signals and therefore more chances to trade under these circumstances. At the moment the signal is too specific and therefore too rare.
Maybe if one of my readers could test this and provide us with the results, it would be much helpfull.
In the mean time I’ll test and analyze another algo strategy for this bot and I will see you in the next post.
Thank you for reading!
Files
See this post: https://www.patreon.com/posts/80918286