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 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

[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_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

[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()
[ ]