Features

Streaming indicators

Update indicators bar by bar with stateful accumulators that match batch results

Can an indicator update with each new bar without recomputing its whole history? VBT computes streaming indicators in Python with accumulators: small functions that take the previous state and one new value and return the next state. They use the same formulas as the batch versions, so you can match batch results with the same inputs, settings, and initialization, without recomputing the full history on each update.

Update an EMA one bar at a time and compare it with the batch result
close = vbt.YFData.pull("BTC-USD", start="2024-01-01", end="2025-01-01").close
span = 20
alpha = 2 / (span + 1)

old_wt, weighted_avg, nobs = 1.0, np.nan, 0  
streamed = []
for i, price in enumerate(close.values):
    if i == 0:
        weighted_avg = price
    state = vbt.nb.ewm_mean_acc_nb(  
        vbt.nb.EWMMeanAIS(i, price, old_wt, weighted_avg, nobs, alpha, 1, True)
    )
    old_wt, weighted_avg, nobs = state.old_wt, state.weighted_avg, state.nobs
    streamed.append(state.value)

batch = close.vbt.ewm_mean(span, minp=1)
print(np.allclose(streamed, batch), round(streamed[-1], 2), round(batch.iloc[-1], 2))
True 96421.8 96421.8

The streamed values match the batch EMA on every bar. Keeping the state instead of the history is what makes the update cost constant:

Time one streaming update against recomputing the history
import timeit

in_state = vbt.nb.EWMMeanAIS(1, 100.0, 1.0, 100.0, 1, alpha, 1, True)  
rows = {}
for n in [1_000, 100_000]:
    arr = np.random.default_rng(42).normal(size=n).cumsum() + 1000
    batch_us = min(timeit.repeat(
        lambda: vbt.nb.ewm_mean_1d_nb(arr, span, 1, True), number=100, repeat=5
    )) / 100 * 1e6
    stream_us = min(timeit.repeat(
        lambda: vbt.nb.ewm_mean_acc_nb(in_state), number=10000, repeat=5
    )) / 10000 * 1e6
    rows[n] = {
        "recompute history (µs)": round(batch_us, 1),
        "one streaming update (µs)": round(stream_us, 1),
    }
pd.DataFrame(rows).T
        recompute history (µs)  one streaming update (µs)
1000                       4.9                        0.8
100000                   450.8                        0.9

Measured on an Apple M3. Recomputing grows with the history, while a streaming update stays under a microsecond, most of it Python's call overhead.

Where streaming indicators help

Research taskHow accumulators help
Update a signal as each completed bar arrivesCalculate the next value from the retained state
Follow many symbols or parameter settingsKeep a separate state for each calculation instead of rerunning every history
Calculate indicators inside a simulation callbackUpdate the indicator alongside the strategy's bar-by-bar decisions
Process history in successive batchesCarry the state across batches so calculations continue from the same starting point

You supply observations from your data workflow or simulation. For example, feed each completed bar into an EMA and RSI, combine their outputs into an entry condition, and pass that condition to your strategy. Data pipelines covers collecting and updating the data, while Live simulation covers advancing a portfolio as observations arrive.

Streaming state

Every streaming indicator is defined by its state. For an EMA it is the running average and its weight, for a rolling mean the running sum and the values about to leave the window, and for RSI the average gains and losses. Save the state, and the indicator can continue later, in another process, or after new data arrives, without replaying the full history it was computed from. Keep the settings and current position with that state. For rolling calculations in Numba, also retain the recent values needed as observations leave the window. Rust accumulators hold those buffers internally.

Each symbol and parameter setting has its own state. This lets you track, for example, a fast and slow average on several assets without mixing their histories.

A streaming indicator still needs a warm-up. Rolling windows return NaN until they have enough observations, and exponential averages depend on where they start: an EMA started a year ago and one started yesterday give different values today. Start streaming from the same point as the batch calculation with the same settings when the two must agree. Feeding earlier bars first also lets you warm up the indicator before the period you want to trade or evaluate, as described in Walk-forward and cross-validation.

Accumulator building blocks

Accumulators exist for rolling sums, means, standard deviations, z-scores, minimums and maximums, exponential means and standard deviations, crossovers, and indicators such as RSI, ADX, on-balance volume, and SuperTrend. Because each one works with the current inputs and its retained state or window values, several can be combined in a single pass over the data, which is how complex indicators are built from simple ones. The Accumulators highlight below combines two of them into a one-pass rolling z-score.

Streaming keeps each update focused on the new observation, making accumulators especially useful for long histories, many symbols, and calculations inside a simulation loop. Combine them with batch calculations for the rest of your indicators. The EMA benchmark above shows both approaches on the same data.

Python, Numba, and Rust

The same accumulators run in a Python loop, inside a Numba-compiled loop or simulation callback, and in Rust. The Rust versions keep their own rolling buffers, so a Rust program only feeds in new values, as the Streaming indicators highlight below shows. That makes it possible to prototype in Python and move the same logic into a compiled live system without changing the math.

Rust accumulators can also be cloned to branch from the same warmed-up state. Feed different future observations into each copy to compare scenarios without replaying the shared history. Use reset() to start a fresh calculation with the same configuration. These are useful building blocks for custom indicators and native research programs, as well as live calculations.

Tutorial

The members-only Superfast SuperTrend tutorial builds a streaming SuperTrend from accumulators and compares it with the batch version.

Streaming indicators

✅ Keep your indicators moving with the market. VBT's Numba and Rust accumulators update one observation at a time, using the same formulas as batch execution. Rust accumulators even manage their own rolling buffers, so you can feed in new data and get straight to the next value.

Update on-balance volume for each bar
prev_close = np.nan
obv = 0.0
for close, volume in [(100.0, 10.0), (102.0, 20.0), (101.0, 5.0)]:
    out = vbt.ind_nb.obv_acc_nb(  
        vbt.ind_enums.OBVAIS(close, prev_close, volume, obv)
    )
    prev_close, obv = close, out.cumsum  
    print(out.value)
10.0
30.0
25.0

Tutorial

Learn more in the Live simulation tutorial.

Accumulators

✅ Most rolling indicators implemented with Pandas and NumPy require multiple passes over the data. For example, calculating the sum of three arrays requires at least two passes. If you want to calculate such an indicator iteratively (bar by bar), you either need to pre-calculate everything and store it in memory or re-calculate each window, which can significantly impact performance. Accumulators, however, maintain an internal state that allows you to compute an indicator value as soon as a new data point arrives, resulting in the best possible performance.

Design a one-pass rolling z-score
@njit
def fastest_rolling_zscore_1d_nb(arr, window, minp=None, ddof=1):
    if minp is None:
        minp = window
    out = np.full(arr.shape, np.nan)
    cumsum = 0.0
    cumsum_sq = 0.0
    nancnt = 0

    for i in range(len(arr)):
        pre_window_value = arr[i - window] if i - window >= 0 else np.nan
        mean_in_state = vbt.nb.RollMeanAIS(
            i, arr[i], pre_window_value, cumsum, nancnt, window, minp
        )
        mean_out_state = vbt.nb.rolling_mean_acc_nb(mean_in_state)
        _, _, _, mean = mean_out_state
        std_in_state = vbt.nb.RollStdAIS(
            i, arr[i], pre_window_value, cumsum, cumsum_sq, nancnt, window, minp, ddof
        )
        std_out_state = vbt.nb.rolling_std_acc_nb(std_in_state)
        cumsum, cumsum_sq, nancnt, _, std = std_out_state
        out[i] = (arr[i] - mean) / std
    return out

data = vbt.YFData.pull("BTC-USD")
rolling_zscore = fastest_rolling_zscore_1d_nb(data.returns.values, 14)
data.symbol_wrapper.wrap(rolling_zscore)
Date
2014-09-17 00:00:00+00:00         NaN
2014-09-18 00:00:00+00:00         NaN
2014-09-19 00:00:00+00:00         NaN
                                  ...
2023-02-01 00:00:00+00:00    0.582381
2023-02-02 00:00:00+00:00   -0.705441
2023-02-03 00:00:00+00:00   -0.217880
Freq: D, Name: BTC-USD, Length: 3062, dtype: float64
(data.returns - data.returns.rolling(14).mean()) / data.returns.rolling(14).std()
Date
2014-09-17 00:00:00+00:00         NaN
2014-09-18 00:00:00+00:00         NaN
2014-09-19 00:00:00+00:00         NaN
                                  ...
2023-02-01 00:00:00+00:00    0.582381
2023-02-02 00:00:00+00:00   -0.705441
2023-02-03 00:00:00+00:00   -0.217880
Freq: D, Name: Close, Length: 3062, dtype: float64

Copyright © 2021–2026 Oleg Polakow. All rights reserved.

Site content and documentation are provided for using and evaluating VectorBT PRO and for educational purposes. Any other use, including building or supporting competing products or services, requires prior written consent.