Triangle is one of the most common chart patterns. There are three types of this pattern: ascending (maximums of prices are at the same level, minimums increase), descending (opposite to ascending), and symmetrical triangles (formed as a consolidation pattern, when a wide range of prices is gradually reduced from both sides under pressure from buyers and sellers).
We highly recommend you to search triangle method in the internet before reading this template. The main idea of triangle chart pattern one can find here. A dozen movies can describe the basic concept of this method. On the other hand, no one will tell you exactly how he finds triangles in real trading and when exactly it is worth to hold a long or short position. You need to select all parameters by yourself, based on historical data. We will do it now.
# standart libraries
import xarray as xr
import numpy as np
import pandas as pd
import qnt.data as qndata # data loading and manipulation
import qnt.stats as qnstats # key statistics
import qnt.graph as qngraph # graphical tools
import qnt.forward_looking as qnfl # forward looking checking
import qnt.xr_talib as xrtl # technical analysis indicators
import qnt.ta as qnta # for weighted moving average
from IPython.display import display # display function for fancy displaying
import plotly.graph_objs as go # lib for charts
import datetime as dt # work with date and time
import re # to find a substring in a string
from scipy.signal import argrelextrema # to find local extrema
# ignore warnings
import warnings
warnings.filterwarnings('ignore')
# load crypto data
data = qndata.load_cryptocurrency_data(min_date = "2013-05-01",
max_date = None,
dims = ("time", "field", "asset"),
forward_order = True)
# we will consider triangle on a bitcoin example
crypt_name = 'BTC'
btc_close = data.loc[:,"close",:].to_pandas()[crypt_name]
btc_high = data.loc[:,"high",:].to_pandas()[crypt_name]
btc_low = data.loc[:,"low",:].to_pandas()[crypt_name]
vol = data.loc[:,"vol",:].to_pandas()[crypt_name]
# we can see historical data on a chart
trend_fig = [
go.Scatter(
x = btc_close.index,
y = btc_close.values,
name="BTC Close",
line = dict(width=1,color='black')),
go.Scatter(
x = btc_high.index,
y = btc_high.values,
name="BTC High",
line = dict(width=1,color='green')),
go.Scatter(
x = btc_low.index,
y = btc_low.values,
name="BTC Low",
line = dict(width=1,color='orange'))]
# draw chart
fig = go.Figure(data = trend_fig)
fig.update_yaxes(fixedrange=False) # unlock vertical scrolling
fig.show()
We will try to find a triangles by the following algorithm:
The next three cells are simply functions that perform items 1-3
def min_max_points(low, high, window_range = 10):
"""
:param low: low values of the instrument
:param high: high values of the instrument
:param window_range: look back days
:return: dataframe with selected extrema
notion: there is always 1 look forward day
"""
# make a list of local maximums and local minimums
local_max = argrelextrema(high.values, np.greater)[0]
local_min = argrelextrema(low.values, np.less)[0]
# we look back in a "window_range" days and select "global extremum" for these days
# maximum
price_local_max_dt = []
for i in local_max:
if (i>window_range) and (i<len(high)-window_range):
loop_ind = local_max[(local_max>=i-window_range) & (local_max<=i)]
price_local_max_dt.append(high.iloc[loop_ind].idxmax())
maxima = pd.DataFrame(high.loc[price_local_max_dt])
# mark them as maximum with "1"
maxima_lbls = pd.DataFrame(np.ones(shape = (len(price_local_max_dt),)),index = maxima.index, columns = ['extremum'])
maxima = pd.concat([maxima, maxima_lbls],axis = 1).sort_index()
# minimum
price_local_min_dt = []
for i in local_min:
if (i>window_range) and (i<len(low)-window_range):
loop_ind = local_min[(local_min>=i-window_range) & (local_min<=i)]
price_local_min_dt.append(low.iloc[loop_ind].idxmin())
minima = pd.DataFrame(low.loc[price_local_min_dt] )
# mark them as minimum with "0"
minima_lbls = pd.DataFrame(np.zeros(shape = (len(price_local_min_dt),)),index = minima.index, columns = ['extremum'])
minima = pd.concat([minima, minima_lbls],axis = 1).sort_index()
# combine, sort, and remove duplicated
max_min = pd.concat([maxima, minima]).sort_index()
max_min.index.name = 'date'
max_min = max_min.reset_index()
max_min = max_min[~max_min.date.duplicated()]
return max_min
def find_triangle(start_hour, extrema, local_pattern):
"""
:param start_hour: reference (start) date of trading
:param extrema: dataframe with all extrema and date
:param local_pattern: desired sequence of extrema
:return triangle: list of selected triangles with detection date, index and trade signal
:return V1,V2: list of linear equation coefficients for line 1 and 2 for all triangles
:return V1_val,V2_val: point coordinates for each line for all triangles
"""
triangle = []
V1,V1_val = [[],[]]
V2,V2_val = [[],[]]
# find a substring in a string (local pattern in a list of extrema)
move_string = ''.join([str(int(i)) for i in extrema['extremum'].values])
indexis = [(m.start(1), m.end(1)) for m in re.finditer(r'(?=(' + local_pattern + '))',move_string)]
point_index = [np.linspace(i[0],i[1]-1,len(local_pattern),dtype='int32') for i in indexis]
# for detected triangles in history:
for i in range(len(point_index)):
# Define the line equetion
# first line index
fli = [n.start() for n in re.finditer("1",local_pattern)]
# second line index
sli = [n.start() for n in re.finditer("0",local_pattern)]
# for the first line
local_y1 = extrema.iloc[point_index[i]][crypt_name].iloc[fli]
local_x1 = [(i - start_hour).seconds//3600 + (i - start_hour).days*24 for i in extrema.iloc[point_index[i]].date.iloc[fli].values]
A = np.vstack([np.ones(shape = (len(fli),)),local_x1]).T
v_1 = np.linalg.inv(A.T@A)@A.T@local_y1
V1.append(v_1)
V1_val.append(np.vstack([local_x1,local_y1.values]))
# for the second line
local_y2 = extrema.iloc[point_index[i]][crypt_name].iloc[sli]
local_x2 = [(i - start_hour).seconds//3600 + (i - start_hour).days*24 for i in extrema.iloc[point_index[i]].date.iloc[sli].values]
A = np.vstack([np.ones(shape = (len(sli),)),local_x2]).T
v_2 = np.linalg.inv(A.T@A)@A.T@local_y2
V2.append(v_2)
V2_val.append(np.vstack([local_x2,local_y2.values]))
# Hour of triangle detection
detection_hour = extrema.iloc[point_index[i]].date.iloc[-1]
# Check triangle parameters and recieve trade decision
triangle_flag, trade_signal = triangle_check(v_1,v_2,local_x1,local_x2,local_y1,local_y2)
# If triangle parameters are appropriate - save it
if triangle_flag == True:
triangle.append([trade_signal,detection_hour,i])
return triangle, V1, V1_val, V2, V2_val
def triangle_check(line1,line2,points_x1,points_x2,points_y1,points_y2):
"""
:param line1, line2: linear equation coefficients for line 1 and 2
:param points_x1, points_x2: extrema coordinates on X-axis
:param points_y1, points_y2: extrema coordinates on Y-axis
:return triangle_flag: True if the triangle fits all conditions
:return trade_signal: +1 if it is a signal to buy; -1 for sell
"""
#
vals = np.hstack([points_y1.values,points_y2.values])
cors = np.hstack([points_x1,points_x2])
# Triangle must converge and converge fast
x0 = (line2[0] - line1[0])/(line1[1] - line2[1])
y0 = line1[1]*x0 + line1[0]
# 4 is an imperical parameter. You can choose another one
if (x0 - max(cors))>0 and (x0 - max(cors))<(max(cors) - min(cors))*4:
converge = True
else:
converge = False
# triangle should be big enough
triangle_height = abs(points_y1.iloc[0] - line2[0] - line2[1]*min(points_x2))
if triangle_height/points_y1.iloc[0] < 0.001: # it is an imperical parameter. You can choose another one
hflag = False
else:
hflag = True
# one line should be more or less horisontal
zero_flag = True
if abs(y0/max(vals) - 1) < 0.002: # You can choose another one.
# If the top line is horizontal, it is a signal to buy
trade_signal = 1
elif abs(y0/min(vals) - 1) < 0.002: # You can choose another one.
# If the bottom line is horisontal, it is a signal to sell
trade_signal = -1
else:
trade_signal = 0
zero_flag = False
hflag = True
return converge*hflag*zero_flag, trade_signal
We can come up with a list of parameters that the triangle must satisfy (see triangle_check function):
# first we can choose "window range" for selecting extrema (in this case in hours)
wr = 10
# dataframe with extrema list
min_max = min_max_points(btc_low, btc_high, wr)
# For instance, we will search triangle based on the following pattern:
pattern = '1010' # [maximum, minimum, maximum, minimum]
# We need to set a reference day for all triangles to make their equations consistent
start_date = btc_close.index[0]
# triangles that we found
tr_param, v1, val1, v2, val2 = find_triangle(start_date, min_max, pattern)
# Info
print(f"All 'triangles' found in a history: {len(v1)}")
print(f"Triangles that satisfy all parameters: {len(tr_param)}")
In the cell below you can analize selected traingles in order to tune parameters in the triangle_check function.
# triangle number we look at (among all 'triangles'):
tr_look = 0
# prepare desired timeframe
local_date = start_date + dt.timedelta(hours = val1[tr_look][0,0])
time_temp = btc_close[local_date - dt.timedelta(hours = 100) :local_date + dt.timedelta(hours = 100)].index
# just print an example
# min max points
Min_max =[
go.Scatter(
x = min_max['date'],
y = min_max[crypt_name],
name="Selected extremum",
mode="markers",
hovertext='stop',
marker_size=9,
line = dict(width=1,color='green')
)]
# triangle extrema
local_triangle = [
go.Scatter(
x = [start_date + dt.timedelta(hours = i) for i in np.hstack([val1[tr_look][0],val2[tr_look][0]])],
y = np.hstack([val1[tr_look][1],val2[tr_look][1]]),
name="Triangle example",
mode="markers",
hovertext='stop',
marker_size=9,
line = dict(width=1,color='blue')
), # two lines - triangle edges
go.Scatter(
x = time_temp,
y = v1[tr_look][0] + v1[tr_look][1]*((time_temp-start_date).days*24 + (time_temp-start_date).seconds//3600),
name="First trading line",
line = dict(width=1,color='green')
),
go.Scatter(
x = time_temp,
y = v2[tr_look][0] + v2[tr_look][1]*((time_temp-start_date).days*24 + (time_temp-start_date).seconds//3600),
name="Second trading line",
line = dict(width=1,color='orange')
)]
# We print whether it pass all filters
if sum(np.array(tr_param)[:,2] == tr_look):
print("This triangle pass all filters")
else:
print("This is not a triangle")
# draw chart
fig = go.Figure(data = trend_fig + Min_max + local_triangle)
fig.update_yaxes(fixedrange=False) # unlock vertical scrolling
fig.show()
Here we make the assumption that it is reasonable to trade long if the last extremum in the pattern is a minimum and vice versa.
You can create and implement any other approach as you wish.
In a more details. For a long position we will wait when price hit the high limit twice and each time price rebound smaller than the previous one. We can set a long position after the second price minimum in this triangle (1 hour after to avoid forward looking):