0% found this document useful (0 votes)
3 views23 pages

Simple Price Chart Patterns With Python

This chapter focuses on creating scripts to identify, store, and parameterize price chart patterns for back-testing and machine learning model training. It discusses the process of defining patterns using start and end indices, calculating moving averages, and identifying crossovers, along with the importance of normalizing features for consistent analysis. Additionally, it covers the computation of potential profit and other relevant features derived from the patterns, emphasizing the need for statistical tools to analyze the results effectively.

Uploaded by

luca.trow
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
3 views23 pages

Simple Price Chart Patterns With Python

This chapter focuses on creating scripts to identify, store, and parameterize price chart patterns for back-testing and machine learning model training. It discusses the process of defining patterns using start and end indices, calculating moving averages, and identifying crossovers, along with the importance of normalizing features for consistent analysis. Additionally, it covers the computation of potential profit and other relevant features derived from the patterns, emphasizing the need for statistical tools to analyze the results effectively.

Uploaded by

luca.trow
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Patterns

The objective in this chapter is to learn how to write scripts capable of recognizing, storing and parameterizing price
chart patterns. This is an exceptionally useful tool for back-testing and running statistical inference on certain
strategies. Even more importantly these patterns will be the basis for training machine learning models.

To define a pattern (sample in ML) we must find start and end points, as indices (numbers of specific rows) of our
data frame, this is will arguably the toughest part. Once we have a start and end point for our sample we can add
and or compute values (features in ML) that characterize it, these values will be grouped together in a list, tuple or
row of an array or data frame, optionally with the start and end indices as well.

We'll then store all of our patterns in data frames or arrays. Notice than in order to do that each sample needs to
have the same length (number of features).

Simple Strategy
Consider a simple strategy, for example buying when two moving averages cross, we have no idea if this will be
profitable or not, but we can find out.

Start & End Indices


We could use the crossover of the faster average over the slower one as the start of our pattern and the crossunder
as the end.

Lets start by defining functions to get data, compute moving averages and find crossovers:

In [156… import requests


import pandas as pd
import numpy as np

kurl = '[Link]

def kline(symbol: str, tf: str, n: int, start=None):


limit = 1000
collected = []
total_collected = 0
fetch_count = min(limit, n - total_collected)

while total_collected < n:


params = {
'symbol': symbol,
'interval': tf,
'limit': fetch_count,
'startTime': start
}

try:
req = [Link](kurl, params=params, timeout=10)
req.raise_for_status()
except [Link] as e:
print(f"[Error] {symbol} {tf} — {e}")
break

try:
resp = [Link]()
except ValueError:
print(f"[JSON Error] {symbol} {tf} — Invalid response")
break

if not resp:
break

collected = resp + collected


total_collected += len(resp)
fetch_count = min(limit, n - total_collected)

start = resp[0][0] - (resp[1][0] - resp[0][0])*fetch_count

if len(resp) < fetch_count:


break

data = [Link](collected)[:, 0:6].astype(float)


df = [Link](data, columns= ['time', 'open', 'high', 'low', 'close', 'volume'])
df.set_index('time', inplace=True)
return df

def ema(series: [Link], period: int) -> [Link]:


if not isinstance(series, [Link]):
raise TypeError("Input must be a pandas Series.")
return [Link](span=period, adjust=False).mean()

def crossover(srs1, srs2):


cross = (srs1[:-3] < srs2[:-3]) & \
(srs1[1:-2] < srs2[1:-2]) & \
(srs2[2:-1] < srs1[2:-1]) & \
(srs2[3:] < srs1[3:])

idx = [Link](cross)[0] + 3
return idx

For a generic symbol and timeframe our data will look like:

In [157… df = kline('SOLUSDT', '1h', 5000)


df['fast'] = ema(df['close'], 50)
df['slow'] = ema(df['close'], 200)

idxStart = crossover(df['fast'].to_numpy(), df['slow'].to_numpy())


idxEnd = crossover(df['slow'].to_numpy(), df['fast'].to_numpy())

df, idxStart, idxEnd

Out[157… ( open high low close volume fast slow


time
1.741244e+12 148.86 149.57 148.35 148.62 111334.257 148.620000 148.620000
1.741248e+12 148.63 151.91 148.34 150.81 195629.077 148.705882 148.641791
1.741252e+12 150.81 150.86 148.80 148.97 167310.350 148.716240 148.645057
1.741255e+12 148.97 152.90 148.64 152.40 228645.590 148.860701 148.682419
1.741259e+12 152.39 152.76 151.20 151.71 144005.096 148.972438 148.712545
... ... ... ... ... ... ... ...
1.759226e+12 205.89 207.01 205.53 206.58 109493.655 208.201318 210.880491
1.759230e+12 206.58 207.42 205.92 207.08 119192.925 208.157344 210.842675
1.759234e+12 207.07 207.48 206.00 206.19 65395.447 208.080194 210.796380
1.759237e+12 206.18 209.64 205.69 207.71 198520.048 208.065676 210.765669
1.759241e+12 207.71 208.13 206.08 206.98 40074.453 208.023101 210.728001

[5000 rows x 7 columns],


array([ 230, 335, 410, 875, 1510, 1759, 1834, 2302, 2461, 2667, 2739, 2940, 3716, 4068, 4324, 446
0], dtype=int64),
array([ 11, 254, 352, 539, 1418, 1741, 1774, 2007, 2373, 2477, 2705, 2914, 3490, 3969, 4311, 4427,
4800], dtype=int64))

Now we can pair every start with the closest end by running:

In [158… idx = [Link](idxEnd, idxStart, side='right')


valid = idx < len(idxEnd)
paired = [Link]([Link], [Link])
paired[valid] = idxEnd[idx[valid]]

pairs = np.column_stack((idxStart, paired))


pairs = pairs[~[Link](pairs).any(axis=1)].astype(int)

pairs[-5:]
Out[158… array([[2940, 3490],
[3716, 3969],
[4068, 4311],
[4324, 4427],
[4460, 4800]])

We can also check to see if we found what we were looking for by plotting the data:

In [159… import mplfinance as mpf

[Link] = pd.to_numeric([Link], errors = 'coerce')


[Link] = pd.to_datetime([Link])

s = 3700
e = 4000

plots = [
mpf.make_addplot(df['fast'].iloc[s:e], type= 'line', color= 'grey', width= 3, label= 'fast'),
mpf.make_addplot(df['slow'].iloc[s:e], type= 'line', color= 'black', width= 3, label= 'slow')
]

[Link]([Link][s:e, 0:4], type= 'candle', style= 'sas', addplot= plots)

In [160… s = 4300
e = 4450

plots = [
mpf.make_addplot(df['fast'].iloc[s:e], type= 'line', color= 'grey', width= 3, label= 'fast'),
mpf.make_addplot(df['slow'].iloc[s:e], type= 'line', color= 'black', width= 3, label= 'slow')
]

[Link]([Link][s:e, 0:4], type= 'candle', style= 'sas', addplot= plots)


Before proceeding let's not a couple of things:

we won't always be able to complete pairs of start and end indices, that's why we have to apply a mask to
pairs and exclude nan that arise from [Link] (using full with [Link] turns the array type to float hence
the astype(int));
when using any indicator that computes values on a certain length of kline it would be best to only consider
indices after the greatest length used.

Features
We now have an array containing our patterns, but we only have two features and to be honest start and end aren't
of much use except for calculating how long the pattern lasts or viewing and checking if the results match our
objective.

There are plenty of additional features we could be considering, arguably the most important is profit we would have
by simply buying when the crossover occurs and selling at the crossunder:

In [161… closes = df['close'].to_numpy()


pnl = (closes[pairs[:, 1]] / closes[pairs[:, 0]] - 1) * 100

patts = np.column_stack([pairs, pnl])


patts

Out[161… array([[ 230. , 254. , -6.7787],


[ 335. , 352. , -5.3412],
[ 410. , 539. , -1.3248],
[ 875. , 1418. , 20.8372],
[1510. , 1741. , 11.5477],
[1759. , 1774. , -6.6873],
[1834. , 2007. , 2.2557],
[2302. , 2373. , -8.7586],
[2461. , 2477. , -4.8506],
[2667. , 2705. , -5.511 ],
[2739. , 2914. , -0.0476],
[2940. , 3490. , 19.9643],
[3716. , 3969. , 4.207 ],
[4068. , 4311. , -1.6133],
[4324. , 4427. , -0.9605],
[4460. , 4800. , 6.8529]])

Notice how we now defined a new array, this is because when stacking a column made of float values it will turn all
other values into that type, rendering our first two columns useless as indices for similar computations.
At this point we could try and see how many times we would have made profit and what the average profit for trade
would be:

In [162… len(patts[patts[:, 2] > 0]) / len(patts), [Link](patts[:, 2])

Out[162… (0.375, 1.4869395909221026)

we could repeat this but considering fees, lets say at 0.15%:

In [163… len(patts[patts[:, 2] > 0.15]) / len(patts), [Link](patts[:, 2] - 0.15)

Out[163… (0.375, 1.3369395909221027)

Other important features could be maximum and minimum points reached, but let's consider something important
before computing them:

as a rule of thumb we should always try to normalize our features so that we can apply the same patterns on
different symbols, for example, and expect values in the same scale and order of magnitude, this aspect will
become increasingly important as we go on.

So that being said we'll compute the maximum and minimum points reached in percentage from our starting point:

In [164… max_vals = [Link](len(pairs))


min_vals = [Link](len(pairs))

for i, (start, end) in enumerate(pairs):


base = closes[start]
window = closes[start:end+1]
max_vals[i] = ([Link]() / base - 1) * 100
min_vals[i] = ([Link]() / base - 1) * 100

patts = np.column_stack([pairs, min_vals, max_vals, pnl])


patts

Out[164… array([[ 230. , 254. , -6.7787, 0.1842, -6.7787],


[ 335. , 352. , -5.7567, 0. , -5.3412],
[ 410. , 539. , -1.4695, 11.436 , -1.3248],
[ 875. , 1418. , -0.2418, 29.2254, 20.8372],
[1510. , 1741. , 0. , 22.3695, 11.5477],
[1759. , 1774. , -6.6873, 1.0897, -6.6873],
[1834. , 2007. , 0. , 11.7932, 2.2557],
[2302. , 2373. , -9.7768, 5.7737, -8.7586],
[2461. , 2477. , -4.8506, 0. , -4.8506],
[2667. , 2705. , -5.511 , 1.0145, -5.511 ],
[2739. , 2914. , -1.014 , 7.4039, -0.0476],
[2940. , 3490. , -2.1683, 35.982 , 19.9643],
[3716. , 3969. , -0.2862, 19.1575, 4.207 ],
[4068. , 4311. , -6.3728, 9.127 , -1.6133],
[4324. , 4427. , -1.9754, 4.9955, -0.9605],
[4460. , 4800. , -0.2745, 21.0595, 6.8529]])

The features used mostly depend on the type of strategy we're studying, but they'll likely be subjective as well.

Statistical Inference
Given the nature of our new features, analyzing them like we did with pnl won't really give relevant results.

So it's worth taking some time to learn some basic statistical tools. According to the Central Distribution Theorem,
with enough data we can assume that our features follow a normal distribution (our patterns aren't many so it's a
bit of stretch but we'll try anyway for instructional purposes)

X ∼ N (μ, σ)

μ is the mean and σ is the standard deviation. This assumption is important because it allows us to carry out the
following calculations:

we can calculate the probability P (X > t) , also known as tail probability (survival function), which mean that
by setting a certain threshold t we can find the probability that or feature be greater than it,
t − μ
P (X > t) = 1 − Φ ( )
σ

where Φ is Cumulative Distribution Function (CDF), we can calculate this in python:

In [165… from [Link] import norm

t = 1

mu_min = [Link](patts[:, 2])


mu_max = [Link](patts[:, 3])
mu_pnl = [Link](patts[:, 4])

std_min = [Link](patts[:, 2])


std_max = [Link](patts[:, 3])
std_pnl = [Link](patts[:, 4])

prob_min = 1 - [Link](-t, loc=mu_min, scale=std_min)


prob_max = 1 - [Link](t, loc=mu_max, scale=std_max)
prob_pnl = 1 - [Link](t, loc=mu_pnl, scale=std_pnl)

prob_min, prob_max, prob_pnl

Out[165… (0.22349056137359846, 0.8281139421956232, 0.5219181937243953)

or we can carry out the inverse operation, or evaluate the quantile function, so for a any given probability we
can find the minimum possible value that our features should present,

P (X > t) = p

in python:

In [166… p = 0.3

t_min = [Link](1 - p, loc=mu_min, scale=std_min)


t_max = [Link](1 - p, loc=mu_max, scale=std_max)
t_pnl = [Link](1 - p, loc=mu_pnl, scale=std_pnl)

t_min, t_max, t_pnl

Out[166… (-1.7210057827332286, 16.986887670817993, 6.132358008018152)

In [167… p = 0.7

t_min = [Link](1 - p, loc=mu_min, scale=std_min)


t_max = [Link](1 - p, loc=mu_max, scale=std_max)
t_pnl = [Link](1 - p, loc=mu_pnl, scale=std_pnl)

t_min, t_max, t_pnl

Out[167… (-4.924433018099445, 5.589551611573512, -3.1584788261739467)

These operations are all we'll need for now, as they're great to help us figure the risk / reward we can set in our
strategy for example.

More Data
If we weren't satisfied with the number of patterns found, we could either attempt to get more klines or, thanks to
the fact that we normalized our features, we could group together patterns from different symbols.

In order to simplify the script, we'll first define a function that finds our patterns:

In [168… def crossPatterns(df):

idxStart = crossover(df['fast'].to_numpy(), df['slow'].to_numpy())


idxEnd = crossover(df['slow'].to_numpy(), df['fast'].to_numpy())

idx = [Link](idxEnd, idxStart, side='right')


valid = idx < len(idxEnd)
paired = [Link]([Link], [Link])
paired[valid] = idxEnd[idx[valid]]

pairs = np.column_stack((idxStart, paired))


pairs = pairs[~[Link](pairs).any(axis=1)].astype(int)

closes = df['close'].to_numpy()
lgt = pairs[:, 1] - pairs[:, 0]

max_vals = [Link](len(pairs))
min_vals = [Link](len(pairs))

for i, (start, end) in enumerate(pairs):


base = closes[start]
window = closes[start:end+1]
max_vals[i] = ([Link]() / base - 1) * 100
min_vals[i] = ([Link]() / base - 1) * 100

pnl = (closes[pairs[:, 1]] / closes[pairs[:, 0]] - 1) * 100

patts = np.column_stack([lgt, min_vals, max_vals, pnl])


return patts

Notice how we dropped the pairs of indices as they lose their significance when comparing patterns on different
symbols, but we did add lgt as a feature.

At this point we iterate over the symbols we want to consider, get the patterns and then group together:

In [169… symbols = ['BTC', 'ETH', 'SOL']


tf = '1h'

first = 0
for symbol in symbols:
sym = symbol + 'USDT'
print(symbol)
df = kline(sym, tf, 5000)
df['fast'] = ema(df['close'], 50)
df['slow'] = ema(df['close'], 200)

if first == 0:
patts = crossPatterns(df)
print([Link](patts))
first += 1
else:
patts0 = crossPatterns(df)
print([Link](patts0))
patts = [Link]([patts, patts0])
print([Link](patts))

BTC
(12, 4)
ETH
(12, 4)
(24, 4)
SOL
(16, 4)
(40, 4)

Lets now run our statistical inference and see what we get:

In [170… t = 1
p = 0.7

mu_lgt = [Link](patts[:, 0])


mu_min = [Link](patts[:, 1])
mu_max = [Link](patts[:, 2])
mu_pnl = [Link](patts[:, 3])

std_lgt = [Link](patts[:, 0])


std_min = [Link](patts[:, 1])
std_max = [Link](patts[:, 2])
std_pnl = [Link](patts[:, 3])
prob_min = 1 - [Link](-t, loc=mu_min, scale=std_min)
prob_max = 1 - [Link](t, loc=mu_max, scale=std_max)
prob_pnl = 1 - [Link](t, loc=mu_pnl, scale=std_pnl)

print(prob_min, prob_max, prob_pnl)

t_lgt = [Link](1 - p, loc=mu_lgt, scale= std_lgt)


t_min = [Link](1 - p, loc=mu_min, scale=std_min)
t_max = [Link](1 - p, loc=mu_max, scale=std_max)
t_pnl = [Link](1 - p, loc=mu_pnl, scale=std_pnl)

print(t_lgt, t_min, t_max, t_pnl)

0.22673654759714235 0.7536935452639243 0.5582407858825793


105.29792520441013 -4.678669059415572 3.2610490494099924 -3.523430529223203

One important thing to note is that we grouped together patterns from different symbols on the same timeframe, i
advise to not proceed the other way around as the results will tend to be on a much different scale, we can check this
by comparing mean values:

In [171… symbols = ['BTC', 'ETH', 'SOL']


tfs = ['1m', '15m', '1h', '4h', '1d']

for tf in tfs:
print('-'*10)
first = 0
for symbol in symbols:
sym = symbol + 'USDT'
df = kline(sym, tf, 5000)
df['fast'] = ema(df['close'], 50)
df['slow'] = ema(df['close'], 200)

if first == 0:
patts = crossPatterns(df)
print(f'{tf} - {symbol} PnL: {[Link](patts[:, -1])}')
first += 1
else:
patts0 = crossPatterns(df)
print(f'{tf} - {symbol} PnL: {[Link](patts0[:, -1])}')
patts = [Link]([patts, patts0])

print(f'All patts PnL: {[Link](patts[:, -1])}')

----------
1m - BTC PnL: 0.20581467590465724
1m - ETH PnL: 0.02173255555795231
1m - SOL PnL: 0.22944089559209901
All patts PnL: 0.1429513360791419
----------
15m - BTC PnL: -0.3410925410879499
15m - ETH PnL: -0.5522653534508974
15m - SOL PnL: 0.10611273291158346
All patts PnL: -0.29374381444629555
----------
1h - BTC PnL: 1.3945711027269019
1h - ETH PnL: 5.80205572281903
1h - SOL PnL: 1.4869395909221026
All patts PnL: 2.75376388403262
----------
4h - BTC PnL: 13.432611473021455
4h - ETH PnL: 8.388097400073388
4h - SOL PnL: 24.914800166100264
All patts PnL: 15.438377622149813
----------
1d - BTC PnL: 16.747407946906044
1d - ETH PnL: 50.43608178141643
1d - SOL PnL: 741.3685623712988
All patts PnL: 163.22004599373804

Obviously the closer the two timeframes the closer the scale of their features, but it can change drastically for very
different timeframe, while for symbols on the same timeframe scale and order of magnitude stay more or less
consistent.

Other Strategies

Long & Short


We can now try to find more complex patterns, the first step could be to consider always our crossover strategy but
trying to incorporate short patterns as well:

In [172… df = kline('SOLUSDT', '1h', 1000)


df['fast'] = ema(df['close'], 50)
df['slow'] = ema(df['close'], 200)

idxLong = crossover(df['fast'].to_numpy(), df['slow'].to_numpy())


idxShort = crossover(df['slow'].to_numpy(), df['fast'].to_numpy())

idxs = [Link]((idxLong, -idxShort))


idxs = idxs[[Link]([Link](idxs))]

pairs = np.column_stack([idxs[:-1], idxs[1:]])


pairs

Out[172… array([[-313, 319],


[ 319, -428],
[-428, 459],
[ 459, -800]], dtype=int64)

This way we have both long and short strategies paired together as the same pattern, but we also have a way of
identifying which type of position we're considering

In [ ]: def crossPatternsLS(df):

idxLong = crossover(df['fast'].to_numpy(), df['slow'].to_numpy())


idxShort = crossover(df['slow'].to_numpy(), df['fast'].to_numpy())

idxs = [Link]((idxLong, -idxShort))


idxs = idxs[[Link]([Link](idxs))]

pairs = np.column_stack([idxs[:-1], idxs[1:]])


pairs = pairs[~[Link](pairs).any(axis=1)].astype(int)

closes = df['close'].to_numpy()
lgt = abs(pairs[:, 1]) - abs(pairs[:, 0])

max_vals = [Link](len(pairs))
min_vals = [Link](len(pairs))
pnl = [Link](len(pairs))

for i, (start, end) in enumerate(pairs):


s = abs(start)
e = abs(end)

base = closes[s]
window = closes[s:e+1]
if start > 0:
max_vals[i] = ([Link]() / base - 1) * 100
min_vals[i] = ([Link]() / base - 1) * 100
pnl[i] = (closes[e] / closes[s] - 1) * 100
else:
max_vals[i] = ([Link]() / base - 1) * -100
min_vals[i] = ([Link]() / base - 1) * -100
pnl[i] = (closes[e] / closes[s] - 1) * -100

patts = np.column_stack([pairs, lgt, min_vals, max_vals, pnl])


return patts
Notice how we had to be careful to compute the absolute value on all the indices as a negative index will find a
position by counting from the end back.

Let's now try running the function:

In [174… df = kline('SOLUSDT', '1h', 1000)


df['fast'] = ema(df['close'], 50)
df['slow'] = ema(df['close'], 200)

patts = crossPatternsLS(df)
patts

Out[174… array([[-313. , 319. , 6. , -3.232 , -0. , -3.232 ],


[ 319. , -428. , 109. , -2.9936, 3.9049, -2.1509],
[-428. , 459. , 31. , -4.0559, -0. , -4.0559],
[ 459. , -800. , 341. , -0.3513, 20.9663, 6.7706]])

Lets gather more patterns and see what our data suggests

In [177… symbols = ['BTC', 'ETH', 'SOL']


tf = '1h'

first = 0
for symbol in symbols:
sym = symbol + 'USDT'
# print(symbol)
df = kline(sym, tf, 5000)
df['fast'] = ema(df['close'], 50)
df['slow'] = ema(df['close'], 200)

if first == 0:
patts = crossPatternsLS(df)
# print([Link](patts))
first += 1
else:
patts0 = crossPatternsLS(df)
# print([Link](patts0))
patts = [Link]([patts, patts0])
# print([Link](patts))

In [179… t = 1
p = 0.7

mu_lgt = [Link](patts[:, 2])


mu_min = [Link](patts[:, 3])
mu_max = [Link](patts[:, 4])
mu_pnl = [Link](patts[:, 5])

std_lgt = [Link](patts[:, 2])


std_min = [Link](patts[:, 3])
std_max = [Link](patts[:, 4])
std_pnl = [Link](patts[:, 5])

prob_min = 1 - [Link](-t, loc=mu_min, scale=std_min)


prob_max = 1 - [Link](t, loc=mu_max, scale=std_max)
prob_pnl = 1 - [Link](t, loc=mu_pnl, scale=std_pnl)

print(prob_min, prob_max, prob_pnl)

t_lgt = [Link](1 - p, loc=mu_lgt, scale= std_lgt)


t_min = [Link](1 - p, loc=mu_min, scale=std_min)
t_max = [Link](1 - p, loc=mu_max, scale=std_max)
t_pnl = [Link](1 - p, loc=mu_pnl, scale=std_pnl)

print(t_lgt, t_min, t_max, t_pnl)

0.20736049753396824 0.7332396311876272 0.48323313782115274


84.17707946132157 -5.057038731954126 2.116953986614498 -4.395624048812524
We'll notice that our performance has gotten slightly worse, so lets check the performance of only the short
strategies (long will be same as before):

In [181… t = 1
p = 0.7

short = patts[patts[:, 0] < 0]

mu_lgt = [Link](short[:, 2])


mu_min = [Link](short[:, 3])
mu_max = [Link](short[:, 4])
mu_pnl = [Link](short[:, 5])

std_lgt = [Link](short[:, 2])


std_min = [Link](short[:, 3])
std_max = [Link](short[:, 4])
std_pnl = [Link](short[:, 5])

prob_min = 1 - [Link](-t, loc=mu_min, scale=std_min)


prob_max = 1 - [Link](t, loc=mu_max, scale=std_max)
prob_pnl = 1 - [Link](t, loc=mu_pnl, scale=std_pnl)

print(prob_min, prob_max, prob_pnl)

t_lgt = [Link](1 - p, loc=mu_lgt, scale= std_lgt)


t_min = [Link](1 - p, loc=mu_min, scale=std_min)
t_max = [Link](1 - p, loc=mu_max, scale=std_max)
t_pnl = [Link](1 - p, loc=mu_pnl, scale=std_pnl)

print(t_lgt, t_min, t_max, t_pnl)

0.18787759431489082 0.7397206860711003 0.3173232004895272


74.82221183872153 -5.416664644617096 1.839438236149598 -4.3738768141543485

Frankly grouping both long and short strategies into the same patterns might not be worth it, we mostly did it this
way for educational purposes. A cleaner way consists of redefining the function in order to find both and then
keeping them separate:

In [ ]: def crossPatterns(df, mode= 'long'):

if mode == 'long':
idxStart = crossover(df['fast'].to_numpy(), df['slow'].to_numpy())
idxEnd = crossover(df['slow'].to_numpy(), df['fast'].to_numpy())
else:
idxStart = crossover(df['slow'].to_numpy(), df['fast'].to_numpy())
idxEnd = crossover(df['fast'].to_numpy(), df['slow'].to_numpy())

idx = [Link](idxEnd, idxStart, side='right')


valid = idx < len(idxEnd)
paired = [Link]([Link], [Link])
paired[valid] = idxEnd[idx[valid]]

pairs = np.column_stack((idxStart, paired))


pairs = pairs[~[Link](pairs).any(axis=1)].astype(int)

closes = df['close'].to_numpy()
lgt = pairs[:, 1] - pairs[:, 0]

max_vals = [Link](len(pairs))
min_vals = [Link](len(pairs))
pnl = [Link](len(pairs))

for i, (start, end) in enumerate(pairs):


base = closes[start]
window = closes[start:end+1]
if mode == 'long':
max_vals[i] = ([Link]() / base - 1) * 100
min_vals[i] = ([Link]() / base - 1) * 100
pnl[i] = (closes[end] / closes[start] - 1) * 100
else:
max_vals[i] = ([Link]() / base - 1) * -100
min_vals[i] = ([Link]() / base - 1) * -100
pnl[i] = (closes[end] / closes[start] - 1) * -100

patts = np.column_stack([lgt, min_vals, max_vals, pnl])


return patts

Multiple Indices
Sometimes a simple condition isn't enough to find a desired pattern, but we have to combine multiple conditions
which might mean having more than one index identifying start or end of our pattern.

Or if we want to run a Machine Learning algorithm to predict the outcome of a pattern two indices won't work, as
we'll need two just to determine the feature range (range where we can save and store features) and an extra index,
effectively defining a second range, that we'll use to get the target value that we're trying to predict for that pattern
(showing the whole pattern to an ML model would be cheating as it also sees the outcome), but we'll tackle this
specific problem further on.

For now focus on grouping multiple indices to define a single pattern.

Considering our previous strategy, lets add a step: say we don't want to but when the two moving averages cross but
when the price goes to retest the slower average. We already know how to find the cross indices, we need to figure
out how to find a retest, one idea could be to find all the times price goes below the retest level and then filter out
the indices by selecting the closest index to our crossover:

In [183… df = kline('SOLUSDT', '1h', 1000)


df['fast'] = ema(df['close'], 50)
df['slow'] = ema(df['close'], 200)

idxStart = crossover(df['fast'].to_numpy(), df['slow'].to_numpy())

condRetest = df['low'].to_numpy() < df['slow'].to_numpy()


idxRetest = [Link](condRetest)[0]

idxEnd = crossover(df['slow'].to_numpy(), df['fast'].to_numpy())

idxStart, idxRetest, idxEnd

Out[183… (array([318, 455], dtype=int64),


array([ 0, 1, 2, 3, 4, 5, 8, 9, 10, 11, 36, 37, 38, 39, 40, 41, 42, 43, 44,
45, 46, 48, 55, 56, 57, 58, 59, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148,
149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 234, 249, 250, 251, 252, 253, 254, 255, 256, 257, 25
8, 259, 284, 285, 286, 287, 288, 289, 290, 291, 292, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303,
304, 305, 306, 307, 308, 309, 310, 311, 321, 322, 324, 325, 372, 373, 374, 375, 376, 377, 378, 379, 38
0, 381, 382, 383, 384, 385, 386, 387, 395, 396, 397, 398, 399, 400, 402, 403, 404, 405, 406, 407, 408,
409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 42
9, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449,
732, 734, 735, 736, 737, 738, 740, 751, 774, 781, 782, 783, 784, 785, 786, 787, 788, 789, 790, 791, 79
2, 793, 794, 795, 796, 797, 798, 799, 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 810, 811, 812,
813, 814, 815, 816, 817, 818, 819, 820, 821, 822, 823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 83
3, 834, 835, 836, 837, 838, 839, 840, 841, 842, 843, 844, 845, 846, 847, 848, 849, 850, 851, 852, 853,
854, 855, 856, 857, 858, 859, 860, 861, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, 873, 87
4, 875, 876, 877, 878, 879, 880, 881, 882, 883, 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, 894,
895, 896, 897, 898, 899, 900, 901, 902, 903, 904, 905, 906, 907, 908, 909, 910, 911, 912, 913, 914, 91
5, 916, 917, 918, 919, 920, 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935,
936, 937, 938, 939, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955, 95
6, 957, 958, 959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976,
982, 983, 984, 985, 986, 987, 988, 989, 990, 991, 992, 993, 994, 995, 996, 997, 998, 999], dtype=int6
4),
array([308, 423, 796], dtype=int64))

We notice straight away that this strategy isn't as straight forward as the previous one, where the indices were
inherently sequential and we just had to order them, here we need to filter out loads of idxRetest , we could do
this like this:

In [188… j = [Link](idxRetest, idxStart, side='right')


valid1 = j < len(idxRetest)
nextR = np.full_like(idxStart, -1)
nextR[valid1] = idxRetest[j[valid1]]

# Step 2: for each "below", find next crossunder


k = [Link](idxEnd, nextR, side='right')
valid2 = valid1 & (k < len(idxEnd))
nextU = np.full_like(idxStart, -1)
nextU[valid2] = idxEnd[k[valid2]]

nextR = nextR[valid2]
idxS = idxStart[valid2]

np.column_stack([idxS, nextR, nextU])

Out[188… array([[318, 321, 423],


[455, 732, 796]], dtype=int64)

In [198… [Link] = pd.to_numeric([Link], errors = 'coerce')


[Link] = pd.to_datetime([Link])

s = 300
e = 450

r = 321

plots = [
mpf.make_addplot(df['fast'].iloc[s:e], type= 'line', color= 'grey', width= 3, label= 'fast'),
mpf.make_addplot(df['slow'].iloc[s:e], type= 'line', color= 'black', width= 3, label= 'slow')
]

line = [Link]([Link], index=[Link][s:e])

start_rel = r - s

# Fill the line from start_rel to the end with the level
[Link][start_rel:] = df['low'].iloc[r]

# Add the horizontal line to plots


[Link](mpf.make_addplot(line, type='line', color='blue', linestyle='--', width=2))

[Link]([Link][s:e, 0:4], type= 'candle', style= 'sas', addplot= plots)

In [197… [Link] = pd.to_numeric([Link], errors = 'coerce')


[Link] = pd.to_datetime([Link])

s = 450
e = 800

r = 732
plots = [
mpf.make_addplot(df['fast'].iloc[s:e], type= 'line', color= 'grey', width= 3, label= 'fast'),
mpf.make_addplot(df['slow'].iloc[s:e], type= 'line', color= 'black', width= 3, label= 'slow')
]

line = [Link]([Link], index=[Link][s:e])

start_rel = r - s

# Fill the line from start_rel to the end with the level
[Link][start_rel:] = df['low'].iloc[r]

# Add the horizontal line to plots


[Link](mpf.make_addplot(line, type='line', color='blue', linestyle='--', width=2))

[Link]([Link][s:e, 0:4], type= 'candle', style= 'sas', addplot= plots)

It appears to be working, but from the second plot we see that considering the retest as low < slow we're leaving
out possibly better entry points, so lets first write a function that finds these patterns and then we can try tweaking
the condition:

In [ ]: def retestPatterns(df):
idxStart = crossover(df['fast'].to_numpy(), df['slow'].to_numpy())

meanChange = [Link](abs(df['close'] / df['open'] - 1))


condRetest = df['low'].to_numpy() < df['slow'].to_numpy() * (1 + meanChange)
idxRetest = [Link](condRetest)[0]

idxEnd = crossover(df['slow'].to_numpy(), df['fast'].to_numpy())

j = [Link](idxRetest, idxStart, side='right')


valid1 = j < len(idxRetest)
nextR = np.full_like(idxStart, -1)
nextR[valid1] = idxRetest[j[valid1]]

# Step 2: for each "below", find next crossunder


k = [Link](idxEnd, nextR, side='right')
valid2 = valid1 & (k < len(idxEnd))
nextU = np.full_like(idxStart, -1)
nextU[valid2] = idxEnd[k[valid2]]

nextR = nextR[valid2]
idxS = idxStart[valid2]
return np.column_stack([idxS, nextR, nextU])

In [ ]: retestPatterns(df)

Out[ ]: array([[318, 320, 423],


[455, 637, 796]], dtype=int64)

In [213… [Link] = pd.to_numeric([Link], errors = 'coerce')


[Link] = pd.to_datetime([Link])

s = 450
e = 800

r = 637

plots = [
mpf.make_addplot(df['fast'].iloc[s:e], type= 'line', color= 'grey', width= 3, label= 'fast'),
mpf.make_addplot(df['slow'].iloc[s:e], type= 'line', color= 'black', width= 3, label= 'slow')
]

line = [Link]([Link], index=[Link][s:e])

start_rel = r - s

# Fill the line from start_rel to the end with the level
[Link][start_rel:] = df['low'].iloc[r]

# Add the horizontal line to plots


[Link](mpf.make_addplot(line, type='line', color='blue', linestyle='--', width=2))

[Link]([Link][s:e, 0:4], type= 'candle', style= 'sas', addplot= plots)

So all we did was modify the retest condition to find low prices within a certain range of the slow moving average, i
advise using the mean percent change as it finds where the low price is within one average candle from touching the
slow moving average. We could also apply the same logic but by using (1 + meanChange * m) where m will
represent how many average candles we are from reaching the desired level.

Now we can proceed on adding the usual features:

In [221… def retestPatterns(df, m=1):


idxStart = crossover(df['fast'].to_numpy(), df['slow'].to_numpy())

meanChange = [Link](abs(df['close'] / df['open'] - 1))


condRetest = df['low'].to_numpy() < df['slow'].to_numpy() * (1 + meanChange*m)
idxRetest = [Link](condRetest)[0]

idxEnd = crossover(df['slow'].to_numpy(), df['fast'].to_numpy())

j = [Link](idxRetest, idxStart, side='right')


valid1 = j < len(idxRetest)
nextR = np.full_like(idxStart, -1)
nextR[valid1] = idxRetest[j[valid1]]

# Step 2: for each "below", find next crossunder


k = [Link](idxEnd, nextR, side='right')
valid2 = valid1 & (k < len(idxEnd))
nextU = np.full_like(idxStart, -1)
nextU[valid2] = idxEnd[k[valid2]]

valid = valid2 & (nextR != -1) & (nextU != -1)

idxS = idxStart[valid]
nextR = nextR[valid]
nextU = nextU[valid]

pairs = np.column_stack([nextR, nextU])

closes = df['close'].to_numpy()
lgt = nextU - nextR

max_vals = [Link](len(nextR))
min_vals = [Link](len(nextR))

for i, (start, end) in enumerate(pairs):


base = closes[start]
window = closes[start:end+1]
max_vals[i] = ([Link]() / base - 1) * 100
min_vals[i] = ([Link]() / base - 1) * 100

pnl = (closes[nextU] / closes[nextR] - 1) * 100

# groups = np.column_stack([idxS, nextR, nextU])

return np.column_stack([idxS, nextR, nextU, lgt, min_vals, max_vals, pnl])

In [226… symbols = ['BTC', 'ETH', 'SOL']


tf = '1h'

first = 0
for symbol in symbols:
sym = symbol + 'USDT'
print(symbol)
df = kline(sym, tf, 5000)
df['fast'] = ema(df['close'], 50)
df['slow'] = ema(df['close'], 200)

if first == 0:
patts = retestPatterns(df)
print([Link](patts))
first += 1
else:
patts0 = retestPatterns(df)
print([Link](patts0))
patts = [Link]([patts, patts0])
print([Link](patts))

BTC
(12, 7)
ETH
(12, 7)
(24, 7)
SOL
(16, 7)
(40, 7)

In [232… t = 1
p = 0.7
mu_lgt = [Link](patts[:, 3])
mu_min = [Link](patts[:, 4])
mu_max = [Link](patts[:, 5])
mu_pnl = [Link](patts[:, 6])

print(mu_lgt, mu_min, mu_max, mu_pnl)

std_lgt = [Link](patts[:, 3])


std_min = [Link](patts[:, 4])
std_max = [Link](patts[:, 5])
std_pnl = [Link](patts[:, 6])

prob_min = 1 - [Link](-t, loc=mu_min, scale=std_min)


prob_max = 1 - [Link](t, loc=mu_max, scale=std_max)
prob_pnl = 1 - [Link](t, loc=mu_pnl, scale=std_pnl)

print(prob_min, prob_max, prob_pnl)

t_lgt = [Link](1 - p, loc=mu_lgt, scale= std_lgt)


t_min = [Link](1 - p, loc=mu_min, scale=std_min)
t_max = [Link](1 - p, loc=mu_max, scale=std_max)
t_pnl = [Link](1 - p, loc=mu_pnl, scale=std_pnl)

print(t_lgt, t_min, t_max, t_pnl)

print(len(patts[patts[:, -1]>0])/len(patts))

169.075 -2.5112227522062702 8.631140841992146 2.192866410682367


0.23434778344752338 0.7177703406280874 0.5430392652642595
66.03327573579136 -3.6049056310834455 1.6863966547794096 -3.5941546233215047
0.35

Multiple End Conditions


As we can see from our last strategy only 35% of times our close at the end of the pattern is greater than the starting
close. We might want to try using a different end condition, but lets say we want to see what would happen if we
used more than one level to find the exit, like a stop loss and take profit level.

First we have to decide how we will define these levels, one idea could be to use the most recent pivot low before the
crossover as a stop loss level and the maximum price reached before our retest as a take profit level.

The maximum price won't require us to find an additional index but the pivot low comes before the crossover in our
logic so we can't look for a minimum between two already found indices:

In [ ]: import [Link]

def retestPatternsTPSL(df, m=1, w=5):


closes = df['close'].to_numpy()
opens = df['open'].to_numpy()
lows = df['low'].to_numpy()
fast = df['fast'].to_numpy()
slow = df['slow'].to_numpy()

# --- local minima ---


foot = [Link](w*2 + 1)
foot[w] = 0
condM = closes < [Link].minimum_filter(
closes, footprint=foot, mode='constant', cval=-[Link]
)
idxM = [Link](condM)[0]
# --- crossovers ---
idxC = crossover(fast, slow)

# --- retests ---


meanChange = [Link](abs(closes/opens-1)) * m
condR = lows < slow*(1+meanChange)
idxR = [Link](condR)[0]

# --- pair M before and R after each C ---


j = [Link](idxM, idxC) - 1
k = [Link](idxR, idxC)
valid = (j >= 0) & (k < len(idxR))
idxC, prevM, nextR = idxC[valid], idxM[j[valid]], idxR[k[valid]]

# --- thresholds ---


high_thr = [Link]([closes[m:r+1].max() for m,r in zip(prevM, nextR)])
low_thr = closes[prevM]

# --- hybrid: loop per pattern with searchsorted ---


idxE = [Link](len(prevM), -1)
for i, (r,hu,ld) in enumerate(zip(nextR,high_thr,low_thr)):
seg = closes[r+1:]
if [Link] == 0: continue
up = [Link](seg > hu)[0]
down = [Link](seg < ld)[0]
if [Link] and [Link]:
idxE[i] = r+1 + min(up[0],down[0])
elif [Link]:
idxE[i] = r+1 + up[0]
elif [Link]:
idxE[i] = r+1 + down[0]

return np.column_stack([prevM, idxC, nextR, idxE])

In [242… df = kline('SOLUSDT', '1h', 1000)


df['fast'] = ema(df['close'], 50)
df['slow'] = ema(df['close'], 200)

patts = retestPatternsTPSL(df)
patts

Out[242… array([[ 5, 11, 27, 52],


[301, 314, 315, 321],
[441, 451, 632, 704]], dtype=int64)

In [ ]: [Link] = pd.to_numeric([Link], errors = 'coerce')


[Link] = pd.to_datetime([Link])

for i in patts:
s = max(0, i[0]-10)
# e = min(len(df)-1, i[3])
e = min(len(df), i[3]+10)

plots = [
mpf.make_addplot(df['fast'].iloc[s:e], type= 'line', color= 'grey', width= 3, label= 'fast'),
mpf.make_addplot(df['slow'].iloc[s:e], type= 'line', color= 'black', width= 3, label= 'slow')
]

hline_data = [
(i[0], df['close'].iloc[i[0]]),
(i[2], df['close'].iloc[i[2]]),
(i[3], df['close'].iloc[i[3]])
]

for r, level in hline_data:


line = [Link]([Link], index=[Link][s:e])
start_rel = r - s
if start_rel < 0: # skip lines starting before plot window
continue
[Link][start_rel:] = level
[Link](mpf.make_addplot(line, type='line', color='blue', linestyle='--', width=2))

[Link]([Link][s:e, 0:4], type='candle', style='sas', addplot=plots)


The function works as expected so we can proceed to add features and run statistical inference on more data:

In [ ]: def retestPatternsTPSL(df, m=1, w=5):


closes = df['close'].to_numpy()
opens = df['open'].to_numpy()
lows = df['low'].to_numpy()
fast = df['fast'].to_numpy()
slow = df['slow'].to_numpy()

# --- local minima ---


foot = [Link](w*2 + 1)
foot[w] = 0
condM = closes < [Link].minimum_filter(
closes, footprint=foot, mode='constant', cval=-[Link]
)
idxM = [Link](condM)[0]

# --- crossovers ---


idxC = crossover(fast, slow)

# --- retests ---


meanChange = [Link](abs(closes/opens-1)) * m
condR = lows < slow*(1+meanChange)
idxR = [Link](condR)[0]

# --- pair M before and R after each C ---


j = [Link](idxM, idxC) - 1
k = [Link](idxR, idxC)
valid = (j >= 0) & (k < len(idxR))
idxC, prevM, nextR = idxC[valid], idxM[j[valid]], idxR[k[valid]]

# --- thresholds ---


high_thr = [Link]([closes[m:r+1].max() for m,r in zip(prevM, nextR)])
low_thr = closes[prevM]

# --- exit index calculation ---


idxE = [Link](len(prevM), -1)
for i, (r,hu,ld) in enumerate(zip(nextR,high_thr,low_thr)):
seg = closes[r+1:]
if [Link] == 0: continue
up = [Link](seg > hu)[0]
down = [Link](seg < ld)[0]
if [Link] and [Link]:
idxE[i] = r+1 + min(up[0],down[0])
elif [Link]:
idxE[i] = r+1 + up[0]
elif [Link]:
idxE[i] = r+1 + down[0]

# --- extra features ---


lgt = idxE - nextR # length of each pattern

max_vals = [Link](len(nextR), [Link])


min_vals = [Link](len(nextR), [Link])
pnl = [Link](len(nextR), [Link])

for i, (start, end) in enumerate(zip(nextR, idxE)):


if end <= start or end < 0: # skip invalid segments
continue
base = closes[start]
window = closes[start:end+1]
max_vals[i] = ([Link]() / base - 1) * 100
min_vals[i] = ([Link]() / base - 1) * 100
pnl[i] = (closes[end] / closes[start] - 1) * 100

return np.column_stack([prevM, idxC, nextR, idxE, lgt, min_vals, max_vals, pnl])

In [252… symbols = ['BTC', 'ETH', 'SOL']


tf = '1h'

first = 0
for symbol in symbols:
sym = symbol + 'USDT'
# print(symbol)
df = kline(sym, tf, 5000)
df['fast'] = ema(df['close'], 50)
df['slow'] = ema(df['close'], 200)

if first == 0:
patts = retestPatternsTPSL(df)
# print([Link](patts))
first += 1
else:
patts0 = retestPatternsTPSL(df)
# print([Link](patts0))
patts = [Link]([patts, patts0])
# print([Link](patts))

t = 1
p = 0.7

mu_lgt = [Link](patts[:, -4])


mu_min = [Link](patts[:, -3])
mu_max = [Link](patts[:, -2])
mu_pnl = [Link](patts[:, -1])

print(mu_lgt, mu_min, mu_max, mu_pnl)

std_lgt = [Link](patts[:, -4])


std_min = [Link](patts[:, -3])
std_max = [Link](patts[:, -2])
std_pnl = [Link](patts[:, -1])

prob_min = 1 - [Link](-t, loc=mu_min, scale=std_min)


prob_max = 1 - [Link](t, loc=mu_max, scale=std_max)
prob_pnl = 1 - [Link](t, loc=mu_pnl, scale=std_pnl)

print(prob_min, prob_max, prob_pnl)

t_lgt = [Link](1 - p, loc=mu_lgt, scale= std_lgt)


t_min = [Link](1 - p, loc=mu_min, scale=std_min)
t_max = [Link](1 - p, loc=mu_max, scale=std_max)
t_pnl = [Link](1 - p, loc=mu_pnl, scale=std_pnl)

print(t_lgt, t_min, t_max, t_pnl)

print(len(patts[patts[:, -1]>0])/len(patts))
55.025 -1.8372973032320936 2.523885499389213 1.2920997444293825
0.35813924798567476 0.7110817061131808 0.5286129033156767
-3.679580688122144 -3.045427412837195 1.0880219945773622 -0.841778196085889
0.65

As we can see we almost doubled the amount of patterns that close at a profit, and the other performance
parameters seem relatively improved overall.

Data Frames
As you add more features or indices, the pattern array becomes harder to read. It might come in handy to save the
patterns as a [Link] to improve readability, unfortunately a lot of the operations you'll run on and with
these patterns require an array type structure (vectors), but you can always convert it back to an array by running
[Link]()

In [ ]: def retestPatternsTPSL(df, m=1, w=5):


closes = df['close'].to_numpy()
opens = df['open'].to_numpy()
lows = df['low'].to_numpy()
fast = df['fast'].to_numpy()
slow = df['slow'].to_numpy()

# --- local minima ---


foot = [Link](w*2 + 1)
foot[w] = 0
condM = closes < [Link].minimum_filter(
closes, footprint=foot, mode='constant', cval=-[Link]
)
idxM = [Link](condM)[0]

# --- crossovers ---


idxC = crossover(fast, slow)

# --- retests ---


meanChange = [Link](abs(closes/opens-1)) * m
condR = lows < slow*(1+meanChange)
idxR = [Link](condR)[0]

# --- pair M before and R after each C ---


j = [Link](idxM, idxC) - 1
k = [Link](idxR, idxC)
valid = (j >= 0) & (k < len(idxR))
idxC, prevM, nextR = idxC[valid], idxM[j[valid]], idxR[k[valid]]

# --- thresholds ---


high_thr = [Link]([closes[m:r+1].max() for m,r in zip(prevM, nextR)])
low_thr = closes[prevM]

# --- exit index calculation ---


idxE = [Link](len(prevM), -1)
for i, (r,hu,ld) in enumerate(zip(nextR,high_thr,low_thr)):
seg = closes[r+1:]
if [Link] == 0: continue
up = [Link](seg > hu)[0]
down = [Link](seg < ld)[0]
if [Link] and [Link]:
idxE[i] = r+1 + min(up[0],down[0])
elif [Link]:
idxE[i] = r+1 + up[0]
elif [Link]:
idxE[i] = r+1 + down[0]

# --- extra features ---


lgt = idxE - nextR # length of each pattern

max_vals = [Link](len(nextR), [Link])


min_vals = [Link](len(nextR), [Link])
pnl = [Link](len(nextR), [Link])

for i, (start, end) in enumerate(zip(nextR, idxE)):


if end <= start or end < 0: # skip invalid segments
continue
base = closes[start]
window = closes[start:end+1]
max_vals[i] = ([Link]() / base - 1) * 100
min_vals[i] = ([Link]() / base - 1) * 100
pnl[i] = (closes[end] / closes[start] - 1) * 100

patts = np.column_stack([prevM, idxC, nextR, idxE, lgt, min_vals, max_vals, pnl])


pattsDf = [Link](patts, columns=['idxM', 'idxC', 'idxR', 'idxE',
'lgt', 'Min', 'Max', 'PnL'])

return pattsDf

In [256… symbols = ['BTC', 'ETH', 'SOL']


tf = '1h'

first = 0
for symbol in symbols:
sym = symbol + 'USDT'
df = kline(sym, tf, 10000)
df['fast'] = ema(df['close'], 50)
df['slow'] = ema(df['close'], 200)

if first == 0:
patts = retestPatternsTPSL(df)
first += 1
else:
patts0 = retestPatternsTPSL(df)
patts = [Link]([patts, patts0])

patts

Out[256… idxM idxC idxR idxE lgt Min Max PnL

0 216.0 239.0 245.0 274.0 29.0 -1.111049 3.545470 3.545470

1 750.0 758.0 763.0 764.0 1.0 -0.506806 0.000000 -0.506806

2 1380.0 1390.0 1394.0 1397.0 3.0 -0.399316 1.356528 1.356528

3 1501.0 1516.0 1541.0 1542.0 1.0 -0.032595 0.000000 -0.032595

4 1784.0 1792.0 1792.0 1793.0 1.0 0.000000 0.284622 0.284622

... ... ... ... ... ... ... ... ...

23 7920.0 7930.0 7951.0 7988.0 37.0 -1.569671 1.862321 1.862321

24 8695.0 8706.0 8793.0 8816.0 23.0 -2.070242 4.297576 4.297576

25 9051.0 9058.0 9121.0 9190.0 69.0 -4.890999 8.801756 8.801756

26 9300.0 9314.0 9314.0 9320.0 6.0 -1.975443 1.396178 1.396178

27 9440.0 9450.0 9631.0 9703.0 72.0 0.000000 7.969229 7.969229

87 rows × 8 columns

You might also like