Skip to content

Candlestick Patterns

TulipRS includes a dedicated candlestick recognition engine covering 77+ classical patterns — from single-bar formations such as Hammer and Shooting Star through to four-bar structures such as Concealing Baby Swallow. Every pattern carries a Japanese name and a forecast type (bullish/bearish reversal or continuation), and all patterns are detected in a single pass over the input bars. The candlestick engine uses internally computed body and wick size averages, plus a trend signal, to ensure that context-dependent patterns (e.g. Hanging Man vs Hammer) are classified correctly.

Output shape differs from regular indicators

The candlestick API takes the same NumPy array inputs as every other indicator in TulipRS, but its output is different — instead of a numeric series it returns a list of pattern-match lists (one entry per output bar, each entry being a list of matched pattern dicts).


Options

The candlestick engine accepts three options in the following order:

Position Name Description
options[0] candle_period Lookback window used to compute rolling averages for body size and upper/lower wick size. A larger value smooths out the reference size over more bars — use a value greater than 5 in practice.
options[1] trend_period Lookback window for the raw trend calculation. Controls how many bars are included when determining whether the market is in an uptrend or downtrend at the point of pattern detection.
options[2] trend_signal_period Smoothing period applied to the raw trend value to produce a trend signal. Higher values reduce noise in trend classification.

Basic Usage

use tulip_rs::indicators::candlestick::indicator;

let open  = vec![81.85_f64, 81.20, 81.55, 82.91, 83.10, 83.41, 82.71, 82.70, 84.20, 84.25];
let high  = vec![82.15_f64, 81.89, 83.03, 83.30, 83.85, 83.90, 83.33, 84.30, 84.84, 85.00];
let low   = vec![81.29_f64, 80.64, 81.31, 82.65, 83.07, 83.11, 82.49, 82.30, 84.15, 84.11];
let close = vec![81.59_f64, 81.06, 82.87, 83.00, 83.61, 83.15, 82.84, 83.99, 84.55, 84.36];

let inputs  = [open.as_slice(), high.as_slice(), low.as_slice(), close.as_slice()];
let options = [5.0_f64, 1.0, 1.0]; // candle_period, trend_period, trend_signal_period

// result is Vec<Option<Vec<Pattern>>> — one entry per output bar
// Pattern is an enum; call .get_info() on each variant to retrieve its metadata
let (result, mut state) = indicator(&inputs, &options, None).unwrap();

for (i, bar) in result.iter().enumerate() {
    if let Some(patterns) = bar.as_ref() {
        for pattern in patterns {
            let info = pattern.get_info();
            println!("Bar {i}: {} ({}), bars: {}",
                info.full_name, info.japanese_name, info.bars);
        }
    }
}
import numpy as np
import tulip_rs

cdl = tulip_rs.indicators.candlestick

open_  = np.array([81.85, 81.20, 81.55, 82.91, 83.10, 83.41, 82.71, 82.70, 84.20, 84.25,
                   87.30, 86.40, 84.30, 85.60], dtype=np.float64)
high_  = np.array([82.15, 81.89, 83.03, 83.30, 83.85, 83.90, 83.33, 84.30, 84.84, 85.00,
                   87.30, 86.40, 85.50, 85.65], dtype=np.float64)
low_   = np.array([81.29, 80.64, 81.31, 82.65, 83.07, 83.11, 82.49, 82.30, 84.15, 84.11,
                   86.30, 85.30, 84.00, 83.85], dtype=np.float64)
close_ = np.array([81.59, 81.06, 82.87, 83.00, 83.61, 83.15, 82.84, 83.99, 84.55, 84.36,
                   86.30, 85.30, 84.00, 83.90], dtype=np.float64)

options = [5.0, 1.0, 1.0]  # candle_period, trend_period, trend_signal_period

# Run all patterns (matching current trend direction)
result, state = cdl.candlestick(open_, high_, low_, close_, options=options)

# result is a list — one entry per output bar — each entry is a list of pattern dicts
for i, bar_patterns in enumerate(result):
    if bar_patterns:
        for p in bar_patterns:
            print(f"Bar {i}: {p['full_name']} ({p['japanese_name']}) — {p['forecast']}")
import * as ti from 'tulip-rs-node';

const open  = [81.85, 81.20, 81.55, 82.91, 83.10, 83.41, 82.71, 82.70, 84.20, 84.25,
               84.03, 85.45, 86.18, 88.00, 87.30, 87.30, 86.40, 84.30, 85.60];
const high  = [82.15, 81.89, 83.03, 83.30, 83.85, 83.90, 83.33, 84.30, 84.84, 85.00,
               85.90, 86.58, 86.98, 88.00, 87.31, 87.30, 86.40, 85.50, 85.65];
const low   = [81.29, 80.64, 81.31, 82.65, 83.07, 83.11, 82.49, 82.30, 84.15, 84.11,
               84.03, 85.39, 85.76, 87.17, 87.20, 86.30, 85.30, 84.00, 83.85];
const close = [81.59, 81.06, 82.87, 83.00, 83.61, 83.15, 82.84, 83.99, 84.55, 84.36,
               85.53, 86.54, 86.89, 87.77, 87.29, 86.30, 85.30, 84.00, 83.90];
const options = [5, 1, 1]; // candle_period, trend_period, trend_signal_period

const [result, state] = ti.candlestick.indicator([open, high, low, close], options);

result.forEach((patterns, i) => {
    if (patterns && patterns.length > 0) {
        patterns.forEach(p => {
            console.log(`Bar ${i}: ${p.fullName} (${p.japaneseName}), bars: ${p.bars}, forecast: ${p.forecast}`);
        });
    }
});

Each matched pattern is returned as a dict (Python) or a struct (Rust) with the following fields:

Field Type Description
name str Short machine-readable pattern name
full_name str Full English pattern name
japanese_name str Traditional Japanese name
bars int Number of bars the pattern spans (1–4)
forecast str / enum One of BullishReversal, BearishReversal, BullishContinuation, BearishContinuation

Filtering by Forecast Type

Pass a forecast_type argument to return only patterns with a specific forecast. This is useful when you only care about, for example, bullish reversal setups.

use tulip_rs::indicators::candlestick::{indicator, ForecastType};

let inputs  = [open.as_slice(), high.as_slice(), low.as_slice(), close.as_slice()];
let options = [5.0_f64, 1.0, 1.0];

// Only bullish reversal patterns
let (result, _) = indicator(&inputs, &options, Some(ForecastType::BullishReversal)).unwrap();

// Inspect the last bar for matches
if let Some(patterns) = result.last().and_then(|bar| bar.as_ref()) {
    for pattern in patterns {
        let info = pattern.get_info();
        println!("  - {} ({}), bars: {}",
            info.full_name, info.japanese_name, info.bars);
    }
}

// Other available variants:
// ForecastType::BearishReversal
// ForecastType::BullishContinuation
// ForecastType::BearishContinuation
import tulip_rs

cdl          = tulip_rs.indicators.candlestick
ForecastType = cdl.ForecastType

options = [5.0, 1.0, 1.0]

# Only bullish reversal patterns
result, _ = cdl.candlestick(open_, high_, low_, close_, options=options,
                            forecast_type=ForecastType.BullishReversal)

# Other available values:
# ForecastType.BearishReversal
# ForecastType.BullishContinuation
# ForecastType.BearishContinuation

for i, bar_patterns in enumerate(result):
    if bar_patterns:
        for p in bar_patterns:
            print(f"Bar {i}: {p['full_name']}{p['forecast']}")
import * as ti from 'tulip-rs-node';

const options = [5, 1, 1];

// Only bullish reversal patterns — pass the filter string as the third argument
const [result] = ti.candlestick.indicator([open, high, low, close], options, 'BullishReversal');

result.forEach((patterns, i) => {
    if (patterns && patterns.length > 0) {
        patterns.forEach(p => console.log(`Bar ${i}: ${p.fullName}${p.forecast}`));
    }
});

// Other available filter strings:
// 'BearishReversal'
// 'BullishContinuation'
// 'BearishContinuation'
// 'BullishReversalOrContinuation'
// 'BearishReversalOrContinuation'

Omit the third argument (or pass undefined) to return all trend-matching patterns.

When forecast_type is omitted (or None), all matched patterns are returned regardless of their forecast direction.


State Continuation

Like every other indicator in TulipRS, the candlestick engine returns a state object that you can use to continue detection on new bars without reprocessing history. Pass only the new bars to batch_indicator.

use tulip_rs::indicators::candlestick::indicator;

let open  = vec![81.85_f64, 81.20, 81.55, 82.91, 83.10, 83.41, 82.71, 82.70, 84.20, 84.25];
let high  = vec![82.15_f64, 81.89, 83.03, 83.30, 83.85, 83.90, 83.33, 84.30, 84.84, 85.00];
let low   = vec![81.29_f64, 80.64, 81.31, 82.65, 83.07, 83.11, 82.49, 82.30, 84.15, 84.11];
let close = vec![81.59_f64, 81.06, 82.87, 83.00, 83.61, 83.15, 82.84, 83.99, 84.55, 84.36];

let inputs  = [open.as_slice(), high.as_slice(), low.as_slice(), close.as_slice()];
let options = [5.0_f64, 1.0, 1.0];

// Step 1: run on historical data and capture state
let (_, mut state) = indicator(&inputs, &options, None).unwrap();

// Step 2: feed only the new bars
let new_open  = [84.00_f64];
let new_high  = [84.50_f64];
let new_low   = [83.20_f64];
let new_close = [83.50_f64];
let new_inputs = [new_open.as_slice(), new_high.as_slice(),
                  new_low.as_slice(), new_close.as_slice()];

let result = state.batch_indicator(&new_inputs, None).unwrap();

if let Some(patterns) = result.last().and_then(|bar| bar.as_ref()) {
    for pattern in patterns {
        let info = pattern.get_info();
        println!("  - {} ({}), bars: {}",
            info.full_name, info.japanese_name, info.bars);
    }
}
import numpy as np
import tulip_rs

cdl     = tulip_rs.indicators.candlestick
options = [5.0, 1.0, 1.0]

# Step 1: run on historical data and capture state
_result, state = cdl.candlestick(open_, high_, low_, close_, options=options)

# Step 2: feed new bars as NumPy arrays
new_open  = np.array([84.00], dtype=np.float64)
new_high  = np.array([84.50], dtype=np.float64)
new_low   = np.array([83.20], dtype=np.float64)
new_close = np.array([83.50], dtype=np.float64)

new_result = state.batch_indicator([new_open, new_high, new_low, new_close])

entry = new_result[0]  # patterns detected on this new bar
if entry:
    for p in entry:
        print(f"{p['full_name']}{p['forecast']}")
import * as ti from 'tulip-rs-node';

const options = [5, 1, 1];

// Step 1: run on historical data and capture state
const [, state] = ti.candlestick.indicator([open, high, low, close], options);

// Step 2: feed only the new bar
const newOpen  = [84.00];
const newHigh  = [84.50];
const newLow   = [83.20];
const newClose = [83.50];

// Continue without filter
const newResult = state.batchIndicator([newOpen, newHigh, newLow, newClose]);

// Or with a forecast filter
const bullishOnly = state.batchIndicator([newOpen, newHigh, newLow, newClose], 'BullishReversal');

const entry = newResult[0];
if (entry && entry.length > 0) {
    entry.forEach(p => {
        console.log(`${p.fullName} (${p.japaneseName}), bars: ${p.bars}, forecast: ${p.forecast}`);
    });
} else {
    console.log('No patterns on new bar.');
}

Pattern Reference

One-Bar Patterns

Pattern Japanese Name Forecast
Hammer Kanazuchi BullishReversal
Hanging Man Kubitsuri BearishReversal
Bullish Belt Hold Yorikiri BullishReversal
Bearish Belt Hold Yorikiri BearishReversal
Bullish Strong Line Yorikiri Sen BullishContinuation
Bearish Strong Line Yorikiri Sen BearishContinuation
Northern Doji Kita no Doji BearishReversal
Southern Doji Minami no Doji BullishReversal
Gapping Up Doji Ue-hanare Doji BearishReversal
Gapping Down Doji Shita-hanare Doji BullishReversal
Shooting Star Nagare Boshi BearishReversal
Takuri Line Takuri BullishReversal

Two-Bar Patterns

Pattern Forecast
Bullish Engulfing BullishReversal
Bearish Engulfing BearishReversal
Dark Cloud Cover BearishReversal
Piercing BullishReversal
Bullish Harami BullishReversal
Bearish Harami BearishReversal
Bullish Harami Cross BullishReversal
Bearish Harami Cross BearishReversal
Inverted Hammer BullishReversal
Bullish Doji Star BullishReversal
Bearish Doji Star BearishReversal
Kicking Bullish BullishReversal
Kicking Bearish BearishReversal
Meeting Lines Bullish BullishReversal
Meeting Lines Bearish BearishReversal
On Neck BearishContinuation
In Neck BearishContinuation
Thrusting BearishContinuation

Three-Bar Patterns

Pattern Forecast
Three White Soldiers BullishReversal
Three Black Crows BearishReversal
Morning Star BullishReversal
Morning Doji Star BullishReversal
Evening Star BearishReversal
Evening Doji Star BearishReversal
Three Inside Up BullishReversal
Three Inside Down BearishReversal
Three Outside Up BullishReversal
Three Outside Down BearishReversal
Bullish Tristar BullishReversal
Bearish Tristar BearishReversal
Advance Block BearishReversal
Deliberation BearishReversal
Unique Three River Bottom BullishReversal
Three Stars in the South BullishReversal
Upside Gap Three Methods BullishContinuation
Downside Gap Three Methods BearishContinuation
Upside Tasuki Gap BullishContinuation
Downside Tasuki Gap BearishContinuation
Upside Gap Two Crows BearishReversal
Two Crows BearishReversal
Identical Three Crows BearishReversal
Bull Side-by-Side White Lines BullishContinuation
Bear Side-by-Side White Lines BearishContinuation
Collapsing Doji Star BearishReversal
Bull Abandoned Baby BullishReversal
Bear Abandoned Baby BearishReversal

Four-Bar Patterns

Pattern Forecast
Concealing Baby Swallow BullishReversal
Bullish Three Line Strike BullishContinuation
Bearish Three Line Strike BearishContinuation