Skip to content

Buffer style indicators for incremental processing ​

Buffer list style provides efficient incremental processing for growing datasets. Use this when bars arrive sequentially and you need to add them one at a time without the overhead of a full hub infrastructure.

When to use buffer lists ​

Ideal for:

  • Building up historical data incrementally
  • Processing data feeds where bars arrive sequentially
  • Self-managed incremental calculations
  • Scenarios without multi-indicator coordination needs
  • Memory-efficient processing with auto-pruning

Not ideal for:

Basic usage ​

Buffer lists maintain incremental state as you add new bars:

csharp
using FacioQuo.Stock.Indicators;

// create buffer list with lookback period
SmaList smaList = new(lookbackPeriods: 20);

// call from your WebSocket or SSE message handler for each closed bar
void OnBarReceived(Bar bar)
{
    smaList.Add(bar);

    SmaResult r = smaList[^1];

    // use result (SMA is null during warmup period)
    if (r.Sma is not null)
    {
        Console.WriteLine($"{r.Timestamp:d}: SMA = {r.Sma:N2}");
    }
}

Indexing [^1] right after Add is safe for indicators that emit one result per bar. RenkoList can add zero or several bricks per bar, so check Count before reading it.

🚩 Add bars in chronological order

A buffer list is a single-pass accumulator: every Add assumes the new value is the newest one. It does not reorder input, detect duplicates (unless you opt in below), or correct revised values — feeding an out-of-order, repeated, or late-arriving bar produces silently incorrect results. If your data can arrive out of order (e.g. a raw WebSocket feed, or merging two sources), sort by timestamp before adding, or use a Stream hub instead — stream hubs are built for late arrivals, same-timestamp corrections, and rollback.

To catch the mistake while developing or testing, set ThrowOnOutOfOrder = true. Add then throws an InvalidOperationException when a result's timestamp is not after the last one. RenkoList compares its brick timestamps, which may repeat, so it misses out-of-order input that completes no brick. It is off by default, and the list has already advanced its state when it throws, so discard it rather than continuing.

csharp
SmaList sma = new(20) { ThrowOnOutOfOrder = true };

Key features ​

List interface ​

Buffer lists are read-only result lists you append to. The base BufferList<TResult> implements IReadOnlyList<TResult> — so you get indexer access, Count, and enumeration — plus indicator-specific Add overloads for feeding new values and a small set of list helpers (Clear, Contains, CopyTo):

csharp
SmaList smaList = new(20);

// add individual bars
smaList.Add(bar);

// or add batches
smaList.Add(barList);

// read-only list access
int count = smaList.Count;
bool isEmpty = smaList.Count == 0;
SmaResult latest = smaList[^1];

Note that a buffer list is not a general-purpose mutable collection: it does not implement ICollection<TResult>, so there is no Remove, no insert-at-index, and Add appends a computed result rather than an arbitrary item. Pruning of old results is automatic (see Memory management).

Automatic buffer management ​

Buffer lists automatically manage internal buffers needed for calculations:

  • Maintains lookback periods internally
  • No manual state management required
  • Efficient incremental updates (typically O(1) or O(log n))

Memory management ​

Control memory usage with MaxListSize:

csharp
SmaList smaList = new(20)
{
    MaxListSize = 1000  // keep only last 1000 results
};

When the list exceeds MaxListSize, older results are automatically pruned. Default is 100,000 elements.

Chaining indicators ​

Chain buffer lists for derived indicators. For the broader concept, see Chaining indicators.

Operator-orchestrated

Unlike series or stream-hub chaining, this is orchestrated by you rather than the library: you manually take each result from one list and add it to the next. The library does not coordinate the cascade.

csharp
ObvList obvList = new();
RsiList rsiList = new(14);

// call from your WebSocket or SSE message handler
void OnBarReceived(Bar bar)
{
    // add the bar to OBV, then chain the new OBV result into RSI
    obvList.Add(bar);
    rsiList.Add(obvList[^1]);

    RsiResult latest = rsiList[^1];  // RSI of OBV
}

Performance characteristics ​

  • Overhead: ~10-20% slower than batch style for equivalent datasets
  • Memory: Maintains internal buffers for lookback periods
  • Latency: Optimized for per-bar updates, typically O(1) or O(log n)
  • Thread safety: Not thread-safe; synchronize external access if needed

Usage patterns ​

Server-sent events feed ​

A buffer list fits a feed that delivers one closed bar per event. This example reads an SSE stream with SseParser from System.Net.ServerSentEvents, which handles multi-line events and dispatch; it ships with .NET 10, and .NET 8 and 9 need the System.Net.ServerSentEvents NuGet package. Mapping each event's data to a Bar depends on your provider's payload.

csharp
using System.Net.ServerSentEvents;

SmaList smaList = new(20);

using HttpClient http = new();
using Stream stream = await http.GetStreamAsync("https://example.com/bars/stream", cancellationToken);

await foreach (SseItem<string> item in SseParser.Create(stream).EnumerateAsync(cancellationToken))
{
    Bar bar = ParseBar(item.Data);  // your payload mapping
    smaList.Add(bar);

    SmaResult latest = smaList[^1];
    Console.WriteLine($"{latest.Timestamp:d}: SMA = {latest.Sma:N2}");
}

Batch addition with incremental updates ​

csharp
SmaList smaList = new(20);

// seed with history before the live feed starts
smaList.Add(historicalBars);

// then add each live bar from your WebSocket or SSE message handler
void OnBarReceived(Bar bar)
{
    smaList.Add(bar);
    ProcessLatestResult(smaList[^1]);
}

WebSocket feed ​

The OnBarReceived handlers above assume your client invokes the handler one message at a time, in arrival order. A callback-based WebSocket client can invoke its handler concurrently, and a lock around Add only prevents corruption — it does not restore arrival order once two callbacks race for it, and a buffer list mis-computes silently on an out-of-order Add. When your client does that, drive Add from a single sequential receive loop instead, so only one bar is ever in flight. Only use a buffer list when the feed guarantees chronological order — see the order warning above; an out-of-order feed needs a Stream hub.

csharp
SmaList smaList = new(20);
using ClientWebSocket socket = new();
await socket.ConnectAsync(feedUri, CancellationToken.None);

byte[] buffer = new byte[4096];
using MemoryStream message = new();

while (socket.State == WebSocketState.Open)
{
    WebSocketReceiveResult result = await socket.ReceiveAsync(buffer, CancellationToken.None);
    if (result.MessageType == WebSocketMessageType.Close)
    {
        break;
    }

    // a message can arrive as several frames; parse only once it is complete
    message.Write(buffer, 0, result.Count);
    if (!result.EndOfMessage)
    {
        continue;
    }

    WebSocketBar wsBar = ParseBar(message.GetBuffer().AsSpan(0, (int)message.Length));
    message.SetLength(0);

    Bar bar = new(wsBar.Timestamp, wsBar.Open, wsBar.High, wsBar.Low, wsBar.Close, wsBar.Volume);
    smaList.Add(bar);

    SmaResult latest = smaList[^1];
    Console.WriteLine($"{latest.Timestamp:d}: SMA = {latest.Sma:N2}");
}

ReceiveAsync returns one frame at a time and the loop accumulates frames until EndOfMessage, so Add and every read of smaList happen on this one loop — no lock needed. If your client library only exposes a callback rather than a receive loop you control, have the callback enqueue onto a single-consumer Channel<Bar>, and do the Add and every read from the one loop that drains it, instead of locking around Add in the callback.

See also ​