Signals·Patterns·Beginner
Candlestick Patterns
Detect classic candlestick patterns — doji, engulfing, hammer, shooting star, and morning/evening star — using TA-Lib.
candlestickTA-Libreversal
Strategy — Candlestick Pattern Detection
1. Dependency Installation
[1]
!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
[2]
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
[3]
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
[4]
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
[5]
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()[ ]