using System.Collections; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// Performance-focused event args for TBar updates. /// Implemented as struct to avoid heap allocations in high-frequency event dispatch. /// [StructLayout(LayoutKind.Auto)] public readonly struct TBarEventArgs : IEquatable { public TBar Value { get; init; } public bool IsNew { get; init; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Equals(TBarEventArgs other) => Value.Equals(other.Value) && IsNew == other.IsNew; public override bool Equals(object? obj) => obj is TBarEventArgs other && Equals(other); public override int GetHashCode() => HashCode.Combine(Value, IsNew); public static bool operator ==(TBarEventArgs left, TBarEventArgs right) => left.Equals(right); public static bool operator !=(TBarEventArgs left, TBarEventArgs right) => !left.Equals(right); } /// /// High-performance enumerator for TBarSeries. /// public struct TBarSeriesEnumerator : IEnumerator, IEquatable { private readonly List _t; private readonly List _o; private readonly List _h; private readonly List _l; private readonly List _c; private readonly List _v; private readonly int _count; private int _index; private TBar _current; [MethodImpl(MethodImplOptions.AggressiveInlining)] internal TBarSeriesEnumerator(List t, List o, List h, List l, List c, List v) { _t = t; _o = o; _h = h; _l = l; _c = c; _v = v; _count = c.Count; _index = -1; _current = default; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool MoveNext() { if (_index + 1 >= _count) { return false; } _index++; _current = new TBar(_t[_index], _o[_index], _h[_index], _l[_index], _c[_index], _v[_index]); return true; } public readonly TBar Current => _current; readonly object IEnumerator.Current => Current; [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() { _index = -1; _current = default; } public readonly void Dispose() { } public readonly bool Equals(TBarSeriesEnumerator other) => ReferenceEquals(_t, other._t) && ReferenceEquals(_c, other._c) && _count == other._count && _index == other._index; public override readonly bool Equals(object? obj) => obj is TBarSeriesEnumerator other && Equals(other); public override readonly int GetHashCode() => HashCode.Combine(RuntimeHelpers.GetHashCode(_t), RuntimeHelpers.GetHashCode(_c), _count, _index); public static bool operator ==(TBarSeriesEnumerator left, TBarSeriesEnumerator right) => left.Equals(right); public static bool operator !=(TBarSeriesEnumerator left, TBarSeriesEnumerator right) => !left.Equals(right); } // Performance-focused event args struct; not derived from EventArgs by design. // We intentionally deviate from the standard EventArgs pattern here for perf. #pragma warning disable MA0046 // The second parameter must be of type 'System.EventArgs' or a derived type public delegate void TBarPublishedHandler(object? sender, in TBarEventArgs args); public class TBarSeries : IReadOnlyList { #pragma warning disable MA0016 // Prefer using collection abstraction instead of implementation private readonly List _t; private readonly List _o; private readonly List _h; private readonly List _l; private readonly List _c; private readonly List _v; #pragma warning restore MA0016 public string Name { get; set; } = "Bar"; public event TBarPublishedHandler? Pub; #pragma warning restore MA0046 // Note: These views share underlying storage. Do not modify directly; use TBarSeries.Add() instead. public TSeries Open { get; } public TSeries High { get; } public TSeries Low { get; } public TSeries Close { get; } public TSeries Volume { get; } // Aliases for convenience public TSeries O => Open; public TSeries H => High; public TSeries L => Low; public TSeries C => Close; public TSeries V => Volume; public TBarSeries() : this(0) { } public TBarSeries(int capacity) { _t = new List(capacity); _o = new List(capacity); _h = new List(capacity); _l = new List(capacity); _c = new List(capacity); _v = new List(capacity); Open = new TSeries(_t, _o, ShareStorageTag.Instance) { Name = "Open" }; High = new TSeries(_t, _h, ShareStorageTag.Instance) { Name = "High" }; Low = new TSeries(_t, _l, ShareStorageTag.Instance) { Name = "Low" }; Close = new TSeries(_t, _c, ShareStorageTag.Instance) { Name = "Close" }; Volume = new TSeries(_t, _v, ShareStorageTag.Instance) { Name = "Volume" }; } public int Count { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _c.Count; } public TBar this[int index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new TBar(_t[index], _o[index], _h[index], _l[index], _c[index], _v[index]); } public TBar Last { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _c.Count > 0 ? new(_t[^1], _o[^1], _h[^1], _l[^1], _c[^1], _v[^1]) : default; } /// /// Tries to get the last bar without allocating a new TBar on failure. /// Returns true if successful; false if the series is empty. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryGetLast(out TBar bar) { if (_c.Count > 0) { bar = new TBar(_t[^1], _o[^1], _h[^1], _l[^1], _c[^1], _v[^1]); return true; } bar = default; return false; } public long LastTime { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _t.Count > 0 ? _t[^1] : 0; } public double LastOpen { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _o.Count > 0 ? _o[^1] : double.NaN; } public double LastHigh { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _h.Count > 0 ? _h[^1] : double.NaN; } public double LastLow { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _l.Count > 0 ? _l[^1] : double.NaN; } public double LastClose { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _c.Count > 0 ? _c[^1] : double.NaN; } public double LastVolume { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _v.Count > 0 ? _v[^1] : double.NaN; } /// /// Direct access to the underlying Time array as a Span. /// public ReadOnlySpan Times { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => CollectionsMarshal.AsSpan(_t); } /// /// Direct access to the underlying Open array as a Span for SIMD operations. /// public ReadOnlySpan OpenValues { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => CollectionsMarshal.AsSpan(_o); } /// /// Direct access to the underlying High array as a Span for SIMD operations. /// public ReadOnlySpan HighValues { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => CollectionsMarshal.AsSpan(_h); } /// /// Direct access to the underlying Low array as a Span for SIMD operations. /// public ReadOnlySpan LowValues { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => CollectionsMarshal.AsSpan(_l); } /// /// Direct access to the underlying Close array as a Span for SIMD operations. /// public ReadOnlySpan CloseValues { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => CollectionsMarshal.AsSpan(_c); } /// /// Direct access to the underlying Volume array as a Span for SIMD operations. /// public ReadOnlySpan VolumeValues { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => CollectionsMarshal.AsSpan(_v); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Add(TBar bar, bool isNew = true) { if (isNew || _c.Count == 0) { _t.Add(bar.Time); _o.Add(bar.Open); _h.Add(bar.High); _l.Add(bar.Low); _c.Add(bar.Close); _v.Add(bar.Volume); } else { int lastIdx = _c.Count - 1; _t[lastIdx] = bar.Time; _o[lastIdx] = bar.Open; _h[lastIdx] = bar.High; _l[lastIdx] = bar.Low; _c[lastIdx] = bar.Close; _v[lastIdx] = bar.Volume; } Pub?.Invoke(this, new TBarEventArgs { Value = bar, IsNew = isNew }); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Add(long time, double open, double high, double low, double close, double volume, bool isNew = true) => Add(new TBar(time, open, high, low, close, volume), isNew); [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Add(DateTime time, double open, double high, double low, double close, double volume, bool isNew = true) => Add(new TBar(time.Ticks, open, high, low, close, volume), isNew); /// /// Bulk add from IEnumerable sources. Fires Pub event for each bar. /// WARNING: This method may allocate if inputs are not already arrays. /// For zero-allocation bulk loading, prefer AddRange with ReadOnlySpan parameters. /// public void Add(IEnumerable t, IEnumerable o, IEnumerable h, IEnumerable l, IEnumerable c, IEnumerable v) { var tArr = t as long[] ?? t.ToArray(); var oArr = o as double[] ?? o.ToArray(); var hArr = h as double[] ?? h.ToArray(); var lArr = l as double[] ?? l.ToArray(); var cArr = c as double[] ?? c.ToArray(); var vArr = v as double[] ?? v.ToArray(); if (tArr.Length != oArr.Length || oArr.Length != hArr.Length || hArr.Length != lArr.Length || lArr.Length != cArr.Length || cArr.Length != vArr.Length) { throw new ArgumentException("All arrays must have the same length", nameof(t)); } for (int i = 0; i < tArr.Length; i++) { Add(tArr[i], oArr[i], hArr[i], lArr[i], cArr[i], vArr[i]); } } /// /// Zero-allocation bulk add using ReadOnlySpan parameters. /// Does not fire Pub events for each bar (use for initial data loading). /// /// Timestamps as ticks /// Open prices /// High prices /// Low prices /// Close prices /// Volume values [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AddRange(ReadOnlySpan t, ReadOnlySpan o, ReadOnlySpan h, ReadOnlySpan l, ReadOnlySpan c, ReadOnlySpan v) { int len = t.Length; if (o.Length != len || h.Length != len || l.Length != len || c.Length != len || v.Length != len) { throw new ArgumentException("All spans must have the same length", nameof(t)); } if (len == 0) { return; } int oldCount = _c.Count; int newCount = oldCount + len; // Pre-allocate capacity to avoid repeated resizing if (_t.Capacity < newCount) { _t.Capacity = newCount; _o.Capacity = newCount; _h.Capacity = newCount; _l.Capacity = newCount; _c.Capacity = newCount; _v.Capacity = newCount; } // Use SetCount to resize lists without zeroing, then copy via span CollectionsMarshal.SetCount(_t, newCount); CollectionsMarshal.SetCount(_o, newCount); CollectionsMarshal.SetCount(_h, newCount); CollectionsMarshal.SetCount(_l, newCount); CollectionsMarshal.SetCount(_c, newCount); CollectionsMarshal.SetCount(_v, newCount); // Direct span copy - zero allocation bulk add t.CopyTo(CollectionsMarshal.AsSpan(_t).Slice(oldCount)); o.CopyTo(CollectionsMarshal.AsSpan(_o).Slice(oldCount)); h.CopyTo(CollectionsMarshal.AsSpan(_h).Slice(oldCount)); l.CopyTo(CollectionsMarshal.AsSpan(_l).Slice(oldCount)); c.CopyTo(CollectionsMarshal.AsSpan(_c).Slice(oldCount)); v.CopyTo(CollectionsMarshal.AsSpan(_v).Slice(oldCount)); } /// /// Zero-allocation bulk add using ReadOnlySpan of TBar structs. /// Does not fire Pub events for each bar (use for initial data loading). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AddRange(ReadOnlySpan bars) { int len = bars.Length; if (len == 0) { return; } int oldCount = _c.Count; int newCount = oldCount + len; // Pre-allocate capacity to avoid repeated resizing if (_t.Capacity < newCount) { _t.Capacity = newCount; _o.Capacity = newCount; _h.Capacity = newCount; _l.Capacity = newCount; _c.Capacity = newCount; _v.Capacity = newCount; } // Use SetCount to resize lists without zeroing CollectionsMarshal.SetCount(_t, newCount); CollectionsMarshal.SetCount(_o, newCount); CollectionsMarshal.SetCount(_h, newCount); CollectionsMarshal.SetCount(_l, newCount); CollectionsMarshal.SetCount(_c, newCount); CollectionsMarshal.SetCount(_v, newCount); // Get mutable spans for direct write Span tSpan = CollectionsMarshal.AsSpan(_t).Slice(oldCount); Span oSpan = CollectionsMarshal.AsSpan(_o).Slice(oldCount); Span hSpan = CollectionsMarshal.AsSpan(_h).Slice(oldCount); Span lSpan = CollectionsMarshal.AsSpan(_l).Slice(oldCount); Span cSpan = CollectionsMarshal.AsSpan(_c).Slice(oldCount); Span vSpan = CollectionsMarshal.AsSpan(_v).Slice(oldCount); // Copy from TBar structs to SoA layout for (int i = 0; i < len; i++) { ref readonly TBar bar = ref bars[i]; tSpan[i] = bar.Time; oSpan[i] = bar.Open; hSpan[i] = bar.High; lSpan[i] = bar.Low; cSpan[i] = bar.Close; vSpan[i] = bar.Volume; } } // IEnumerable implementation with struct enumerator for zero-allocation iteration [MethodImpl(MethodImplOptions.AggressiveInlining)] public TBarSeriesEnumerator GetEnumerator() => new(_t, _o, _h, _l, _c, _v); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); }