- name
- price-action
- description
- Pure price action analysis: candlestick patterns & statistics, chart pattern recognition, harmonic patterns, Elliott Wave theory, and trendline/S&R detection. USE FOR: price action trading, candlestick pattern identification, candle pattern analysis, chart pattern recognition, harmonic patterns ABCD Gartley Butterfly Bat Crab, Elliott Wave count, trendline drawing, support resistance levels, head and shoulders, double top double bottom, engulfing candle, pin bar, doji, hammer, shooting star, inside bar, key level reaction, naked chart analysis, no-indicator trading.
- related_skills
- ["technical-analysis","price-action","ict-smart-money","chart-vision"]
- tags
- ["trading","analysis","price-action","harmonics","elliott-wave","technical-analysis"]
- skill_level
- intermediate
- kind
- reference
- category
- trading/strategies
- status
- active
> **Skill:** Price Action | **Domain:** trading | **Category:** analysis | **Level:** intermediate
> **Tags:** `trading`, `analysis`, `price-action`, `harmonics`, `elliott-wave`, `technical-analysis`
---
## Price Action Pure Engine
# Price Action Pure Engine — No Indicators
```python
import pandas as pd, numpy as np
from scipy.signal import argrelextrema
class PriceActionEngine:
@staticmethod
def key_level_reaction(df: pd.DataFrame, levels: list[float], tolerance_atr_mult: float = 0.3) -> list[dict]:
"""Detect price reactions at key levels — the core of PA trading."""
atr = (df["high"] - df["low"]).rolling(14).mean()
reactions = []
for level in levels:
recent = df.tail(20)
for i, (idx, bar) in enumerate(recent.iterrows()):
tol = atr.loc[idx] * tolerance_atr_mult
touching = bar["low"] <= level + tol and bar["high"] >= level - tol
if touching:
body = abs(bar["close"] - bar["open"])
lower_wick = min(bar["open"], bar["close"]) - bar["low"]
upper_wick = bar["high"] - max(bar["open"], bar["close"])
if lower_wick > body * 2 and bar["close"] > bar["open"]:
reactions.append({"level": level, "time": idx, "type": "bullish_rejection",
"signal": "BUY — rejection pin bar at key level"})
elif upper_wick > body * 2 and bar["close"] < bar["open"]:
reactions.append({"level": level, "time": idx, "type": "bearish_rejection",
"signal": "SELL — rejection pin bar at key level"})
elif bar["close"] > level + tol and bar["open"] < level:
reactions.append({"level": level, "time": idx, "type": "bullish_engulf_level",
"signal": "BUY — bullish engulfing through key level"})
return reactions
@staticmethod
def inside_bar_breakout(df: pd.DataFrame) -> list[dict]:
"""Inside bar = compression before expansion. Trade the breakout."""
signals = []
for i in range(1, min(20, len(df))):
idx = len(df) - i
mother = df.iloc[idx - 1]
inside = df.iloc[idx]
if inside["high"] < mother["high"] and inside["low"] > mother["low"]:
if idx + 1 < len(df):
breakout = df.iloc[idx + 1]
if breakout["close"] > mother["high"]:
signals.append({"type": "inside_bar_bullish_breakout", "idx": idx,
"entry": round(mother["high"], 5), "stop": round(mother["low"], 5)})
elif breakout["close"] < mother["low"]:
signals.append({"type": "inside_bar_bearish_breakout", "idx": idx,
"entry": round(mother["low"], 5), "stop": round(mother["high"], 5)})
else:
signals.append({"type": "inside_bar_forming", "idx": idx,
"buy_trigger": round(mother["high"], 5),
"sell_trigger": round(mother["low"], 5)})
return signals
@staticmethod
def engulfing_at_structure(df: pd.DataFrame, order: int = 10) -> list[dict]:
"""Engulfing candles at swing highs/lows — highest probability PA setup."""
highs = argrelextrema(df["high"].values, np.greater, order=order)[0]
lows = argrelextrema(df["low"].values, np.less, order=order)[0]
signals = []
for i in range(1, min(10, len(df))):
idx = len(df) - i
curr = df.iloc[idx]
prev = df.iloc[idx - 1]
# Bullish engulfing near swing low
near_low = any(abs(df["low"].iloc[l] - curr["low"]) < (df["high"] - df["low"]).rolling(14).mean().iloc[idx] for l in lows if abs(l - idx) < 20)
if curr["close"] > curr["open"] and prev["close"] < prev["open"] and curr["close"] > prev["open"] and curr["open"] < prev["close"] and near_low:
signals.append({"type": "bullish_engulfing_at_structure", "idx": idx, "signal": "A+ BUY"})
# Bearish engulfing near swing high
near_high = any(abs(df["high"].iloc[h] - curr["high"]) < (df["high"] - df["low"]).rolling(14).mean().iloc[idx] for h in highs if abs(h - idx) < 20)
if curr["close"] < curr["open"] and prev["close"] > prev["open"] and curr["open"] > prev["close"] and curr["close"] < prev["open"] and near_high:
signals.append({"type": "bearish_engulfing_at_structure", "idx": idx, "signal": "A+ SELL"})
return signals
@staticmethod
def full_pa_scan(df: pd.DataFrame, key_levels: list[float] = None) -> dict:
levels = key_levels or []
return {
"level_reactions": PriceActionEngine.key_level_reaction(df, levels) if levels else [],
"inside_bars": PriceActionEngine.inside_bar_breakout(df),
"engulfing_at_structure": PriceActionEngine.engulfing_at_structure(df),
"principle": "Trade what you SEE, not what you think. PA at key levels = highest probability.",
}
```
---
## Candlestick Pattern Vision
# Candlestick Pattern Vision
## Overview
Pure computer vision approach to candlestick pattern detection. Extracts individual candle
geometries from chart images via contour detection, then classifies patterns using geometric
ratios. Works on any chart screenshot — TradingView, MT5, phone captures.
## Stack
- **OpenCV 4.13** — contour detection, morphological analysis, connected components
- **scikit-image 0.26** — region properties, label analysis
- **numpy 2.4** — geometric computations
---
## 1. Candle Geometry Extractor
```python
import cv2
import numpy as np
from skimage import measure, morphology as sk_morphology
from dataclasses import dataclass
from typing import Optional
@dataclass
class CandleGeometry:
"""Geometric properties of a single candlestick extracted from image."""
x_center: int # horizontal position (pixel)
y_top: int # highest point (wick top)
y_bottom: int # lowest point (wick bottom)
body_top: int # body top (max of open/close)
body_bottom: int # body bottom (min of open/close)
width: int # body width
is_bullish: bool # green/white = bullish
confidence: float # detection confidence
@property
def total_height(self) -> int:
return self.y_bottom - self.y_top
@property
def body_height(self) -> int:
return self.body_bottom - self.body_top
@property
def upper_wick(self) -> int:
return self.body_top - self.y_top
@property
def lower_wick(self) -> int:
return self.y_bottom - self.body_bottom
@property
def body_ratio(self) -> float:
"""Body size relative to total candle."""
return self.body_height / max(self.total_height, 1)
@property
def upper_wick_ratio(self) -> float:
return self.upper_wick / max(self.total_height, 1)
@property
def lower_wick_ratio(self) -> float:
return self.lower_wick / max(self.total_height, 1)
class CandleExtractor:
"""Extract individual candlestick geometries from a preprocessed chart image."""
@staticmethod
def extract_candles(img: np.ndarray, color_info: dict = None) -> list[CandleGeometry]:
"""
Extract all candlesticks from a chart image.
Uses color segmentation + contour analysis + connected components.
"""
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
h, w = img.shape[:2]
# Separate bullish (green) and bearish (red) candles
green_mask = cv2.inRange(hsv, (35, 30, 30), (85, 255, 255))
red_mask1 = cv2.inRange(hsv, (0, 30, 30), (15, 255, 255))
red_mask2 = cv2.inRange(hsv, (165, 30, 30), (180, 255, 255))
red_mask = cv2.bitwise_or(red_mask1, red_mask2)
candles = []
for mask, is_bull in [(green_mask, True), (red_mask, False)]:
# Morphological cleanup
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=2)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=1)
# Connected components (scikit-image 0.26)
labels = measure.label(mask, connectivity=2)
regions = measure.regionprops(labels)
for region in regions:
# Filter by size — candles have specific aspect ratios
bbox = region.bbox # (min_row, min_col, max_row, max_col)
region_h = bbox[2] - bbox[0]
region_w = bbox[3] - bbox[1]
if region_h < 5 or region_w < 2: # Too small
continue
if region_w > w * 0.1: # Too wide (probably not a candle)
continue
if region.area < 20: # Too few pixels
continue
# Determine body vs wick
# Body is the thickest part; wick is thin
col_slice = mask[bbox[0]:bbox[2], bbox[1]:bbox[3]]
row_widths = np.sum(col_slice > 0, axis=1)
# Body rows: where width is > 50% of max width
max_width = row_widths.max()
body_rows = np.where(row_widths > max_width * 0.5)[0]
if len(body_rows) > 0:
body_top_local = body_rows[0]
body_bottom_local = body_rows[-1]
else:
body_top_local = 0
body_bottom_local = region_h
candles.append(CandleGeometry(
x_center=int(region.centroid[1]),
y_top=bbox[0],
y_bottom=bbox[2],
body_top=bbox[0] + body_top_local,
body_bottom=bbox[0] + body_bottom_local,
width=region_w,
is_bullish=is_bull,
confidence=min(region.area / 100, 1.0),
))
# Sort by x position (left to right = chronological)
candles.sort(key=lambda c: c.x_center)
return candles
```
---
## 2. Single Candle Pattern Classifier
```python
class SingleCandleClassifier:
"""Classify individual candlestick patterns from geometry."""
@staticmethod
def classify(candle: CandleGeometry) -> dict:
br = candle.body_ratio
uwr = candle.upper_wick_ratio
lwr = candle.lower_wick_ratio
patterns = []
# Doji: very small body
if br < 0.1:
if uwr > 0.3 and lwr > 0.3:
patterns.append({"pattern": "long_legged_doji", "bias": "reversal", "strength": 0.7})
elif uwr > 0.4:
patterns.append({"pattern": "gravestone_doji", "bias": "bearish_reversal", "strength": 0.75})
elif lwr > 0.4:
patterns.append({"pattern": "dragonfly_doji", "bias": "bullish_reversal", "strength": 0.75})
else:
patterns.append({"pattern": "doji", "bias": "indecision", "strength": 0.5})
# Hammer / Hanging Man: small body at top, long lower wick
elif br < 0.35 and lwr > 0.55 and uwr < 0.1:
if candle.is_bullish:
patterns.append({"pattern": "hammer", "bias": "bullish_reversal", "strength": 0.8})
else:
patterns.append({"pattern": "hanging_man", "bias": "bearish_reversal", "strength": 0.7})
# Inverted Hammer / Shooting Star: small body at bottom, long upper wick
elif br < 0.35 and uwr > 0.55 and lwr < 0.1:
if candle.is_bullish:
patterns.append({"pattern": "inverted_hammer", "bias": "bullish_reversal", "strength": 0.65})
else:
patterns.append({"pattern": "shooting_star", "bias": "bearish_reversal", "strength": 0.8})
# Marubozu: full body, no wicks
elif br > 0.85 and uwr < 0.05 and lwr < 0.05:
GitHubで見る