C# Streaming API
Not yet released
The C# API is not yet released. Estimated release: Q1 2027.
The streaming API is built for live feeds: open a stream once, then feed it one bar at a time. The stream carries its state from bar to bar, so a new bar never costs a pass over the history.
Every value is bit-identical to what the batch method would return by recomputing over the whole array (see detailed specs for a few exceptions).
Each streamable function adds two factory methods on Core and a handful of members on its stream (a class nested in Core, e.g. Core.SmaStream):
| Call | When | Does |
|---|---|---|
core.<Name>Open(history, params) | once | validate params, consume warm-up history, return a stream |
stream.Update(bar) | once per closed bar | commit one bar, return the new value |
stream.Peek(bar) | any time on the forming bar | evaluate a provisional bar without committing |
One more call, OpenAndFill, writes array output instead of a single value — see Array-Fill Open below.
Additional utility functions are available.
There is no Dispose: a stream owns only managed state — its arrays, its sub-streams and a Core reference — so an unreferenced stream is simply collected. The stream types deliberately do not implement IDisposable.
Example (SMA)
using TALib;
var core = Core.Default;
// Seed with warm-up history (>= SmaLookback(period) + 1 bars).
double[] history = [ /* ...your closing prices... */ ];
Core.SmaStream s = core.SmaOpen(history, 30); // Value starts at the last history bar
// Each time a bar closes:
double v = s.Update(newClose); // throws on a non-finite bar, or past IndexMax
// Intra-bar, on the not-yet-closed bar (repeat as the price ticks):
double provisional = s.Peek(formingClose); // state left unchangedOpen returns the stream directly; its Value starts at the last history bar's value. After a successful Open, what Update and Peek reject is invalid input such as NaN or ±Inf; Update also rejects a bar past Core.IndexMax, the last index the batch API addresses. A rejection changes nothing at all — no state, no value, and no range. To count a rejected bar rather than re-feed it, call Advance(); Value then answers the value(s) at the last bar the stream counted (see Utility Calls).
Rules
- Warm-up.
Opensucceeds only ifhistory.Length >= <Name>Lookback(params) + 1— with fewer bars there is no defined value yet. Too little history throwsInsufficientHistoryException(see Error model). AfterOpen, the history can be discarded — the stream keeps everything it needs. - Closed vs forming bar.
Updatecommits state irreversibly, so use it only for closed bars.Peekreturns exactly the value the nextUpdatewould, without committing — call it as often as the forming bar ticks.Valuere-reads the last committed value without recomputing. - The unstable period is resolved once, at
Open. The lookback above is the one of theCorethe stream is opened on. Under an Auto level it is the Auto lookback, soOpenneeds that many bars plus one. - Parameters are fixed at
Open. Changing a parameter means a new stream. Unstable period and candle settings are read from the owningCoreatOpen. SinceCoreis immutable they cannot change underneath a live stream — to stream with different settings, build a newCoreand open from that. - Threads. A stream is single-writer:
Updatemust not race with any other call on the same stream. Processing forks are possible by cloning the stream, and each clone becomes fully independent and can be updated concurrently. - Spans, not arrays. Series parameters are
ReadOnlySpan<double>in andSpan<double>out, so a warm-up window can be a slice of a larger buffer with no copy. Arrays convert implicitly, soSmaOpen(history, 30)on adouble[]is unchanged. Because a span is never null, a null history arrives as an empty span and is rejected as one.
Multi-input / multi-output
Update and Peek take one argument per input series, in the batch call's order, and return one value per output. Multi-output indicators return a generated readonly record struct nested in Core and named after the function, whose members are the output names with the leading out stripped:
Core.BbandsStream b = core.BbandsOpen(history, 20, 2.0, 2.0, MAType.SMA);
Core.BbandsValue v = b.Update(newClose);
Console.WriteLine($"{v.RealUpperBand} {v.RealMiddleBand} {v.RealLowerBand}");
// It deconstructs, too:
var (upper, middle, lower) = b.Value;Equality on a value type
These are record structs, so == is .NET's double equality: NaN equals NaN and +0.0 equals -0.0. (Java's record differs on the second.) Compare BitConverter.DoubleToInt64Bits per component when bit-level identity is what you mean.
Array-Fill Open
Open and Update each write a single value. One more call writes a full array instead — the same shape the batch method would produce — while still opening the stream:
| Call | When | Does |
|---|---|---|
core.<Name>OpenAndFill(..) | once, instead of Open | like Open, but also fills the output for every history bar |
double[] history = [ /* ...your closing prices... */ ];
var outReal = new double[history.Length];
Core.SmaStream s = core.SmaOpenAndFill(history, 30, outReal);
OutRange r = s.OutRange; // the bars it has an output for
// outReal[0 .. r.Count - 1] == what core.Sma(0, history.Length - 1, ...) writes
// ...and s is live, ready for Update.The output arguments are the batch call's, in the same order. An output must not overlap an input or another output. Passing the same buffer throws ArgumentException and mints no stream (rS6); any other overlap is unspecified (specification).
Utility Calls
| Call | When | Does |
|---|---|---|
stream.Value | any time | the value(s) at the last bar the stream counted, without recomputing |
stream.Clone() | any time | an independent fork of the stream, at the same bar |
stream.OutRange | any time | the bars the stream has an output for — the batch range over the same bars |
stream.Advance() | after a bar you will not feed | advances the range without affecting any other internal state of the stream |
Core.SmaStream s = core.SmaOpen(history, 30);
double v = s.Value; // the value at the last bar s counted
Core.SmaStream fork = s.Clone(); // independent from here on
OutRange r = s.OutRange; // the bars s has an output for
s.Advance(); // a bar you skipped, countedClone() is a typed Clone(), deliberately not ICloneable — that interface returns object and leaves deep-versus-shallow unsaid, and this is neither.
See Rules for when concurrent reads of these are safe.
Error model
Open and OpenAndFill throw. After a successful open, Update and Peek reject invalid input such as NaN or ±Inf, and Update also rejects a bar past Core.IndexMax. A rejection changes nothing at all — no state, no value, and no range. Value, Clone() and OutRange never throw; Advance() throws only at the IndexMax ceiling.
| Condition | Exception |
|---|---|
| An empty history (zero bars, or a null array) | ArgumentOutOfRangeException carrying RetCode.OutOfRangeStartIndex |
A history longer than Core.IndexMax + 1 bars | ArgumentOutOfRangeException carrying RetCode.OutOfRangeEndIndex |
| An optional parameter outside its documented range, a non-finite real parameter included | ArgumentException |
| An input series whose length differs from the history's | ArgumentException naming it |
(OpenAndFill) an output shorter than the values the fill writes, or the same buffer as an input or another output | ArgumentException |
Fewer than lookback + 1 history bars | InsufficientHistoryException |
| A non-finite bar (NaN or ±Inf) | ArgumentException naming the input |
A bar past Core.IndexMax, the last index the batch API addresses | ArgumentException carrying RetCode.OutOfRangeEndIndex |
InsufficientHistoryException derives from ArgumentException, so you can catch it specifically — it is the one routine, data-dependent rejection — or catch every open failure uniformly. An empty history is not that case: zero bars is an index fault, so a loop that retries on InsufficientHistoryException until enough bars arrive must not start before the first bar. Messages carry a stable "<NAME> <verb>: " prefix (open, openAndFill, update, peek or advance), and <NAME> is always the called function's name: core.MaOpen(...) rejecting reports MA open:, never the name of whatever moving average it delegates to.
Insufficient history is knowable in advance, so it need not be exceptional in your code: compare against <Name>Lookback(params) + 1 before opening.
Absolute-index outputs
MININDEX, MAXINDEX and MINMAXINDEX report bar indices. In the streaming tier those count bars fed to the stream rather than positions in an array — so treat an index as a position within this handle's own window, not as one you can compare against an index a different handle reported.
Discovering streamable functions
The catalogue flags them, so you do not have to hardcode a list:
using TALib;
using TALib.Metadata;
foreach (var f in Core.Functions)
{
if ((f.Flags & FuncFlags.Stream) != 0)
{
Console.WriteLine(f.Name);
}
}That is discovery only. Unlike the batch tier, there is no name-based way to open a stream — ParamHolder binds batch calls, and nothing binds streams. Opening one means calling its typed <Name>Open directly.
This is deliberate rather than an oversight, and the same is true of the other language bindings. A generic opener would have to return a stream whose type varies per function, and Update varies in both arity and return type, so the values would have to be boxed, an allocation on every Update. Worth designing properly if there is a call for it; not worth guessing at.