Practical Guide to Automated Detection Trading Patterns with Python

EODHD APIs
9 min readApr 4, 2024

In order to discuss the topic of a Practical Guide to Automated Detection of Trading Patterns with Python, let’s start with the basics. Most are acquainted with the appearance of a trading graph, typically illustrated with green and red bars that together form a line graph. This simple visual encapsulates a wealth of data and information. A single candle represents a data interval, for instance, one hour on the graph, and encapsulates four critical pieces of information: the Open, High, Low, and Close for that interval, commonly abbreviated as OHLC data. If the closing price is higher than the opening price, the candle will appear green; conversely, it will appear red if the closing price is lower. This concept is more clearly illustrated in the image below, which serves as an introduction to our Practical Guide to Automated Detection of Trading Patterns with Python.

Image from ig.com

An example of this is depicted as follows:

US500 hourly from ig.com

What you might notice is the emergence of specific patterns within the sequence of candles, referred to as candlestick patterns. Trading strategies are often developed based on these patterns.

Using S&P 500 from EODHD APIs to demonstrate this

The initial step involves acquiring the dataset required for our analysis, for which we will use the official EODHD API Python library. The code snippet provided below fetches 720 hours of data, equivalent to 30 days.

import config as cfg
from eodhd import APIClient

api = APIClient(cfg.API_KEY)


def get_ohlc_data():
df = api.get_historical_data("AAPL.US", "1h", results=(24*30))
return df


if __name__ == "__main__":
df = get_ohlc_data()
print(df)

We’ll begin with a straightforward candlestick pattern known as the hammer or hammer pattern. This pattern is a bullish reversal indicator that typically appears at the end of a downtrend. It’s characterized by the Open and Close prices being nearly identical at the top, coupled with a long lower wick that is at least twice the length of the short body. For a more detailed understanding of this pattern, the description on Investopedia is highly recommended.

A modified version of the candlestick image to illustrate this pattern is presented as follows:

Image from ig.com

In summary, the hammer pattern signals a bullish turn and is viewed as a modest reversal pattern, indicating a potential shift in market direction from downward to upward.

We’ve developed some code to pinpoint these candles within our dataset.

import pandas as pd
import config as cfg
from eodhd import APIClient

api = APIClient(cfg.API_KEY)


def candle_hammer(df: pd.DataFrame = None) -> pd.Series:
"""* Candlestick Detected: Hammer ("Weak - Reversal - Bullish Signal - Up"""

# Fill NaN values with 0
df = df.fillna(0)

return (
((df["high"] - df["low"]) > 3 * (df["open"] - df["close"]))
& (((df["close"] - df["low"]) / (0.001 + df["high"] - df["low"])) > 0.6)
& (((df["open"] - df["low"]) / (0.001 + df["high"] - df["low"])) > 0.6)
)


def get_ohlc_data():
df = api.get_historical_data("AAPL.US", "1h", results=(24*30))
return df


if __name__ == "__main__":
df = get_ohlc_data()
df["hammer"] = candle_hammer(df)
print(df)
print(df[df["hammer"] == True])

For demonstration, we’re displaying the dataset twice. The initial dataset reveals the identification of the hammer candle. The subsequent dataset exclusively presents instances where the hammer pattern was detected. Across the last 720 hours, the hammer pattern surfaced 67 times.

Progressing from this, there’s a related candlestick pattern that naturally follows — the inverted hammer pattern. Serving as the counterpart to the hammer, it resembles a hammer turned upside down on the graph. It remains a bullish signal, typically emerging at the end of a downtrend.

def candle_inverted_hammer(df: pd.DataFrame = None) -> pd.Series:
"""* Candlestick Detected: Inverted Hammer ("Weak - Continuation - Bullish Pattern - Up")"""

# Fill NaN values with 0
df = df.fillna(0)

return (
((df["high"] - df["low"]) > 3 * (df["open"] - df["close"]))
& ((df["high"] - df["close"]) / (0.001 + df["high"] - df["low"]) > 0.6)
& ((df["high"] - df["open"]) / (0.001 + df["high"] - df["low"]) > 0.6)
)

Candlestick Patterns

Candlestick patterns can essentially be divided into two categories:

  1. Bullish or Bearish
  2. Indecision / Neutral, Weak, Reliable, or Strong

Here’s a brief overview of prevalent candlestick patterns:

Indecision / Neutral

  • Doji

Weak

  • Hammer (bullish)
  • Inverted Hammer (bullish)
  • Shooting Star (bearish)

Reliable

  • Hanging Man (bearish)
  • Three Line Strike (bullish)
  • Two Black Gapping (bearish)
  • Abandoned Baby (bullish)
  • Morning Doji Star (bullish)
  • Evening Doji Star (bearish)

Strong

  • Three White Soldiers (bullish)
  • Three Black Crows (bearish)
  • Morning Star (bullish)
  • Evening Star (bearish)

We’ve encoded these candlestick patterns into Python functions, employing Numpy for some of the more intricate patterns.

import numpy as np

def candle_hammer(df: pd.DataFrame = None) -> pd.Series:
"""* Candlestick Detected: Hammer ("Weak - Reversal - Bullish Signal - Up"""

# Fill NaN values with 0
df = df.fillna(0)

return (
((df["high"] - df["low"]) > 3 * (df["open"] - df["close"]))
& (((df["close"] - df["low"]) / (0.001 + df["high"] - df["low"])) > 0.6)
& (((df["open"] - df["low"]) / (0.001 + df["high"] - df["low"])) > 0.6)
)


def candle_inverted_hammer(df: pd.DataFrame = None) -> pd.Series:
"""* Candlestick Detected: Inverted Hammer ("Weak - Reversal - Bullish Pattern - Up")"""

# Fill NaN values with 0
df = df.fillna(0)

return (
((df["high"] - df["low"]) > 3 * (df["open"] - df["close"]))
& ((df["high"] - df["close"]) / (0.001 + df["high"] - df["low"]) > 0.6)
& ((df["high"] - df["open"]) / (0.001 + df["high"] - df["low"]) > 0.6)
)


def candle_shooting_star(df: pd.DataFrame = None) -> pd.Series:
"""* Candlestick Detected: Shooting Star ("Weak - Reversal - Bearish Pattern - Down")"""

# Fill NaN values with 0
df = df.fillna(0)

return (
((df["open"].shift(1) < df["close"].shift(1)) & (df["close"].shift(1) < df["open"]))
& (df["high"] - np.maximum(df["open"], df["close"]) >= (abs(df["open"] - df["close"]) * 3))
& ((np.minimum(df["close"], df["open"]) - df["low"]) <= abs(df["open"] - df["close"]))
)


def candle_hanging_man(df: pd.DataFrame = None) -> pd.Series:
"""* Candlestick Detected: Hanging Man ("Weak - Reliable - Bearish Pattern - Down")"""

# Fill NaN values with 0
df = df.fillna(0)

return (
((df["high"] - df["low"]) > (4 * (df["open"] - df["close"])))
& (((df["close"] - df["low"]) / (0.001 + df["high"] - df["low"])) >= 0.75)
& (((df["open"] - df["low"]) / (0.001 + df["high"] - df["low"])) >= 0.75)
& (df["high"].shift(1) < df["open"])
& (df["high"].shift(2) < df["open"])
)


def candle_three_white_soldiers(df: pd.DataFrame = None) -> pd.Series:
"""*** Candlestick Detected: Three White Soldiers ("Strong - Reversal - Bullish Pattern - Up")"""

# Fill NaN values with 0
df = df.fillna(0)

return (
((df["open"] > df["open"].shift(1)) & (df["open"] < df["close"].shift(1)))
& (df["close"] > df["high"].shift(1))
& (df["high"] - np.maximum(df["open"], df["close"]) < (abs(df["open"] - df["close"])))
& ((df["open"].shift(1) > df["open"].shift(2)) & (df["open"].shift(1) < df["close"].shift(2)))
& (df["close"].shift(1) > df["high"].shift(2))
& (
df["high"].shift(1) - np.maximum(df["open"].shift(1), df["close"].shift(1))
< (abs(df["open"].shift(1) - df["close"].shift(1)))
)
)


def candle_three_black_crows(df: pd.DataFrame = None) -> pd.Series:
"""* Candlestick Detected: Three Black Crows ("Strong - Reversal - Bearish Pattern - Down")"""

# Fill NaN values with 0
df = df.fillna(0)

return (
((df["open"] < df["open"].shift(1)) & (df["open"] > df["close"].shift(1)))
& (df["close"] < df["low"].shift(1))
& (df["low"] - np.maximum(df["open"], df["close"]) < (abs(df["open"] - df["close"])))
& ((df["open"].shift(1) < df["open"].shift(2)) & (df["open"].shift(1) > df["close"].shift(2)))
& (df["close"].shift(1) < df["low"].shift(2))
& (
df["low"].shift(1) - np.maximum(df["open"].shift(1), df["close"].shift(1))
< (abs(df["open"].shift(1) - df["close"].shift(1)))
)
)


def candle_doji(df: pd.DataFrame = None) -> pd.Series:
"""! Candlestick Detected: Doji ("Indecision / Neutral")"""

# Fill NaN values with 0
df = df.fillna(0)

return (
((abs(df["close"] - df["open"]) / (df["high"] - df["low"])) < 0.1)
& ((df["high"] - np.maximum(df["close"], df["open"])) > (3 * abs(df["close"] - df["open"])))
& ((np.minimum(df["close"], df["open"]) - df["low"]) > (3 * abs(df["close"] - df["open"])))
)


def candle_three_line_strike(df: pd.DataFrame = None) -> pd.Series:
"""** Candlestick Detected: Three Line Strike ("Reliable - Reversal - Bullish Pattern - Up")"""

# Fill NaN values with 0
df = df.fillna(0)

return (
((df["open"].shift(1) < df["open"].shift(2)) & (df["open"].shift(1) > df["close"].shift(2)))
& (df["close"].shift(1) < df["low"].shift(2))
& (
df["low"].shift(1) - np.maximum(df["open"].shift(1), df["close"].shift(1))
< (abs(df["open"].shift(1) - df["close"].shift(1)))
)
& ((df["open"].shift(2) < df["open"].shift(3)) & (df["open"].shift(2) > df["close"].shift(3)))
& (df["close"].shift(2) < df["low"].shift(3))
& (
df["low"].shift(2) - np.maximum(df["open"].shift(2), df["close"].shift(2))
< (abs(df["open"].shift(2) - df["close"].shift(2)))
)
& ((df["open"] < df["low"].shift(1)) & (df["close"] > df["high"].shift(3)))
)


def candle_two_black_gapping(df: pd.DataFrame = None) -> pd.Series:
"""*** Candlestick Detected: Two Black Gapping ("Reliable - Reversal - Bearish Pattern - Down")"""

# Fill NaN values with 0
df = df.fillna(0)

return (
((df["open"] < df["open"].shift(1)) & (df["open"] > df["close"].shift(1)))
& (df["close"] < df["low"].shift(1))
& (df["low"] - np.maximum(df["open"], df["close"]) < (abs(df["open"] - df["close"])))
& (df["high"].shift(1) < df["low"].shift(2))
)


def candle_morning_star(df: pd.DataFrame = None) -> pd.Series:
"""*** Candlestick Detected: Morning Star ("Strong - Reversal - Bullish Pattern - Up")"""

# Fill NaN values with 0
df = df.fillna(0)

return (
(np.maximum(df["open"].shift(1), df["close"].shift(1)) < df["close"].shift(2)) & (df["close"].shift(2) < df["open"].shift(2))
) & ((df["close"] > df["open"]) & (df["open"] > np.maximum(df["open"].shift(1), df["close"].shift(1))))


def candle_evening_star(df: pd.DataFrame = None) -> np.ndarray:
"""*** Candlestick Detected: Evening Star ("Strong - Reversal - Bearish Pattern - Down")"""

# Fill NaN values with 0
df = df.fillna(0)

return (
(np.minimum(df["open"].shift(1), df["close"].shift(1)) > df["close"].shift(2)) & (df["close"].shift(2) > df["open"].shift(2))
) & ((df["close"] < df["open"]) & (df["open"] < np.minimum(df["open"].shift(1), df["close"].shift(1))))


def candle_abandoned_baby(df: pd.DataFrame = None) -> pd.Series:
"""** Candlestick Detected: Abandoned Baby ("Reliable - Reversal - Bullish Pattern - Up")"""

# Fill NaN values with 0
df = df.fillna(0)

return (
(df["open"] < df["close"])
& (df["high"].shift(1) < df["low"])
& (df["open"].shift(2) > df["close"].shift(2))
& (df["high"].shift(1) < df["low"].shift(2))
)


def candle_morning_doji_star(df: pd.DataFrame = None) -> pd.Series:
"""** Candlestick Detected: Morning Doji Star ("Reliable - Reversal - Bullish Pattern - Up")"""

# Fill NaN values with 0
df = df.fillna(0)

return (df["close"].shift(2) < df["open"].shift(2)) & (
abs(df["close"].shift(2) - df["open"].shift(2)) / (df["high"].shift(2) - df["low"].shift(2)) >= 0.7
) & (abs(df["close"].shift(1) - df["open"].shift(1)) / (df["high"].shift(1) - df["low"].shift(1)) < 0.1) & (
df["close"] > df["open"]
) & (
abs(df["close"] - df["open"]) / (df["high"] - df["low"]) >= 0.7
) & (
df["close"].shift(2) > df["close"].shift(1)
) & (
df["close"].shift(2) > df["open"].shift(1)
) & (
df["close"].shift(1) < df["open"]
) & (
df["open"].shift(1) < df["open"]
) & (
df["close"] > df["close"].shift(2)
) & (
(df["high"].shift(1) - np.maximum(df["close"].shift(1), df["open"].shift(1)))
> (3 * abs(df["close"].shift(1) - df["open"].shift(1)))
) & (
np.minimum(df["close"].shift(1), df["open"].shift(1)) - df["low"].shift(1)
) > (
3 * abs(df["close"].shift(1) - df["open"].shift(1))
)


def candle_evening_doji_star(df: pd.DataFrame = None) -> pd.Series:
"""** Candlestick Detected: Evening Doji Star ("Reliable - Reversal - Bearish Pattern - Down")"""

# Fill NaN values with 0
df = df.fillna(0)

return (df["close"].shift(2) > df["open"].shift(2)) & (
abs(df["close"].shift(2) - df["open"].shift(2)) / (df["high"].shift(2) - df["low"].shift(2)) >= 0.7
) & (abs(df["close"].shift(1) - df["open"].shift(1)) / (df["high"].shift(1) - df["low"].shift(1)) < 0.1) & (
df["close"] < df["open"]
) & (
abs(df["close"] - df["open"]) / (df["high"] - df["low"]) >= 0.7
) & (
df["close"].shift(2) < df["close"].shift(1)
) & (
df["close"].shift(2) < df["open"].shift(1)
) & (
df["close"].shift(1) > df["open"]
) & (
df["open"].shift(1) > df["open"]
) & (
df["close"] < df["close"].shift(2)
) & (
(df["high"].shift(1) - np.maximum(df["close"].shift(1), df["open"].shift(1)))
> (3 * abs(df["close"].shift(1) - df["open"].shift(1)))
) & (
np.minimum(df["close"].shift(1), df["open"].shift(1)) - df["low"].shift(1)
) > (
3 * abs(df["close"].shift(1) - df["open"].shift(1))
)

Let’s explore our dataset to see if we can identify any of the Strong candlestick patterns…

if __name__ == "__main__":
df = get_ohlc_data()

df["three_white_soldiers"] = candle_three_white_soldiers(df)
df["three_black_crows"] = candle_three_black_crows(df)
df["morning_star"] = candle_morning_star(df)
df["evening_star"] = candle_evening_star(df)

print(df[(df["three_white_soldiers"] == True) | (df["three_black_crows"] == True) | (df["morning_star"] == True) | (df["evening_star"] == True)])

Indeed, the Strong patterns have appeared multiple times throughout the past 30 days. From the data shared, it’s possible to discern when a candlestick pattern was identified and its type. An engaging exercise would be to examine an S&P 500 hourly graph for those specific intervals and attempt to pinpoint the patterns.

Conclusion

It’s important to remember that no single strategy offers a guarantee of success in trading. The viability of strategies based on candlestick patterns, like any trading approach, is subject to market dynamics, liquidity, and volatility. Thus, traders are advised to incorporate these strategies within a more comprehensive, diversified approach to trading, enhancing them with additional analyses and tools for a well-rounded perspective.

For those eager to delve deeper into such insightful analyses and broaden their understanding of Python’s application in financial markets, we invite you to follow our account. Stay tuned for more valuable articles that aim to enhance your data science skills and market analysis capabilities.

--

--

EODHD APIs

eodhd.com — stock market fundamental and historical prices API for stocks, ETFs, mutual funds and bonds all over the world.