Signals·Chart Pattern Detection·Intermediate

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.

pattern-recognitiontrading-signals

Strategy — Candlestick Pattern Detection


1. Dependency Installation

[ ]
!pip install pandas numpy plotly
Requirement 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_subplots

3. 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:

PatternConditionSignal
Dojiabs(close - open) / (high - low) < 0.1Indecision (0)
HammerBody in upper 30 %, lower wick ≥ 2× bodyBullish reversal (+1)
Shooting StarBody in lower 30 %, upper wick ≥ 2× bodyBearish reversal (−1)
Bullish EngulfingClose > prev open AND Open < prev closeBullish (+1)
Bearish EngulfingClose < prev open AND Open > prev closeBearish (−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.