Candlestick Basics: Anatomy and Construction

Decode candlestick structure as compressed order flow and build candlestick features programmatically

25 min read
Beginner

Introduction

Candlestick charts are the most popular visualization method in technical analysis. But they're not just pretty graphics - each candlestick is a compressed representation of order flow within a time period.

This lesson deconstructs the candlestick from first principles:

  • What each component represents in auction market theory
  • How to interpret candlestick anatomy algorithmically
  • Building candlestick charts from OHLCV data
  • Calculating candlestick features for quantitative analysis

Candlestick Anatomy: Encoding Intraperiod Dynamics

A single candlestick compresses four critical price points (OHLC) plus volume into a visual format:

Candlestick Components
Component
Meaning
Market Interpretation
OpenFirst trade of the periodInitial consensus price
CloseLast trade of the periodFinal consensus (most important)
HighMaximum price reachedUpper boundary of acceptance
LowMinimum price reachedLower boundary of acceptance
BodyDistance between O and CDirectional conviction strength
Wick/ShadowDistance from body to H/LRejected price levels

The Body: Directional Conviction

The body (rectangle between open and close) represents the net directional movement during the period.

  • Bullish candle (Close > Open): Buyers dominated, price moved up
  • Bearish candle (Close < Open): Sellers dominated, price moved down
  • Body size indicates strength: Large body = strong conviction, small body = indecision

The body size relative to the Average True Range (ATR) indicates whether the period's movement was significant. A body that is 2x the ATR represents unusually strong directional pressure.

The Wicks: Rejected Price Levels

Upper wick (high - max(open, close)): Price tested higher levels but sellers rejected them Lower wick (min(open, close) - low): Price tested lower levels but buyers rejected them

Long wicks indicate failed attempts to move price in that direction - critical information about supply/demand boundaries.

Constructing Candlesticks from OHLCV Data

Let's build candlestick features programmatically:

python
import yfinance as yf
import pandas as pd
import numpy as np

# Download data
df = yf.download('AAPL', start='2024-01-01', end='2024-02-01', progress=False)

# Calculate candlestick features
df['Body'] = df['Close'] - df['Open']
df['Body_Pct'] = (df['Body'] / df['Open']) * 100
df['Upper_Wick'] = df['High'] - df[['Open', 'Close']].max(axis=1)
df['Lower_Wick'] = df[['Open', 'Close']].min(axis=1) - df['Low']
df['Total_Range'] = df['High'] - df['Low']
df['Body_Ratio'] = abs(df['Body']) / df['Total_Range']

print("Candlestick Features:")
print(df[['Open', 'High', 'Low', 'Close', 'Body', 'Upper_Wick', 'Lower_Wick', 'Body_Ratio']].head(10))

Key observations:

  • Body is negative for bearish candles, positive for bullish
  • Body_Ratio near 1.0 means the body fills most of the range (strong trend)
  • Body_Ratio near 0.0 means wicks dominate (indecision, reversal signal)

Candlestick Classification

We can classify candlesticks algorithmically based on body and wick characteristics:

python
def classify_candlestick(row):
    """
    Classify candlestick type based on body and wick ratios.

    Returns: string classification
    """
    body_ratio = row['Body_Ratio']
    body = row['Body']
    upper_wick = row['Upper_Wick']
    lower_wick = row['Lower_Wick']
    total_range = row['Total_Range']

    # Doji: body is <10% of total range
    if body_ratio < 0.1:
        return 'Doji'

    # Hammer/Hanging Man: small body, long lower wick
    if body_ratio < 0.3 and lower_wick > 2 * abs(body) and upper_wick < abs(body):
        return 'Hammer' if body > 0 else 'Hanging_Man'

    # Shooting Star/Inverted Hammer: small body, long upper wick
    if body_ratio < 0.3 and upper_wick > 2 * abs(body) and lower_wick < abs(body):
        return 'Shooting_Star' if body < 0 else 'Inverted_Hammer'

    # Marubozu: body is >80% of range (minimal wicks)
    if body_ratio > 0.8:
        return 'Bullish_Marubozu' if body > 0 else 'Bearish_Marubozu'

    # Default: regular candle
    return 'Bullish' if body > 0 else 'Bearish'

# Apply classification
df['Candle_Type'] = df.apply(classify_candlestick, axis=1)

print("Candlestick Classification:")
print(df[['Open', 'Close', 'Body_Ratio', 'Candle_Type']].head(15))

# Count distribution
print("\nCandle type distribution:")
print(df['Candle_Type'].value_counts())

Visualizing Candlesticks with Matplotlib

Create professional candlestick charts using matplotlib's financial plotting capabilities:

python
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches

def plot_candlestick(df, start_idx=0, num_candles=20):
    """
    Plot candlestick chart with custom styling.
    """
    data = df.iloc[start_idx:start_idx+num_candles].copy()
    data['Index'] = range(len(data))

    fig, ax = plt.subplots(figsize=(14, 6))

    # Plot each candlestick
    for idx, row in data.iterrows():
        x = row['Index']
        open_price = row['Open']
        close_price = row['Close']
        high_price = row['High']
        low_price = row['Low']

        # Determine color
        color = 'green' if close_price >= open_price else 'red'

        # Draw high-low line (wick)
        ax.plot([x, x], [low_price, high_price], color='black', linewidth=1)

        # Draw body rectangle
        body_height = abs(close_price - open_price)
        body_bottom = min(open_price, close_price)
        rect = mpatches.Rectangle((x - 0.3, body_bottom), 0.6, body_height,
                                   facecolor=color, edgecolor='black', linewidth=1)
        ax.add_patch(rect)

    ax.set_xlabel('Days', fontsize=12)
    ax.set_ylabel('Price ($)', fontsize=12)
    ax.set_title('AAPL Candlestick Chart', fontsize=14, fontweight='bold')
    ax.grid(True, alpha=0.3)

    plt.tight_layout()
    plt.savefig('candlestick_chart.png', dpi=150)
    print("Chart saved to candlestick_chart.png")

# Plot first 20 candles
plot_candlestick(df, start_idx=0, num_candles=20)

Pro tip: Use the mplfinance library for more advanced candlestick plotting with volume bars, indicators, and multiple subplots: pip install mplfinance

Candlestick Statistics and Distribution

Analyze candlestick characteristics statistically to understand market behavior:

python
# Download longer history
df_long = yf.download('AAPL', period='2y', progress=False)

# Calculate features
df_long['Body'] = df_long['Close'] - df_long['Open']
df_long['Body_Pct'] = (df_long['Body'] / df_long['Open']) * 100
df_long['Upper_Wick'] = df_long['High'] - df_long[['Open', 'Close']].max(axis=1)
df_long['Lower_Wick'] = df_long[['Open', 'Close']].min(axis=1) - df_long['Low']
df_long['Total_Range'] = df_long['High'] - df_long['Low']
df_long['Body_Ratio'] = abs(df_long['Body']) / df_long['Total_Range']

# Statistical summary
print("Candlestick Statistics (2 years AAPL):")
print("="*50)
print(f"Total candles: {len(df_long)}")
print(f"\nBody Size (% of open):")
print(f"  Mean: {df_long['Body_Pct'].mean():.3f}%")
print(f"  Std: {df_long['Body_Pct'].std():.3f}%")
print(f"  Median: {df_long['Body_Pct'].median():.3f}%")

print(f"\nBody Ratio (body/total range):")
print(f"  Mean: {df_long['Body_Ratio'].mean():.3f}")
print(f"  Median: {df_long['Body_Ratio'].median():.3f}")

print(f"\nWick Analysis:")
print(f"  Avg Upper Wick: ${df_long['Upper_Wick'].mean():.2f}")
print(f"  Avg Lower Wick: ${df_long['Lower_Wick'].mean():.2f}")
print(f"  Max Upper Wick: ${df_long['Upper_Wick'].max():.2f}")
print(f"  Max Lower Wick: ${df_long['Lower_Wick'].max():.2f}")

# Bullish vs bearish distribution
bullish = (df_long['Body'] > 0).sum()
bearish = (df_long['Body'] < 0).sum()
doji = (df_long['Body'] == 0).sum()

print(f"\nDirectional Distribution:")
print(f"  Bullish: {bullish} ({bullish/len(df_long)*100:.1f}%)")
print(f"  Bearish: {bearish} ({bearish/len(df_long)*100:.1f}%)")
print(f"  Doji: {doji} ({doji/len(df_long)*100:.1f}%)")

Key insights:

  • Body ratio ~0.5 means on average, half the range is body, half is wicks
  • Bullish/bearish split ~53/47 reflects AAPL's general uptrend over 2 years
  • Wicks are symmetric (~$1.20 average) suggesting balanced intraday volatility

Candlestick Context: Previous Candle Comparison

Single candles have limited information. Compare to previous candles for context:

python
# Calculate relative features
df_long['Body_vs_Prev'] = df_long['Body_Pct'] - df_long['Body_Pct'].shift(1)
df_long['Range_vs_Prev'] = df_long['Total_Range'] - df_long['Total_Range'].shift(1)
df_long['Close_vs_Prev_Close'] = df_long['Close'] - df_long['Close'].shift(1)

# Identify engulfing patterns
df_long['Bullish_Engulfing'] = (
    (df_long['Body'] > 0) &                           # Current is bullish
    (df_long['Body'].shift(1) < 0) &                  # Previous is bearish
    (df_long['Open'] < df_long['Close'].shift(1)) &   # Opens below prev close
    (df_long['Close'] > df_long['Open'].shift(1))     # Closes above prev open
)

df_long['Bearish_Engulfing'] = (
    (df_long['Body'] < 0) &                           # Current is bearish
    (df_long['Body'].shift(1) > 0) &                  # Previous is bullish
    (df_long['Open'] > df_long['Close'].shift(1)) &   # Opens above prev close
    (df_long['Close'] < df_long['Open'].shift(1))     # Closes below prev open
)

print("Engulfing Pattern Detection:")
print(f"Bullish Engulfing: {df_long['Bullish_Engulfing'].sum()} occurrences")
print(f"Bearish Engulfing: {df_long['Bearish_Engulfing'].sum()} occurrences")

# Show examples
print("\nBullish Engulfing Examples:")
bullish_eng = df_long[df_long['Bullish_Engulfing']].head(3)
for date, row in bullish_eng.iterrows():
    print(f"{date.date()}: Open=${row['Open']:.2f}, Close=${row['Close']:.2f}, Body=${row['Body']:.2f}")

Summary

Key Takeaways

  1. Candlesticks compress OHLC + volume into visual format representing intraperiod dynamics
  2. Body size indicates conviction - large bodies show strong directional pressure
  3. Wicks show rejection - long wicks indicate failed attempts to move price
  4. Body ratio (body/range) distinguishes trending candles (>0.7) from indecision (<0.3)
  5. Classification is algorithmic - Doji, Hammer, Marubozu can be detected programmatically
  6. Context matters - compare to previous candles using engulfing, continuation patterns

Next Steps

Now that you understand candlestick structure, the next lesson covers candlestick patterns: multi-candle formations that signal potential reversals or continuations. We'll build pattern detection algorithms and test their statistical validity.