Using Python to detect triangular price patterns in market data

Mon, 21 Nov 2022 · 8min read

In this tutorial, we are going to write a Python program that detects triangular patterns in market data. The high-level idea is as follows:
  1. Load the market data with pandas.
  2. Find all the pivots.
  3. Connect the pivots to find triangular patterns.

Reading the data

First of all, we need market data to do the analysis, you can download Binance market data for free from their website. For this example, we are going to use a 5m CSV file of the $BTCUSDT pair. Once we have this file, we will use pandas to read it.
For this case, we only need the first 5 columns of the CSV file, so we will use usecols=range(5) and the names of these first 5 columns to avoid loading the full CSV.
import pandas as pd

path = "BTCUSDT-5m-2022-11-19.csv"
cols = ["time", "open", "high", "low", "close"]

df = pd.read_csv(path, usecols=range(5), names=cols)
df.head()

Finding the pivots

Now we have to write down all the pivots in the data frame. To do this, we will iterate over each candle and compare its value with 5 candles to the left and 5 to the right. If the candle is the highest of its neighbors, then we have a high pivot, if it is the lowest, then we have a low pivot.
def assign_pivot_id(dataframe, l, n_candles):
    if l-n_candles < 0 or l+n_candles >= len(dataframe):
        return 2
    
    pivot_low = 1
    pivot_high = 1
    for i in range(l-n_candles, l+n_candles+1):
        if dataframe.low[l] > dataframe.low[i]:
            pivot_low = 0
        if dataframe.high[l] < dataframe.high[i]:
            pivot_high = 0

    if pivot_low and pivot_high:
        return 2

    elif pivot_low:
        return 0

    elif pivot_high:
        return 1

    else:
        return 2
    
df['pivot'] = df.apply(lambda x: assign_pivot_id(df, x.name, 5), axis=1)
We now have a new column pivot with 3 different values:
  1. (0) The row is a pivot low
  2. (1) The row is a pivot high
  3. (2) The row is not a pivot

Visualizing the data

Once we have all the pivots in our data frame, we can visualize them. For each pivot, we will add a new value to the row named pivot_value with the value of the pivot for visualization.
import numpy as np

def add_pivot(x):
    if x['pivot'] == 1:
        return x['high']

    elif x['pivot'] == 0:
        return x['low']

df['pivot_value'] = df.apply(lambda row: add_pivot(row), axis=1)
Now we are going to use plotly to plot the data, first we are going to plot the candles, and then we will add the pivots as data points on the same chart.
from plotly import graph_objects

fig = graph_objects.Figure(data=[graph_objects.Candlestick(x=df.index,
                                                open=df['open'],
                                                high=df['high'],
                                                low=df['low'],
                                                close=df['close'])])
fig.layout.template = "plotly_dark"
fig.layout.xaxis.rangeslider.visible = False

# Add the pivots
fig.add_scatter(x=df.index, y=df['pivot_value'], mode="markers", marker=dict(size=5, color="orange"),name="pivot")
fig.show()

Finding triangular patterns

Now we are ready to find all the triangle patterns in the data set. The main idea is to iterate over each candle and take the last 20 candles, with this portion of the data frame, we will find all the pivots (high and low pivots) and add them to some lists. Once we have all the pivots, we want to calculate their linear regression, but we only want to do this, when there are more than 2 pivots, otherwise we jump to the next candle. We will use numpy to calculate the linear regression.
from scipy.stats import linregress

BACK_CANDLES = 25

patterns_ids = []
for n in range(0, len(df)):
    high_pivots = np.array([])
    low_pivots = np.array([])
    low_candles = np.array([])
    high_candles = np.array([])

    for i in range(n-BACK_CANDLES, n+1):
        if df.iloc[i].pivot == 1:
            high_pivots = np.append(high_pivots, df.iloc[i].high)
            high_candles = np.append(high_candles, df.iloc[i].name)

        elif df.iloc[i].pivot == 0:
            low_pivots = np.append(low_pivots, df.iloc[i].low)
            low_candles = np.append(low_candles, df.iloc[i].name)
          
    if high_candles.size == 0 or low_candles.size == 0 or (high_candles.size <= 2 and low_candles.size <= 2):
        # If there are no pivots or no resistances/supports (less than 3 pivots)
        continue


    slope_low, _, correlation_low, _, _ = linregress(low_candles, low_pivots)
    slope_high, _, correlation_high, _, _ = linregress(high_candles, high_pivots)
        
    if abs(correlation_high)>=0.7 and abs(correlation_low)>=0.7 and slope_low > 0 and slope_high < 0:
        patterns_ids.append(n)

patterns_ids
[50, 51, 77, 123, 124, 125, 126]
We only need two of the 5 variables returned by linregress: the slope and the Pearson correlation. The slope is the steepness of the regression line, if there is a triangular pattern, the lower line (support line) will have a positive slope and the upper line (resistances) will have a negative one. We will also take into account the Pearson correlation, which measures the strength of the linear relationship. The returned list contains the candlesticks with triangular patterns.

Visualization of triangular patterns

We will take each of the candles of patterns_ids and calculate its regression line as before, only this time, we will store another value returned by linregress: the intercept, which is the predicted value for y when x is 0. With the intercept we can reconstruct the regression function y = slope_x*x + intercept.
for candle_id in patterns_ids:
high_pivots = np.array([])
low_pivots = np.array([])
low_candles = np.array([])
high_candles = np.array([])

for i in range(candle_id-BACK_CANDLES, candle_id+1):
    if df.iloc[i].pivot == 1:
        high_pivots = np.append(high_pivots, df.iloc[i].high)
        high_candles = np.append(high_candles, df.iloc[i].name)

    elif df.iloc[i].pivot == 0:
        low_pivots = np.append(low_pivots, df.iloc[i].low)
        low_candles = np.append(low_candles, df.iloc[i].name)
      
if high_candles.size == 0 or low_candles.size == 0 or (high_candles.size <= 2 and low_candles.size <= 2):
    # If there are no pivots or no resistances/supports (less than 3 pivots)
    continue


slope_low, intercept_low, correlation_low, _, _ = linregress(low_candles, low_pivots)
slope_high, intercept_high, correlation_high, _, _ = linregress(high_candles, high_pivots)


fig = graph_objects.Figure(data=[graph_objects.Candlestick(x=df.index,
                                                           open=df['open'],
                                                           high=df['high'],
                                                           low=df['low'],
                                                           close=df['close'])])
fig.layout.template = "plotly_dark"
fig.layout.xaxis.rangeslider.visible = False

fig.add_scatter(x=df.index, y=df['pivot_value'], mode="markers",
                marker=dict(size=4, color="orange"), name="pivot")

low_candles = np.append(low_candles, low_candles[-1]+10)
high_candles = np.append(high_candles, high_candles[-1]+10)
fig.add_trace(graph_objects.Scatter(x=low_candles, y=slope_low*low_candles + intercept_low, mode='lines', name='min slope'))
fig.add_trace(graph_objects.Scatter(x=high_candles, y=slope_high*high_candles + intercept_high, mode='lines', name='max slope'))


fig.show()

At Similarcharts we are building an alert system that lets you discover chart patterns at scale before everyone else.

Have a question, feedback or need some guidance? Email us to get in touch!