indicators¶
Typed aliases for TA-Lib functions. No formula in this project is reimplemented, so none can drift from the reference.
indicators
¶
topstep_backtest.indicators — every indicator is TA-Lib.
TalibIndicator drives any of TA-Lib's ~160 functions bar by bar; the named
classes are typed spellings of the common ones. See talib_adapter for the
causality, streaming-equals-batch, and bounded-history parity guarantees.
Indicator
¶
Bases: Protocol
The surface SymbolStrategy.use() requires of a registered indicator.
NotReadyError
¶
Bases: Exception
value was read before the indicator had seen lookback bars.
ValueSource
¶
Bases: Protocol
Minimal structural input for Cross: anything exposing a Decimal series.
Runtime-checkable so use() can refuse a Cross over something with no
value (notably another Cross) at registration, rather than letting it
die with an AttributeError hours into a run — and only once both inner
inputs happen to be ready.
WarmIndicator
¶
Bases: Protocol
An Indicator that also declares where its value stops depending on
where the run started.
lookback is where a value EXISTS; history_bars is where it is a pure
function of the last history_bars bars and nothing earlier — the
ready versus warm distinction. Runtime-checkable because not every
Indicator has a buffer of its own: a Cross holds no history, so its
warmth is entirely its inputs', and those are registered separately.
Consumers that need a warm-start bar count should fall back to lookback
for anything that fails this check.
Adx
¶
Bases: TalibIndicator
Average directional index (TA-Lib ADX).
Source code in src/topstep_backtest/indicators/library.py
Atr
¶
Bases: TalibIndicator
Wilder's average true range (TA-Lib ATR); lookback == period + 1.
Source code in src/topstep_backtest/indicators/library.py
BBands
¶
Bases: TalibIndicator
Bollinger bands (TA-Lib BBANDS): upperband/middleband/lowerband.
The bands inherit StdDev's naive-variance error (~7e-7 relative at
100,000), so a band EDGE is not an exact number: a Cross against one can
flip on that noise, and a "touch" of a band is never exact. Use it as a
threshold, not as a price.
Source code in src/topstep_backtest/indicators/library.py
Cross
¶
Cross(a: ValueSource, b: ValueSource)
Crossover of two value series: up/down fire only on the crossing bar.
Tracks the sign of a.value - b.value per update: up when the sign
goes from <= 0 to > 0, down when it goes from >= 0 to < 0 — an exact
a == b touch therefore fires on the bar that resolves it, not on the
touch itself. Both are False until the detector holds a previous AND
a current comparison with both inputs ready, so
lookback = max(a.lookback, b.lookback) + 1.
Note that TA-Lib values are float64, so an exact a == b touch is
vanishingly rare in practice — the zero-sign branch is a correctness
guarantee, not a common path.
update(bar) ignores the bar and reads a.value / b.value — it
REQUIRES both inputs to have been updated for the same bar first. Under
SymbolStrategy's registration-order update rule that means use()
the inputs BEFORE the Cross that reads them (use() enforces it via
inputs, resolving a TalibLine to the indicator that owns it).
Source code in src/topstep_backtest/indicators/library.py
inputs
property
¶
inputs: tuple[ValueSource, ValueSource]
The (a, b) sources read on update, for registration-order checks.
mark_registered
¶
Called by use(). A Cross nobody registered never updates, so its
up/down would read False forever and the strategy would take zero
trades in silence — up/down raise instead once that is provable.
Source code in src/topstep_backtest/indicators/library.py
Ema
¶
Bases: TalibIndicator
Exponential moving average of closes (TA-Lib EMA).
Seeded with the SMA of the first period closes, then k = 2/(period+1)
recursion — TA-Lib's convention.
Source code in src/topstep_backtest/indicators/library.py
Highest
¶
Bases: TalibIndicator
Rolling maximum of bar.high (TA-Lib MAX redirected onto highs).
Source code in src/topstep_backtest/indicators/library.py
Lowest
¶
Bases: TalibIndicator
Rolling minimum of bar.low (TA-Lib MIN redirected onto lows).
Source code in src/topstep_backtest/indicators/library.py
Macd
¶
Bases: TalibIndicator
MACD (TA-Lib MACD): outputs macd, macdsignal, macdhist.
value is the MACD line; cross the lines with
Cross(macd.line("macd"), macd.line("macdsignal")).
Source code in src/topstep_backtest/indicators/library.py
Obv
¶
Bases: TalibIndicator
On-balance volume (TA-Lib OBV).
OBV accumulates without decay, so — unlike every other indicator here — its
LEVEL depends on where accumulation started and is therefore windowed to
history_bars like everything else. Use its slope or divergence, never
the absolute level; an inception-anchored level could not be reproduced
live in any case.
Source code in src/topstep_backtest/indicators/library.py
Rsi
¶
Bases: TalibIndicator
Wilder's relative strength index (TA-Lib RSI); lookback == period + 1.
Source code in src/topstep_backtest/indicators/library.py
Sma
¶
Bases: TalibIndicator
Simple moving average of closes (TA-Lib SMA).
Source code in src/topstep_backtest/indicators/library.py
StdDev
¶
Bases: TalibIndicator
POPULATION standard deviation of closes (TA-Lib STDDEV, nbdev=1).
TA-Lib evaluates the NAIVE E[x^2] - E[x]^2 form, which cancels badly
once the mean is large relative to the spread — at futures price levels the
relative error reaches ~2e-9 at 5,000 and ~7e-7 at 100,000. Harmless for a
threshold read; do not treat a value derived from it as exact.
Source code in src/topstep_backtest/indicators/library.py
Stoch
¶
Bases: TalibIndicator
Slow stochastic (TA-Lib STOCH): outputs slowk and slowd.
Source code in src/topstep_backtest/indicators/library.py
TalibIndicator
¶
TalibIndicator(name: str, *, price: str | None = None, history: int | None = None, **params: int | float)
Any TA-Lib function, driven bar by bar.
TalibIndicator("RSI", timeperiod=14) is the whole API; the named
classes in library.py are thin, typed spellings of it. Parameters are
TA-Lib's own (timeperiod, fastperiod, nbdevup, matype, …),
validated against the function's declared signature AND run past TA-Lib
once at construction, so a typo or an out-of-range value raises here rather
than silently using a default or dying mid-backtest.
price redirects a single-series function onto another bar field —
TalibIndicator("MAX", price="high", timeperiod=20) is a rolling high.
Functions that declare their own multi-field inputs (ATR wants high/low/
close) reject it.
Thread-safety. talib's Function keeps its configured parameters
in a threading.local, so a Function configured on one thread silently
reverts to TA-Lib's DEFAULTS on another — an EMA(50) built on the main
thread would quietly compute EMA(30) inside a worker, with no error.
This adapter therefore treats the Function object as stateless: parameters
are held here as plain data and passed on EVERY call, and a redirected
price is applied by choosing which bar field fills the input slot rather
than by mutating input_names. Instances are safe to build on one thread
and drive on another (though a single instance is still not safe to drive
from two threads at once — it has per-bar state).
Source code in src/topstep_backtest/indicators/talib_adapter.py
lookback
property
¶
Bars needed before the first value exists (TA-Lib's lookback + 1).
history_bars
property
¶
Bars retained. Preload this many live for bit-exact sim/live parity.
warm
property
¶
True once the buffer is full, i.e. from the first PARITY-exact bar.
ready says a value exists; warm says the value no longer depends
on where this run started. Gate on this when sim and live must agree
bit-for-bit (see the module docstring).
outputs
property
¶
This function's output names, TA-Lib's order (value is the first).
get
¶
A named output for the most recent bar.
Source code in src/topstep_backtest/indicators/talib_adapter.py
line
¶
line(output: str) -> TalibLine
A Cross-compatible view of one named output.
Source code in src/topstep_backtest/indicators/talib_adapter.py
TalibLine
¶
TalibLine(owner: TalibIndicator, name: str)
One named output of a multi-output indicator, usable wherever a single
value series is (notably as a Cross input).
A line does NOT update itself — it reads whatever its owner last computed,
so registering the OWNER with use() is what keeps it current.
SymbolStrategy.use() resolves a Cross input through owner for
exactly this reason.
Deliberately NO update method: that keeps a line from satisfying
Indicator, so use(macd.line("macd")) — which would register
something that can never advance and would gate the strategy forever — is a
type error, and use() rejects it at runtime too. Register the owner.
Source code in src/topstep_backtest/indicators/talib_adapter.py
owner
property
¶
owner: TalibIndicator
The indicator that computes this line (what use() registers).
talib_function_names
¶
Every TA-Lib function name TalibIndicator can wrap, sorted.