
Ta Lib
- 230 installs
- 257 repo stars
- Updated June 24, 2026
- agiprolabs/claude-trading-skills
ta-lib is a Claude Code skill for C-optimized technical analysis via TA-Lib, offering 150+ indicator functions and 61 candlestick pattern recognition functions.
About
ta-lib is a Claude Code skill for using TA-Lib, a C-optimized technical analysis library with a Python wrapper providing 150+ indicator functions and 61 candlestick pattern recognition functions. It covers installation of the C dependency, the function and abstract APIs, indicator groups, when to prefer TA-Lib over pandas-ta, and crypto-specific considerations. A developer uses it for fast indicator computation in performance-critical trading pipelines and backtests.
- 150+ technical-analysis functions plus 61 candlestick patterns
- C-speed computation, 10-100x faster than pure-Python
- Function and abstract APIs over NumPy arrays
Ta Lib by the numbers
- 230 all-time installs (skills.sh)
- Ranked #413 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
ta-lib capabilities & compatibility
- Capabilities
- technical indicators · candlestick patterns · backtesting · feature engineering
- Use cases
- trading · data analysis
- Platforms
- macOS · Linux
- Pricing
- Free
What ta-lib says it does
TA-Lib (Technical Analysis Library) is a C library with a Python wrapper providing 150+ technical analysis functions and 61 candlestick pattern recognition functions.
**C-speed computation** — 10-100x faster than pure-Python equivalents on large datasets
npx skills add https://github.com/agiprolabs/claude-trading-skills --skill ta-libAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 230 |
|---|---|
| repo stars | ★ 257 |
| Last updated | June 24, 2026 |
| Repository | agiprolabs/claude-trading-skills ↗ |
What it does
Compute 150+ technical indicators and 61 candlestick patterns at C speed for trading pipelines.
Who is it for?
Fast indicator and candlestick-pattern computation in production trading pipelines and large-scale backtests.
Skip if: Cases where installation simplicity matters and no C dependency is desired, where the skill points to pandas-ta instead.
When should I use this skill?
You need fast, standard technical indicators or candlestick pattern detection over price arrays.
What you get
C-speed indicator and candlestick-pattern computation, 10-100x faster than pure-Python equivalents.
- Computed technical indicators and candlestick pattern signals
By the numbers
- 150+ indicator functions
- 61 candlestick pattern functions
- 10-100x faster than pure-Python
Files
ta-lib — C-Optimized Technical Analysis
TA-Lib (Technical Analysis Library) is a C library with a Python wrapper providing 150+ technical analysis functions and 61 candlestick pattern recognition functions. It is the industry standard for performance-critical indicator computation, used in production trading systems where pandas-ta or pure-Python alternatives are too slow.
What TA-Lib Is
TA-Lib was originally written in C for financial market data analysis. The Python wrapper (TA-Lib on PyPI, imported as talib) provides:
- 150+ indicator functions across overlap, momentum, volume, volatility, cycle, and math categories
- 61 candlestick pattern recognition functions — the most comprehensive pattern library available
- C-speed computation — 10-100x faster than pure-Python equivalents on large datasets
- Two APIs: a function API (pass arrays directly) and an abstract API (pass dict of arrays)
- NumPy native — all inputs and outputs are NumPy arrays
Installation
TA-Lib requires the underlying C library to be installed first:
# macOS
brew install ta-lib
uv pip install TA-Lib numpy pandas
# Ubuntu/Debian
sudo apt-get install -y ta-lib
uv pip install TA-Lib numpy pandas
# From source (any platform)
wget https://github.com/ta-lib/ta-lib/releases/download/v0.6.4/ta-lib-0.6.4-src.tar.gz
tar -xzf ta-lib-0.6.4-src.tar.gz
cd ta-lib-0.6.4
./configure --prefix=/usr/local
make && sudo make install
uv pip install TA-Lib numpy pandasIf the C library is not installed, import talib will fail with an ImportError. The scripts in this skill include fallback logic for environments without TA-Lib installed.
When to Use TA-Lib vs pandas-ta
| Criterion | TA-Lib | pandas-ta |
|---|---|---|
| Speed | C-optimized, 10-100x faster | Pure Python, slower on large data |
| Candlestick patterns | 61 built-in patterns | Limited pattern support |
| Installation | Requires C library | pip install only |
| API style | NumPy arrays | DataFrame .ta accessor |
| Indicator count | 150+ | 130+ |
| Streaming | Single-value update possible | Recompute entire series |
| Dependencies | C lib + numpy | pandas only |
Use TA-Lib when:
- Processing millions of bars or running backtests at scale
- You need candlestick pattern recognition (TA-Lib is unmatched here)
- You are building a production pipeline where latency matters
- You need cycle indicators (Hilbert Transform family)
Use pandas-ta when:
- You want DataFrame-native convenience
- Installation simplicity matters (no C dependency)
- You need indicators not in TA-Lib (pandas-ta has some extras)
Quick Start
import numpy as np
import talib
# Create sample data
close = np.random.randn(100).cumsum() + 50
high = close + np.abs(np.random.randn(100))
low = close - np.abs(np.random.randn(100))
open_ = close + np.random.randn(100) * 0.5
volume = np.random.randint(1000, 10000, 100).astype(float)
# Function API — pass arrays directly
rsi = talib.RSI(close, timeperiod=14)
macd, signal, hist = talib.MACD(close, fastperiod=12, slowperiod=26, signalperiod=9)
upper, middle, lower = talib.BBANDS(close, timeperiod=20, nbdevup=2, nbdevdn=2)
atr = talib.ATR(high, low, close, timeperiod=14)
# Candlestick patterns — return +100 (bullish), -100 (bearish), or 0
doji = talib.CDLDOJI(open_, high, low, close)
hammer = talib.CDLHAMMER(open_, high, low, close)
engulfing = talib.CDLENGULFING(open_, high, low, close)Function API vs Abstract API
Function API (Recommended)
Call functions directly with NumPy arrays:
import talib
rsi = talib.RSI(close, timeperiod=14)
sma = talib.SMA(close, timeperiod=20)
upper, mid, lower = talib.BBANDS(close)Abstract API
Pass a dictionary of arrays and get results by name:
from talib import abstract
inputs = {"open": open_, "high": high, "low": low, "close": close, "volume": volume}
# Call by function name
rsi = abstract.RSI(inputs, timeperiod=14)
macd = abstract.MACD(inputs) # returns (macd, signal, hist)The abstract API is useful for dynamic indicator selection (e.g., looping over a list of indicator names).
Function Groups
TA-Lib organizes functions into these groups:
Overlap Studies
Moving averages and envelope indicators that overlay price charts.
sma = talib.SMA(close, timeperiod=20)
ema = talib.EMA(close, timeperiod=12)
upper, mid, lower = talib.BBANDS(close, timeperiod=20, nbdevup=2, nbdevdn=2)
sar = talib.SAR(high, low, acceleration=0.02, maximum=0.2)
mama, fama = talib.MAMA(close, fastlimit=0.5, slowlimit=0.05)Momentum Indicators
Oscillators and trend-strength measures.
rsi = talib.RSI(close, timeperiod=14)
macd, signal, hist = talib.MACD(close, fastperiod=12, slowperiod=26, signalperiod=9)
slowk, slowd = talib.STOCH(high, low, close)
cci = talib.CCI(high, low, close, timeperiod=14)
willr = talib.WILLR(high, low, close, timeperiod=14)
adx = talib.ADX(high, low, close, timeperiod=14)
mfi = talib.MFI(high, low, close, volume, timeperiod=14)Volume Indicators
Volume-based analysis functions.
obv = talib.OBV(close, volume)
ad = talib.AD(high, low, close, volume)
adosc = talib.ADOSC(high, low, close, volume, fastperiod=3, slowperiod=10)Volatility Indicators
Measures of price variability.
atr = talib.ATR(high, low, close, timeperiod=14)
natr = talib.NATR(high, low, close, timeperiod=14)
trange = talib.TRANGE(high, low, close)Pattern Recognition (Candlestick)
61 functions that detect candlestick patterns. All return integer arrays:
+100= bullish pattern detected-100= bearish pattern detected0= no pattern
# Single patterns
doji = talib.CDLDOJI(open_, high, low, close)
hammer = talib.CDLHAMMER(open_, high, low, close)
engulfing = talib.CDLENGULFING(open_, high, low, close)
# Scan all 61 patterns at once
candle_names = talib.get_function_groups()["Pattern Recognition"]
for name in candle_names:
func = getattr(talib, name)
result = func(open_, high, low, close)
hits = np.nonzero(result)[0]
if len(hits) > 0:
print(f"{name}: {len(hits)} detections")See references/candlestick_patterns.md for the full list of 61 patterns with reliability ratings and crypto relevance.
Math Transform & Math Operators
Mathematical functions (sin, cos, ln, etc.) and operators (add, sub, mult, div) on arrays. Rarely used directly but available.
Crypto Considerations
24/7 Markets
- Candlestick patterns designed for traditional markets with opening/closing gaps may behave differently on crypto's continuous markets
- Gap-based patterns (morning star, evening star) are less reliable without session gaps
- Body-ratio patterns (doji, hammer, engulfing) still work well on any timeframe
Timeframe Selection
- 1m-5m: Patterns are noisy; combine with volume confirmation
- 15m-1h: Good for intraday signals on high-cap tokens
- 4h-1d: Most reliable for pattern recognition
- Tip: Higher timeframes produce fewer but more reliable pattern signals
NaN Handling
TA-Lib returns NaN for the initial lookback period of each indicator. Always account for this:
rsi = talib.RSI(close, timeperiod=14)
# First 14 values will be NaN
valid_rsi = rsi[~np.isnan(rsi)]Solana Token Data
When using TA-Lib with Solana token OHLCV data:
- Ensure arrays are
float64dtype — TA-Lib requires this - Sort by timestamp ascending before passing to TA-Lib
- Handle gaps in low-liquidity token data before computing indicators
# Convert to float64 for TA-Lib compatibility
close = df["close"].values.astype(np.float64)
high = df["high"].values.astype(np.float64)
low = df["low"].values.astype(np.float64)Integration with Other Skills
With pandas-ta
pandas-ta can use TA-Lib as a backend when installed, getting C-speed through the pandas-ta API:
import pandas_ta as ta
# pandas-ta auto-detects TA-Lib and uses it for supported indicators
# Set explicitly:
ta.Imports["talib"] = True # Force TA-Lib backend
df.ta.rsi(length=14) # Uses TA-Lib under the hood if availableWith vectorbt
vectorbt integrates with TA-Lib for fast backtesting:
import vectorbt as vbt
# Use TA-Lib indicators in vectorbt
rsi = vbt.talib("RSI").run(close, timeperiod=14)
entries = rsi.real_crossed_below(30)
exits = rsi.real_crossed_above(70)With Birdeye/DexScreener Data
Fetch OHLCV data from API skills, then process with TA-Lib:
# After fetching OHLCV from birdeye-api or dexscreener-api
close = np.array(ohlcv_data["close"], dtype=np.float64)
rsi = talib.RSI(close, timeperiod=14)Listing Available Functions
import talib
# All function groups
groups = talib.get_function_groups()
for group, funcs in groups.items():
print(f"{group}: {len(funcs)} functions")
# All function names
all_funcs = talib.get_functions()
print(f"Total: {len(all_funcs)} functions")
# Info about a specific function
info = talib.abstract.Function("RSI").info
print(info["display_name"], info["group"])Files
| File | Description |
|---|---|
references/function_reference.md | Most useful functions by category with syntax and parameters |
references/candlestick_patterns.md | All 61 candlestick patterns grouped by type with reliability ratings |
scripts/compute_indicators.py | Computes common indicators with TA-Lib/fallback comparison |
scripts/pattern_scanner.py | Scans OHLCV data for all 61 candlestick patterns |
TA-Lib Candlestick Patterns — Complete Reference
All 61 candlestick pattern recognition functions. Every function takes (open, high, low, close) as float64 NumPy arrays and returns an integer array: +100 = bullish, -100 = bearish, 0 = no pattern detected. Some patterns return +200/-200 for strong confirmation.
Return Value Convention
result = talib.CDLHAMMER(open_, high, low, close)
# result[i] == 100 → bullish hammer on bar i
# result[i] == -100 → bearish variant (if applicable)
# result[i] == 0 → no pattern on bar iReversal Bullish (12 patterns)
Patterns that signal a potential bottom/reversal from bearish to bullish trend.
| # | Function | Name | Bars | Reliability | Crypto Notes |
|---|---|---|---|---|---|
| 1 | CDLHAMMER | Hammer | 1 | High | Works well on all timeframes |
| 2 | CDLINVERTEDHAMMER | Inverted Hammer | 1 | Medium | Needs volume confirmation |
| 3 | CDLENGULFING | Bullish Engulfing | 2 | High | Strong on 4h+ timeframes |
| 4 | CDLPIERCING | Piercing Line | 2 | High | Less reliable without gaps |
| 5 | CDLMORNINGSTAR | Morning Star | 3 | High | Rare on crypto (needs gaps) |
| 6 | CDLMORNINGDOJISTAR | Morning Doji Star | 3 | High | Rare on crypto |
| 7 | CDLHARAMI | Bullish Harami | 2 | Medium | Common but weak alone |
| 8 | CDLHARAMICROSS | Bullish Harami Cross | 2 | Medium | Harami with doji inside |
| 9 | CDLDRAGONFLYDOJI | Dragonfly Doji | 1 | Medium | Good at support levels |
| 10 | CDLABANDONEDBABY | Abandoned Baby (Bull) | 3 | High | Very rare on crypto |
| 11 | CDLTHREEWHITESOLDIERS | Three White Soldiers | 3 | High | Strong trend reversal |
| 12 | CDLKICKING | Kicking (Bull) | 2 | High | Needs gaps, rare on crypto |
Reversal Bearish (12 patterns)
Patterns that signal a potential top/reversal from bullish to bearish trend.
| # | Function | Name | Bars | Reliability | Crypto Notes |
|---|---|---|---|---|---|
| 13 | CDLSHOOTINGSTAR | Shooting Star | 1 | High | Works well, confirm with volume |
| 14 | CDLHANGINGMAN | Hanging Man | 1 | Medium | Hammer at top, needs confirmation |
| 15 | CDLENGULFING | Bearish Engulfing | 2 | High | Returns -100 for bearish |
| 16 | CDLDARKCLOUDCOVER | Dark Cloud Cover | 2 | High | Less reliable without gaps |
| 17 | CDLEVENINGSTAR | Evening Star | 3 | High | Rare on crypto (needs gaps) |
| 18 | CDLEVENINGDOJISTAR | Evening Doji Star | 3 | High | Rare on crypto |
| 19 | CDLHARAMI | Bearish Harami | 2 | Medium | Returns -100 for bearish |
| 20 | CDLHARAMICROSS | Bearish Harami Cross | 2 | Medium | Harami with doji inside |
| 21 | CDLGRAVESTONEDOJI | Gravestone Doji | 1 | Medium | Good at resistance levels |
| 22 | CDLABANDONEDBABY | Abandoned Baby (Bear) | 3 | High | Very rare on crypto |
| 23 | CDLTHREEBLACKCROWS | Three Black Crows | 3 | High | Strong downtrend signal |
| 24 | CDLKICKING | Kicking (Bear) | 2 | High | Needs gaps, rare on crypto |
Continuation Patterns (8 patterns)
Patterns that suggest the existing trend will continue.
| # | Function | Name | Bars | Reliability | Crypto Notes |
|---|---|---|---|---|---|
| 25 | CDLRISEFALL3METHODS | Rising/Falling Three | 5 | High | Needs clear prior trend |
| 26 | CDLTASUKIGAP | Tasuki Gap | 3 | Medium | Gap-dependent, rare crypto |
| 27 | CDLGAPSIDESIDEWHITE | Side-by-Side White | 3 | Low | Gap-dependent |
| 28 | CDLSEPARATINGLINES | Separating Lines | 2 | Medium | Gap-dependent |
| 29 | CDLMATHOLD | Mat Hold | 5 | High | Rare but reliable |
| 30 | CDLINNECK | In-Neck | 2 | Low | Bearish continuation |
| 31 | CDLONNECK | On-Neck | 2 | Low | Bearish continuation |
| 32 | CDLTHRUSTING | Thrusting | 2 | Low | Bearish continuation |
Indecision Patterns (5 patterns)
Patterns indicating market indecision — potential inflection points.
| # | Function | Name | Bars | Reliability | Crypto Notes |
|---|---|---|---|---|---|
| 33 | CDLDOJI | Doji | 1 | Medium | Very common on low TFs |
| 34 | CDLDRAGONFLYDOJI | Dragonfly Doji | 1 | Medium | Bullish bias at support |
| 35 | CDLGRAVESTONEDOJI | Gravestone Doji | 1 | Medium | Bearish bias at resistance |
| 36 | CDLLONGLEGGEDDOJI | Long-Legged Doji | 1 | Medium | High volatility indecision |
| 37 | CDLSPINNINGTOP | Spinning Top | 1 | Low | Very common, weak signal |
Complex Multi-Bar Patterns (14 patterns)
| # | Function | Name | Bars | Reliability |
|---|---|---|---|---|
| 38 | CDLADVANCEBLOCK | Advance Block | 3 | Medium |
| 39 | CDLBELTHOLD | Belt Hold | 1 | Low |
| 40 | CDLBREAKAWAY | Breakaway | 5 | Medium |
| 41 | CDLCLOSINGMARUBOZU | Closing Marubozu | 1 | Medium |
| 42 | CDLCONCEALBABYSWALL | Concealing Baby Swallow | 4 | High |
| 43 | CDLCOUNTERATTACK | Counterattack | 2 | Medium |
| 44 | CDLDOJISTAR | Doji Star | 2 | Medium |
| 45 | CDLHIGHWAVE | High Wave | 1 | Low |
| 46 | CDLHIKKAKE | Hikkake | 3 | Medium |
| 47 | CDLHIKKAKEMOD | Modified Hikkake | 3 | Medium |
| 48 | CDLHOMINGPIGEON | Homing Pigeon | 2 | Low |
| 49 | CDLIDENTICAL3CROWS | Identical Three Crows | 3 | High |
| 50 | CDLLADDERBOTTOM | Ladder Bottom | 5 | Medium |
| 51 | CDLLONGLINE | Long Line | 1 | Low |
Remaining Patterns (10 patterns)
| # | Function | Name | Bars | Reliability |
|---|---|---|---|---|
| 52 | CDLMARUBOZU | Marubozu | 1 | High |
| 53 | CDLMATCHINGLOW | Matching Low | 2 | Medium |
| 54 | CDLRICKSHAWMAN | Rickshaw Man | 1 | Low |
| 55 | CDLSHORTLINE | Short Line | 1 | Low |
| 56 | CDLSTALLEDPATTERN | Stalled Pattern | 3 | Medium |
| 57 | CDLSTICKSANDWICH | Stick Sandwich | 3 | Medium |
| 58 | CDLTAKURI | Takuri | 1 | Medium |
| 59 | CDLTRISTAR | Tri-Star | 3 | Medium |
| 60 | CDLUNIQUE3RIVER | Unique Three River | 3 | Medium |
| 61 | CDLXSIDEGAP3METHODS | Side Gap Three Methods | 3 | Medium |
Reliability Tiers for Crypto
Tier 1 — Most Reliable on 24/7 Markets
These patterns depend on body ratios, not gaps, so they work well on continuous crypto markets:
CDLHAMMER,CDLSHOOTINGSTAR— Single-bar reversal, body/wick ratio basedCDLENGULFING— Two-bar reversal, strong on 4h+ timeframesCDLDOJIvariants — Indecision at key levelsCDLMARUBOZU— Strong momentum candleCDLTHREEWHITESOLDIERS,CDLTHREEBLACKCROWS— Three-bar trend
Tier 2 — Moderately Reliable
Work on crypto but less frequently than traditional markets:
CDLHARAMI,CDLHARAMICROSS— Inside bar patternsCDLPIERCING,CDLDARKCLOUDCOVER— Two-bar patternsCDLRISEFALL3METHODS— Continuation patterns
Tier 3 — Less Reliable on Crypto
Gap-dependent patterns that rarely form on 24/7 markets:
CDLMORNINGSTAR,CDLEVENINGSTAR— Need session gapsCDLABANDONEDBABY— Requires true gapsCDLKICKING— Marubozu with gapCDLTASUKIGAP,CDLGAPSIDESIDEWHITE— Gap-dependent
Scanning All Patterns
import talib
import numpy as np
def scan_all_patterns(open_: np.ndarray, high: np.ndarray,
low: np.ndarray, close: np.ndarray) -> dict:
"""Scan OHLC data for all 61 candlestick patterns."""
candle_funcs = talib.get_function_groups()["Pattern Recognition"]
results = {}
for name in candle_funcs:
func = getattr(talib, name)
result = func(open_, high, low, close)
hits = np.nonzero(result)[0]
if len(hits) > 0:
results[name] = {
"count": len(hits),
"bars": hits.tolist(),
"values": result[hits].tolist()
}
return resultsCombining Patterns with Indicators
Candlestick patterns alone generate many false signals. Combine with trend/momentum:
rsi = talib.RSI(close, timeperiod=14)
hammer = talib.CDLHAMMER(open_, high, low, close)
# Only take bullish hammer when RSI is oversold
confirmed = np.where((hammer > 0) & (rsi < 30), hammer, 0)TA-Lib Function Reference
Most useful functions by category with syntax, parameters, defaults, and return values.
Overlap Studies
SMA — Simple Moving Average
sma = talib.SMA(close, timeperiod=30)- Parameters:
timeperiod(default 30) - Returns: Single array. NaN for first
timeperiod - 1bars. - Use: Trend direction, dynamic support/resistance.
EMA — Exponential Moving Average
ema = talib.EMA(close, timeperiod=30)- Parameters:
timeperiod(default 30) - Returns: Single array. NaN for first
timeperiod - 1bars. - Use: Faster-reacting trend filter than SMA. EMA 12/26 used in MACD.
BBANDS — Bollinger Bands
upper, middle, lower = talib.BBANDS(close, timeperiod=5, nbdevup=2, nbdevdn=2, matype=0)- Parameters:
timeperiod(default 5),nbdevup(default 2),nbdevdn(default 2),matype(0=SMA, 1=EMA, etc.) - Returns: Three arrays (upper, middle, lower). Middle is the moving average.
- Use: Volatility bands, mean reversion entries at band touches.
SAR — Parabolic SAR
sar = talib.SAR(high, low, acceleration=0.02, maximum=0.2)- Parameters:
acceleration(default 0.02),maximum(default 0.2) - Returns: Single array of SAR values.
- Use: Trailing stop placement, trend direction (price above SAR = bullish).
MAMA — MESA Adaptive Moving Average
mama, fama = talib.MAMA(close, fastlimit=0.5, slowlimit=0.05)- Parameters:
fastlimit(default 0.5),slowlimit(default 0.05) - Returns: Two arrays (MAMA, FAMA). MAMA crosses above FAMA = bullish.
- Use: Adaptive trend following with reduced lag.
DEMA / TEMA — Double/Triple EMA
dema = talib.DEMA(close, timeperiod=30)
tema = talib.TEMA(close, timeperiod=30)- Use: Reduced-lag moving averages for faster signal generation.
WMA — Weighted Moving Average
wma = talib.WMA(close, timeperiod=30)KAMA — Kaufman Adaptive Moving Average
kama = talib.KAMA(close, timeperiod=30)- Use: Adapts speed based on market noise. Slower in choppy markets, faster in trends.
Momentum Indicators
RSI — Relative Strength Index
rsi = talib.RSI(close, timeperiod=14)- Parameters:
timeperiod(default 14) - Returns: Single array, values 0-100. NaN for first
timeperiodbars. - Interpretation: < 30 oversold, > 70 overbought. Crypto often uses 20/80 thresholds.
MACD — Moving Average Convergence Divergence
macd, signal, hist = talib.MACD(close, fastperiod=12, slowperiod=26, signalperiod=9)- Parameters:
fastperiod(12),slowperiod(26),signalperiod(9) - Returns: Three arrays (MACD line, signal line, histogram).
- Interpretation: MACD crosses above signal = bullish. Histogram shows momentum.
STOCH — Stochastic Oscillator
slowk, slowd = talib.STOCH(high, low, close, fastk_period=5, slowk_period=3, slowk_matype=0, slowd_period=3, slowd_matype=0)- Returns: Two arrays (SlowK, SlowD), values 0-100.
- Interpretation: < 20 oversold, > 80 overbought. K crossing D = signal.
STOCHRSI — Stochastic RSI
fastk, fastd = talib.STOCHRSI(close, timeperiod=14, fastk_period=5, fastd_period=3, fastd_matype=0)- Returns: Two arrays (FastK, FastD), values 0-100.
- Use: More sensitive than RSI alone. Popular in crypto analysis.
CCI — Commodity Channel Index
cci = talib.CCI(high, low, close, timeperiod=14)- Returns: Single array, unbounded. Typically oscillates -200 to +200.
- Interpretation: > +100 overbought, < -100 oversold.
WILLR — Williams %R
willr = talib.WILLR(high, low, close, timeperiod=14)- Returns: Single array, values -100 to 0.
- Interpretation: < -80 oversold, > -20 overbought.
ADX — Average Directional Index
adx = talib.ADX(high, low, close, timeperiod=14)- Returns: Single array, values 0-100.
- Interpretation: > 25 trending, < 20 ranging. Does not indicate direction.
PLUS_DI / MINUS_DI — Directional Indicators
plus_di = talib.PLUS_DI(high, low, close, timeperiod=14)
minus_di = talib.MINUS_DI(high, low, close, timeperiod=14)- Use: Combined with ADX. +DI > -DI = bullish trend, -DI > +DI = bearish.
MFI — Money Flow Index
mfi = talib.MFI(high, low, close, volume, timeperiod=14)- Returns: Single array, values 0-100.
- Interpretation: Volume-weighted RSI. < 20 oversold, > 80 overbought.
ROC — Rate of Change
roc = talib.ROC(close, timeperiod=10)- Returns: Percentage change over
timeperiodbars.
MOM — Momentum
mom = talib.MOM(close, timeperiod=10)- Returns: Price difference over
timeperiodbars (close - close[n]).
Volume Indicators
OBV — On Balance Volume
obv = talib.OBV(close, volume)- Returns: Cumulative volume series. Rising OBV = buying pressure.
AD — Chaikin A/D Line
ad = talib.AD(high, low, close, volume)- Returns: Accumulation/Distribution line. Divergence from price signals reversals.
ADOSC — Chaikin A/D Oscillator
adosc = talib.ADOSC(high, low, close, volume, fastperiod=3, slowperiod=10)- Returns: Difference between fast and slow A/D EMAs.
Volatility Indicators
ATR — Average True Range
atr = talib.ATR(high, low, close, timeperiod=14)- Returns: Single array in price units. Higher = more volatile.
- Use: Stop-loss placement, position sizing. Common: 2x ATR stop.
NATR — Normalized ATR
natr = talib.NATR(high, low, close, timeperiod=14)- Returns: ATR as percentage of close. Comparable across different price levels.
TRANGE — True Range
trange = talib.TRANGE(high, low, close)- Returns: Single-bar true range (max of high-low, |high-prevclose|, |low-prevclose|).
Pattern Recognition (Top 20)
All candlestick functions take (open, high, low, close) and return integer arrays: +100 = bullish, -100 = bearish, 0 = no pattern. Some return +200/-200 for strong signals.
| Function | Pattern | Bars | Reliability |
|---|---|---|---|
CDLDOJI | Doji | 1 | Medium |
CDLHAMMER | Hammer | 1 | High |
CDLINVERTEDHAMMER | Inverted Hammer | 1 | Medium |
CDLSHOOTINGSTAR | Shooting Star | 1 | High |
CDLHANGINGMAN | Hanging Man | 1 | Medium |
CDLENGULFING | Engulfing | 2 | High |
CDLHARAMI | Harami | 2 | Medium |
CDLPIERCING | Piercing Line | 2 | High |
CDLDARKCLOUDCOVER | Dark Cloud Cover | 2 | High |
CDLMORNINGSTAR | Morning Star | 3 | High |
CDLEVENINGSTAR | Evening Star | 3 | High |
CDLMORNINGDOJISTAR | Morning Doji Star | 3 | High |
CDLEVENINGDOJISTAR | Evening Doji Star | 3 | High |
CDLTHREEWHITESOLDIERS | Three White Soldiers | 3 | High |
CDLTHREEBLACKCROWS | Three Black Crows | 3 | High |
CDLMARUBOZU | Marubozu | 1 | High |
CDLSPINNINGTOP | Spinning Top | 1 | Low |
CDLDRAGONFLYDOJI | Dragonfly Doji | 1 | Medium |
CDLGRAVESTONEDOJI | Gravestone Doji | 1 | Medium |
CDLABANDONEDBABY | Abandoned Baby | 3 | High |
See candlestick_patterns.md for the complete list of all 61 patterns.
Discovering Functions
# List all groups and their functions
groups = talib.get_function_groups()
for group, funcs in groups.items():
print(f"\n{group} ({len(funcs)}):")
for f in funcs:
print(f" {f}")
# Get info about any function
info = talib.abstract.Function("RSI").info
print(info["display_name"]) # "Relative Strength Index"
print(info["group"]) # "Momentum Indicators"
print(info["parameters"]) # {"timeperiod": 14}
print(info["output_names"]) # ["real"]#!/usr/bin/env python3
"""Compute common TA-Lib technical indicators on synthetic OHLCV data.
Demonstrates RSI, MACD, Bollinger Bands, ATR, and ADX using TA-Lib when
available, with manual fallback implementations for environments without
the C library installed. Includes a performance comparison between
TA-Lib and manual computation.
Usage:
python scripts/compute_indicators.py
python scripts/compute_indicators.py --demo
python scripts/compute_indicators.py --bars 5000
Dependencies:
uv pip install numpy pandas
Optional: uv pip install TA-Lib (requires C library)
"""
import argparse
import sys
import time
from typing import Optional, Tuple
import numpy as np
import pandas as pd
# ── TA-Lib Import (optional) ───────────────────────────────────────
TALIB_AVAILABLE = False
try:
import talib
TALIB_AVAILABLE = True
except ImportError:
pass
# ── Synthetic Data Generation ──────────────────────────────────────
def generate_ohlcv(
bars: int = 500,
start_price: float = 100.0,
volatility: float = 0.02,
seed: int = 42,
) -> pd.DataFrame:
"""Generate synthetic OHLCV data with realistic price dynamics.
Uses geometric Brownian motion for close prices with random
high/low/open offsets and volume proportional to price changes.
Args:
bars: Number of bars to generate.
start_price: Starting price.
volatility: Per-bar return standard deviation.
seed: Random seed for reproducibility.
Returns:
DataFrame with columns: open, high, low, close, volume.
"""
rng = np.random.default_rng(seed)
# Geometric Brownian motion for close prices
returns = rng.normal(0, volatility, bars)
close = start_price * np.exp(np.cumsum(returns))
# Generate OHLV around close
spread = close * volatility
high = close + np.abs(rng.normal(0, 1, bars)) * spread
low = close - np.abs(rng.normal(0, 1, bars)) * spread
open_ = close + rng.normal(0, 0.5, bars) * spread
# Ensure high >= max(open, close) and low <= min(open, close)
high = np.maximum(high, np.maximum(open_, close))
low = np.minimum(low, np.minimum(open_, close))
# Volume correlated with absolute returns
base_volume = 100_000
volume = base_volume * (1 + 5 * np.abs(returns))
volume = volume.astype(np.float64)
df = pd.DataFrame({
"open": open_,
"high": high,
"low": low,
"close": close,
"volume": volume,
})
return df
# ── Manual Indicator Implementations (Fallback) ───────────────────
def manual_sma(data: np.ndarray, period: int) -> np.ndarray:
"""Simple Moving Average using cumulative sum trick.
Args:
data: Input price array.
period: Lookback window.
Returns:
Array with SMA values (NaN for first period-1 bars).
"""
result = np.full_like(data, np.nan)
if len(data) < period:
return result
cumsum = np.cumsum(data)
cumsum[period:] = cumsum[period:] - cumsum[:-period]
result[period - 1:] = cumsum[period - 1:] / period
return result
def manual_ema(data: np.ndarray, period: int) -> np.ndarray:
"""Exponential Moving Average.
Args:
data: Input price array.
period: Lookback period (determines alpha = 2/(period+1)).
Returns:
Array with EMA values (NaN for first period-1 bars).
"""
result = np.full_like(data, np.nan, dtype=np.float64)
if len(data) < period:
return result
alpha = 2.0 / (period + 1)
# Seed with SMA of first `period` values
result[period - 1] = np.mean(data[:period])
for i in range(period, len(data)):
result[i] = alpha * data[i] + (1 - alpha) * result[i - 1]
return result
def manual_rsi(close: np.ndarray, period: int = 14) -> np.ndarray:
"""Relative Strength Index using Wilder's smoothing.
Args:
close: Close price array.
period: RSI lookback period.
Returns:
Array with RSI values 0-100 (NaN for first period bars).
"""
result = np.full_like(close, np.nan, dtype=np.float64)
if len(close) < period + 1:
return result
deltas = np.diff(close)
gains = np.where(deltas > 0, deltas, 0.0)
losses = np.where(deltas < 0, -deltas, 0.0)
# Initial average gain/loss
avg_gain = np.mean(gains[:period])
avg_loss = np.mean(losses[:period])
if avg_loss == 0:
result[period] = 100.0
else:
rs = avg_gain / avg_loss
result[period] = 100.0 - (100.0 / (1.0 + rs))
# Wilder's smoothing
for i in range(period, len(deltas)):
avg_gain = (avg_gain * (period - 1) + gains[i]) / period
avg_loss = (avg_loss * (period - 1) + losses[i]) / period
if avg_loss == 0:
result[i + 1] = 100.0
else:
rs = avg_gain / avg_loss
result[i + 1] = 100.0 - (100.0 / (1.0 + rs))
return result
def manual_macd(
close: np.ndarray,
fast: int = 12,
slow: int = 26,
signal_period: int = 9,
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
"""MACD: difference of two EMAs plus signal line.
Args:
close: Close price array.
fast: Fast EMA period.
slow: Slow EMA period.
signal_period: Signal line EMA period.
Returns:
Tuple of (macd_line, signal_line, histogram).
"""
ema_fast = manual_ema(close, fast)
ema_slow = manual_ema(close, slow)
macd_line = ema_fast - ema_slow
# Signal line: EMA of MACD line (only where MACD is valid)
valid_start = slow - 1
signal_line = np.full_like(close, np.nan, dtype=np.float64)
macd_valid = macd_line[valid_start:]
if len(macd_valid) >= signal_period:
sig = manual_ema(macd_valid, signal_period)
signal_line[valid_start:] = sig
histogram = macd_line - signal_line
return macd_line, signal_line, histogram
def manual_bbands(
close: np.ndarray,
period: int = 20,
nbdev: float = 2.0,
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
"""Bollinger Bands: SMA +/- N standard deviations.
Args:
close: Close price array.
period: Moving average period.
nbdev: Number of standard deviations.
Returns:
Tuple of (upper, middle, lower) band arrays.
"""
middle = manual_sma(close, period)
upper = np.full_like(close, np.nan, dtype=np.float64)
lower = np.full_like(close, np.nan, dtype=np.float64)
for i in range(period - 1, len(close)):
std = np.std(close[i - period + 1: i + 1], ddof=0)
upper[i] = middle[i] + nbdev * std
lower[i] = middle[i] - nbdev * std
return upper, middle, lower
def manual_true_range(
high: np.ndarray, low: np.ndarray, close: np.ndarray
) -> np.ndarray:
"""True Range: max(H-L, |H-prevC|, |L-prevC|).
Args:
high: High price array.
low: Low price array.
close: Close price array.
Returns:
True range array (NaN for first bar).
"""
tr = np.full_like(close, np.nan, dtype=np.float64)
tr[0] = high[0] - low[0]
for i in range(1, len(close)):
hl = high[i] - low[i]
hc = abs(high[i] - close[i - 1])
lc = abs(low[i] - close[i - 1])
tr[i] = max(hl, hc, lc)
return tr
def manual_atr(
high: np.ndarray,
low: np.ndarray,
close: np.ndarray,
period: int = 14,
) -> np.ndarray:
"""Average True Range using Wilder's smoothing.
Args:
high: High price array.
low: Low price array.
close: Close price array.
period: ATR lookback period.
Returns:
ATR array (NaN for first period bars).
"""
tr = manual_true_range(high, low, close)
atr = np.full_like(close, np.nan, dtype=np.float64)
if len(close) < period + 1:
return atr
atr[period] = np.mean(tr[1: period + 1])
for i in range(period + 1, len(close)):
atr[i] = (atr[i - 1] * (period - 1) + tr[i]) / period
return atr
def manual_adx(
high: np.ndarray,
low: np.ndarray,
close: np.ndarray,
period: int = 14,
) -> np.ndarray:
"""Average Directional Index.
Computes +DM, -DM, smoothed +DI, -DI, DX, then ADX.
Args:
high: High price array.
low: Low price array.
close: Close price array.
period: ADX lookback period.
Returns:
ADX array (NaN for initial lookback bars).
"""
n = len(close)
adx_out = np.full(n, np.nan, dtype=np.float64)
if n < 2 * period + 1:
return adx_out
# Directional movement
plus_dm = np.zeros(n, dtype=np.float64)
minus_dm = np.zeros(n, dtype=np.float64)
tr = manual_true_range(high, low, close)
for i in range(1, n):
up_move = high[i] - high[i - 1]
down_move = low[i - 1] - low[i]
plus_dm[i] = up_move if (up_move > down_move and up_move > 0) else 0.0
minus_dm[i] = down_move if (down_move > up_move and down_move > 0) else 0.0
# Wilder's smoothing for TR, +DM, -DM
smooth_tr = np.sum(tr[1: period + 1])
smooth_plus = np.sum(plus_dm[1: period + 1])
smooth_minus = np.sum(minus_dm[1: period + 1])
dx_values = []
for i in range(period, n):
if i > period:
smooth_tr = smooth_tr - smooth_tr / period + tr[i]
smooth_plus = smooth_plus - smooth_plus / period + plus_dm[i]
smooth_minus = smooth_minus - smooth_minus / period + minus_dm[i]
if smooth_tr == 0:
dx_values.append(0.0)
continue
plus_di = 100.0 * smooth_plus / smooth_tr
minus_di = 100.0 * smooth_minus / smooth_tr
di_sum = plus_di + minus_di
if di_sum == 0:
dx_values.append(0.0)
else:
dx = 100.0 * abs(plus_di - minus_di) / di_sum
dx_values.append(dx)
# ADX is smoothed DX
if len(dx_values) >= period:
adx_val = np.mean(dx_values[:period])
adx_out[2 * period - 1] = adx_val
for i in range(period, len(dx_values)):
adx_val = (adx_val * (period - 1) + dx_values[i]) / period
adx_out[period + i] = adx_val
return adx_out
# ── Computation Engine ─────────────────────────────────────────────
def compute_with_talib(df: pd.DataFrame) -> pd.DataFrame:
"""Compute indicators using TA-Lib.
Args:
df: OHLCV DataFrame with float64 columns.
Returns:
DataFrame with indicator columns added.
"""
close = df["close"].values.astype(np.float64)
high = df["high"].values.astype(np.float64)
low = df["low"].values.astype(np.float64)
volume = df["volume"].values.astype(np.float64)
result = df.copy()
result["RSI_14"] = talib.RSI(close, timeperiod=14)
macd, signal, hist = talib.MACD(close, fastperiod=12, slowperiod=26, signalperiod=9)
result["MACD"] = macd
result["MACD_signal"] = signal
result["MACD_hist"] = hist
upper, middle, lower = talib.BBANDS(close, timeperiod=20, nbdevup=2, nbdevdn=2)
result["BB_upper"] = upper
result["BB_middle"] = middle
result["BB_lower"] = lower
result["ATR_14"] = talib.ATR(high, low, close, timeperiod=14)
result["ADX_14"] = talib.ADX(high, low, close, timeperiod=14)
return result
def compute_manual(df: pd.DataFrame) -> pd.DataFrame:
"""Compute indicators using manual fallback implementations.
Args:
df: OHLCV DataFrame.
Returns:
DataFrame with indicator columns added.
"""
close = df["close"].values.astype(np.float64)
high = df["high"].values.astype(np.float64)
low = df["low"].values.astype(np.float64)
result = df.copy()
result["RSI_14"] = manual_rsi(close, 14)
macd, signal, hist = manual_macd(close, 12, 26, 9)
result["MACD"] = macd
result["MACD_signal"] = signal
result["MACD_hist"] = hist
upper, middle, lower = manual_bbands(close, 20, 2.0)
result["BB_upper"] = upper
result["BB_middle"] = middle
result["BB_lower"] = lower
result["ATR_14"] = manual_atr(high, low, close, 14)
result["ADX_14"] = manual_adx(high, low, close, 14)
return result
# ── Performance Benchmark ─────────────────────────────────────────
def benchmark(df: pd.DataFrame, iterations: int = 50) -> None:
"""Compare TA-Lib vs manual computation speed.
Args:
df: OHLCV DataFrame for benchmarking.
iterations: Number of iterations for timing.
"""
print(f"\n{'='*60}")
print(f"Performance Benchmark ({len(df)} bars, {iterations} iterations)")
print(f"{'='*60}")
# Manual timing
start = time.perf_counter()
for _ in range(iterations):
compute_manual(df)
manual_time = time.perf_counter() - start
print(f"Manual fallback: {manual_time:.3f}s total, "
f"{manual_time/iterations*1000:.1f}ms per call")
if TALIB_AVAILABLE:
start = time.perf_counter()
for _ in range(iterations):
compute_with_talib(df)
talib_time = time.perf_counter() - start
print(f"TA-Lib (C): {talib_time:.3f}s total, "
f"{talib_time/iterations*1000:.1f}ms per call")
speedup = manual_time / talib_time if talib_time > 0 else float("inf")
print(f"Speedup: {speedup:.1f}x faster with TA-Lib")
else:
print("TA-Lib not installed — skipping C library benchmark.")
# ── Display Functions ──────────────────────────────────────────────
def display_results(df: pd.DataFrame, label: str, tail_n: int = 10) -> None:
"""Print the last N rows of computed indicators.
Args:
df: DataFrame with indicator columns.
label: Label for the output section.
tail_n: Number of recent bars to display.
"""
cols = ["close", "RSI_14", "MACD", "MACD_signal", "MACD_hist",
"BB_upper", "BB_middle", "BB_lower", "ATR_14", "ADX_14"]
available = [c for c in cols if c in df.columns]
print(f"\n{'='*60}")
print(f"Indicator Values — {label} (last {tail_n} bars)")
print(f"{'='*60}")
print(df[available].tail(tail_n).to_string(float_format="{:.4f}".format))
# Summary statistics
print(f"\n--- Summary ---")
last = df.iloc[-1]
if "RSI_14" in df.columns and not np.isnan(last["RSI_14"]):
rsi_val = last["RSI_14"]
zone = "OVERSOLD" if rsi_val < 30 else ("OVERBOUGHT" if rsi_val > 70 else "NEUTRAL")
print(f"RSI(14): {rsi_val:.2f} [{zone}]")
if "MACD_hist" in df.columns and not np.isnan(last["MACD_hist"]):
hist_val = last["MACD_hist"]
trend = "BULLISH" if hist_val > 0 else "BEARISH"
print(f"MACD Hist: {hist_val:.4f} [{trend}]")
if "ATR_14" in df.columns and not np.isnan(last["ATR_14"]):
atr_pct = last["ATR_14"] / last["close"] * 100
print(f"ATR(14): {last['ATR_14']:.4f} ({atr_pct:.2f}% of price)")
if "ADX_14" in df.columns and not np.isnan(last["ADX_14"]):
adx_val = last["ADX_14"]
strength = "STRONG TREND" if adx_val > 25 else "RANGING/WEAK"
print(f"ADX(14): {adx_val:.2f} [{strength}]")
if "BB_upper" in df.columns and not np.isnan(last["BB_upper"]):
bb_pos = (last["close"] - last["BB_lower"]) / (last["BB_upper"] - last["BB_lower"])
print(f"BB %B: {bb_pos:.2f} (0=lower band, 1=upper band)")
def compare_results(talib_df: pd.DataFrame, manual_df: pd.DataFrame) -> None:
"""Compare TA-Lib and manual results to validate accuracy.
Args:
talib_df: DataFrame computed with TA-Lib.
manual_df: DataFrame computed with manual fallback.
"""
print(f"\n{'='*60}")
print("Accuracy Comparison: TA-Lib vs Manual")
print(f"{'='*60}")
indicators = ["RSI_14", "MACD", "MACD_signal", "ATR_14", "ADX_14",
"BB_upper", "BB_middle", "BB_lower"]
for ind in indicators:
if ind not in talib_df.columns or ind not in manual_df.columns:
continue
t = talib_df[ind].values
m = manual_df[ind].values
# Compare only where both are valid
mask = ~(np.isnan(t) | np.isnan(m))
if mask.sum() == 0:
print(f" {ind:15s}: no overlapping valid values")
continue
diff = np.abs(t[mask] - m[mask])
max_diff = np.max(diff)
mean_diff = np.mean(diff)
print(f" {ind:15s}: max_diff={max_diff:.6f}, mean_diff={mean_diff:.6f}")
# ── Main ───────────────────────────────────────────────────────────
def main() -> None:
"""Entry point: generate data, compute indicators, display results."""
parser = argparse.ArgumentParser(
description="Compute TA-Lib indicators on synthetic OHLCV data."
)
parser.add_argument(
"--demo", action="store_true",
help="Run in demo mode with default settings."
)
parser.add_argument(
"--bars", type=int, default=500,
help="Number of OHLCV bars to generate (default: 500)."
)
parser.add_argument(
"--benchmark", action="store_true",
help="Run performance benchmark comparing TA-Lib vs manual."
)
args = parser.parse_args()
bars = args.bars
print(f"TA-Lib available: {TALIB_AVAILABLE}")
print(f"Generating {bars} bars of synthetic OHLCV data...")
df = generate_ohlcv(bars=bars)
print(f"Price range: {df['close'].min():.2f} - {df['close'].max():.2f}")
if TALIB_AVAILABLE:
talib_df = compute_with_talib(df)
display_results(talib_df, "TA-Lib (C Library)")
manual_df = compute_manual(df)
display_results(manual_df, "Manual Fallback")
compare_results(talib_df, manual_df)
else:
print("\nTA-Lib not installed. Using manual fallback implementations.")
print("Install TA-Lib for C-optimized performance:")
print(" brew install ta-lib && uv pip install TA-Lib")
manual_df = compute_manual(df)
display_results(manual_df, "Manual Fallback")
if args.benchmark or args.demo:
benchmark(df)
print("\nNote: This is synthetic data for demonstration purposes only.")
print("Not financial advice. Use with real market data for actual analysis.")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Scan OHLCV data for candlestick patterns using TA-Lib.
Detects all 61 TA-Lib candlestick patterns on synthetic or provided data.
Falls back to manual detection of doji, hammer, and engulfing patterns
when TA-Lib is not installed.
Usage:
python scripts/pattern_scanner.py
python scripts/pattern_scanner.py --demo
python scripts/pattern_scanner.py --bars 1000
Dependencies:
uv pip install numpy pandas
Optional: uv pip install TA-Lib (requires C library)
"""
import argparse
import sys
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple
import numpy as np
import pandas as pd
# ── TA-Lib Import (optional) ───────────────────────────────────────
TALIB_AVAILABLE = False
try:
import talib
TALIB_AVAILABLE = True
except ImportError:
pass
# ── Data Classes ───────────────────────────────────────────────────
@dataclass
class PatternDetection:
"""A single pattern detection on a specific bar."""
bar_index: int
value: int # +100 bullish, -100 bearish
close_price: float
@dataclass
class PatternResult:
"""Results for a single pattern type across all bars."""
name: str
display_name: str
detections: List[PatternDetection] = field(default_factory=list)
@property
def count(self) -> int:
"""Total number of detections."""
return len(self.detections)
@property
def bullish_count(self) -> int:
"""Number of bullish detections."""
return sum(1 for d in self.detections if d.value > 0)
@property
def bearish_count(self) -> int:
"""Number of bearish detections."""
return sum(1 for d in self.detections if d.value < 0)
# ── Synthetic Data with Clear Patterns ─────────────────────────────
def generate_pattern_data(bars: int = 500, seed: int = 42) -> pd.DataFrame:
"""Generate synthetic OHLCV data that contains clear candlestick patterns.
Embeds specific pattern structures (doji, hammer, engulfing) at known
positions so the scanner can demonstrate detection.
Args:
bars: Number of bars to generate.
seed: Random seed for reproducibility.
Returns:
DataFrame with columns: open, high, low, close, volume.
"""
rng = np.random.default_rng(seed)
# Base price movement
returns = rng.normal(0, 0.015, bars)
close = 100.0 * np.exp(np.cumsum(returns))
spread = close * 0.015
high = close + np.abs(rng.normal(0, 1, bars)) * spread
low = close - np.abs(rng.normal(0, 1, bars)) * spread
open_ = close + rng.normal(0, 0.5, bars) * spread
# Ensure OHLC consistency
high = np.maximum(high, np.maximum(open_, close))
low = np.minimum(low, np.minimum(open_, close))
volume = (100_000 * (1 + 3 * np.abs(returns))).astype(np.float64)
# ── Embed clear patterns ───────────────────────────────────
# Doji patterns: open ~= close, with wicks
doji_bars = [50, 150, 250, 350, 450]
for i in doji_bars:
if i < bars:
mid = close[i]
open_[i] = mid * 1.0005
close[i] = mid * 0.9995
high[i] = mid * 1.015
low[i] = mid * 0.985
# Hammer patterns: small body at top, long lower wick
hammer_bars = [75, 175, 275, 375]
for i in hammer_bars:
if i < bars:
body_top = close[i]
body_size = body_top * 0.003
open_[i] = body_top - body_size
close[i] = body_top
high[i] = body_top + body_size * 0.5
low[i] = body_top - body_size * 4 # Long lower wick
# Bullish engulfing: small bearish bar followed by large bullish bar
engulfing_bars = [100, 200, 300, 400]
for i in engulfing_bars:
if i + 1 < bars:
mid = close[i]
# Bar i: small bearish
open_[i] = mid * 1.002
close[i] = mid * 0.998
high[i] = mid * 1.003
low[i] = mid * 0.997
# Bar i+1: large bullish engulfing
open_[i + 1] = mid * 0.996
close[i + 1] = mid * 1.005
high[i + 1] = mid * 1.006
low[i + 1] = mid * 0.995
# Shooting star: small body at bottom, long upper wick
shooting_bars = [125, 225, 325]
for i in shooting_bars:
if i < bars:
body_bottom = close[i]
body_size = body_bottom * 0.003
open_[i] = body_bottom + body_size
close[i] = body_bottom
high[i] = body_bottom + body_size * 4 # Long upper wick
low[i] = body_bottom - body_size * 0.5
# Re-enforce OHLC consistency after pattern embedding
high = np.maximum(high, np.maximum(open_, close))
low = np.minimum(low, np.minimum(open_, close))
df = pd.DataFrame({
"open": open_,
"high": high,
"low": low,
"close": close,
"volume": volume,
})
return df
# ── TA-Lib Pattern Scanner ─────────────────────────────────────────
# Display names for TA-Lib candlestick functions
PATTERN_DISPLAY_NAMES: Dict[str, str] = {
"CDL2CROWS": "Two Crows",
"CDL3BLACKCROWS": "Three Black Crows",
"CDL3INSIDE": "Three Inside Up/Down",
"CDL3LINESTRIKE": "Three-Line Strike",
"CDL3OUTSIDE": "Three Outside Up/Down",
"CDL3STARSINSOUTH": "Three Stars in the South",
"CDL3WHITESOLDIERS": "Three White Soldiers",
"CDLABANDONEDBABY": "Abandoned Baby",
"CDLADVANCEBLOCK": "Advance Block",
"CDLBELTHOLD": "Belt Hold",
"CDLBREAKAWAY": "Breakaway",
"CDLCLOSINGMARUBOZU": "Closing Marubozu",
"CDLCONCEALBABYSWALL": "Concealing Baby Swallow",
"CDLCOUNTERATTACK": "Counterattack",
"CDLDARKCLOUDCOVER": "Dark Cloud Cover",
"CDLDOJI": "Doji",
"CDLDOJISTAR": "Doji Star",
"CDLDRAGONFLYDOJI": "Dragonfly Doji",
"CDLENGULFING": "Engulfing",
"CDLEVENINGDOJISTAR": "Evening Doji Star",
"CDLEVENINGSTAR": "Evening Star",
"CDLGAPSIDESIDEWHITE": "Gap Side-by-Side White",
"CDLGRAVESTONEDOJI": "Gravestone Doji",
"CDLHAMMER": "Hammer",
"CDLHANGINGMAN": "Hanging Man",
"CDLHARAMI": "Harami",
"CDLHARAMICROSS": "Harami Cross",
"CDLHIGHWAVE": "High Wave",
"CDLHIKKAKE": "Hikkake",
"CDLHIKKAKEMOD": "Modified Hikkake",
"CDLHOMINGPIGEON": "Homing Pigeon",
"CDLIDENTICAL3CROWS": "Identical Three Crows",
"CDLINNECK": "In-Neck",
"CDLINVERTEDHAMMER": "Inverted Hammer",
"CDLKICKING": "Kicking",
"CDLKICKINGBYLENGTH": "Kicking by Length",
"CDLLADDERBOTTOM": "Ladder Bottom",
"CDLLONGLEGGEDDOJI": "Long-Legged Doji",
"CDLLONGLINE": "Long Line",
"CDLMARUBOZU": "Marubozu",
"CDLMATCHINGLOW": "Matching Low",
"CDLMATHOLD": "Mat Hold",
"CDLMORNINGDOJISTAR": "Morning Doji Star",
"CDLMORNINGSTAR": "Morning Star",
"CDLONNECK": "On-Neck",
"CDLPIERCING": "Piercing",
"CDLRICKSHAWMAN": "Rickshaw Man",
"CDLRISEFALL3METHODS": "Rising/Falling Three Methods",
"CDLSEPARATINGLINES": "Separating Lines",
"CDLSHOOTINGSTAR": "Shooting Star",
"CDLSHORTLINE": "Short Line",
"CDLSPINNINGTOP": "Spinning Top",
"CDLSTALLEDPATTERN": "Stalled Pattern",
"CDLSTICKSANDWICH": "Stick Sandwich",
"CDLTAKURI": "Takuri",
"CDLTASUKIGAP": "Tasuki Gap",
"CDLTHRUSTING": "Thrusting",
"CDLTRISTAR": "Tri-Star",
"CDLUNIQUE3RIVER": "Unique Three River",
"CDLXSIDEGAP3METHODS": "Side Gap Three Methods",
}
def scan_talib_patterns(
open_: np.ndarray,
high: np.ndarray,
low: np.ndarray,
close: np.ndarray,
) -> List[PatternResult]:
"""Scan for all 61 TA-Lib candlestick patterns.
Args:
open_: Open price array (float64).
high: High price array (float64).
low: Low price array (float64).
close: Close price array (float64).
Returns:
List of PatternResult for patterns with at least one detection.
"""
candle_funcs = talib.get_function_groups()["Pattern Recognition"]
results: List[PatternResult] = []
for func_name in candle_funcs:
func = getattr(talib, func_name)
output = func(open_, high, low, close)
hits = np.nonzero(output)[0]
if len(hits) > 0:
display = PATTERN_DISPLAY_NAMES.get(func_name, func_name)
pr = PatternResult(name=func_name, display_name=display)
for idx in hits:
pr.detections.append(PatternDetection(
bar_index=int(idx),
value=int(output[idx]),
close_price=float(close[idx]),
))
results.append(pr)
# Sort by detection count descending
results.sort(key=lambda r: r.count, reverse=True)
return results
# ── Manual Pattern Detection (Fallback) ────────────────────────────
def detect_doji(
open_: np.ndarray,
high: np.ndarray,
low: np.ndarray,
close: np.ndarray,
body_threshold: float = 0.001,
) -> np.ndarray:
"""Detect doji patterns: open ~= close with visible wicks.
A doji has a very small body relative to its total range,
indicating indecision.
Args:
open_: Open prices.
high: High prices.
low: Low prices.
close: Close prices.
body_threshold: Max body/range ratio to qualify as doji.
Returns:
Array with +100 where doji detected, 0 otherwise.
"""
n = len(close)
result = np.zeros(n, dtype=np.int32)
for i in range(n):
total_range = high[i] - low[i]
if total_range == 0:
continue
body = abs(close[i] - open_[i])
body_ratio = body / total_range
if body_ratio <= body_threshold and total_range > close[i] * 0.005:
result[i] = 100
return result
def detect_hammer(
open_: np.ndarray,
high: np.ndarray,
low: np.ndarray,
close: np.ndarray,
lower_wick_ratio: float = 2.0,
) -> np.ndarray:
"""Detect hammer patterns: small body at top, long lower shadow.
A hammer has a lower wick at least N times the body size, with
minimal upper wick.
Args:
open_: Open prices.
high: High prices.
low: Low prices.
close: Close prices.
lower_wick_ratio: Min ratio of lower wick to body.
Returns:
Array with +100 where bullish hammer detected, 0 otherwise.
"""
n = len(close)
result = np.zeros(n, dtype=np.int32)
for i in range(n):
body = abs(close[i] - open_[i])
if body == 0:
continue
body_top = max(open_[i], close[i])
body_bottom = min(open_[i], close[i])
lower_wick = body_bottom - low[i]
upper_wick = high[i] - body_top
# Hammer: long lower wick, short upper wick, small body
if (lower_wick >= lower_wick_ratio * body
and upper_wick <= body * 0.5):
result[i] = 100
return result
def detect_shooting_star(
open_: np.ndarray,
high: np.ndarray,
low: np.ndarray,
close: np.ndarray,
upper_wick_ratio: float = 2.0,
) -> np.ndarray:
"""Detect shooting star patterns: small body at bottom, long upper shadow.
Args:
open_: Open prices.
high: High prices.
low: Low prices.
close: Close prices.
upper_wick_ratio: Min ratio of upper wick to body.
Returns:
Array with -100 where shooting star detected, 0 otherwise.
"""
n = len(close)
result = np.zeros(n, dtype=np.int32)
for i in range(n):
body = abs(close[i] - open_[i])
if body == 0:
continue
body_top = max(open_[i], close[i])
body_bottom = min(open_[i], close[i])
upper_wick = high[i] - body_top
lower_wick = body_bottom - low[i]
if (upper_wick >= upper_wick_ratio * body
and lower_wick <= body * 0.5):
result[i] = -100
return result
def detect_engulfing(
open_: np.ndarray,
high: np.ndarray,
low: np.ndarray,
close: np.ndarray,
) -> np.ndarray:
"""Detect bullish and bearish engulfing patterns.
Bullish engulfing: bearish bar followed by bullish bar whose body
completely engulfs the prior bar's body.
Bearish engulfing: bullish bar followed by bearish bar that engulfs.
Args:
open_: Open prices.
high: High prices.
low: Low prices.
close: Close prices.
Returns:
Array with +100 (bullish) or -100 (bearish) engulfing, 0 otherwise.
"""
n = len(close)
result = np.zeros(n, dtype=np.int32)
for i in range(1, n):
prev_body_top = max(open_[i - 1], close[i - 1])
prev_body_bot = min(open_[i - 1], close[i - 1])
curr_body_top = max(open_[i], close[i])
curr_body_bot = min(open_[i], close[i])
prev_bearish = close[i - 1] < open_[i - 1]
prev_bullish = close[i - 1] > open_[i - 1]
curr_bearish = close[i] < open_[i]
curr_bullish = close[i] > open_[i]
# Bullish engulfing: prev bearish, current bullish, engulfs
if (prev_bearish and curr_bullish
and curr_body_bot <= prev_body_bot
and curr_body_top >= prev_body_top
and (curr_body_top - curr_body_bot) > (prev_body_top - prev_body_bot)):
result[i] = 100
# Bearish engulfing: prev bullish, current bearish, engulfs
elif (prev_bullish and curr_bearish
and curr_body_bot <= prev_body_bot
and curr_body_top >= prev_body_top
and (curr_body_top - curr_body_bot) > (prev_body_top - prev_body_bot)):
result[i] = -100
return result
def scan_manual_patterns(
open_: np.ndarray,
high: np.ndarray,
low: np.ndarray,
close: np.ndarray,
) -> List[PatternResult]:
"""Scan using manual fallback pattern detection.
Detects doji, hammer, shooting star, and engulfing patterns
without TA-Lib.
Args:
open_: Open prices.
high: High prices.
low: Low prices.
close: Close prices.
Returns:
List of PatternResult for detected patterns.
"""
manual_scanners = [
("CDLDOJI", "Doji (manual)", detect_doji),
("CDLHAMMER", "Hammer (manual)", detect_hammer),
("CDLSHOOTINGSTAR", "Shooting Star (manual)", detect_shooting_star),
("CDLENGULFING", "Engulfing (manual)", detect_engulfing),
]
results: List[PatternResult] = []
for name, display, func in manual_scanners:
output = func(open_, high, low, close)
hits = np.nonzero(output)[0]
if len(hits) > 0:
pr = PatternResult(name=name, display_name=display)
for idx in hits:
pr.detections.append(PatternDetection(
bar_index=int(idx),
value=int(output[idx]),
close_price=float(close[idx]),
))
results.append(pr)
results.sort(key=lambda r: r.count, reverse=True)
return results
# ── Display Functions ──────────────────────────────────────────────
def print_pattern_summary(results: List[PatternResult], bars: int) -> None:
"""Print a summary table of all detected patterns.
Args:
results: List of PatternResult objects.
bars: Total number of bars scanned.
"""
total_detections = sum(r.count for r in results)
print(f"\n{'='*70}")
print(f"Pattern Scan Summary — {bars} bars, "
f"{len(results)} pattern types, {total_detections} total detections")
print(f"{'='*70}")
if not results:
print("No patterns detected.")
return
print(f"{'Pattern':<35} {'Total':>6} {'Bull':>6} {'Bear':>6}")
print(f"{'-'*35} {'-'*6} {'-'*6} {'-'*6}")
for r in results:
print(f"{r.display_name:<35} {r.count:>6} "
f"{r.bullish_count:>6} {r.bearish_count:>6}")
print(f"{'-'*35} {'-'*6} {'-'*6} {'-'*6}")
total_bull = sum(r.bullish_count for r in results)
total_bear = sum(r.bearish_count for r in results)
print(f"{'TOTAL':<35} {total_detections:>6} {total_bull:>6} {total_bear:>6}")
def print_recent_detections(
results: List[PatternResult],
n_recent: int = 10,
) -> None:
"""Print the N most recent pattern detections across all patterns.
Args:
results: List of PatternResult objects.
n_recent: Number of recent detections to show.
"""
# Flatten all detections with pattern name
all_detections: List[Tuple[str, PatternDetection]] = []
for r in results:
for d in r.detections:
all_detections.append((r.display_name, d))
# Sort by bar index descending
all_detections.sort(key=lambda x: x[1].bar_index, reverse=True)
recent = all_detections[:n_recent]
print(f"\n{'='*70}")
print(f"Most Recent {min(n_recent, len(recent))} Detections")
print(f"{'='*70}")
if not recent:
print("No detections to display.")
return
print(f"{'Bar':>6} {'Pattern':<35} {'Signal':>8} {'Price':>12}")
print(f"{'-'*6} {'-'*35} {'-'*8} {'-'*12}")
for name, det in recent:
signal = "BULL" if det.value > 0 else "BEAR"
color_signal = f"+{det.value}" if det.value > 0 else str(det.value)
print(f"{det.bar_index:>6} {name:<35} {color_signal:>8} "
f"{det.close_price:>12.4f}")
def print_pattern_distribution(results: List[PatternResult], bars: int) -> None:
"""Print where patterns cluster in the data.
Args:
results: List of PatternResult objects.
bars: Total number of bars.
"""
if not results:
return
# Divide into quartiles
q_size = bars // 4
quartile_counts = [0, 0, 0, 0]
for r in results:
for d in r.detections:
q = min(d.bar_index // q_size, 3)
quartile_counts[q] += 1
total = sum(quartile_counts)
if total == 0:
return
print(f"\n--- Pattern Distribution by Quarter ---")
labels = ["Q1 (oldest)", "Q2", "Q3", "Q4 (newest)"]
for i, label in enumerate(labels):
pct = quartile_counts[i] / total * 100 if total > 0 else 0
bar = "#" * int(pct / 2)
print(f" {label:<15} {quartile_counts[i]:>4} ({pct:>5.1f}%) {bar}")
# ── Main ───────────────────────────────────────────────────────────
def main() -> None:
"""Entry point: generate data, scan patterns, display results."""
parser = argparse.ArgumentParser(
description="Scan OHLCV data for candlestick patterns."
)
parser.add_argument(
"--demo", action="store_true",
help="Run demo with synthetic data containing embedded patterns."
)
parser.add_argument(
"--bars", type=int, default=500,
help="Number of OHLCV bars to generate (default: 500)."
)
parser.add_argument(
"--recent", type=int, default=15,
help="Number of recent detections to display (default: 15)."
)
args = parser.parse_args()
bars = args.bars
print(f"TA-Lib available: {TALIB_AVAILABLE}")
print(f"Generating {bars} bars of synthetic OHLCV data with embedded patterns...")
df = generate_pattern_data(bars=bars)
open_ = df["open"].values.astype(np.float64)
high = df["high"].values.astype(np.float64)
low = df["low"].values.astype(np.float64)
close = df["close"].values.astype(np.float64)
print(f"Price range: {close.min():.2f} - {close.max():.2f}")
if TALIB_AVAILABLE:
print(f"\nScanning all 61 TA-Lib candlestick patterns...")
results = scan_talib_patterns(open_, high, low, close)
else:
print(f"\nTA-Lib not installed. Using manual fallback for 4 patterns.")
print("Install TA-Lib for all 61 patterns:")
print(" brew install ta-lib && uv pip install TA-Lib")
results = scan_manual_patterns(open_, high, low, close)
print_pattern_summary(results, bars)
print_recent_detections(results, n_recent=args.recent)
print_pattern_distribution(results, bars)
# If both available, compare
if TALIB_AVAILABLE:
print(f"\n--- Manual Fallback Comparison ---")
manual_results = scan_manual_patterns(open_, high, low, close)
for mr in manual_results:
# Find corresponding TA-Lib result
talib_match = next((r for r in results if r.name == mr.name), None)
talib_count = talib_match.count if talib_match else 0
print(f" {mr.display_name:<35} manual={mr.count:>4}, "
f"talib={talib_count:>4}")
print("\nNote: This is synthetic data for demonstration purposes only.")
print("Not financial advice. Use with real market data for actual analysis.")
if __name__ == "__main__":
main()
Related skills
FAQ
How many functions does TA-Lib provide?
Over 150 indicator functions across overlap, momentum, volume, volatility, cycle, and math groups, plus 61 candlestick pattern recognition functions.
When should you use TA-Lib over pandas-ta?
Use TA-Lib for processing millions of bars, large-scale backtests, candlestick pattern recognition, cycle indicators, or production pipelines where latency matters.