TA-Lib and Python: Build Your Own Stock & Crypto Analysis Engine

TA-Lib is the gold standard for technical analysis in Python. This guide covers installation, every major indicator category, real working code examples, and how to build a multi-symbol screener from scratch.

2026-06-01 Β· 14 min read
Code displayed on a dark screen β€” Python programming for financial analysis

Photo by Lukas on Pexels

TA-Lib (Technical Analysis Library) is a C library with Python bindings that implements over 150 technical indicators. It's the closest thing the quantitative finance world has to a standard library for indicator calculation β€” used in production at hedge funds, data vendors, and retail trading platforms alike. This guide gets you from zero to a working multi-symbol signal screener.

Installation: The Part Nobody Warns You About

TA-Lib has a C dependency (libta-lib) that must be installed before the Python package. This trips up most beginners.

macOS

brew install ta-lib
pip install TA-Lib

Ubuntu / Debian

sudo apt-get install -y build-essential wget
wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
tar -xzf ta-lib-0.4.0-src.tar.gz
cd ta-lib/
./configure --prefix=/usr
make
sudo make install
pip install TA-Lib

Windows

Download the pre-built wheel from the cgohlke/talib-build releases page β€” pick the version matching your Python version β€” and install with pip install TA_Lib‑*.whl.

Docker (easiest for production)

FROM python:3.11-slim
RUN apt-get update && apt-get install -y build-essential wget \
    && wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz \
    && tar -xzf ta-lib-0.4.0-src.tar.gz && cd ta-lib \
    && ./configure --prefix=/usr && make && make install \
    && pip install TA-Lib yfinance pandas numpy

Getting Market Data

TA-Lib operates on NumPy arrays β€” it doesn't fetch data itself. You need a data provider. For getting started, yfinance is the easiest option:

import yfinance as yf
import numpy as np
import talib

ticker = yf.Ticker("NVDA")
df = ticker.history(period="6mo")

# TA-Lib expects numpy float64 arrays
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)

For crypto, use ccxt:

import ccxt
import pandas as pd

exchange = ccxt.binance()
ohlcv = exchange.fetch_ohlcv("BTC/USDT", timeframe="1d", limit=200)
df = pd.DataFrame(ohlcv, columns=["timestamp","open","high","low","close","volume"])

close  = df["close"].values.astype(np.float64)
high   = df["high"].values.astype(np.float64)
low    = df["low"].values.astype(np.float64)

Core Indicator Categories

1. Momentum Indicators

These measure the rate of price change β€” useful for identifying overbought/oversold conditions and trend strength.

# RSI β€” Relative Strength Index
# Readings above 70 suggest overbought, below 30 oversold
rsi = talib.RSI(close, timeperiod=14)
print(f"Current RSI: {rsi[-1]:.1f}")

# MACD β€” Moving Average Convergence Divergence
# macd crosses above signal = bullish, below = bearish
macd, signal, hist = talib.MACD(close, fastperiod=12, slowperiod=26, signalperiod=9)

# Stochastic Oscillator
slowk, slowd = talib.STOCH(high, low, close,
    fastk_period=5, slowk_period=3, slowk_matype=0,
    slowd_period=3, slowd_matype=0)

# CCI β€” Commodity Channel Index (works for stocks too)
cci = talib.CCI(high, low, close, timeperiod=14)

# ADX β€” Average Directional Index (trend strength, not direction)
# ADX > 25 = strong trend; < 20 = weak/no trend
adx = talib.ADX(high, low, close, timeperiod=14)
print(f"Trend strength (ADX): {adx[-1]:.1f}")

2. Overlap Studies (Trend Following)

These are drawn on the price chart itself and define trend direction.

# Simple and Exponential Moving Averages
sma_20  = talib.SMA(close, timeperiod=20)
sma_50  = talib.SMA(close, timeperiod=50)
sma_200 = talib.SMA(close, timeperiod=200)
ema_20  = talib.EMA(close, timeperiod=20)

# Golden/Death Cross detection
golden_cross = (sma_50[-1] > sma_200[-1]) and (sma_50[-2] <= sma_200[-2])
death_cross  = (sma_50[-1] < sma_200[-1]) and (sma_50[-2] >= sma_200[-2])

# Bollinger Bands
upper, middle, lower = talib.BBANDS(close, timeperiod=20, nbdevup=2, nbdevdn=2)
bb_percent = (close[-1] - lower[-1]) / (upper[-1] - lower[-1])  # 0=at lower, 1=at upper
bandwidth  = (upper[-1] - lower[-1]) / middle[-1]               # volatility measure

# VWAP is not in TA-Lib natively, but you can approximate:
typical_price = (high + low + close) / 3
vwap = np.cumsum(typical_price * volume) / np.cumsum(volume)

3. Volume Indicators

Volume confirms or contradicts price moves. A breakout on high volume is more reliable than one on thin volume.

# OBV β€” On-Balance Volume: rising OBV confirms uptrend
obv = talib.OBV(close, volume)

# AD β€” Chaikin Accumulation/Distribution
ad = talib.AD(high, low, close, volume)

# ADOSC β€” Chaikin A/D Oscillator
adosc = talib.ADOSC(high, low, close, volume, fastperiod=3, slowperiod=10)

# MFI β€” Money Flow Index (RSI with volume weighting)
mfi = talib.MFI(high, low, close, volume, timeperiod=14)

4. Volatility Indicators

# ATR β€” Average True Range: the most useful volatility measure
# Use for position sizing: risk 1% of capital per ATR unit
atr = talib.ATR(high, low, close, timeperiod=14)
print(f"ATR (daily risk per share): ${atr[-1]:.2f}")

# NATR β€” Normalised ATR (percentage of price)
natr = talib.NATR(high, low, close, timeperiod=14)

# TRANGE β€” True Range (single period)
trange = talib.TRANGE(high, low, close)

5. Pattern Recognition

TA-Lib includes 61 candlestick pattern recognisers. They return 100 (bullish), -100 (bearish), or 0 (pattern not present).

open_p = df["Open"].values.astype(np.float64)

# Single-candle patterns
doji      = talib.CDLDOJI(open_p, high, low, close)
hammer    = talib.CDLHAMMER(open_p, high, low, close)
shooting  = talib.CDLSHOOTINGSTAR(open_p, high, low, close)

# Multi-candle patterns
engulfing     = talib.CDLENGULFING(open_p, high, low, close)
morning_star  = talib.CDLMORNINGSTAR(open_p, high, low, close)
evening_star  = talib.CDLEVENINGSTAR(open_p, high, low, close)
three_white   = talib.CDL3WHITESOLDIERS(open_p, high, low, close)

# Check last candle
if engulfing[-1] == 100:
    print("Bullish engulfing pattern on the latest candle")
elif engulfing[-1] == -100:
    print("Bearish engulfing pattern on the latest candle")

Honest note on pattern recognition: TA-Lib's candlestick patterns are mechanically correct implementations, but most academic studies show candlestick patterns alone have marginal predictive value (slightly better than random in some markets, not others). Use them as confluence signals, not standalone triggers.

Building a Multi-Symbol Screener

The real power of TA-Lib is running it across many symbols at once. Here's a complete screener that flags symbols with multiple bullish confluences:

import yfinance as yf
import numpy as np
import talib
from dataclasses import dataclass
from typing import Optional
import pandas as pd

@dataclass
class SignalResult:
    symbol: str
    price: float
    signal: str          # 'BUY', 'SELL', 'HOLD'
    score: int           # count of bullish/bearish confluences
    rsi: float
    macd_cross: str      # 'bullish', 'bearish', 'none'
    above_sma200: bool
    adx: float
    atr: float
    notes: list[str]

def analyse_symbol(symbol: str) -> Optional[SignalResult]:
    try:
        df = yf.Ticker(symbol).history(period="1y")
        if len(df) < 200:
            return None

        c = df["Close"].values.astype(np.float64)
        h = df["High"].values.astype(np.float64)
        l = df["Low"].values.astype(np.float64)
        v = df["Volume"].values.astype(np.float64)

        rsi      = talib.RSI(c, 14)[-1]
        sma200   = talib.SMA(c, 200)[-1]
        adx      = talib.ADX(h, l, c, 14)[-1]
        atr      = talib.ATR(h, l, c, 14)[-1]
        macd, sig, _ = talib.MACD(c, 12, 26, 9)

        # Score bullish confluences
        score = 0
        notes = []

        if rsi < 35:
            score += 1; notes.append(f"RSI oversold ({rsi:.0f})")
        elif rsi > 65:
            score -= 1; notes.append(f"RSI overbought ({rsi:.0f})")

        above_200 = c[-1] > sma200
        if above_200:
            score += 1; notes.append("Above 200-day SMA")
        else:
            score -= 1; notes.append("Below 200-day SMA")

        macd_cross = "none"
        if macd[-1] > sig[-1] and macd[-2] <= sig[-2]:
            macd_cross = "bullish"; score += 2; notes.append("MACD bullish crossover")
        elif macd[-1] < sig[-1] and macd[-2] >= sig[-2]:
            macd_cross = "bearish"; score -= 2; notes.append("MACD bearish crossover")

        if adx > 25:
            notes.append(f"Strong trend (ADX {adx:.0f})")

        signal = "BUY" if score >= 2 else "SELL" if score <= -2 else "HOLD"

        return SignalResult(
            symbol=symbol, price=c[-1], signal=signal, score=score,
            rsi=rsi, macd_cross=macd_cross, above_sma200=above_200,
            adx=adx, atr=atr, notes=notes
        )
    except Exception as e:
        print(f"Error analysing {symbol}: {e}")
        return None


# Run across a watchlist
watchlist = ["NVDA", "AAPL", "MSFT", "TSLA", "BTC-USD", "ETH-USD"]
results = [r for s in watchlist if (r := analyse_symbol(s))]

# Sort by score descending
results.sort(key=lambda x: x.score, reverse=True)

for r in results:
    print(f"{r.symbol:10} {r.signal:4} score={r.score:+d}  "
          f"RSI={r.rsi:.0f}  ADX={r.adx:.0f}  "
          f"${r.price:.2f}  | {', '.join(r.notes)}")

Position Sizing with ATR

ATR is more than a volatility measure β€” it's the cleanest way to size positions consistently. The standard formula: risk a fixed percentage of capital per trade, where the stop is placed 1.5Γ— ATR below entry.

def calculate_position_size(
    capital: float,
    risk_pct: float,     # e.g. 0.01 = 1% of capital per trade
    entry_price: float,
    atr: float,
    atr_multiplier: float = 1.5,
) -> dict:
    stop_distance = atr * atr_multiplier
    stop_price    = entry_price - stop_distance
    risk_per_share = stop_distance
    risk_amount    = capital * risk_pct
    shares         = risk_amount / risk_per_share
    position_value = shares * entry_price

    return {
        "shares": int(shares),
        "position_value": round(position_value, 2),
        "stop_price": round(stop_price, 2),
        "risk_amount": round(risk_amount, 2),
        "position_pct_of_capital": round(position_value / capital * 100, 1),
    }

# Example: NVDA at $900, ATR=$28, 1% risk on Β£50,000 account
result = calculate_position_size(50_000, 0.01, 900, 28)
print(result)
# {'shares': 11, 'position_value': 9900.0, 'stop_price': 858.0,
#  'risk_amount': 500.0, 'position_pct_of_capital': 19.8}

Adding Fundamentals: Where TA-Lib Ends

TA-Lib is purely technical β€” it knows nothing about earnings, P/E ratios, or macro conditions. For a complete picture you need to combine it with fundamental data. Libraries worth knowing:

  • yfinance β€” ticker.info returns P/E, revenue, and basic fundamentals for free
  • Financial Modeling Prep API β€” much richer fundamental data (income statement, DCF, ratios) with a free tier
  • Alpha Vantage β€” earnings data, sector performance, economic indicators
import yfinance as yf

ticker = yf.Ticker("NVDA")
info   = ticker.info

pe_ratio      = info.get("trailingPE")
revenue_growth = info.get("revenueGrowth")
debt_to_equity = info.get("debtToEquity")
sector        = info.get("sector")

print(f"P/E: {pe_ratio:.1f}  Revenue growth: {revenue_growth:.1%}  D/E: {debt_to_equity}")

What TA-Lib Can't Do

Be clear-eyed about the limitations:

  • No predictive guarantee. Every indicator is derived from past prices. It describes what has happened, not what will happen.
  • Curve-fitting risk. If you tune indicator parameters on historical data, you can make almost any system look profitable in backtests. Test on genuinely out-of-sample data.
  • No news or sentiment. A technically perfect BUY setup can be obliterated by an earnings miss or a geopolitical shock.
  • Lag. Moving averages and MACD are lagging indicators β€” by definition they confirm trends rather than predict them. The bigger the lookback window, the more lag.

TA-Lib as an Input to AI Analysis

The most robust approach is using TA-Lib to prepare the data that feeds a more sophisticated model rather than using indicator outputs as direct trading rules.

This is exactly how Indikators works: TA-Lib computes RSI, MACD, Bollinger Bands, ATR, and support/resistance levels, which are then formatted into a structured prompt fed to an AI model. The AI can reason about confluences, weigh them against fundamentals, and produce a calibrated BUY/SELL/HOLD signal with explicit reasoning β€” something a mechanical indicator rule system cannot do.

The combination of TA-Lib's precision and AI reasoning is genuinely more powerful than either alone. You get the rigor of exact indicator calculation plus the contextual reasoning that turns indicator states into actionable analysis.

Not financial advice. This article is for educational purposes only. Always do your own research and consult a qualified financial adviser before making investment decisions.

Related reading

See AI signals in action

Browse verified historical signals for stocks and crypto β€” free, no account needed.