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:
- Complete historical datasets (use Batch style)
- Multiple indicators needing coordinated updates (use Stream hubs)
- One-time calculations (use Batch style)
Basic usage
Buffer lists maintain incremental state as you add new bars:
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, 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.
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):
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:
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.
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.
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
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.
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
- Batch style for one-time calculations
- Stream hubs for coordinated real-time updates
- Indicators for available buffer list indicators