Source code for wraquant.ta.patterns

"""Candlestick pattern recognition.

Each function returns a ``pd.Series`` of integers:

- **1** — bullish signal
- **-1** — bearish signal
- **0** — no pattern detected

Some patterns are inherently one-directional (e.g. Morning Star is always
bullish), but where a pattern has both bullish and bearish variants the
sign distinguishes them.
"""

from __future__ import annotations

import numpy as np
import pandas as pd

__all__ = [
    "doji",
    "hammer",
    "engulfing",
    "morning_star",
    "evening_star",
    "three_white_soldiers",
    "three_black_crows",
    "harami",
    "spinning_top",
    "marubozu",
    "piercing_pattern",
    "dark_cloud_cover",
    "hanging_man",
    "inverted_hammer",
    "shooting_star",
    "tweezer_top",
    "tweezer_bottom",
    "three_inside_up",
    "three_inside_down",
    "abandoned_baby",
    "kicking",
    "belt_hold",
    "rising_three_methods",
    "falling_three_methods",
    "tasuki_gap",
    "on_neck",
    "in_neck",
    "thrusting",
    "separating_lines",
    "closing_marubozu",
    "rickshaw_man",
    "long_legged_doji",
    "dragonfly_doji",
    "gravestone_doji",
    "tri_star",
    "unique_three_river",
    "concealing_baby_swallow",
]


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


from wraquant.ta._validators import validate_series as _validate_series


def _body(open_: pd.Series, close: pd.Series) -> pd.Series:
    """Absolute body size."""
    return (close - open_).abs()


def _range(high: pd.Series, low: pd.Series) -> pd.Series:
    """Full candle range (high - low)."""
    return high - low


def _upper_shadow(open_: pd.Series, high: pd.Series, close: pd.Series) -> pd.Series:
    return high - pd.concat([open_, close], axis=1).max(axis=1)


def _lower_shadow(open_: pd.Series, low: pd.Series, close: pd.Series) -> pd.Series:
    return pd.concat([open_, close], axis=1).min(axis=1) - low


# ---------------------------------------------------------------------------
# Doji
# ---------------------------------------------------------------------------


[docs] def doji( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, threshold: float = 0.05, ) -> pd.Series: """Doji pattern. A Doji occurs when the body is very small relative to the total range. Interpretation: - **Indecision**: Open and close are nearly equal -- buyers and sellers are in balance. - **At resistance**: Bearish signal (potential reversal down). - **At support**: Bullish signal (potential reversal up). - **In a trend**: Warning that momentum may be fading. - Requires confirmation from the next candle -- a doji alone is not a signal. - Reliability: Moderate. More significant after a strong trend. Parameters ---------- open_ : pd.Series Open prices. high : pd.Series High prices. low : pd.Series Low prices. close : pd.Series Close prices. threshold : float, default 0.05 Maximum body-to-range ratio to qualify as a Doji. Returns ------- pd.Series 1 where a Doji is detected, 0 otherwise. """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") body = _body(open_, close) rng = _range(high, low) ratio = pd.Series( np.where(rng != 0, body / rng, 0.0), index=close.index, ) result = pd.Series( np.where(ratio <= threshold, 1, 0), index=close.index, name="doji", dtype=int, ) return result
# --------------------------------------------------------------------------- # Hammer / Hanging Man # ---------------------------------------------------------------------------
[docs] def hammer( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, ) -> pd.Series: """Hammer and Hanging Man pattern. A hammer has a small body near the top and a long lower shadow (at least 2x the body). Returns 1 for bullish hammer (after a downtrend proxy: prior close < close), -1 for hanging man (after an uptrend proxy: prior close > close), 0 otherwise. Interpretation: - **Hammer (1)**: Bullish reversal after a downtrend. Sellers pushed price down during the session but buyers fought back, closing near the open. The long lower shadow shows rejected selling pressure. - **Hanging Man (-1)**: Bearish warning after an uptrend. Same shape, but context differs -- suggests sellers are emerging. - Confirmation needed: Wait for the next candle to close in the reversal direction. - Reliability: High for hammers at major support levels. Parameters ---------- open_ : pd.Series Open prices. high : pd.Series High prices. low : pd.Series Low prices. close : pd.Series Close prices. Returns ------- pd.Series 1 (bullish hammer), -1 (hanging man), or 0. """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") body = _body(open_, close) lower = _lower_shadow(open_, low, close) upper = _upper_shadow(open_, high, close) rng = _range(high, low) # Conditions: lower shadow >= 2*body, upper shadow small, body exists is_hammer_shape = (lower >= 2 * body) & (upper <= body * 0.5) & (rng > 0) prev_close = close.shift(1) direction = np.where( is_hammer_shape & (prev_close > close), 1, # bullish (prior downtrend) np.where(is_hammer_shape & (prev_close < close), -1, 0), # bearish ) # Default: if shape matches but no clear prior trend, mark bullish direction = np.where(is_hammer_shape & (direction == 0), 1, direction) return pd.Series(direction, index=close.index, name="hammer", dtype=int)
# --------------------------------------------------------------------------- # Engulfing # ---------------------------------------------------------------------------
[docs] def engulfing( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, ) -> pd.Series: """Bullish/Bearish Engulfing pattern. Interpretation: - **Bullish engulfing (1)**: A small bearish candle completely engulfed by a larger bullish candle. Strong reversal signal at the bottom of a downtrend. - **Bearish engulfing (-1)**: A small bullish candle completely engulfed by a larger bearish candle. Strong reversal signal at the top of an uptrend. - Volume confirmation strengthens the signal. - Reliability: High -- one of the most reliable reversal patterns, especially at key support/resistance levels. Parameters ---------- open_ : pd.Series Open prices. high : pd.Series High prices. low : pd.Series Low prices. close : pd.Series Close prices. Returns ------- pd.Series 1 (bullish engulfing), -1 (bearish engulfing), or 0. """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") prev_open = open_.shift(1) prev_close = close.shift(1) # Current candle body must engulf previous candle body curr_bullish = close > open_ curr_bearish = close < open_ prev_bearish = prev_close < prev_open prev_bullish = prev_close > prev_open bullish_engulf = ( curr_bullish & prev_bearish & (open_ <= prev_close) & (close >= prev_open) ) bearish_engulf = ( curr_bearish & prev_bullish & (open_ >= prev_close) & (close <= prev_open) ) result = np.where(bullish_engulf, 1, np.where(bearish_engulf, -1, 0)) return pd.Series(result, index=close.index, name="engulfing", dtype=int)
# --------------------------------------------------------------------------- # Morning Star # ---------------------------------------------------------------------------
[docs] def morning_star( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, ) -> pd.Series: """Morning Star (3-candle bullish reversal). Interpretation: - A strong bullish reversal signal at the bottom of a downtrend. - Day 1 (large bearish): Bears are in control. - Day 2 (small body / doji): Indecision -- selling pressure is exhausting. - Day 3 (large bullish): Bulls take over, closing above the midpoint of Day 1's body. - Reliability: High -- three-candle confirmation reduces false signals. Best at established support levels. Parameters ---------- open_ : pd.Series Open prices. high : pd.Series High prices. low : pd.Series Low prices. close : pd.Series Close prices. Returns ------- pd.Series 1 where a Morning Star is detected, 0 otherwise. """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") body = _body(open_, close) rng = _range(high, low) # Day 1: large bearish candle d1_bearish = (open_.shift(2) > close.shift(2)) & ( body.shift(2) > rng.shift(2) * 0.5 ) # Day 2: small body (star) — gap down optional d2_small = body.shift(1) < rng.shift(1) * 0.3 # Day 3: large bullish candle closing above midpoint of Day 1 body d3_bullish = close > open_ midpoint_d1 = (open_.shift(2) + close.shift(2)) / 2.0 d3_above_mid = close > midpoint_d1 signal = d1_bearish & d2_small & d3_bullish & d3_above_mid return pd.Series(signal.astype(int), index=close.index, name="morning_star")
# --------------------------------------------------------------------------- # Evening Star # ---------------------------------------------------------------------------
[docs] def evening_star( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, ) -> pd.Series: """Evening Star (3-candle bearish reversal). Interpretation: - The bearish counterpart of the Morning Star. - Day 1 (large bullish): Bulls are in control. - Day 2 (small body / doji): Indecision at the top. - Day 3 (large bearish): Bears take over. - Reliability: High -- the mirror of Morning Star. Best at established resistance levels. Parameters ---------- open_ : pd.Series Open prices. high : pd.Series High prices. low : pd.Series Low prices. close : pd.Series Close prices. Returns ------- pd.Series -1 where an Evening Star is detected, 0 otherwise. """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") body = _body(open_, close) rng = _range(high, low) # Day 1: large bullish candle d1_bullish = (close.shift(2) > open_.shift(2)) & ( body.shift(2) > rng.shift(2) * 0.5 ) # Day 2: small body (star) d2_small = body.shift(1) < rng.shift(1) * 0.3 # Day 3: large bearish candle closing below midpoint of Day 1 body d3_bearish = close < open_ midpoint_d1 = (open_.shift(2) + close.shift(2)) / 2.0 d3_below_mid = close < midpoint_d1 signal = d1_bullish & d2_small & d3_bearish & d3_below_mid result = np.where(signal, -1, 0) return pd.Series(result, index=close.index, name="evening_star", dtype=int)
# --------------------------------------------------------------------------- # Three White Soldiers # ---------------------------------------------------------------------------
[docs] def three_white_soldiers( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, ) -> pd.Series: """Three White Soldiers (strong bullish continuation). Three consecutive bullish candles, each opening within the prior body and closing at a new high. Interpretation: - Strong bullish signal indicating sustained buying pressure. - Each candle should have a full body (not spinning tops). - Best after a downtrend or consolidation as a reversal signal. - Reliability: Very high -- three consecutive strong closes show committed buying. Weakened if upper shadows are long (advance block pattern). Parameters ---------- open_ : pd.Series Open prices. high : pd.Series High prices. low : pd.Series Low prices. close : pd.Series Close prices. Returns ------- pd.Series 1 where the pattern is detected, 0 otherwise. """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") # All three days bullish d1_bull = close.shift(2) > open_.shift(2) d2_bull = close.shift(1) > open_.shift(1) d3_bull = close > open_ # Each opens within previous body d2_opens_in_d1 = (open_.shift(1) >= open_.shift(2)) & ( open_.shift(1) <= close.shift(2) ) d3_opens_in_d2 = (open_ >= open_.shift(1)) & (open_ <= close.shift(1)) # Each closes higher higher_closes = (close.shift(1) > close.shift(2)) & (close > close.shift(1)) signal = ( d1_bull & d2_bull & d3_bull & d2_opens_in_d1 & d3_opens_in_d2 & higher_closes ) return pd.Series(signal.astype(int), index=close.index, name="three_white_soldiers")
# --------------------------------------------------------------------------- # Three Black Crows # ---------------------------------------------------------------------------
[docs] def three_black_crows( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, ) -> pd.Series: """Three Black Crows (strong bearish continuation). Three consecutive bearish candles, each opening within the prior body and closing at a new low. Interpretation: - Strong bearish signal indicating sustained selling pressure. - The bearish counterpart of Three White Soldiers. - Reliability: Very high -- three consecutive strong closes downward show committed selling. Parameters ---------- open_ : pd.Series Open prices. high : pd.Series High prices. low : pd.Series Low prices. close : pd.Series Close prices. Returns ------- pd.Series -1 where the pattern is detected, 0 otherwise. """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") # All three days bearish d1_bear = close.shift(2) < open_.shift(2) d2_bear = close.shift(1) < open_.shift(1) d3_bear = close < open_ # Each opens within previous body d2_opens_in_d1 = (open_.shift(1) <= open_.shift(2)) & ( open_.shift(1) >= close.shift(2) ) d3_opens_in_d2 = (open_ <= open_.shift(1)) & (open_ >= close.shift(1)) # Each closes lower lower_closes = (close.shift(1) < close.shift(2)) & (close < close.shift(1)) signal = ( d1_bear & d2_bear & d3_bear & d2_opens_in_d1 & d3_opens_in_d2 & lower_closes ) result = np.where(signal, -1, 0) return pd.Series(result, index=close.index, name="three_black_crows", dtype=int)
# --------------------------------------------------------------------------- # Harami # ---------------------------------------------------------------------------
[docs] def harami( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, ) -> pd.Series: """Harami pattern (bullish and bearish). The second candle's body is entirely contained within the first candle's body. Interpretation: - **Bullish harami (1)**: A large bearish candle followed by a small bullish candle within its body. Suggests selling pressure is weakening. Reversal may follow. - **Bearish harami (-1)**: A large bullish candle followed by a small bearish candle within its body. Suggests buying pressure is weakening. - Reliability: Moderate -- requires confirmation from the following candle. Less reliable than engulfing patterns. Parameters ---------- open_ : pd.Series Open prices. high : pd.Series High prices. low : pd.Series Low prices. close : pd.Series Close prices. Returns ------- pd.Series 1 (bullish harami), -1 (bearish harami), or 0. """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") prev_body_high = pd.concat([open_.shift(1), close.shift(1)], axis=1).max(axis=1) prev_body_low = pd.concat([open_.shift(1), close.shift(1)], axis=1).min(axis=1) curr_body_high = pd.concat([open_, close], axis=1).max(axis=1) curr_body_low = pd.concat([open_, close], axis=1).min(axis=1) # Current body within previous body contained = (curr_body_high <= prev_body_high) & (curr_body_low >= prev_body_low) prev_bearish = close.shift(1) < open_.shift(1) prev_bullish = close.shift(1) > open_.shift(1) bullish_harami = contained & prev_bearish # bullish reversal bearish_harami = contained & prev_bullish # bearish reversal result = np.where(bullish_harami, 1, np.where(bearish_harami, -1, 0)) return pd.Series(result, index=close.index, name="harami", dtype=int)
# --------------------------------------------------------------------------- # Spinning Top # ---------------------------------------------------------------------------
[docs] def spinning_top( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, body_threshold: float = 0.3, ) -> pd.Series: """Spinning Top (indecision candle). A candle with a small body relative to its range and roughly equal upper and lower shadows. Interpretation: - **Indecision**: Neither buyers nor sellers gained control. - **In an uptrend**: Warns that bulls are losing momentum. - **In a downtrend**: Warns that bears are losing momentum. - Not a reversal signal on its own -- needs confirmation. - Reliability: Low as a standalone signal; moderate when appearing at key levels after a sustained trend. Parameters ---------- open_ : pd.Series Open prices. high : pd.Series High prices. low : pd.Series Low prices. close : pd.Series Close prices. body_threshold : float, default 0.3 Maximum body-to-range ratio. Returns ------- pd.Series 1 where a Spinning Top is detected, 0 otherwise. """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") body = _body(open_, close) rng = _range(high, low) upper = _upper_shadow(open_, high, close) lower = _lower_shadow(open_, low, close) body_ratio = pd.Series( np.where(rng != 0, body / rng, 0.0), index=close.index, ) small_body = body_ratio <= body_threshold # Both shadows should be meaningfully present has_shadows = (upper > body * 0.3) & (lower > body * 0.3) signal = small_body & has_shadows & (rng > 0) return pd.Series(signal.astype(int), index=close.index, name="spinning_top")
# --------------------------------------------------------------------------- # Marubozu # ---------------------------------------------------------------------------
[docs] def marubozu( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, threshold: float = 0.01, ) -> pd.Series: """Marubozu (full-body candle with no/tiny wicks). Interpretation: - **Bullish marubozu (1)**: Opens at the low and closes at the high -- buyers dominated the entire session with no pushback. Very strong bullish conviction. - **Bearish marubozu (-1)**: Opens at the high and closes at the low -- sellers dominated completely. - Reliability: High -- the absence of shadows shows one side had complete control. Often marks the beginning of a new trend leg. Parameters ---------- open_ : pd.Series Open prices. high : pd.Series High prices. low : pd.Series Low prices. close : pd.Series Close prices. threshold : float, default 0.01 Maximum shadow-to-range ratio for each shadow. Returns ------- pd.Series 1 (bullish marubozu), -1 (bearish marubozu), or 0. """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") rng = _range(high, low) upper = _upper_shadow(open_, high, close) lower = _lower_shadow(open_, low, close) upper_ratio = pd.Series(np.where(rng != 0, upper / rng, 0.0), index=close.index) lower_ratio = pd.Series(np.where(rng != 0, lower / rng, 0.0), index=close.index) tiny_shadows = (upper_ratio <= threshold) & (lower_ratio <= threshold) & (rng > 0) bullish = close > open_ bearish = close < open_ result = np.where( tiny_shadows & bullish, 1, np.where(tiny_shadows & bearish, -1, 0), ) return pd.Series(result, index=close.index, name="marubozu", dtype=int)
# --------------------------------------------------------------------------- # Piercing Pattern # ---------------------------------------------------------------------------
[docs] def piercing_pattern( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, ) -> pd.Series: """Piercing Pattern (bullish reversal). A two-candle pattern: Day 1 is bearish, Day 2 opens below Day 1's low and closes above the midpoint of Day 1's body. Interpretation: - Bullish reversal signal at the bottom of a downtrend. - The gap down open followed by a strong close above the midpoint shows buyers stepping in aggressively. - Reliability: Moderate-high. Stronger when the close is deeper into Day 1's body (closer to engulfing). Parameters ---------- open_ : pd.Series Open prices. high : pd.Series High prices. low : pd.Series Low prices. close : pd.Series Close prices. Returns ------- pd.Series 1 where a Piercing Pattern is detected, 0 otherwise. Example ------- >>> signal = piercing_pattern(open_, high, low, close) """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") prev_bearish = close.shift(1) < open_.shift(1) curr_bullish = close > open_ # Current opens below previous low opens_below = open_ < low.shift(1) # Closes above midpoint of previous body prev_midpoint = (open_.shift(1) + close.shift(1)) / 2.0 closes_above_mid = close > prev_midpoint # Does not close above previous open (otherwise it's engulfing) not_engulfing = close < open_.shift(1) signal = ( prev_bearish & curr_bullish & opens_below & closes_above_mid & not_engulfing ) return pd.Series(signal.astype(int), index=close.index, name="piercing_pattern")
# --------------------------------------------------------------------------- # Dark Cloud Cover # ---------------------------------------------------------------------------
[docs] def dark_cloud_cover( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, ) -> pd.Series: """Dark Cloud Cover (bearish reversal). The bearish counterpart of the Piercing Pattern. Day 1 is bullish, Day 2 opens above Day 1's high and closes below the midpoint of Day 1's body. Interpretation: - Bearish reversal signal at the top of an uptrend. - The gap up open followed by selling down into Day 1's body shows sellers overpowering buyers. - Reliability: Moderate-high. More significant with high volume. Parameters ---------- open_ : pd.Series Open prices. high : pd.Series High prices. low : pd.Series Low prices. close : pd.Series Close prices. Returns ------- pd.Series -1 where a Dark Cloud Cover is detected, 0 otherwise. Example ------- >>> signal = dark_cloud_cover(open_, high, low, close) """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") prev_bullish = close.shift(1) > open_.shift(1) curr_bearish = close < open_ # Current opens above previous high opens_above = open_ > high.shift(1) # Closes below midpoint of previous body prev_midpoint = (open_.shift(1) + close.shift(1)) / 2.0 closes_below_mid = close < prev_midpoint # Does not close below previous open (otherwise it's engulfing) not_engulfing = close > open_.shift(1) signal = ( prev_bullish & curr_bearish & opens_above & closes_below_mid & not_engulfing ) result = np.where(signal, -1, 0) return pd.Series(result, index=close.index, name="dark_cloud_cover", dtype=int)
# --------------------------------------------------------------------------- # Hanging Man # ---------------------------------------------------------------------------
[docs] def hanging_man( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, trend_period: int = 5, ) -> pd.Series: """Hanging Man (bearish reversal at top). Same hammer shape (small body near top, long lower shadow) but appears after an uptrend, signalling potential reversal. Interpretation: - Bearish warning signal after an uptrend. The long lower shadow shows sellers tested lower prices during the session. - Needs confirmation: a bearish close the next day confirms the reversal. - Reliability: Moderate -- requires confirmation and context. Parameters ---------- open_ : pd.Series Open prices. high : pd.Series High prices. low : pd.Series Low prices. close : pd.Series Close prices. trend_period : int, default 5 Number of bars to assess prior uptrend. Returns ------- pd.Series -1 where a Hanging Man is detected, 0 otherwise. Example ------- >>> signal = hanging_man(open_, high, low, close) """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") body = _body(open_, close) lower = _lower_shadow(open_, low, close) upper = _upper_shadow(open_, high, close) rng = _range(high, low) is_hammer_shape = (lower >= 2 * body) & (upper <= body * 0.5) & (rng > 0) # Prior uptrend: close higher than close N bars ago prior_uptrend = close.shift(1) > close.shift(trend_period) signal = is_hammer_shape & prior_uptrend result = np.where(signal, -1, 0) return pd.Series(result, index=close.index, name="hanging_man", dtype=int)
# --------------------------------------------------------------------------- # Inverted Hammer # ---------------------------------------------------------------------------
[docs] def inverted_hammer( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, trend_period: int = 5, ) -> pd.Series: """Inverted Hammer (bullish reversal at bottom). A candle with a long upper shadow (at least 2x the body) and a small lower shadow, appearing after a downtrend. Interpretation: - Bullish reversal signal at the bottom of a downtrend. - The long upper shadow shows buyers tested higher prices but could not hold them yet. If confirmed by next bar, buyers are gaining strength. - Reliability: Moderate -- requires a bullish confirmation candle the next day. Parameters ---------- open_ : pd.Series Open prices. high : pd.Series High prices. low : pd.Series Low prices. close : pd.Series Close prices. trend_period : int, default 5 Number of bars to assess prior downtrend. Returns ------- pd.Series 1 where an Inverted Hammer is detected, 0 otherwise. Example ------- >>> signal = inverted_hammer(open_, high, low, close) """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") body = _body(open_, close) upper = _upper_shadow(open_, high, close) lower = _lower_shadow(open_, low, close) rng = _range(high, low) is_inverted_shape = (upper >= 2 * body) & (lower <= body * 0.5) & (rng > 0) # Prior downtrend: close lower than close N bars ago prior_downtrend = close.shift(1) < close.shift(trend_period) signal = is_inverted_shape & prior_downtrend return pd.Series(signal.astype(int), index=close.index, name="inverted_hammer")
# --------------------------------------------------------------------------- # Shooting Star # ---------------------------------------------------------------------------
[docs] def shooting_star( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, trend_period: int = 5, ) -> pd.Series: """Shooting Star (bearish reversal at top). Same shape as inverted hammer (long upper shadow, small lower shadow) but appears after an uptrend. Interpretation: - Bearish reversal signal at the top of an uptrend. Buyers pushed price higher but sellers overwhelmed them, closing near the open. - The long upper shadow represents rejected higher prices. - Reliability: Moderate-high -- stronger when it appears at resistance with high volume. Parameters ---------- open_ : pd.Series Open prices. high : pd.Series High prices. low : pd.Series Low prices. close : pd.Series Close prices. trend_period : int, default 5 Number of bars to assess prior uptrend. Returns ------- pd.Series -1 where a Shooting Star is detected, 0 otherwise. Example ------- >>> signal = shooting_star(open_, high, low, close) """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") body = _body(open_, close) upper = _upper_shadow(open_, high, close) lower = _lower_shadow(open_, low, close) rng = _range(high, low) is_shooting_shape = (upper >= 2 * body) & (lower <= body * 0.5) & (rng > 0) # Prior uptrend prior_uptrend = close.shift(1) > close.shift(trend_period) signal = is_shooting_shape & prior_uptrend result = np.where(signal, -1, 0) return pd.Series(result, index=close.index, name="shooting_star", dtype=int)
# --------------------------------------------------------------------------- # Tweezer Top # ---------------------------------------------------------------------------
[docs] def tweezer_top( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, tolerance: float = 0.001, ) -> pd.Series: """Tweezer Top (bearish reversal). Two consecutive candles with nearly the same highs. The first candle is bullish and the second is bearish. Interpretation: - Bearish reversal at a resistance level. Both candles tested the same high and were rejected, showing strong resistance. - Reliability: Moderate -- stronger at established resistance. Parameters ---------- open_ : pd.Series Open prices. high : pd.Series High prices. low : pd.Series Low prices. close : pd.Series Close prices. tolerance : float, default 0.001 Maximum relative difference between highs to be considered equal. Returns ------- pd.Series -1 where a Tweezer Top is detected, 0 otherwise. Example ------- >>> signal = tweezer_top(open_, high, low, close) """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") prev_bullish = close.shift(1) > open_.shift(1) curr_bearish = close < open_ # Same highs (within tolerance) high_diff = (high - high.shift(1)).abs() / high.shift(1) same_highs = high_diff <= tolerance signal = prev_bullish & curr_bearish & same_highs result = np.where(signal, -1, 0) return pd.Series(result, index=close.index, name="tweezer_top", dtype=int)
# --------------------------------------------------------------------------- # Tweezer Bottom # ---------------------------------------------------------------------------
[docs] def tweezer_bottom( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, tolerance: float = 0.001, ) -> pd.Series: """Tweezer Bottom (bullish reversal). Two consecutive candles with nearly the same lows. The first candle is bearish and the second is bullish. Interpretation: - Bullish reversal at a support level. Both candles tested the same low and were rejected, showing strong support. - Reliability: Moderate -- stronger at established support. Parameters ---------- open_ : pd.Series Open prices. high : pd.Series High prices. low : pd.Series Low prices. close : pd.Series Close prices. tolerance : float, default 0.001 Maximum relative difference between lows to be considered equal. Returns ------- pd.Series 1 where a Tweezer Bottom is detected, 0 otherwise. Example ------- >>> signal = tweezer_bottom(open_, high, low, close) """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") prev_bearish = close.shift(1) < open_.shift(1) curr_bullish = close > open_ # Same lows (within tolerance) low_diff = (low - low.shift(1)).abs() / low.shift(1) same_lows = low_diff <= tolerance signal = prev_bearish & curr_bullish & same_lows return pd.Series(signal.astype(int), index=close.index, name="tweezer_bottom")
# --------------------------------------------------------------------------- # Three Inside Up # ---------------------------------------------------------------------------
[docs] def three_inside_up( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, ) -> pd.Series: """Three Inside Up (bullish reversal). A three-candle pattern: bullish harami (Days 1-2) confirmed by a third bullish candle closing above Day 1's open. Interpretation: - A confirmed bullish harami. The third candle provides the confirmation that a simple harami lacks. - Reliability: High -- three-candle confirmation is stronger than the two-candle harami alone. Parameters ---------- open_ : pd.Series Open prices. high : pd.Series High prices. low : pd.Series Low prices. close : pd.Series Close prices. Returns ------- pd.Series 1 where the pattern is detected, 0 otherwise. Example ------- >>> signal = three_inside_up(open_, high, low, close) """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") # Day 1: large bearish candle d1_bearish = close.shift(2) < open_.shift(2) # Day 2: bullish candle contained within Day 1 body (harami) d2_bullish = close.shift(1) > open_.shift(1) d2_body_high = pd.concat([open_.shift(1), close.shift(1)], axis=1).max(axis=1) d2_body_low = pd.concat([open_.shift(1), close.shift(1)], axis=1).min(axis=1) d2_contained = (d2_body_high <= open_.shift(2)) & (d2_body_low >= close.shift(2)) # Day 3: bullish candle closing above Day 1 open d3_bullish = close > open_ d3_above_d1 = close > open_.shift(2) signal = d1_bearish & d2_bullish & d2_contained & d3_bullish & d3_above_d1 return pd.Series(signal.astype(int), index=close.index, name="three_inside_up")
# --------------------------------------------------------------------------- # Three Inside Down # ---------------------------------------------------------------------------
[docs] def three_inside_down( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, ) -> pd.Series: """Three Inside Down (bearish reversal). A three-candle pattern: bearish harami (Days 1-2) confirmed by a third bearish candle closing below Day 1's open. Interpretation: - A confirmed bearish harami. The bearish counterpart of Three Inside Up. - Reliability: High -- three-candle confirmation pattern. Parameters ---------- open_ : pd.Series Open prices. high : pd.Series High prices. low : pd.Series Low prices. close : pd.Series Close prices. Returns ------- pd.Series -1 where the pattern is detected, 0 otherwise. Example ------- >>> signal = three_inside_down(open_, high, low, close) """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") # Day 1: large bullish candle d1_bullish = close.shift(2) > open_.shift(2) # Day 2: bearish candle contained within Day 1 body (harami) d2_bearish = close.shift(1) < open_.shift(1) d2_body_high = pd.concat([open_.shift(1), close.shift(1)], axis=1).max(axis=1) d2_body_low = pd.concat([open_.shift(1), close.shift(1)], axis=1).min(axis=1) d2_contained = (d2_body_high <= close.shift(2)) & (d2_body_low >= open_.shift(2)) # Day 3: bearish candle closing below Day 1 open d3_bearish = close < open_ d3_below_d1 = close < open_.shift(2) signal = d1_bullish & d2_bearish & d2_contained & d3_bearish & d3_below_d1 result = np.where(signal, -1, 0) return pd.Series(result, index=close.index, name="three_inside_down", dtype=int)
# --------------------------------------------------------------------------- # Abandoned Baby # ---------------------------------------------------------------------------
[docs] def abandoned_baby( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, ) -> pd.Series: """Abandoned Baby (reversal pattern). A rare three-candle pattern with gaps. A Doji star gaps away from the first candle and the third candle gaps in the opposite direction. Interpretation: - **Bullish abandoned baby (1)**: Bearish candle, gap-down doji, gap-up bullish candle. Very strong reversal signal. - **Bearish abandoned baby (-1)**: Bullish candle, gap-up doji, gap-down bearish candle. Very strong reversal signal. - Reliability: Very high -- but extremely rare. The gap isolation of the doji shows a dramatic sentiment shift. Parameters ---------- open_ : pd.Series Open prices. high : pd.Series High prices. low : pd.Series Low prices. close : pd.Series Close prices. Returns ------- pd.Series 1 (bullish abandoned baby), -1 (bearish abandoned baby), or 0. Example ------- >>> signal = abandoned_baby(open_, high, low, close) """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") body = _body(open_, close) rng = _range(high, low) # Day 2 is a doji-like candle ratio_d2 = pd.Series( np.where(rng.shift(1) != 0, body.shift(1) / rng.shift(1), 0.0), index=close.index, ) d2_doji = ratio_d2 <= 0.1 # Bullish: Day 1 bearish, gap down to Day 2, gap up to Day 3 bullish d1_bearish = close.shift(2) < open_.shift(2) gap_down = high.shift(1) < low.shift(2) # Day 2 high < Day 1 low d3_bullish = close > open_ gap_up = low > high.shift(1) # Day 3 low > Day 2 high bullish_baby = d1_bearish & d2_doji & gap_down & d3_bullish & gap_up # Bearish: Day 1 bullish, gap up to Day 2, gap down to Day 3 bearish d1_bullish = close.shift(2) > open_.shift(2) gap_up_d2 = low.shift(1) > high.shift(2) # Day 2 low > Day 1 high d3_bearish = close < open_ gap_down_d3 = high < low.shift(1) # Day 3 high < Day 2 low bearish_baby = d1_bullish & d2_doji & gap_up_d2 & d3_bearish & gap_down_d3 result = np.where(bullish_baby, 1, np.where(bearish_baby, -1, 0)) return pd.Series(result, index=close.index, name="abandoned_baby", dtype=int)
# --------------------------------------------------------------------------- # Kicking # ---------------------------------------------------------------------------
[docs] def kicking( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, threshold: float = 0.01, ) -> pd.Series: """Kicking pattern. Two consecutive marubozu candles in opposite directions with a gap between them. One of the strongest reversal signals. Interpretation: - **Bullish kicking (1)**: Bearish marubozu followed by a gap-up bullish marubozu. Extremely strong reversal. - **Bearish kicking (-1)**: Bullish marubozu followed by a gap-down bearish marubozu. - Reliability: Very high -- one of the most powerful candlestick patterns. The opposing full-body candles with a gap show a complete and sudden sentiment reversal. Parameters ---------- open_ : pd.Series Open prices. high : pd.Series High prices. low : pd.Series Low prices. close : pd.Series Close prices. threshold : float, default 0.01 Maximum shadow-to-range ratio for marubozu qualification. Returns ------- pd.Series 1 (bullish kicking), -1 (bearish kicking), or 0. Example ------- >>> signal = kicking(open_, high, low, close) """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") rng = _range(high, low) upper = _upper_shadow(open_, high, close) lower = _lower_shadow(open_, low, close) prev_rng = _range(high.shift(1), low.shift(1)) prev_upper = _upper_shadow(open_.shift(1), high.shift(1), close.shift(1)) prev_lower = _lower_shadow(open_.shift(1), low.shift(1), close.shift(1)) # Current candle is marubozu curr_upper_ratio = pd.Series( np.where(rng != 0, upper / rng, 0.0), index=close.index ) curr_lower_ratio = pd.Series( np.where(rng != 0, lower / rng, 0.0), index=close.index ) curr_maru = ( (curr_upper_ratio <= threshold) & (curr_lower_ratio <= threshold) & (rng > 0) ) # Previous candle is marubozu prev_upper_ratio = pd.Series( np.where(prev_rng != 0, prev_upper / prev_rng, 0.0), index=close.index ) prev_lower_ratio = pd.Series( np.where(prev_rng != 0, prev_lower / prev_rng, 0.0), index=close.index ) prev_maru = ( (prev_upper_ratio <= threshold) & (prev_lower_ratio <= threshold) & (prev_rng > 0) ) # Bullish kicking: prev bearish marubozu, curr bullish marubozu with gap up prev_bear = close.shift(1) < open_.shift(1) curr_bull = close > open_ gap_up = open_ > open_.shift(1) # gap up from previous open # Bearish kicking: prev bullish marubozu, curr bearish marubozu with gap down prev_bull = close.shift(1) > open_.shift(1) curr_bear = close < open_ gap_down = open_ < open_.shift(1) # gap down from previous open bullish_kick = prev_maru & curr_maru & prev_bear & curr_bull & gap_up bearish_kick = prev_maru & curr_maru & prev_bull & curr_bear & gap_down result = np.where(bullish_kick, 1, np.where(bearish_kick, -1, 0)) return pd.Series(result, index=close.index, name="kicking", dtype=int)
# --------------------------------------------------------------------------- # Belt Hold # ---------------------------------------------------------------------------
[docs] def belt_hold( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, threshold: float = 0.01, ) -> pd.Series: """Belt Hold pattern. A long marubozu candle that opens with a gap in the direction of the prior trend. A bullish belt hold gaps down and opens at the low; a bearish belt hold gaps up and opens at the high. Interpretation: - **Bullish belt hold (1)**: Gaps down then rallies all day closing near the high -- strong rejection of lower prices. - **Bearish belt hold (-1)**: Gaps up then sells off all day. - Reliability: Moderate -- the gap adds significance. Parameters ---------- open_ : pd.Series Open prices. high : pd.Series High prices. low : pd.Series Low prices. close : pd.Series Close prices. threshold : float, default 0.01 Maximum shadow-to-range ratio for the relevant shadow. Returns ------- pd.Series 1 (bullish belt hold), -1 (bearish belt hold), or 0. Example ------- >>> signal = belt_hold(open_, high, low, close) """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") rng = _range(high, low) lower = _lower_shadow(open_, low, close) upper = _upper_shadow(open_, high, close) lower_ratio = pd.Series(np.where(rng != 0, lower / rng, 0.0), index=close.index) upper_ratio = pd.Series(np.where(rng != 0, upper / rng, 0.0), index=close.index) body = _body(open_, close) large_body = body > rng * 0.6 # Bullish belt hold: gaps down, opens at low (tiny lower shadow), bullish curr_bullish = close > open_ gap_down = open_ < close.shift(1) bullish_belt = ( curr_bullish & gap_down & (lower_ratio <= threshold) & large_body & (rng > 0) ) # Bearish belt hold: gaps up, opens at high (tiny upper shadow), bearish curr_bearish = close < open_ gap_up = open_ > close.shift(1) bearish_belt = ( curr_bearish & gap_up & (upper_ratio <= threshold) & large_body & (rng > 0) ) result = np.where(bullish_belt, 1, np.where(bearish_belt, -1, 0)) return pd.Series(result, index=close.index, name="belt_hold", dtype=int)
# --------------------------------------------------------------------------- # Rising Three Methods # ---------------------------------------------------------------------------
[docs] def rising_three_methods( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, ) -> pd.Series: """Rising Three Methods (bullish continuation). A five-candle pattern: a long bullish candle, followed by three small bearish candles that stay within the first candle's range, then a final long bullish candle that closes above the first candle's close. Interpretation: - Bullish continuation pattern -- the three small bearish candles are a rest/consolidation within the uptrend. - The final bullish candle confirms the trend resumes. - Reliability: High -- five-candle confirmation shows clear trend continuation with a healthy pullback. Parameters: open_: Open prices. high: High prices. low: Low prices. close: Close prices. Returns: 1 where the pattern is detected, 0 otherwise. Example: >>> signal = rising_three_methods(open_, high, low, close) """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") result = pd.Series(0, index=close.index, name="rising_three_methods", dtype=int) for i in range(4, len(close)): # Day 1 (i-4): long bullish candle d1_open = open_.iloc[i - 4] d1_close = close.iloc[i - 4] d1_high = high.iloc[i - 4] d1_low = low.iloc[i - 4] d1_body = abs(d1_close - d1_open) d1_range = d1_high - d1_low if d1_close <= d1_open or d1_range == 0 or d1_body < d1_range * 0.5: continue # Days 2-4 (i-3, i-2, i-1): small candles within Day 1 range middle_ok = True for j in range(i - 3, i): if high.iloc[j] > d1_high or low.iloc[j] < d1_low: middle_ok = False break if close.iloc[j] >= open_.iloc[j]: # must be bearish or small mid_body = abs(close.iloc[j] - open_.iloc[j]) if mid_body > d1_body * 0.5: middle_ok = False break if not middle_ok: continue # Day 5 (i): long bullish candle closing above Day 1 close d5_body = abs(close.iloc[i] - open_.iloc[i]) d5_range = high.iloc[i] - low.iloc[i] if ( close.iloc[i] > open_.iloc[i] and close.iloc[i] > d1_close and d5_range > 0 and d5_body > d5_range * 0.5 ): result.iloc[i] = 1 return result
# --------------------------------------------------------------------------- # Falling Three Methods # ---------------------------------------------------------------------------
[docs] def falling_three_methods( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, ) -> pd.Series: """Falling Three Methods (bearish continuation). The bearish counterpart of Rising Three Methods. A long bearish candle, three small bullish candles inside its range, then a final long bearish candle that closes below the first candle's close. Interpretation: - Bearish continuation pattern -- the small bullish candles are a brief consolidation within the downtrend. - Reliability: High -- mirror of Rising Three Methods. Parameters: open_: Open prices. high: High prices. low: Low prices. close: Close prices. Returns: -1 where the pattern is detected, 0 otherwise. Example: >>> signal = falling_three_methods(open_, high, low, close) """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") result = pd.Series(0, index=close.index, name="falling_three_methods", dtype=int) for i in range(4, len(close)): # Day 1 (i-4): long bearish candle d1_open = open_.iloc[i - 4] d1_close = close.iloc[i - 4] d1_high = high.iloc[i - 4] d1_low = low.iloc[i - 4] d1_body = abs(d1_close - d1_open) d1_range = d1_high - d1_low if d1_close >= d1_open or d1_range == 0 or d1_body < d1_range * 0.5: continue # Days 2-4: small candles within Day 1 range middle_ok = True for j in range(i - 3, i): if high.iloc[j] > d1_high or low.iloc[j] < d1_low: middle_ok = False break if close.iloc[j] <= open_.iloc[j]: # must be bullish or small mid_body = abs(close.iloc[j] - open_.iloc[j]) if mid_body > d1_body * 0.5: middle_ok = False break if not middle_ok: continue # Day 5: long bearish candle closing below Day 1 close d5_body = abs(close.iloc[i] - open_.iloc[i]) d5_range = high.iloc[i] - low.iloc[i] if ( close.iloc[i] < open_.iloc[i] and close.iloc[i] < d1_close and d5_range > 0 and d5_body > d5_range * 0.5 ): result.iloc[i] = -1 return result
# --------------------------------------------------------------------------- # Tasuki Gap # ---------------------------------------------------------------------------
[docs] def tasuki_gap( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, ) -> pd.Series: """Upside/Downside Tasuki Gap. **Upside** (1): two bullish candles with a gap up, followed by a bearish candle that opens within the second body and closes within the gap but does not fill it. **Downside** (-1): two bearish candles with a gap down, followed by a bullish candle that opens within the second body and closes within the gap but does not fill it. Parameters: open_: Open prices. high: High prices. low: Low prices. close: Close prices. Returns: 1 (upside Tasuki gap), -1 (downside Tasuki gap), or 0. Example: >>> signal = tasuki_gap(open_, high, low, close) """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") # Upside Tasuki gap d1_bull = close.shift(2) > open_.shift(2) d2_bull = close.shift(1) > open_.shift(1) gap_up = open_.shift(1) > close.shift(2) # gap up between d1 and d2 d3_bear = close < open_ d3_opens_in_d2 = (open_ >= open_.shift(1)) & (open_ <= close.shift(1)) d3_closes_in_gap = (close < open_.shift(1)) & (close > close.shift(2)) upside = d1_bull & d2_bull & gap_up & d3_bear & d3_opens_in_d2 & d3_closes_in_gap # Downside Tasuki gap d1_bear = close.shift(2) < open_.shift(2) d2_bear = close.shift(1) < open_.shift(1) gap_down = open_.shift(1) < close.shift(2) # gap down between d1 and d2 d3_bull = close > open_ d3_opens_in_d2_dn = (open_ <= open_.shift(1)) & (open_ >= close.shift(1)) d3_closes_in_gap_dn = (close > open_.shift(1)) & (close < close.shift(2)) downside = ( d1_bear & d2_bear & gap_down & d3_bull & d3_opens_in_d2_dn & d3_closes_in_gap_dn ) result = np.where(upside, 1, np.where(downside, -1, 0)) return pd.Series(result, index=close.index, name="tasuki_gap", dtype=int)
# --------------------------------------------------------------------------- # On Neck # ---------------------------------------------------------------------------
[docs] def on_neck( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, tolerance: float = 0.001, ) -> pd.Series: """On Neck pattern (bearish continuation). A bearish candle followed by a small bullish candle that opens below the previous low and closes at or near the previous low. Parameters: open_: Open prices. high: High prices. low: Low prices. close: Close prices. tolerance: Maximum relative difference between close and previous low. Returns: -1 where the pattern is detected, 0 otherwise. Example: >>> signal = on_neck(open_, high, low, close) """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") prev_bearish = close.shift(1) < open_.shift(1) curr_bullish = close > open_ opens_below = open_ < low.shift(1) # Close at or near previous low prev_low = low.shift(1) close_near_prev_low = ((close - prev_low).abs() / prev_low) <= tolerance signal = prev_bearish & curr_bullish & opens_below & close_near_prev_low result = np.where(signal, -1, 0) return pd.Series(result, index=close.index, name="on_neck", dtype=int)
# --------------------------------------------------------------------------- # In Neck # ---------------------------------------------------------------------------
[docs] def in_neck( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, tolerance: float = 0.003, ) -> pd.Series: """In Neck pattern (slight bullish variant of On Neck). A bearish candle followed by a small bullish candle that opens below the previous low and closes slightly above (but near) the previous close. Parameters: open_: Open prices. high: High prices. low: Low prices. close: Close prices. tolerance: Maximum relative distance above the previous close. Returns: -1 where the pattern is detected, 0 otherwise. Example: >>> signal = in_neck(open_, high, low, close) """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") prev_bearish = close.shift(1) < open_.shift(1) curr_bullish = close > open_ opens_below = open_ < low.shift(1) prev_close = close.shift(1) # Close slightly above previous close close_near_prev_close = (close >= prev_close) & ( ((close - prev_close) / prev_close) <= tolerance ) signal = prev_bearish & curr_bullish & opens_below & close_near_prev_close result = np.where(signal, -1, 0) return pd.Series(result, index=close.index, name="in_neck", dtype=int)
# --------------------------------------------------------------------------- # Thrusting # ---------------------------------------------------------------------------
[docs] def thrusting( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, ) -> pd.Series: """Thrusting pattern (moderate bearish continuation). A bearish candle followed by a bullish candle that opens below the previous low and closes above the previous close but below the midpoint of the previous body. Parameters: open_: Open prices. high: High prices. low: Low prices. close: Close prices. Returns: -1 where the pattern is detected, 0 otherwise. Example: >>> signal = thrusting(open_, high, low, close) """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") prev_bearish = close.shift(1) < open_.shift(1) curr_bullish = close > open_ opens_below = open_ < low.shift(1) prev_midpoint = (open_.shift(1) + close.shift(1)) / 2.0 closes_above_prev_close = close > close.shift(1) closes_below_mid = close < prev_midpoint signal = ( prev_bearish & curr_bullish & opens_below & closes_above_prev_close & closes_below_mid ) result = np.where(signal, -1, 0) return pd.Series(result, index=close.index, name="thrusting", dtype=int)
# --------------------------------------------------------------------------- # Separating Lines # ---------------------------------------------------------------------------
[docs] def separating_lines( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, tolerance: float = 0.001, ) -> pd.Series: """Bullish/Bearish Separating Lines. **Bullish** (1): a bearish candle followed by a bullish candle that opens at the same level as the previous open. **Bearish** (-1): a bullish candle followed by a bearish candle that opens at the same level as the previous open. Parameters: open_: Open prices. high: High prices. low: Low prices. close: Close prices. tolerance: Maximum relative difference between opens. Returns: 1 (bullish), -1 (bearish), or 0. Example: >>> signal = separating_lines(open_, high, low, close) """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") prev_open = open_.shift(1) same_open = ((open_ - prev_open).abs() / prev_open) <= tolerance prev_bearish = close.shift(1) < open_.shift(1) prev_bullish = close.shift(1) > open_.shift(1) curr_bullish = close > open_ curr_bearish = close < open_ bullish = prev_bearish & curr_bullish & same_open bearish = prev_bullish & curr_bearish & same_open result = np.where(bullish, 1, np.where(bearish, -1, 0)) return pd.Series(result, index=close.index, name="separating_lines", dtype=int)
# --------------------------------------------------------------------------- # Closing Marubozu # ---------------------------------------------------------------------------
[docs] def closing_marubozu( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, threshold: float = 0.01, ) -> pd.Series: """Closing Marubozu — no shadow on the closing side only. A **bullish closing marubozu** (1) has no upper shadow (close == high) but may have a lower shadow. A **bearish closing marubozu** (-1) has no lower shadow (close == low) but may have an upper shadow. Parameters: open_: Open prices. high: High prices. low: Low prices. close: Close prices. threshold: Maximum shadow-to-range ratio for the closing side. Returns: 1 (bullish), -1 (bearish), or 0. Example: >>> signal = closing_marubozu(open_, high, low, close) """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") rng = _range(high, low) upper = _upper_shadow(open_, high, close) lower = _lower_shadow(open_, low, close) upper_ratio = pd.Series(np.where(rng != 0, upper / rng, 0.0), index=close.index) lower_ratio = pd.Series(np.where(rng != 0, lower / rng, 0.0), index=close.index) body = _body(open_, close) has_body = body > rng * 0.5 # Bullish: close near high (tiny upper shadow), bullish candle bullish = (close > open_) & (upper_ratio <= threshold) & has_body & (rng > 0) # Bearish: close near low (tiny lower shadow), bearish candle bearish = (close < open_) & (lower_ratio <= threshold) & has_body & (rng > 0) result = np.where(bullish, 1, np.where(bearish, -1, 0)) return pd.Series(result, index=close.index, name="closing_marubozu", dtype=int)
# --------------------------------------------------------------------------- # Rickshaw Man # ---------------------------------------------------------------------------
[docs] def rickshaw_man( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, body_threshold: float = 0.05, shadow_threshold: float = 0.3, ) -> pd.Series: """Rickshaw Man -- a Doji with very long shadows and tiny body near center. Interpretation: - Extreme indecision: price moved significantly in both directions but closed near the open at the midpoint. - Suggests the market is at a critical juncture. - Reliability: Moderate -- needs context and confirmation. Parameters: open_: Open prices. high: High prices. low: Low prices. close: Close prices. body_threshold: Maximum body-to-range ratio to qualify. shadow_threshold: Minimum shadow-to-range ratio for each shadow. Returns: 1 where detected, 0 otherwise. Example: >>> signal = rickshaw_man(open_, high, low, close) """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") body = _body(open_, close) rng = _range(high, low) upper = _upper_shadow(open_, high, close) lower = _lower_shadow(open_, low, close) body_ratio = pd.Series(np.where(rng != 0, body / rng, 0.0), index=close.index) upper_ratio = pd.Series(np.where(rng != 0, upper / rng, 0.0), index=close.index) lower_ratio = pd.Series(np.where(rng != 0, lower / rng, 0.0), index=close.index) # Body near center: midpoint of body near midpoint of range body_mid = (open_ + close) / 2.0 range_mid = (high + low) / 2.0 near_center = pd.Series( np.where(rng != 0, (body_mid - range_mid).abs() / rng, 0.0), index=close.index, ) signal = ( (body_ratio <= body_threshold) & (upper_ratio >= shadow_threshold) & (lower_ratio >= shadow_threshold) & (near_center <= 0.1) & (rng > 0) ) return pd.Series(signal.astype(int), index=close.index, name="rickshaw_man")
# --------------------------------------------------------------------------- # Long Legged Doji # ---------------------------------------------------------------------------
[docs] def long_legged_doji( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, body_threshold: float = 0.05, shadow_threshold: float = 0.3, ) -> pd.Series: """Long Legged Doji -- Doji with unusually long upper and lower shadows. Interpretation: - Strong indecision with high volatility. Both buyers and sellers were active but neither won. - More significant than a standard doji due to the wide range. - Reliability: Moderate -- similar to Rickshaw Man. Parameters: open_: Open prices. high: High prices. low: Low prices. close: Close prices. body_threshold: Maximum body-to-range ratio. shadow_threshold: Minimum shadow-to-range ratio for each shadow. Returns: 1 where detected, 0 otherwise. Example: >>> signal = long_legged_doji(open_, high, low, close) """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") body = _body(open_, close) rng = _range(high, low) upper = _upper_shadow(open_, high, close) lower = _lower_shadow(open_, low, close) body_ratio = pd.Series(np.where(rng != 0, body / rng, 0.0), index=close.index) upper_ratio = pd.Series(np.where(rng != 0, upper / rng, 0.0), index=close.index) lower_ratio = pd.Series(np.where(rng != 0, lower / rng, 0.0), index=close.index) signal = ( (body_ratio <= body_threshold) & (upper_ratio >= shadow_threshold) & (lower_ratio >= shadow_threshold) & (rng > 0) ) return pd.Series(signal.astype(int), index=close.index, name="long_legged_doji")
# --------------------------------------------------------------------------- # Dragonfly Doji # ---------------------------------------------------------------------------
[docs] def dragonfly_doji( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, body_threshold: float = 0.05, upper_threshold: float = 0.05, lower_min: float = 0.3, ) -> pd.Series: """Dragonfly Doji -- Doji with a long lower shadow and no upper shadow. Interpretation: - Bullish signal, especially after a downtrend. Sellers pushed price down but buyers brought it all the way back to the open. - The long lower shadow shows strong rejection of lower prices. - Reliability: Moderate-high at support levels. Parameters: open_: Open prices. high: High prices. low: Low prices. close: Close prices. body_threshold: Maximum body-to-range ratio. upper_threshold: Maximum upper-shadow-to-range ratio. lower_min: Minimum lower-shadow-to-range ratio. Returns: 1 where detected, 0 otherwise. Example: >>> signal = dragonfly_doji(open_, high, low, close) """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") body = _body(open_, close) rng = _range(high, low) upper = _upper_shadow(open_, high, close) lower = _lower_shadow(open_, low, close) body_ratio = pd.Series(np.where(rng != 0, body / rng, 0.0), index=close.index) upper_ratio = pd.Series(np.where(rng != 0, upper / rng, 0.0), index=close.index) lower_ratio = pd.Series(np.where(rng != 0, lower / rng, 0.0), index=close.index) signal = ( (body_ratio <= body_threshold) & (upper_ratio <= upper_threshold) & (lower_ratio >= lower_min) & (rng > 0) ) return pd.Series(signal.astype(int), index=close.index, name="dragonfly_doji")
# --------------------------------------------------------------------------- # Gravestone Doji # ---------------------------------------------------------------------------
[docs] def gravestone_doji( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, body_threshold: float = 0.05, lower_threshold: float = 0.05, upper_min: float = 0.3, ) -> pd.Series: """Gravestone Doji -- Doji with a long upper shadow and no lower shadow. Interpretation: - Bearish signal, especially after an uptrend. Buyers pushed price up but sellers brought it all the way back to the open. - The long upper shadow shows strong rejection of higher prices. - Reliability: Moderate-high at resistance levels. Parameters: open_: Open prices. high: High prices. low: Low prices. close: Close prices. body_threshold: Maximum body-to-range ratio. lower_threshold: Maximum lower-shadow-to-range ratio. upper_min: Minimum upper-shadow-to-range ratio. Returns: 1 where detected, 0 otherwise. Example: >>> signal = gravestone_doji(open_, high, low, close) """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") body = _body(open_, close) rng = _range(high, low) upper = _upper_shadow(open_, high, close) lower = _lower_shadow(open_, low, close) body_ratio = pd.Series(np.where(rng != 0, body / rng, 0.0), index=close.index) upper_ratio = pd.Series(np.where(rng != 0, upper / rng, 0.0), index=close.index) lower_ratio = pd.Series(np.where(rng != 0, lower / rng, 0.0), index=close.index) signal = ( (body_ratio <= body_threshold) & (lower_ratio <= lower_threshold) & (upper_ratio >= upper_min) & (rng > 0) ) return pd.Series(signal.astype(int), index=close.index, name="gravestone_doji")
# --------------------------------------------------------------------------- # Tri Star # ---------------------------------------------------------------------------
[docs] def tri_star( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, doji_threshold: float = 0.05, ) -> pd.Series: """Tri-Star pattern -- three consecutive dojis with gaps. Interpretation: - **Bullish tri-star (1)**: Three dojis where the middle gaps below. Very rare, strong reversal at bottoms. - **Bearish tri-star (-1)**: Three dojis where the middle gaps above. Very rare, strong reversal at tops. - Reliability: Very high when it occurs, but extremely rare. **Bullish** (1): three dojis where the middle gaps below the other two. **Bearish** (-1): three dojis where the middle gaps above the other two. Parameters: open_: Open prices. high: High prices. low: Low prices. close: Close prices. doji_threshold: Maximum body-to-range ratio for doji qualification. Returns: 1 (bullish tri-star), -1 (bearish tri-star), or 0. Example: >>> signal = tri_star(open_, high, low, close) """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") body = _body(open_, close) rng = _range(high, low) ratio = pd.Series(np.where(rng != 0, body / rng, 0.0), index=close.index) is_doji = ratio <= doji_threshold d1_doji = is_doji.shift(2) d2_doji = is_doji.shift(1) d3_doji = is_doji # Middle candle gaps d2_mid = (open_.shift(1) + close.shift(1)) / 2.0 d1_mid = (open_.shift(2) + close.shift(2)) / 2.0 d3_mid = (open_ + close) / 2.0 bullish = d1_doji & d2_doji & d3_doji & (d2_mid < d1_mid) & (d2_mid < d3_mid) bearish = d1_doji & d2_doji & d3_doji & (d2_mid > d1_mid) & (d2_mid > d3_mid) result = np.where(bullish, 1, np.where(bearish, -1, 0)) return pd.Series(result, index=close.index, name="tri_star", dtype=int)
# --------------------------------------------------------------------------- # Unique Three River # ---------------------------------------------------------------------------
[docs] def unique_three_river( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, ) -> pd.Series: """Unique Three River Bottom (rare bullish reversal). Day 1: long bearish candle. Day 2: harami-like bearish candle with a lower low. Day 3: small bullish candle that closes below Day 2's close. Parameters: open_: Open prices. high: High prices. low: Low prices. close: Close prices. Returns: 1 where detected, 0 otherwise. Example: >>> signal = unique_three_river(open_, high, low, close) """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") body = _body(open_, close) rng = _range(high, low) # Day 1: long bearish d1_bearish = close.shift(2) < open_.shift(2) d1_body = body.shift(2) d1_range = rng.shift(2) d1_large = d1_body > d1_range * 0.5 # Day 2: bearish candle inside Day 1 body but with a lower low d2_bearish = close.shift(1) < open_.shift(1) d2_inside = (close.shift(1) >= close.shift(2)) & (open_.shift(1) <= open_.shift(2)) d2_lower_low = low.shift(1) < low.shift(2) # Day 3: small bullish candle closing below Day 2 close d3_bullish = close > open_ d3_small = body < body.shift(1) d3_below_d2 = close < close.shift(1) signal = ( d1_bearish & d1_large & d2_bearish & d2_inside & d2_lower_low & d3_bullish & d3_small & d3_below_d2 ) return pd.Series(signal.astype(int), index=close.index, name="unique_three_river")
# --------------------------------------------------------------------------- # Concealing Baby Swallow # ---------------------------------------------------------------------------
[docs] def concealing_baby_swallow( open_: pd.Series, high: pd.Series, low: pd.Series, close: pd.Series, marubozu_threshold: float = 0.02, ) -> pd.Series: """Concealing Baby Swallow (four-candle bearish pattern). Four bearish candles: Days 1-2 are bearish marubozus. Day 3 gaps down, has a long upper shadow into Day 2's body. Day 4 engulfs Day 3 including the shadow. Parameters: open_: Open prices. high: High prices. low: Low prices. close: Close prices. marubozu_threshold: Maximum shadow-to-range ratio for marubozu. Returns: -1 where detected, 0 otherwise. Example: >>> signal = concealing_baby_swallow(open_, high, low, close) """ open_ = _validate_series(open_, "open_") high = _validate_series(high, "high") low = _validate_series(low, "low") close = _validate_series(close, "close") rng = _range(high, low) upper = _upper_shadow(open_, high, close) lower = _lower_shadow(open_, low, close) def _is_bear_maru(shift: int) -> pd.Series: s_rng = rng.shift(shift) if shift > 0 else rng s_upper = upper.shift(shift) if shift > 0 else upper s_lower = lower.shift(shift) if shift > 0 else lower s_close = close.shift(shift) if shift > 0 else close s_open = open_.shift(shift) if shift > 0 else open_ ur = pd.Series(np.where(s_rng != 0, s_upper / s_rng, 0.0), index=close.index) lr = pd.Series(np.where(s_rng != 0, s_lower / s_rng, 0.0), index=close.index) return ( (s_close < s_open) & (ur <= marubozu_threshold) & (lr <= marubozu_threshold) & (s_rng > 0) ) # Days 1 and 2: bearish marubozus d1_maru = _is_bear_maru(3) d2_maru = _is_bear_maru(2) # Day 3: bearish, gaps down, upper shadow reaches into Day 2 body d3_bearish = close.shift(1) < open_.shift(1) d3_gap_down = open_.shift(1) < close.shift(2) d3_upper_into_d2 = high.shift(1) > close.shift(2) # Day 4: bearish, engulfs Day 3 (including shadow) d4_bearish = close < open_ d4_engulfs = (open_ >= high.shift(1)) & (close <= low.shift(1)) signal = ( d1_maru & d2_maru & d3_bearish & d3_gap_down & d3_upper_into_d2 & d4_bearish & d4_engulfs ) result = np.where(signal, -1, 0) return pd.Series( result, index=close.index, name="concealing_baby_swallow", dtype=int )