(G)I-DLE trading strategy (Patreon)
Downloads
Missing 2 files, as this post has not yet been imported.
Content
Intro
In this post will present a trading strategy that made over 200.000 dollars at one moment.
The strategy
As you might already know, besides testing other peoples algo's found on Youtube, the internet, github and more, I also try to create my own mechanical trading strategies.
And sometimes I challenge myself to think out of the box and I try to be as creative as possible.
That's why I gave myself a sidequest to find actual working trading strategies based on the first letters of a Kpop group name and that also have a reasonable probability to make money.
I've been to the (G)I-dle concert with my daughter a couple of weeks ago in Amsterdam and it was great fun. This inspired me to see If I could find a trading strategy that used their letter combination.
However, this time, to find the right trading indicators that fit the GIDLE letter combination was quite a challenge because I wanted to avoid overlapping indicators, that are of the same type. Also these indicators should be available in a TA library or at least I have to be able to code this so that I can use it in the algorithm.
And finally I hope to find a combination that can provide me a base signal, a confirmation signal, a indicator that should prevent overtrading and one exit signal.
So let me show what I came up with:
For this mechanical trading strategy I have selected the following indicators:
G - Gann Hilo
I - Intraday momentum Index
D - Detrended Price Oscillator
L - Least Squares Moving Average
E - Ehler Fisher Transform
Gann Hilo
Gann HiLo Indicator: Gann HiLo is a trend-following indicator that helps traders identify the current trend direction and potential reversal points. It combines moving averages and price volatility to offer insights into market momentum.
Intraday momentum Index
Intraday Momentum Index (IMI): The Intraday Momentum Index is a technical indicator designed for short-term traders, measuring price and volume to gauge the strength of intraday market trends, aiding in identifying potential reversal or continuation points. In my specific case I will also try to see if this also works on the daily timeframe though.
Detrended Price Oscillator
Detrended Price Oscillator (DPO): The DPO is a momentum-based indicator that eliminates long-term trends from price data, helping traders focus on shorter-term price cycles and potential overbought or oversold conditions.
Least Squares Moving Average
Least Squares Moving Average (LSMA): LSMA is a sophisticated moving average indicator that minimizes the sum of squared deviations to provide a smoother representation of price trends, reducing lag and offering more precise insights into market direction.
Ehler Fisher Transform
Ehler Fisher Transform: The Ehler Fisher Transform is a technical indicator designed to transform price data into a Gaussian distribution, making it easier to identify market turning points and potential reversal zones, particularly in oscillatory markets.
On the chart their combnination looks like this:
Trading rules
Based on the indicators chosen, the trading rules for this strategy are as follows:
BUY LONG
To buy long, the signal occurs if:
- And the Least Squares Moving Average (14) is above Gann Hilo (13/13)
- The Detrended price oscillator (14) gets above 0
- And for the Intraday momentum index I have decided that I will add an additional exponential moving average on that indicator and only trade if the IMI is above its EMA. This EMA is not shown in the tradingview indicator by the way.
As a sidenote, I thought of also using a overbought/oversold indication of the IMI, but after testing I decided not to use this.
I will use a simple exit signal, and that exit occurs when:
- Ehlers Fisher Transfor indicator (9) has a bearish crossover (cross down below its trigger line)
SELL SHORT
The short selling conditions are the reverse of the long signals.
So sell signals occur if:
- And the Least Squares Moving Average is below Gann Hilo
- The Detrended price oscillator gets below 0
- And the Intraday Momentum Index should be below its exponential moving average before entering a short trade.
Also here the exit signal depends on Ehlers FTI and this time it exits the trade when there is a bullish crossover, that indicates that the negative momentum has past.
- Ehlers Fisher Transfor indicator has a bullish crossover (cross above below its trigger line)
I will not use the backtest engine's default trade exit tools because I want to let this strategy do it's work as it is intended. Maybe I will use this if I get bad initial backtest results and try to find more edge with better percentage based exit signals.
Building the strategy
So after I have determined the initial strategy, I created the Python code for this algorithm first in a Jupyter notebook.
And since the Intraday Momentum Index and Least squared moving average are not part of any default TA library, I have additionally created these ones too.
Let me walk you through the Jupyter notebook and show you what I've done.
Creating the Pandas dataframe, indicators and signals
First of all I loaded the necessary libraries and created the Pandas dataframe. There I also renamed the columns so that I can use these later with the analysis and functions.
# Let's load the libraries, read the json with trading data, make a dataframe of it and show some data about the dataframe.
import pandas as pd
from pandas import DataFrame
import numpy as np
import pandas_ta as pta
df = pd.read_json("./BTC_USDT-1d.json")
df.columns=['date','open','high','low','close','volume']
df['date']=(pd.to_datetime(df['date'],unit='ms'))
df.info()<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1972 entries, 0 to 1971
Data columns (total 6 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 date 1972 non-null datetime64[ns]
1 open 1972 non-null float64
2 high 1972 non-null float64
3 low 1972 non-null float64
4 close 1972 non-null float64
5 volume 1972 non-null float64
dtypes: datetime64[ns](1), float64(5)
memory usage: 92.6 KBHere I show some generic information about the dataframe. Like the dataframe info, some statistics information about the values in the dataframe and finally the dataframe itself.
df.describe()
df

Adding the indicators
Now that the dataframe exists, let's add all the indicators to the dataframe too.
Here I add the Hilo, DPO and FSI from the Pandas_TA library. And I also created two functions that calculate the IMI and LSMA indicator.
All these indicators will add their respective indicator columns to the original dataframe after I have executed this Notebook cell.
Ps. Help can be asked withdf.ta.indicators() and help(pta.hilo).
# Gann Hilo
df['hilo'] = pta.hilo(high=df['high'], low=df['low'], close=df['close'], high_length=13, low_length=13, mamode=None, offset=None)["HILO_13_13"]
# Ichimoku Chikou span
# df['chikou'] = pta.ichimoku(high=df['high'], low=df['low'], close=df['close'], tenkan=9, kijun=10, senkou=52, include_chikou=True, offset=0)[0]['ICS_10']
# Intraday Momentum Index
def calculate_imi(df, length=14):
# Calculate gains and losses
df['gain'] = 0.0
df['loss'] = 0.0
for i in range(1, len(df)):
if df['close'].iloc[i] > df['open'].iloc[i]:
df.at[df.index[i], 'gain'] = df['close'].iloc[i] - df['open'].iloc[i]
else:
df.at[df.index[i], 'loss'] = df['open'].iloc[i] - df['close'].iloc[i]
# Calculate sums of gains and losses over 'length' periods
df['up_sum'] = df['gain'].rolling(window=length).sum()
df['down_sum'] = df['loss'].rolling(window=length).sum()
# Calculate IMI
df['imi'] = 100 * df['up_sum'] / (df['up_sum'] + df['down_sum'])
# Clean up and drop intermediate columns
df.drop(['gain', 'loss', 'up_sum', 'down_sum'], axis=1, inplace=True)
return df['imi']
df['imi'] = calculate_imi(df, length=14)
df['imi_ema'] = pta.ema(close=df['imi'],length=7) # I will create this, but will not use this in the strategy.
# Detrended Price Oscillator
df['dpo'] = pta.dpo(close=df['close'],length=14, centered=True, lookahead=False)
# Least Squares Moving Average
def calculate_lsma(data, period=21):
lsma_values = []
for i in range(period, len(data)):
# Extract the most recent N data points
subset = data.iloc[i - period:i]
# Perform linear regression to fit a line
x = np.arange(len(subset))
y = subset['close'].values
slope, intercept = np.polyfit(x, y, 1)
# Calculate the LSMA value using the linear equation
lsma = intercept + slope * (period - 1)
lsma_values.append(lsma)
lsma_series = pd.Series(lsma_values, index=data.index[period:])
return lsma_series
df['lsma'] = calculate_lsma(df, 14)
# Ehlers Ficher transform indicator
df['fisherT'] = pta.fisher(high=df['high'], low=df['low'],close=df['close'], length=9)['FISHERT_9_1']
df['fishers'] = pta.fisher(high=df['high'], low=df['low'],close=df['close'], length=9)['FISHERTs_9_1']The cell below will not be handled since I did not use the OBOS indicator in this strategy. But I will leave this here as a reference for you.
# Adding the overbought/oversold signals
# Overbought/Oversold detector
def obos(dataframe, indicator_col, overbought_value=75, oversold_value=30, previous_rows=5):
"""
Determine overbought, oversold, and trigger conditions based on a Stochastic indicator.
Parameters:
- dataframe (pd.DataFrame): The input DataFrame containing the indicator.
- indicator_col (str): The name of the column containing the indicator values.
- overbought_value (float): The overbought threshold.
- oversold_value (float): The oversold threshold.
- previous_rows (int): Number of previous rows to consider for trigger conditions.
Returns:
pd.Series: A Series containing the 'obos_condition' values.
"""
# Create a copy of the DataFrame to avoid modifying the original
dataframe = dataframe.copy()
# Create a new column to save values to first
dataframe['obos_condition'] = ''
for i in range(len(dataframe)):
# Get the current value from the DataFrame
current_value = dataframe.at[i, indicator_col]
# Set the neutral signal so that it will be the 'default' signal after previous_rows setting
neutral = True # Assume neutral until proven otherwise
# Determine the current signal according to the current value
if current_value < oversold_value:
dataframe.at[i, 'obos_condition'] = 'oversold'
neutral = False
elif current_value > overbought_value:
dataframe.at[i, 'obos_condition'] = 'overbought'
neutral = False
else:
# Check cell value of the previous rows
previous_values = dataframe[indicator_col].iloc[max(0, i - previous_rows):i]
# If any of the previous rows values is above or below the trigger value, then
# change the value of the 'obos_condition' cell
if any(previous_values < oversold_value):
dataframe.at[i, 'obos_condition'] = 'oversold_trigger'
neutral = False
elif any(previous_values > overbought_value):
dataframe.at[i, 'obos_condition'] = 'overbought_trigger'
neutral = False
if neutral:
dataframe.at[i, 'obos_condition'] = 'neutral'
# Return the 'obos_condition' Series
return dataframe['obos_condition']
# Use this function with:
# df['obos'] = obos(df, indicator_col='imi', overbought_value=70, oversold_value=25, previous_rows=15)df['obos'] = obos(df, indicator_col='imi', overbought_value=70, oversold_value=30, previous_rows=15)Now I can ask Pandas to show me the columns, and if all the indicators exist, let me show you these added colums in the output cell.
df.columnsIndex(['date', 'open', 'high', 'low', 'close', 'volume', 'hilo', 'imi',
'imi_ema', 'dpo', 'lsma', 'fisherT', 'fishers', 'obos'],
dtype='object')df[['date', 'close', 'volume', 'hilo', 'imi', 'dpo', 'lsma', 'fisherT', 'fishers']].tail(10)
Entry signals
Normally I will let the strategy file of the backtesting engine handle the entry and exit signals for both long and short. But since I want to see them in the dataframe as well, I will need to make a function that helps me to show the entry and exit signals in the dataframe too.
This way I can check on the tradingview chart if these signals are at the correct moments and spot possible problems so that I can correct them.
import numpy as np
def entry_signals(df):
# Create boolean masks for each condition
long_condition = (df['dpo'] > 0) & (df['lsma'] > df['hilo']) & (df['imi'] > df['imi_ema'])
short_condition = (df['dpo'] < 0) & (df['lsma'] < df['hilo']) & (df['imi'] < df['imi_ema'])
# Create 'trade_direction' column based on conditions
df['entry_direction'] = np.where(long_condition, 'Go_long', np.where(short_condition, 'Go_short', 'Stay_neutral'))
# Calculate trade signals based on changes in 'trade_direction' and trend condition
df['entry_changed'] = df['entry_direction'].shift(1) != df['entry_direction']
df['entry_signal'] = np.where(df['entry_changed'] & ((df['entry_direction'] == 'Go_long') | (df['entry_direction'] == 'Go_short')), True, False)
# Clean up by dropping the 'trend_changed' column
df.drop(columns=['entry_changed'], inplace=True)
return df
entry_signals(df)
1972 rows × 16 columns
I can also filter the entry signals of the dataframe column. This way I only have the dates where long and short trades are actually happening. Then I will check them on Tradingview with the correct pair and exchange.
df[['date', 'close', 'volume', 'hilo', 'imi', 'dpo', 'lsma','entry_direction','entry_signal']][(df['entry_signal'] == 1)].tail(20)
And here you see all the "go long" and "go short" signals in the dataframe column. If I use these buy and sell conditions in the Freqtrade strategy file, I can be pretty sure that these are the same signals here too.
Exit signals
Next come the exit signals.
Here I will use the same principle as well. I will create a somewhat similar function to the exit signals and then let the function make an additional column that contains the exit signal. Note that this function does not look at the dataframe to see if there is any actual entry signal or virtual trade made. It just makes exit signals and I'll have to check for myself if these are correct on the Tradingview chart.
The trading bot will actually keep track of the actual trades and acts on these exit signals.
import numpy as np
def exit_signals(df):
# Create boolean masks for exit conditions
exit_long_condition = (df['fisherT'] < df['fishers'])
exit_short_condition = (df['fisherT'] > df['fishers'])
# Create 'exit_signal' column based on exit conditions
df['exit_direction'] = np.where(exit_long_condition, 'Exit_long', np.where(exit_short_condition, 'Exit_short', 'Do_nothing'))
# Calculate exit trade signals based on changes in 'exit_signal'
df['exit_signal'] = df['exit_direction'].shift(1) != df['exit_direction']
return df
exit_signals(df)
1972 rows × 18 columns
And let me filter out these exit signals as well to see where these Fisher crossovers happen and make these trades exit.
df[['date', 'close','fisherT', 'fishers','exit_direction','exit_signal']][(df['exit_signal'] == 1)].tail(20)
Plot of the dataframe with indicators
Now as an additional view on how the dataframe looks like, I also created a plot to show you the indicators on the price.
This way I have an additional check to see if it resembles the Tradingview chart too.
import plotly.graph_objs as go
from plotly.subplots import make_subplots
def create_plot(df, start_date=None, end_date=None):
if start_date is not None and end_date is not None:
df = df[(df['date'] >= start_date) & (df['date'] <= end_date)]
# Create subplots with shared x-axis
fig = make_subplots(rows=3, cols=1, shared_xaxes=True, vertical_spacing=0.1)
# Add candlestick chart to the first subplot
candlestick = go.Candlestick(
x=df['date'],
open=df['open'],
high=df['high'],
low=df['low'],
close=df['close'],
name='OHLCV'
)
fig.add_trace(candlestick, row=1, col=1)
hilo = go.Scatter(x=df['date'], y=df['hilo'], mode='lines', name='HiLo')
fig.add_trace(hilo, row=1, col=1)
lsma = go.Scatter(x=df['date'], y=df['lsma'], mode='lines', name='LSMA')
fig.add_trace(lsma, row=1, col=1)
# Add IMI to the second subplot
imi = go.Scatter(x=df['date'], y=df['imi'], mode='lines', name='IMI')
imi_ema = go.Scatter(x=df['date'], y=df['imi_ema'], mode='lines', name='IMI_EMA')
fig.add_trace(imi, row=2, col=1)
fig.add_trace(imi_ema, row=2, col=1)
# Add 'fisherT' and 'fishers' to the third subplot
fisherT = go.Scatter(x=df['date'], y=df['fisherT'], mode='lines', name='FisherT')
fishers = go.Scatter(x=df['date'], y=df['fishers'], mode='lines', name='Fishers')
fig.add_trace(fisherT, row=3, col=1)
fig.add_trace(fishers, row=3, col=1)
# Set subplot titles and labels
fig.update_layout(
title='Financial Data',
xaxis=dict(title='Date', rangeslider=dict(visible=False), title_standoff=0), # Hide the rangeslider
yaxis=dict(title='OHLCV', domain=[0.6, 1]),
yaxis2=dict(title='IMI', domain=[0.4, 0.6]),
yaxis3=dict(title='FisherT & Fishers', domain=[0, 0.4])
)
# Show the figure
fig.show()
# Example usage:
# create_plot(df, start_date='2023-01-01', end_date='2023-12-31')
create_plot(df, start_date='2022-01-01', end_date='2022-08-10')As you see, this plot looks pretty similar to the Tradingview chart. And it gives me confidence that, if I code this strategy into the actual Python code that can be read by the trading bot, and it will also work.
Let me do this coding offscreen and then I will start doing the backtests to see if I found an actual profitable trading strategy.
You have to know that, the last time I did this, I found the best and easiest trading strategy found to this date.
So anything can happen here...
Initial backtest results
Futures trading
The first backtests I did was with the Futures trading option.
And here I already got some optimistic results.
The algorithm seems to perform well on the 1 day timeframe but slowly loses its edge going to smaller timeframes.
Looking at these results, the indicators and the way they work together, it looks like this can be used more as a swing trading strategy than scalp.
It has a slightly higher winrate but the drawdown is also somewhat higher than the other swing type strategies. furthermore only 38% of the 50 pairs used react well to this strategy.
At it's peak this strategy earned almost a hypothetical 100.000 dollar but after the peak of the bull market the profits slowly drained back to the market again and caused a 60% drawdown.
The remarkable thing here is that this strategy catched a huge pump of GALA which created this peak in the profits.
Furthermore I expected this futures strategy to perform well under bearish trends, but it seems like this is not happening at all. So further analysis is necessary to find out why this is the case.
Spot trading
After the Futures trading results it was time to see if this strategy would perform even better if I would let it only do long trades.
Again , the results look to be very promising on the higher timeframes and it's clear that shorter timeframes suffer from overtrading. With too many signals that prove to be losing trades.
The profits look to be amazing here, but the underlying factors and percentages show a different story.
On average every 4 losing trades only have 2 winning trades and again it's clear that you can endure a long streak of losers eventually.
Also the winrate is not that great and the drawdown clearly shows that these numbers are comparable with the average swing trade strategies performance. Also you'll have to find out which pairs respond the best with this algo since only 40% of the 50 pairs tested react well. Also Sortino and Sharpe are not that enthousiastic about this algo. Als you will see why if I open the profit plot.
The major profits come from the pumps some crypto pairs. In this example this backtest also caught the major pump of GALA around may 2022 and this probably caused the profit to skyrocket to over 200.000 dollars at one moment. Which is a great profit if you started with a 1000 bucks here.
But this wouldn't be me if there was no catch behind this. And to stay realistic here, I also have to say that after this major profit the strategy slowly started to bleed its profits back to the market here. To an endbalance of the backtest period of around 60.000 dollars here.
Still not too bad, but you gave away 70% of your profits back to the market. This is more than the 60% drawdown of the futures trading strategy. And this is why the futures trading version scores eventually scores higher than the spot version. I do not weigh profits as much as I do the prevention of drawdowns and better ratios like Calmar, Sortino and Sharpe.
And this is also what most of the other Youtube channels do not show you. That a strategy can be profitable up to a certain moment, but when it loses it's magic, then you'd better be prepared to walk away and keep your hard earned gains.
At which point this happens is a personal choice, but there are systems that say that after a certain amount of drawdown, the strategy should not be used anymore and more investigations are necessary to find out what happened and why.
Which should be clearly the case in this situation.
Optimizing
So would this algo be performing better if I'd used the trading bots default mechanics of exiting the market after a certain percentage gain or loss?
Well I also did an optimization run on the Stop loss and ROI settings here.
And these are the results of that optimization run:
Futures optimized results
With the optimization, the strategy also got some settings that made it stop out earlier and take profit sooner without having to wait for the Fisher crossover exit signal I programmed.
Adding these additional take profit and stop loss points made the strategy perform much better in terms of winrate, calmar, Sortino and Sharpe ratio. Also more pairs responded better to these addditional exit points.
The main disadvantage here is that, because of these earlier exits, there is not much 'letting your winners run'. But you will get a better feeling when you have more trades that exit with a profit.
The other main problem I still have is that futures trading, so also making profits in bear markets, is stil not working as I had hoped for.
A possible reason for this is that moves to the downside are more violent and deeper and this trading strategy does not act on these violent drops succesfully. It has better results with more steady and slowly inclining bull markets because the signals are better matched with each other.
But that's just a theory that has to be examined further for the people that find this trading idea attractive and want to finetune it more to their likings.
Conclusion and Strategy League
I admit that I did not present the most optimal trading strategy, but I think it is certainly worthwile to use this as a baseline and tweak the algorithm further to detect its major flaws and try to mitigate them.
Since it is a relatively easy strategy to follow, you could also use this with manual swing trading and try to catch some big market moves here. Especially when the markets are bullish again and you probably would see some major crypto pumps happening again. And hopefully catch one or two of these pumps too.
On the other hand, it was another good experiment where I created some more unknown additional indicators. And since I have collected a bunch of them in the last couple of months, it might be a good idea to create a library or repo from them.
Anyway, let's not deviate too much from the real goal, there is only so much time and I have so many plans at the moment.
And the most important one of them is to find the best performing trading algorithm and make some life changing wealth here.
Goodbye!