Notebooks/Technical Analysis with pandas-ta
Signals·Technical Analysis·Beginner

Technical Analysis with pandas-ta

Use the pandas-ta library for fast, vectorised indicator computation — covers setup, strategy objects, and bulk indicator runs.

pandas-taindicatorsvectorized

Technical Indicators Framework — pandas-ta

This notebook defines a standardized protocol for computing technical indicators using the pandas-ta library on OHLCV data. It covers trend, momentum, volatility, and volume indicators on a representative synthetic dataset.


1. Dependency Installation

[8]
import sys
!{sys.executable} -m pip install pandas-ta
Requirement already satisfied: pandas-ta in /usr/local/lib/python3.12/dist-packages (0.4.71b0)
Requirement already satisfied: numba==0.61.2 in /usr/local/lib/python3.12/dist-packages (from pandas-ta) (0.61.2)
Requirement already satisfied: numpy>=2.2.6 in /usr/local/lib/python3.12/dist-packages (from pandas-ta) (2.2.6)
Requirement already satisfied: pandas>=2.3.2 in /usr/local/lib/python3.12/dist-packages (from pandas-ta) (3.0.2)
Requirement already satisfied: tqdm>=4.67.1 in /usr/local/lib/python3.12/dist-packages (from pandas-ta) (4.67.3)
Requirement already satisfied: llvmlite<0.45,>=0.44.0dev0 in /usr/local/lib/python3.12/dist-packages (from numba==0.61.2->pandas-ta) (0.44.0)
Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.12/dist-packages (from pandas>=2.3.2->pandas-ta) (2.9.0.post0)
Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil>=2.8.2->pandas>=2.3.2->pandas-ta) (1.17.0)

2. Library Imports

[9]
import warnings
warnings.filterwarnings("ignore")

import pandas as pd
import pandas_ta as ta
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots

3. What Is pandas-ta?

pandas-ta is a Python library that adds over 130 technical indicators directly to a pandas DataFrame. Instead of writing indicator formulas manually, a single method call computes and appends the result as a new column. It is built on top of pandas and numpy, requires no compiled dependencies, and works on any OHLCV DataFrame.

[10]
def generate_larger_data(periods: int) -> pd.DataFrame:
    """Generates a larger synthetic OHLCV dataset with more realistic price fluctuations."""
    start_date = pd.to_datetime("2024-01-01 00:00:00+00:00")
    datetime_index = pd.date_range(start_date, periods=periods, freq="1min", tz="UTC")

    price_data = []
    last_close = 42000 # Starting price
    volatility_scale = 0.005 # Controls the general magnitude of price changes
    wick_deviation_scale = 0.002 # Controls how much wicks extend beyond body

    for i in range(periods):
        # Open price drifts slightly from the previous close
        open_price = last_close + np.random.normal(0, last_close * volatility_scale * 0.1)

        # Simulate a price change to determine the closing price
        price_change = np.random.normal(0, last_close * volatility_scale)
        close_price = open_price + price_change

        # Determine the high and low of the candle body
        body_high = max(open_price, close_price)
        body_low = min(open_price, close_price)

        # Simulate wicks extending beyond the body
        # High wick should be above the body_high
        high_wick_extension = np.abs(np.random.normal(0, last_close * wick_deviation_scale))
        high_price = body_high + high_wick_extension

        # Low wick should be below the body_low
        low_wick_extension = np.abs(np.random.normal(0, last_close * wick_deviation_scale))
        low_price = body_low - low_wick_extension

        # Ensure OHLC integrity: High must be the absolute highest, Low the absolute lowest
        high_price = max(high_price, open_price, close_price)
        low_price = min(low_price, open_price, close_price)

        # Ensure High is never less than Low
        if high_price < low_price:
            high_price, low_price = low_price, high_price # Swap if somehow invalid

        # Ensure all values are positive integers
        open_price = max(1, int(open_price))
        high_price = max(1, int(high_price))
        low_price = max(1, int(low_price))
        close_price = max(1, int(close_price))

        price_data.append({
            "open": open_price,
            "high": high_price,
            "low": low_price,
            "close": close_price
        })
        last_close = close_price # Update last_close for the next iteration

    df_large = pd.DataFrame(price_data, index=datetime_index)
    df_large.index.name = "datetime"

    # Simulate volume with some fluctuation
    df_large["volume"] = np.random.uniform(100.0, 500.0, periods)

    return df_large

4. Initial Sample Dataset

[11]
df = generate_larger_data(periods=30)

print("--- Raw OHLCV Data ---")
display(df.head())
--- Raw OHLCV Data ---
open high low close volume
datetime
2024-01-01 00:00:00+00:00 42000 42003 41856 41960 166.144497
2024-01-01 00:01:00+00:00 41955 41966 41795 41865 336.282519
2024-01-01 00:02:00+00:00 41882 41971 41739 41814 314.321868
2024-01-01 00:03:00+00:00 41817 41988 41754 41958 127.443400
2024-01-01 00:04:00+00:00 41924 41970 41566 41593 186.226600
[12]
def compute_indicators_pandas_ta(df: pd.DataFrame) -> pd.DataFrame:
    df = df.copy()

    df.ta.sma(length=10,  append=True)   # Simple Moving Average
    df.ta.ema(length=10,  append=True)   # Exponential Moving Average
    df.ta.rsi(length=14,  append=True)   # Relative Strength Index
    df.ta.macd(append=True)              # MACD, Signal, Histogram
    df.ta.bbands(length=10, append=True) # Bollinger Bands
    df.ta.atr(length=10,  append=True)   # Average True Range
    df.ta.obv(append=True)               # On-Balance Volume
    df.ta.stoch(append=True)             # Stochastic Oscillator

    return df

df_indicators = compute_indicators_pandas_ta(df)

print("--- Indicators Output ---")
display(df_indicators.tail())
df_indicators.info()
--- Indicators Output ---
open high low close volume SMA_10 EMA_10 RSI_14 BBL_10_2.0_2.0 BBM_10_2.0_2.0 BBU_10_2.0_2.0 BBB_10_2.0_2.0 BBP_10_2.0_2.0 ATRr_10 OBV STOCHk_14_3_3 STOCHd_14_3_3 STOCHh_14_3_3
datetime
2024-01-01 00:25:00+00:00 40415 40645 40367 40610 162.829932 41124.3 40994.630431 29.533279 40249.671419 41124.3 41998.928581 4.253585 0.205989 290.722800 -2204.311787 6.327272 12.496478 -6.169206
2024-01-01 00:26:00+00:00 40623 40668 40157 40276 312.512870 41008.1 40863.970352 25.663124 40017.617536 41008.1 41998.582464 4.830667 0.130433 312.750520 -2516.824658 7.817989 7.463132 0.354857
2024-01-01 00:27:00+00:00 40279 40486 40231 40453 115.295272 40902.6 40789.248470 30.835737 39924.095835 40902.6 41881.104165 4.784557 0.270262 306.975468 -2401.529386 13.539840 9.228367 4.311473
2024-01-01 00:28:00+00:00 40474 40636 40433 40494 165.690232 40812.4 40735.566930 32.015810 39870.426091 40812.4 41754.373909 4.616116 0.330993 296.577921 -2235.839154 14.710485 12.022771 2.687714
2024-01-01 00:29:00+00:00 40517 40525 40229 40338 120.323779 40712.9 40663.282034 29.923775 39805.770976 40712.9 41620.029024 4.456224 0.293359 296.520129 -2356.162933 15.923318 14.724548 1.198770
<class 'pandas.DataFrame'>
DatetimeIndex: 30 entries, 2024-01-01 00:00:00+00:00 to 2024-01-01 00:29:00+00:00
Freq: min
Data columns (total 18 columns):
 #   Column          Non-Null Count  Dtype  
---  ------          --------------  -----  
 0   open            30 non-null     int64  
 1   high            30 non-null     int64  
 2   low             30 non-null     int64  
 3   close           30 non-null     int64  
 4   volume          30 non-null     float64
 5   SMA_10          21 non-null     float64
 6   EMA_10          21 non-null     float64
 7   RSI_14          29 non-null     float64
 8   BBL_10_2.0_2.0  21 non-null     float64
 9   BBM_10_2.0_2.0  21 non-null     float64
 10  BBU_10_2.0_2.0  21 non-null     float64
 11  BBB_10_2.0_2.0  21 non-null     float64
 12  BBP_10_2.0_2.0  21 non-null     float64
 13  ATRr_10         21 non-null     float64
 14  OBV             29 non-null     float64
 15  STOCHk_14_3_3   15 non-null     float64
 16  STOCHd_14_3_3   13 non-null     float64
 17  STOCHh_14_3_3   13 non-null     float64
dtypes: float64(14), int64(4)
memory usage: 5.5 KB

Code Logic

  • df.ta.{indicator}(append=True): Each call computes the indicator and appends its output column(s) directly to the DataFrame. No manual column assignment is required.
  • sma / ema: Trend-following averages. SMA weights all periods equally; EMA weights recent periods more heavily — it reacts faster to new price information.
  • rsi: Measures how overbought or oversold a market is on a 0–100 scale. Above 70 typically signals overbought; below 30 signals oversold.
  • macd: Computes the difference between a fast and slow EMA, a signal line (EMA of MACD), and a histogram (MACD minus signal). Used to identify momentum direction and potential reversals.
  • bbands: Bollinger Bands — upper, middle, and lower bands based on rolling mean and standard deviation.
  • atr: Average True Range — a measure of how much the price typically moves per candle.
  • obv: On-Balance Volume — a running total of volume that adds volume on up days and subtracts on down days. Used to confirm price trends with volume.
  • stoch: Stochastic Oscillator — measures where the close sits relative to the high-low range over a lookback window, expressed as a 0–100 value.

5.1 Larger Dataset Generation and Indicator Recomputation

[13]
# Generate a larger dataset (e.g., 200 data points)
df_large = generate_larger_data(200)

print("--- Larger Raw OHLCV Data ---")
display(df_large.head())
display(df_large.tail())
df_large.info()

# Recompute indicators on the larger dataset
df_indicators_large = compute_indicators_pandas_ta(df_large)

print("--- Indicators Output for Larger Data ---")
display(df_indicators_large.tail())
df_indicators_large.info()
--- Larger Raw OHLCV Data ---
open high low close volume
datetime
2024-01-01 00:00:00+00:00 41964 42166 41937 42125 371.943585
2024-01-01 00:01:00+00:00 42090 42112 41902 41919 468.555769
2024-01-01 00:02:00+00:00 41908 42043 41817 41969 460.370758
2024-01-01 00:03:00+00:00 42019 42250 41977 42183 451.094123
2024-01-01 00:04:00+00:00 42172 42568 42151 42381 272.489297
open high low close volume
datetime
2024-01-01 03:15:00+00:00 42053 42172 41997 42156 348.269917
2024-01-01 03:16:00+00:00 42160 42272 42104 42248 385.621538
2024-01-01 03:17:00+00:00 42214 42249 42181 42244 491.590707
2024-01-01 03:18:00+00:00 42231 42363 42175 42270 153.290961
2024-01-01 03:19:00+00:00 42313 42609 42180 42289 238.091390
<class 'pandas.DataFrame'>
DatetimeIndex: 200 entries, 2024-01-01 00:00:00+00:00 to 2024-01-01 03:19:00+00:00
Freq: min
Data columns (total 5 columns):
 #   Column  Non-Null Count  Dtype  
---  ------  --------------  -----  
 0   open    200 non-null    int64  
 1   high    200 non-null    int64  
 2   low     200 non-null    int64  
 3   close   200 non-null    int64  
 4   volume  200 non-null    float64
dtypes: float64(1), int64(4)
memory usage: 9.4 KB
--- Indicators Output for Larger Data ---
open high low close volume SMA_10 EMA_10 RSI_14 MACD_12_26_9 MACDh_12_26_9 ... BBL_10_2.0_2.0 BBM_10_2.0_2.0 BBU_10_2.0_2.0 BBB_10_2.0_2.0 BBP_10_2.0_2.0 ATRr_10 OBV STOCHk_14_3_3 STOCHd_14_3_3 STOCHh_14_3_3
datetime
2024-01-01 03:15:00+00:00 42053 42172 41997 42156 348.269917 41744.9 41776.105629 77.660185 379.484120 55.008683 ... 41230.181997 41744.9 42259.618003 2.466016 0.899345 260.241942 6465.365068 95.411247 90.537876 4.873371
2024-01-01 03:16:00+00:00 42160 42272 42104 42248 385.621538 41831.7 41861.904606 78.922298 394.607246 56.105447 ... 41298.067542 41831.7 42365.332458 2.551330 0.890062 251.017748 6850.986606 97.147110 94.116714 3.030397
2024-01-01 03:17:00+00:00 42214 42249 42181 42244 491.590707 41903.8 41931.376496 78.714075 401.639812 50.510410 ... 41360.792081 41903.8 42446.807919 2.591688 0.813255 232.715973 6359.395899 97.827522 96.795293 1.032229
2024-01-01 03:18:00+00:00 42231 42363 42175 42270 153.290961 41975.6 41992.944406 79.100061 404.646639 42.813790 ... 41449.728702 41975.6 42501.471298 2.505605 0.779916 228.244376 6512.686860 95.548652 96.841095 -1.292443
2024-01-01 03:19:00+00:00 42313 42609 42180 42289 238.091390 42049.1 42046.772696 79.394120 403.906727 33.659103 ... 41583.068575 42049.1 42515.131425 2.216606 0.757386 248.319938 6750.778250 88.103241 93.826472 -5.723231

5 rows × 21 columns

<class 'pandas.DataFrame'>
DatetimeIndex: 200 entries, 2024-01-01 00:00:00+00:00 to 2024-01-01 03:19:00+00:00
Freq: min
Data columns (total 21 columns):
 #   Column          Non-Null Count  Dtype  
---  ------          --------------  -----  
 0   open            200 non-null    int64  
 1   high            200 non-null    int64  
 2   low             200 non-null    int64  
 3   close           200 non-null    int64  
 4   volume          200 non-null    float64
 5   SMA_10          191 non-null    float64
 6   EMA_10          191 non-null    float64
 7   RSI_14          199 non-null    float64
 8   MACD_12_26_9    175 non-null    float64
 9   MACDh_12_26_9   167 non-null    float64
 10  MACDs_12_26_9   167 non-null    float64
 11  BBL_10_2.0_2.0  191 non-null    float64
 12  BBM_10_2.0_2.0  191 non-null    float64
 13  BBU_10_2.0_2.0  191 non-null    float64
 14  BBB_10_2.0_2.0  191 non-null    float64
 15  BBP_10_2.0_2.0  191 non-null    float64
 16  ATRr_10         191 non-null    float64
 17  OBV             199 non-null    float64
 18  STOCHk_14_3_3   185 non-null    float64
 19  STOCHd_14_3_3   183 non-null    float64
 20  STOCHh_14_3_3   183 non-null    float64
dtypes: float64(17), int64(4)
memory usage: 42.5 KB

6. Data Visualization with Plotly

6.1 Candlestick Chart with Moving Averages (SMA, EMA)

[14]
fig = go.FigureWidget(data=[
    go.Candlestick(
        x=df_indicators_large.index,
        open=df_indicators_large['open'],
        high=df_indicators_large['high'],
        low=df_indicators_large['low'],
        close=df_indicators_large['close'],
        name='Price'
    )
])

# Add SMA_10
fig.add_trace(go.Scatter(
    x=df_indicators_large.index,
    y=df_indicators_large['SMA_10'],
    mode='lines',
    name='SMA 10',
    line=dict(color='blue', width=1)
))

# Add EMA_10
fig.add_trace(go.Scatter(
    x=df_indicators_large.index,
    y=df_indicators_large['EMA_10'],
    mode='lines',
    name='EMA 10',
    line=dict(color='orange', width=1)
))

fig.update_layout(
    title_text='Candlestick Chart with SMA and EMA',
    xaxis_rangeslider_visible=False,
    xaxis_title='Date',
    yaxis_title='Price',
    height=600,
    yaxis=dict(autorange=True) # Ensure y-axis scales to visible candles
)

fig.show()

6.2 Candlestick Chart with Bollinger Bands

[15]
fig = go.FigureWidget(data=[
    go.Candlestick(
        x=df_indicators_large.index,
        open=df_indicators_large['open'],
        high=df_indicators_large['high'],
        low=df_indicators_large['low'],
        close=df_indicators_large['close'],
        name='Price'
    )
])

# Add Bollinger Bands
fig.add_trace(go.Scatter(
    x=df_indicators_large.index,
    y=df_indicators_large['BBL_10_2.0_2.0'],
    mode='lines',
    name='BB Lower',
    line=dict(color='red', width=1)
))
fig.add_trace(go.Scatter(
    x=df_indicators_large.index,
    y=df_indicators_large['BBM_10_2.0_2.0'],
    mode='lines',
    name='BB Middle',
    line=dict(color='green', width=1, dash='dot')
))
fig.add_trace(go.Scatter(
    x=df_indicators_large.index,
    y=df_indicators_large['BBU_10_2.0_2.0'],
    mode='lines',
    name='BB Upper',
    line=dict(color='red', width=1)
))

fig.update_layout(
    title_text='Candlestick Chart with Bollinger Bands',
    xaxis_rangeslider_visible=False,
    xaxis_title='Date',
    yaxis_title='Price',
    height=600,
    yaxis=dict(autorange=True) # Ensure y-axis scales to visible candles
)

fig.show()

6.3 Relative Strength Index (RSI)

[16]
fig = make_subplots(
    rows=2, cols=1,
    shared_xaxes=True,
    vertical_spacing=0.03,
    row_heights=[0.7, 0.3]
)

# Candlestick chart
fig.add_trace(go.Candlestick(
    x=df_indicators_large.index,
    open=df_indicators_large['open'],
    high=df_indicators_large['high'],
    low=df_indicators_large['low'],
    close=df_indicators_large['close'],
    name='Price'
), row=1, col=1)

# RSI chart
fig.add_trace(go.Scatter(
    x=df_indicators_large.index,
    y=df_indicators_large['RSI_14'],
    mode='lines',
    name='RSI 14',
    line=dict(color='purple', width=1)
), row=2, col=1)

# Add RSI overbought/oversold levels
fig.add_hline(y=70, line_dash="dot", line_color="red", row=2, col=1)
fig.add_hline(y=30, line_dash="dot", line_color="green", row=2, col=1)

fig.update_layout(
    title_text='Candlestick Chart with RSI',
    xaxis_rangeslider_visible=False,
    xaxis_title='Date',
    yaxis_title='Price',
    height=800,
    yaxis=dict(autorange=True) # Ensure y-axis scales to visible candles for candlestick
)

fig.update_yaxes(title_text="RSI", row=2, col=1)

fig.show()

6.4 Moving Average Convergence Divergence (MACD)

[17]
fig = make_subplots(
    rows=2, cols=1,
    shared_xaxes=True,
    vertical_spacing=0.03,
    row_heights=[0.7, 0.3]
)

# Candlestick chart
fig.add_trace(go.Candlestick(
    x=df_indicators_large.index,
    open=df_indicators_large['open'],
    high=df_indicators_large['high'],
    low=df_indicators_large['low'],
    close=df_indicators_large['close'],
    name='Price'
), row=1, col=1)

# MACD traces
# Note: pandas-ta names MACD as MACD_12_26_9, Signal as MACDs_12_26_9, Histogram as MACDh_12_26_9
fig.add_trace(go.Scatter(
    x=df_indicators_large.index,
    y=df_indicators_large['MACD_12_26_9'],
    mode='lines',
    name='MACD',
    line=dict(color='blue', width=1)
), row=2, col=1)

fig.add_trace(go.Scatter(
    x=df_indicators_large.index,
    y=df_indicators_large['MACDs_12_26_9'],
    mode='lines',
    name='Signal',
    line=dict(color='red', width=1)
), row=2, col=1)

fig.add_trace(go.Bar(
    x=df_indicators_large.index,
    y=df_indicators_large['MACDh_12_26_9'],
    name='Histogram',
    marker_color='green' # Green for positive, could add logic for red for negative
), row=2, col=1)

fig.update_layout(
    title_text='Candlestick Chart with MACD',
    xaxis_rangeslider_visible=False,
    xaxis_title='Date',
    yaxis_title='Price',
    height=800,
    yaxis=dict(autorange=True) # Ensure y-axis scales to visible candles for candlestick
)

fig.update_yaxes(title_text="MACD", row=2, col=1)

fig.show()

6.5 Average True Range (ATR)

[18]
fig = make_subplots(
    rows=2, cols=1,
    shared_xaxes=True,
    vertical_spacing=0.03,
    row_heights=[0.7, 0.3]
)

# Candlestick chart
fig.add_trace(go.Candlestick(
    x=df_indicators_large.index,
    open=df_indicators_large['open'],
    high=df_indicators_large['high'],
    low=df_indicators_large['low'],
    close=df_indicators_large['close'],
    name='Price'
), row=1, col=1)

# ATR chart
fig.add_trace(go.Scatter(
    x=df_indicators_large.index,
    y=df_indicators_large['ATRr_10'],
    mode='lines',
    name='ATR 10',
    line=dict(color='darkorange', width=1)
), row=2, col=1)

fig.update_layout(
    title_text='Candlestick Chart with ATR',
    xaxis_rangeslider_visible=False,
    xaxis_title='Date',
    yaxis_title='Price',
    height=800,
    yaxis=dict(autorange=True) # Ensure y-axis scales to visible candles for candlestick
)

fig.update_yaxes(title_text="ATR", row=2, col=1)

fig.show()

6.6 On-Balance Volume (OBV)

[19]
fig = make_subplots(
    rows=2, cols=1,
    shared_xaxes=True,
    vertical_spacing=0.03,
    row_heights=[0.7, 0.3]
)

# Candlestick chart
fig.add_trace(go.Candlestick(
    x=df_indicators_large.index,
    open=df_indicators_large['open'],
    high=df_indicators_large['high'],
    low=df_indicators_large['low'],
    close=df_indicators_large['close'],
    name='Price'
), row=1, col=1)

# OBV chart
fig.add_trace(go.Scatter(
    x=df_indicators_large.index,
    y=df_indicators_large['OBV'],
    mode='lines',
    name='OBV',
    line=dict(color='brown', width=1)
), row=2, col=1)

fig.update_layout(
    title_text='Candlestick Chart with OBV',
    xaxis_rangeslider_visible=False,
    xaxis_title='Date',
    yaxis_title='Price',
    height=800,
    yaxis=dict(autorange=True) # Ensure y-axis scales to visible candles for candlestick
)

fig.update_yaxes(title_text="OBV", row=2, col=1)

fig.show()

6.7 Stochastic Oscillator

[20]
fig = make_subplots(
    rows=2, cols=1,
    shared_xaxes=True,
    vertical_spacing=0.03,
    row_heights=[0.7, 0.3]
)

# Candlestick chart
fig.add_trace(go.Candlestick(
    x=df_indicators_large.index,
    open=df_indicators_large['open'],
    high=df_indicators_large['high'],
    low=df_indicators_large['low'],
    close=df_indicators_large['close'],
    name='Price'
), row=1, col=1)

# Stochastic Oscillator chart
# Note: pandas-ta names Stochastic %K as STOCHk_14_3_3 and %D as STOCHd_14_3_3
fig.add_trace(go.Scatter(
    x=df_indicators_large.index,
    y=df_indicators_large['STOCHk_14_3_3'],
    mode='lines',
    name='%K',
    line=dict(color='blue', width=1)
), row=2, col=1)

fig.add_trace(go.Scatter(
    x=df_indicators_large.index,
    y=df_indicators_large['STOCHd_14_3_3'],
    mode='lines',
    name='%D',
    line=dict(color='red', width=1)
), row=2, col=1)

# Add overbought/oversold levels for Stochastic
fig.add_hline(y=80, line_dash="dot", line_color="red", row=2, col=1)
fig.add_hline(y=20, line_dash="dot", line_color="green", row=2, col=1)

fig.update_layout(
    title_text='Candlestick Chart with Stochastic Oscillator',
    xaxis_rangeslider_visible=False,
    xaxis_title='Date',
    yaxis_title='Price',
    height=800,
    yaxis=dict(autorange=True) # Ensure y-axis scales to visible candles for candlestick
)

fig.update_yaxes(title_text="Stochastic", row=2, col=1)

fig.show()
[20]