229 lines
5.6 KiB
C#
229 lines
5.6 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
|
|
namespace FinlyticTechnicals.Timeframe;
|
|
|
|
/// <summary>
|
|
/// Thread-safe, high-performance circular ring buffer with zero allocations on updates.
|
|
/// Holds a fixed capacity of items (e.g. 500 candles).
|
|
/// </summary>
|
|
/// <typeparam name="T">The item type (e.g. CandleDto)</typeparam>
|
|
public class CircularRingBuffer<T> : IReadOnlyList<T>
|
|
{
|
|
private readonly T[] _buffer;
|
|
private readonly int _capacity;
|
|
private int _start;
|
|
private int _count;
|
|
private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.NoRecursion);
|
|
|
|
public CircularRingBuffer(int capacity = 500)
|
|
{
|
|
if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be positive.");
|
|
_capacity = capacity;
|
|
_buffer = new T[capacity];
|
|
_start = 0;
|
|
_count = 0;
|
|
}
|
|
|
|
public int Capacity => _capacity;
|
|
|
|
public int Count
|
|
{
|
|
get
|
|
{
|
|
_lock.EnterReadLock();
|
|
try { return _count; }
|
|
finally { _lock.ExitReadLock(); }
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds an item to the buffer. If capacity is reached, the oldest element is overwritten in O(1).
|
|
/// </summary>
|
|
public void Add(T item)
|
|
{
|
|
_lock.EnterWriteLock();
|
|
try
|
|
{
|
|
if (_count < _capacity)
|
|
{
|
|
int nextIndex = (_start + _count) % _capacity;
|
|
_buffer[nextIndex] = item;
|
|
_count++;
|
|
}
|
|
else
|
|
{
|
|
_buffer[_start] = item;
|
|
_start = (_start + 1) % _capacity;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_lock.ExitWriteLock();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the last (most recent) item in place.
|
|
/// </summary>
|
|
public void UpdateLast(T item)
|
|
{
|
|
_lock.EnterWriteLock();
|
|
try
|
|
{
|
|
if (_count == 0)
|
|
{
|
|
_buffer[_start] = item;
|
|
_count = 1;
|
|
}
|
|
else
|
|
{
|
|
int lastIndex = (_start + _count - 1) % _capacity;
|
|
_buffer[lastIndex] = item;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_lock.ExitWriteLock();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the most recent item or default if empty.
|
|
/// </summary>
|
|
public T? GetLast()
|
|
{
|
|
_lock.EnterReadLock();
|
|
try
|
|
{
|
|
if (_count == 0) return default;
|
|
int lastIndex = (_start + _count - 1) % _capacity;
|
|
return _buffer[lastIndex];
|
|
}
|
|
finally
|
|
{
|
|
_lock.ExitReadLock();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Indexer accessing items from oldest (0) to newest (Count - 1).
|
|
/// </summary>
|
|
public T this[int index]
|
|
{
|
|
get
|
|
{
|
|
_lock.EnterReadLock();
|
|
try
|
|
{
|
|
if (index < 0 || index >= _count)
|
|
throw new ArgumentOutOfRangeException(nameof(index), "Index out of range.");
|
|
|
|
int actualIndex = (_start + index) % _capacity;
|
|
return _buffer[actualIndex];
|
|
}
|
|
finally
|
|
{
|
|
_lock.ExitReadLock();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns an ordered immutable array snapshot of all elements.
|
|
/// </summary>
|
|
public T[] ToArray()
|
|
{
|
|
_lock.EnterReadLock();
|
|
try
|
|
{
|
|
if (_count == 0) return Array.Empty<T>();
|
|
var result = new T[_count];
|
|
for (int i = 0; i < _count; i++)
|
|
{
|
|
int actualIndex = (_start + i) % _capacity;
|
|
result[i] = _buffer[actualIndex];
|
|
}
|
|
return result;
|
|
}
|
|
finally
|
|
{
|
|
_lock.ExitReadLock();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Populates the buffer in bulk with historical data (oldest to newest).
|
|
/// </summary>
|
|
public void LoadBulk(IEnumerable<T> items)
|
|
{
|
|
_lock.EnterWriteLock();
|
|
try
|
|
{
|
|
_start = 0;
|
|
_count = 0;
|
|
foreach (var item in items)
|
|
{
|
|
if (_count < _capacity)
|
|
{
|
|
_buffer[_count] = item;
|
|
_count++;
|
|
}
|
|
else
|
|
{
|
|
_buffer[_start] = item;
|
|
_start = (_start + 1) % _capacity;
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_lock.ExitWriteLock();
|
|
}
|
|
}
|
|
|
|
public Enumerator GetEnumerator() => new(this);
|
|
IEnumerator<T> IEnumerable<T>.GetEnumerator() => new Enumerator(this);
|
|
IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this);
|
|
|
|
public struct Enumerator : IEnumerator<T>
|
|
{
|
|
private readonly CircularRingBuffer<T> _buffer;
|
|
private int _index;
|
|
private T? _current;
|
|
|
|
internal Enumerator(CircularRingBuffer<T> buffer)
|
|
{
|
|
_buffer = buffer;
|
|
_index = 0;
|
|
_current = default;
|
|
}
|
|
|
|
public readonly T Current => _current!;
|
|
readonly object? IEnumerator.Current => Current;
|
|
|
|
public bool MoveNext()
|
|
{
|
|
if (_index < _buffer.Count)
|
|
{
|
|
_current = _buffer[_index];
|
|
_index++;
|
|
return true;
|
|
}
|
|
_current = default;
|
|
return false;
|
|
}
|
|
|
|
public void Reset()
|
|
{
|
|
_index = 0;
|
|
_current = default;
|
|
}
|
|
|
|
public readonly void Dispose() { }
|
|
}
|
|
}
|
|
|