Skip to main content
Market Maker's Blog

Integrating Custom Trading Indicators with Pandas

On 18 February 2025 - tagged resources, python

Pandas has become the de facto standard for financial data analysis in Python. Its powerful data structures and efficient operations make it ideal for working with time series data. However, while pandas comes with many built-in functions, traders often need custom technical indicators for their analysis. In this post, we'll explore how to implement common trading indicators using NumPy and pandas in computationally efficient way and integrate them with Pandas dataframes seamlessly for convenience.

Let's start by defining a few utility functions that we'll use for efficient computation of the indicators:

Utility Functions #

Efficient Rolling Window Calculations #

When working with financial time series, we often need to calculate metrics over a rolling window of data. The naive approach of iterating through the data can be slow, especially with large datasets. Instead, we can use NumPy's stride tricks to create efficient rolling window views of our data:

def numpy_rolling_window(data, window):
    shape = data.shape[:-1] + (data.shape[-1] - window + 1, window)
    strides = data.strides + (data.strides[-1],)
    return np.lib.stride_tricks.as_strided(data, shape=shape, strides=strides)

The Decorator Pattern #

To make our indicator functions work seamlessly with both raw NumPy arrays and pandas Series, we use a decorator pattern:

def numpy_rolling_series(func):
    def func_wrapper(data, window, as_source=False):
        series = data.values if isinstance(data, pd.Series) else data

        new_series = np.empty(len(series)) * np.nan
        calculated = func(series, window)
        new_series[-len(calculated):] = calculated

        if as_source and isinstance(data, pd.Series):
            return pd.Series(index=data.index, data=new_series)

        return new_series
    return func_wrapper

This pattern allows us to write our core indicator logic using NumPy for performance, while still maintaining pandas compatibility.

Indicator Examples #

True Range #

The True Range indicator measures market volatility, taking into account gaps between periods:

def true_range(bars):
    return pd.DataFrame({
        "hl": bars['high'] - bars['low'],
        "hc": abs(bars['high'] - bars['close'].shift(1)),
        "lc": abs(bars['low'] - bars['close'].shift(1))
    }).max(axis=1)

Average True Range (ATR) #

The ATR is a moving average of the True Range, commonly used to measure market volatility:

def atr(bars, window=14):
    tr = true_range(bars)
    res = rolling_mean(tr, window)
    return pd.Series(res)

Relative Strength Index (RSI) #

The RSI is a momentum oscillator that measures the speed and change of price movements:

def rsi(series, window=14):
    deltas = np.diff(series)
    seed = deltas[:window + 1]

    ups = seed[seed > 0].sum() / window
    downs = -seed[seed < 0].sum() / window
    rsival = np.zeros_like(series)
    rsival[:window] = 100. - 100. / (1. + ups / downs)

    for i in range(window, len(series)):
        delta = deltas[i - 1]
        if delta > 0:
            upval = delta
            downval = 0
        else:
            upval = 0
            downval = -delta

        ups = (ups * (window - 1) + upval) / window
        downs = (downs * (window - 1.) + downval) / window
        rsival[i] = 100. - 100. / (1. + ups / downs)

    return pd.Series(index=series.index, data=rsival)

Bollinger Bands #

Bollinger Bands consist of a moving average and (usually) two standard deviation bands, used to identify overbought/oversold conditions:

def bollinger_bands(series, window=20, stds=2):
    ma = rolling_mean(series, window=window)
    std = rolling_std(series, window=window)
    upper = ma + std * stds
    lower = ma - std * stds

    return pd.DataFrame(index=series.index, data={
        'upper': upper,
        'mid': ma,
        'lower': lower
    })

Z-Score #

The Z-Score measures how many standard deviations a price is from its mean, useful for identifying extremes. Note that this implementation is not forward looking thanks to using rolling statistics!

def zscore(series, window=20, stds=1):
    std = numpy_rolling_std(series, window)
    mean = numpy_rolling_mean(series, window)
    return (series - mean) / (std * stds)

Pandas Integration #

To make our indicators easily accessible in pandas workflows, we can extend the PandasObject class:

from pandas.core.base import PandasObject

PandasObject.true_range = true_range
PandasObject.atr = atr
PandasObject.rsi = rsi
PandasObject.bollinger_bands = bollinger_bands
PandasObject.zscore = zscore

Now we can use these indicators directly on our DataFrame objects:

df['ATR'] = df.atr(window=14)
df['RSI'] = df.rsi(window=14)

Practical Example #

Let's apply these indicators to BTC prices from Crypto Lake free dataset:

import lakeapi
import matplotlib.pyplot as plt

# Get free sample data
lakeapi.use_sample_data(anonymous_access = True)
data = lakeapi.load_data(
    table = 'trades', 
    exchanges = ['BINANCE'], 
    symbols = ['BTC-USDT']
).set_index('origin_time')
data = data.iloc[-1000:] # Select just a few minutes of trades

# Calculate indicator
bands = data['price'].bollinger_bands(window=50)

# Plot results
plt.plot(data['price'], label='Price')
plt.plot(bands['upper'], label='Upper Band', linestyle='--')
plt.plot(bands['mid'], label='Moving Average')
plt.plot(bands['lower'], label='Lower Band', linestyle='--')
plt.legend()
plt.show()

Pandas Trading Indicators Example

Conclusion #

Full code in a single python file can be downloaded here. If you save the file, delete the usage in the bottom part and then import the module in your notebooks/analyses, you can build a convenient indicator library for your use-cases.

New posts are announced on twitter @jan_skoda or @crypto_lake_com, so follow us!