Backtesting wants to chew through 500,000 bars in microseconds. Live trading wants sub-microsecond updates on every tick. These are fundamentally different computational models: one is stateless bulk math, the other is stateful incremental math. Most libraries pick one and apologize for the other.
We picked both. Then made them share a consistent API surface so you do not have to remember two different libraries depending on whether it is Saturday backtesting or Monday morning.
All indicators inherit from `AbstractBase` and implement `ITValuePublisher`. Every indicator, all 393 of them, exposes the same interface. No special cases. No "oh, this oscillator works differently." Consistency is not optional when you have 393 things to keep straight.
## The Contract
Every indicator exposes these properties and methods. No exceptions.
**When:** Backtesting. Historical analysis. Optimization sweeps where you are running 10,000 parameter combinations and need results before your coffee gets cold.
This is the path where the garbage collector sleeps. Operates directly on memory spans. AVX-512 on hardware that supports it, AVX2 where it does not, NEON on ARM. The runtime picks the widest vector width your CPU can handle.
500,000 bars of SMA in 328 microseconds. That is 0.66 nanoseconds per value. Faster than a single L1 cache miss on most architectures.
Zero allocations. The `Span<double>` points directly at the arrays you already own. No intermediate collections. No LINQ. No temporary lists that make the GC twitch.
When you need timestamps aligned with your results. Slightly slower because metadata travels with the data, but still fast enough that you will not notice unless you are benchmarking.
Timestamps survive the calculation. Time alignment is preserved. You can correlate results back to the original bars without maintaining a separate index.
**When:** Live trading. Real-time charting. Anything where data arrives one value at a time and you need answers before the next value shows up.
Streaming mode maintains internal state: circular buffers, running sums, previous values. Each `Update` call is O(1). Not amortized O(1). Actual O(1). The indicator does constant work regardless of period length or history depth.
A bar opens. Ticks update it. The close price changes six times before the bar finalizes. Naive implementations accumulate drift: each "correction" adds another data point instead of replacing the current one. After enough corrections, your 14-period SMA quietly becomes a 14-plus-however-many-corrections-period SMA.
When `isNew` is `false`, the indicator rolls back to state before the current bar, then applies the new value. No drift. No accumulated phantom data points. The SMA of 14 periods remains an SMA of 14 periods, no matter how many intrabar corrections arrive.
I once spent an entire weekend debugging a live trading system where the EMA was drifting by 0.3% over the course of a trading day. The cause: bar corrections being treated as new bars. That is thirty basis points of compounding error, invisible on any single bar, devastating over 390 bars. The `isNew` flag exists because of weekends like that.
The chain fires in subscription order. Each indicator receives the output of its upstream dependency, computes, publishes. By the time `source.Add` returns, all three indicators have updated values.
This replaces the typical pattern of "calculate SMA, then pass result to RSI, then pass result to EMA, and do not forget to check IsHot on each one, and do not forget the isNew flag, and make sure the timestamps align." That pattern is a bug factory. Event-driven chaining eliminates the manual wiring.
A common mistake: run batch mode to get historical results, create a fresh streaming instance for live data, and start with a cold indicator that needs another `WarmupPeriod` bars before producing valid output. During those warmup bars, your live trading system either produces garbage signals or sits idle. Neither is acceptable.
Priming solves this. It hydrates a streaming indicator using the minimal tail of historical data.
`Prime` does not process all 100,000 bars. It takes the last `WarmupPeriod` values (or however many the specific indicator needs for convergence), processes them through the streaming path, and leaves the indicator in the exact state it would be in if it had processed that history bar by bar.
This eliminates the "two instances" problem entirely. The `indicator` returned from `Calculate` has full internal state. It does not need warmup. It does not need priming. It is ready.
| Fixed-window (SMA, WMA) | Exactly `period` bars | SMA(14): hot after 14 bars |
| Recursive (EMA, DEMA, T3) | ~3 * period for convergence | EMA(14): hot after ~42 bars |
| Multi-component (Bollinger) | Longest component's warmup | Bollinger(20): hot after 20 bars |
| Composite chains | Sum of all component warmups | EMA(SMA(14), 5): hot after 14 + 15 bars |
For recursive indicators, "convergence" means the output has settled within 1% of the value it would produce with infinite history. A 14-period EMA technically never fully converges (the impulse response is infinite, hence "IIR"), but after 42 bars the residual error is smaller than floating-point noise. Good enough for trading. Good enough for math.
Constructors validate eagerly. Bad parameters throw `ArgumentException` with the parameter name. You find out at construction time, not 50,000 bars later when the division by zero surfaces.
Financial data contains holes. Feeds drop. Connections hiccup. The resulting `NaN` values, if allowed to propagate through an indicator chain, will poison every downstream calculation. NaN is infectious: `NaN + anything = NaN`. One bad tick and your entire indicator pipeline produces `NaN` for the rest of the session.
QuanTAlib substitutes the last valid value when it encounters non-finite input.
Indicators are not thread-safe. Each instance maintains mutable internal state: circular buffers, running sums, previous values. Concurrent access will corrupt that state in ways that produce subtly wrong numbers rather than obvious crashes. The worst kind of bug.
For multi-threaded scenarios: one indicator instance per thread. Or external synchronization. But if you are synchronizing access to a sub-microsecond indicator, the lock contention will cost more than the calculation. Just create separate instances.
## IDisposable
All indicators implement `IDisposable`. In practice, there is nothing unmanaged to release. But the interface is there because indicators subscribe to upstream events via `Pub`, and proper disposal ensures those subscriptions are cleaned up. Use `using` statements or explicit `Dispose` calls in long-running applications where indicator instances are created and destroyed dynamically.
```csharp
usingvarsma=newSma(source,period:14);
// sma unsubscribes from source when disposed
```
Full indicator catalog: [393 indicators](../lib/_index.md)