Candlestick Patterns
Detect and classify classic Japanese candlestick patterns including doji, hammer, shooting star, engulfing, morning star, evening star, and harami formations using algorithmic pattern recognition on OHLCV price data.
Strategy — Candlestick Pattern Detection
1. Dependency Installation
!pip install pandas numpy plotlyRequirement already satisfied: pandas in /usr/local/lib/python3.12/dist-packages (2.2.2) Requirement already satisfied: numpy in /usr/local/lib/python3.12/dist-packages (2.0.2) Requirement already satisfied: plotly in /usr/local/lib/python3.12/dist-packages (5.24.1) Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.12/dist-packages (from pandas) (2.9.0.post0) Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.12/dist-packages (from pandas) (2025.2) Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.12/dist-packages (from pandas) (2026.1) Requirement already satisfied: tenacity>=6.2.0 in /usr/local/lib/python3.12/dist-packages (from plotly) (9.1.4) Requirement already satisfied: packaging in /usr/local/lib/python3.12/dist-packages (from plotly) (26.1) Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)
2. Library Imports
import warnings; warnings.filterwarnings("ignore")
import pandas as pd
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots3. Strategy Overview
Candlestick patterns are price formations derived from a single bar's open, high, low, and close (OHLC) values. Each pattern encodes a specific balance of buying and selling pressure within the period.
Core pattern logic:
| Pattern | Condition | Signal |
|---|---|---|
| Doji | abs(close - open) / (high - low) < 0.1 | Indecision (0) |
| Hammer | Body in upper 30 %, lower wick ≥ 2× body | Bullish reversal (+1) |
| Shooting Star | Body in lower 30 %, upper wick ≥ 2× body | Bearish reversal (−1) |
| Bullish Engulfing | Close > prev open AND Open < prev close | Bullish (+1) |
| Bearish Engulfing | Close < prev open AND Open > prev close | Bearish (−1) |
Signal logic:
+1— Bullish pattern detected; potential long entry.−1— Bearish pattern detected; potential short entry.0— No actionable pattern.
Limitation: Single-candle patterns are low-precision in isolation; they require confirmation from trend, volume, or multi-timeframe context.
4. Data Generation
def generate_data(periods: int) -> pd.DataFrame:
"""
Generate synthetic OHLCV price data using a geometric random walk.
Parameters
----------
periods : int
Number of 1-minute bars to generate.
Returns
-------
pd.DataFrame
DataFrame with columns: open, high, low, close, volume, datetime.
"""
start_date = pd.to_datetime("2024-01-01 00:00:00+00:00")
datetime_index = pd.date_range(start_date, periods=periods, freq="1min", tz="UTC")
price_data = []
last_close = 42000
for i in range(periods):
open_price = last_close + np.random.normal(0, last_close * 0.0005)
close_price = open_price + np.random.normal(0, last_close * 0.005)
body_high = max(open_price, close_price)
body_low = min(open_price, close_price)
high_price = max(body_high + abs(np.random.normal(0, last_close * 0.002)), open_price, close_price)
low_price = min(body_low - abs(np.random.normal(0, last_close * 0.002)), open_price, close_price)
if high_price < low_price:
high_price, low_price = low_price, high_price
price_data.append({
"open": max(1, int(open_price)),
"high": max(1, int(high_price)),
"low": max(1, int(low_price)),
"close": max(1, int(close_price)),
})
last_close = close_price
df = pd.DataFrame(price_data, index=datetime_index)
df.index.name = "datetime"
df["volume"] = np.random.uniform(100.0, 500.0, periods)
df["datetime"] = df.index.to_series()
return df.reset_index(drop=True)
df = generate_data(500)
display(df.head())| open | high | low | close | volume | datetime | |
|---|---|---|---|---|---|---|
| 0 | 41978 | 42177 | 41708 | 41881 | 470.022377 | 2024-01-01 00:00:00+00:00 |
| 1 | 41871 | 41888 | 41595 | 41697 | 170.651371 | 2024-01-01 00:01:00+00:00 |
| 2 | 41699 | 41725 | 41202 | 41238 | 471.939920 | 2024-01-01 00:02:00+00:00 |
| 3 | 41261 | 41276 | 41166 | 41256 | 131.061953 | 2024-01-01 00:03:00+00:00 |
| 4 | 41262 | 41544 | 41206 | 41484 | 436.578600 | 2024-01-01 00:04:00+00:00 |
5. Strategy Function
def candlestick_patterns(df: pd.DataFrame) -> pd.DataFrame:
"""
Detect common single- and two-candle patterns and emit trading signals.
Core logic
----------
1. Compute body size, full range, and wick lengths.
2. Classify each bar into one of five pattern types using ratio thresholds.
3. Map each pattern to a directional signal (+1 bullish, -1 bearish, 0 neutral).
Parameters
----------
df : pd.DataFrame
OHLCV DataFrame with columns: open, high, low, close, volume, datetime.
Returns
-------
pd.DataFrame
Original DataFrame extended with: body, full_range, upper_wick,
lower_wick, pattern, signal.
"""
df = df.copy().sort_values("datetime", ignore_index=True)
# ── Candle geometry ──────────────────────────────────────────────────────
df["body"] = (df["close"] - df["open"]).abs() # absolute body size
df["full_range"] = df["high"] - df["low"] # bar range
df["upper_wick"] = df["high"] - df[["open", "close"]].max(axis=1)
df["lower_wick"] = df[["open", "close"]].min(axis=1) - df["low"]
# ── Pattern classification ───────────────────────────────────────────────
range_safe = df["full_range"].replace(0, np.nan)
# Doji: body occupies < 10 % of the full range → indecision
doji = df["body"] / range_safe < 0.1
# Hammer: body in upper 30 % of range; lower wick ≥ 2× body → bullish reversal
hammer = (
(df["lower_wick"] >= 2 * df["body"].replace(0, np.nan)) &
(df[["open", "close"]].min(axis=1) > df["low"] + 0.7 * df["full_range"])
)
# Shooting Star: body in lower 30 % of range; upper wick ≥ 2× body → bearish reversal
shooting_star = (
(df["upper_wick"] >= 2 * df["body"].replace(0, np.nan)) &
(df[["open", "close"]].max(axis=1) < df["low"] + 0.3 * df["full_range"])
)
# Bullish Engulfing: current close engulfs prior open (two-candle)
bull_engulf = (df["close"] > df["open"].shift(1)) & (df["open"] < df["close"].shift(1))
# Bearish Engulfing: current open engulfs prior close (two-candle)
bear_engulf = (df["close"] < df["open"].shift(1)) & (df["open"] > df["close"].shift(1))
# Assign pattern label (priority order)
df["pattern"] = "none"
df.loc[doji, "pattern"] = "doji"
df.loc[hammer, "pattern"] = "hammer"
df.loc[shooting_star,"pattern"] = "shooting_star"
df.loc[bull_engulf, "pattern"] = "bullish_engulfing"
df.loc[bear_engulf, "pattern"] = "bearish_engulfing"
# ── Signal mapping ───────────────────────────────────────────────────────
signal_map = {
"none": 0, "doji": 0,
"hammer": 1, "bullish_engulfing": 1,
"shooting_star": -1, "bearish_engulfing": -1,
}
df["signal"] = df["pattern"].map(signal_map)
return df
df_signals = candlestick_patterns(df)
print("--- Pattern Distribution ---")
print(df_signals["pattern"].value_counts())
print("\n--- Signal Distribution ---")
print(df_signals["signal"].value_counts())--- Pattern Distribution --- pattern none 250 bullish_engulfing 122 bearish_engulfing 108 doji 10 shooting_star 7 hammer 3 Name: count, dtype: int64 --- Signal Distribution --- signal 0 260 1 125 -1 115 Name: count, dtype: int64
Explanation:
body / full_range: Normalised body ratio used to identify Doji (< 0.10) conditions.lower_wick / body: Wick-to-body ratio used to classify Hammer and Shooting Star.- Two-candle engulfing patterns use
.shift(1)to reference the prior bar's OHLC.
6. Visualization
buy_signals = df_signals[df_signals["signal"] == 1]
sell_signals = df_signals[df_signals["signal"] == -1]
fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
subplot_titles=["Price + Pattern Signals", "Pattern Labels"],
row_heights=[0.7, 0.3])
# ── Candlestick chart ────────────────────────────────────────────────────────
fig.add_trace(go.Candlestick(
x=df_signals["datetime"],
open=df_signals["open"], high=df_signals["high"],
low=df_signals["low"], close=df_signals["close"],
name="Price"), row=1, col=1)
# ── Signal markers ───────────────────────────────────────────────────────────
fig.add_trace(go.Scatter(
x=buy_signals["datetime"], y=buy_signals["low"] * 0.999,
mode="markers", marker=dict(symbol="triangle-up", size=10, color="green"),
name="Bullish (+1)"), row=1, col=1)
fig.add_trace(go.Scatter(
x=sell_signals["datetime"], y=sell_signals["high"] * 1.001,
mode="markers", marker=dict(symbol="triangle-down", size=10, color="red"),
name="Bearish (-1)"), row=1, col=1)
# ── Pattern signal line ──────────────────────────────────────────────────────
fig.add_trace(go.Scatter(
x=df_signals["datetime"], y=df_signals["signal"],
mode="lines", name="Signal", line=dict(color="purple", width=1)),
row=2, col=1)
fig.add_hline(y=0, line_dash="dot", line_color="gray", row=2, col=1)
fig.update_layout(
title_text="Candlestick Pattern Detection",
xaxis_rangeslider_visible=False,
height=700,
xaxis2_title="Datetime",
)
fig.show()Conclusion
This notebook demonstrates how to detect common candlestick patterns and generate trading signals based on synthetic OHLCV data. The candlestick_patterns function classifies individual bars and two-bar formations (engulfing patterns) and assigns a directional signal (+1 for bullish, -1 for bearish, 0 for neutral). The visualization provides a clear overview of the price action, detected patterns, and corresponding signals. It's important to note that single-candle patterns often require further confirmation from other indicators or market context for robust trading strategies.