mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-20 11:38:05 +00:00
SIMD Refactor: Merge simd-dev into dev (#55)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) <aider@aider.chat> Co-authored-by: Warp <agent@warp.dev>
This commit is contained in:
co-authored by
Claude Opus 4.5
aider
Warp
parent
5bcdf8d614
commit
86fe32a682
@@ -0,0 +1,601 @@
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class TBarSeriesTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_Default_CreatesEmptySeries()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
|
||||
Assert.Empty(series);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithCapacity_CreatesEmptySeries()
|
||||
{
|
||||
var series = new TBarSeries(100);
|
||||
|
||||
Assert.Empty(series);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Name_DefaultValue_IsBar()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
|
||||
Assert.Equal("Bar", series.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Name_CanBeSet()
|
||||
{
|
||||
var series = new TBarSeries { Name = "TestBars" };
|
||||
|
||||
Assert.Equal("TestBars", series.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_NewBar_IncreasesCount()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
var bar = new TBar(DateTime.UtcNow.Ticks, 100, 110, 90, 105, 1000);
|
||||
|
||||
series.Add(bar, isNew: true);
|
||||
|
||||
Assert.Single(series);
|
||||
Assert.Equal(105.0, series.Last.Close);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_UpdateBar_DoesNotIncreaseCount()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
var bar1 = new TBar(time, 100, 110, 90, 105, 1000);
|
||||
var bar2 = new TBar(time, 100, 112, 90, 108, 1200);
|
||||
|
||||
series.Add(bar1, isNew: true);
|
||||
series.Add(bar2, isNew: false);
|
||||
|
||||
Assert.Single(series);
|
||||
Assert.Equal(108.0, series.Last.Close);
|
||||
Assert.Equal(112.0, series.Last.High);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_UpdateOnEmptySeries_AddsNewBar()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
var bar = new TBar(DateTime.UtcNow.Ticks, 100, 110, 90, 105, 1000);
|
||||
|
||||
series.Add(bar, isNew: false);
|
||||
|
||||
Assert.Single(series);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithLongTime_AddsBar()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
|
||||
series.Add(time, 100, 110, 90, 105, 1000, isNew: true);
|
||||
|
||||
Assert.Single(series);
|
||||
Assert.Equal(time, series.Last.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithDateTime_AddsBar()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
var dt = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc);
|
||||
|
||||
series.Add(dt, 100, 110, 90, 105, 1000, isNew: true);
|
||||
|
||||
Assert.Single(series);
|
||||
Assert.Equal(dt.Ticks, series.Last.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithEnumerables_AddsMultipleBars()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
long[] times = [100, 200, 300];
|
||||
double[] opens = [10, 20, 30];
|
||||
double[] highs = [15, 25, 35];
|
||||
double[] lows = [5, 15, 25];
|
||||
double[] closes = [12, 22, 32];
|
||||
double[] volumes = [100, 200, 300];
|
||||
|
||||
series.Add(times, opens, highs, lows, closes, volumes);
|
||||
|
||||
Assert.Equal(3, series.Count);
|
||||
Assert.Equal(10, series[0].Open);
|
||||
Assert.Equal(32, series[2].Close);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubSeries_AreUpdated()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
var bar = new TBar(DateTime.UtcNow.Ticks, 100, 110, 90, 105, 1000);
|
||||
|
||||
series.Add(bar, isNew: true);
|
||||
|
||||
Assert.Single(series.Open);
|
||||
Assert.Single(series.High);
|
||||
Assert.Single(series.Low);
|
||||
Assert.Single(series.Close);
|
||||
Assert.Single(series.Volume);
|
||||
|
||||
Assert.Equal(100.0, series.Open.Last.Value);
|
||||
Assert.Equal(110.0, series.High.Last.Value);
|
||||
Assert.Equal(90.0, series.Low.Last.Value);
|
||||
Assert.Equal(105.0, series.Close.Last.Value);
|
||||
Assert.Equal(1000.0, series.Volume.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubSeries_Aliases_Work()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
var bar = new TBar(DateTime.UtcNow.Ticks, 100, 110, 90, 105, 1000);
|
||||
series.Add(bar, isNew: true);
|
||||
|
||||
Assert.Same(series.Open, series.O);
|
||||
Assert.Same(series.High, series.H);
|
||||
Assert.Same(series.Low, series.L);
|
||||
Assert.Same(series.Close, series.C);
|
||||
Assert.Same(series.Volume, series.V);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubSeries_HaveCorrectNames()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
|
||||
Assert.Equal("Open", series.Open.Name);
|
||||
Assert.Equal("High", series.High.Name);
|
||||
Assert.Equal("Low", series.Low.Name);
|
||||
Assert.Equal("Close", series.Close.Name);
|
||||
Assert.Equal("Volume", series.Volume.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Last_EmptySeries_ReturnsDefault()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
|
||||
var last = series.Last;
|
||||
|
||||
Assert.Equal(0, last.Time);
|
||||
Assert.Equal(0.0, last.Open);
|
||||
Assert.Equal(0.0, last.Close);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Last_NonEmptySeries_ReturnsLastBar()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
|
||||
var last = series.Last;
|
||||
|
||||
Assert.Equal(200, last.Time);
|
||||
Assert.Equal(22.0, last.Close);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LastTime_EmptySeries_ReturnsZero()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
Assert.Equal(0, series.LastTime);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LastTime_NonEmptySeries_ReturnsLastTime()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
|
||||
Assert.Equal(200, series.LastTime);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LastOpen_EmptySeries_ReturnsNaN()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
Assert.True(double.IsNaN(series.LastOpen));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LastOpen_NonEmptySeries_ReturnsLastOpen()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
|
||||
Assert.Equal(20.0, series.LastOpen);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LastHigh_EmptySeries_ReturnsNaN()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
Assert.True(double.IsNaN(series.LastHigh));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LastHigh_NonEmptySeries_ReturnsLastHigh()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
|
||||
Assert.Equal(25.0, series.LastHigh);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LastLow_EmptySeries_ReturnsNaN()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
Assert.True(double.IsNaN(series.LastLow));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LastLow_NonEmptySeries_ReturnsLastLow()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
|
||||
Assert.Equal(15.0, series.LastLow);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LastClose_EmptySeries_ReturnsNaN()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
Assert.True(double.IsNaN(series.LastClose));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LastClose_NonEmptySeries_ReturnsLastClose()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
|
||||
Assert.Equal(22.0, series.LastClose);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LastVolume_EmptySeries_ReturnsNaN()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
Assert.True(double.IsNaN(series.LastVolume));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LastVolume_NonEmptySeries_ReturnsLastVolume()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
|
||||
Assert.Equal(200.0, series.LastVolume);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_ReturnsCorrectBar()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
series.Add(300, 30, 35, 25, 32, 300);
|
||||
|
||||
Assert.Equal(100, series[0].Time);
|
||||
Assert.Equal(10.0, series[0].Open);
|
||||
Assert.Equal(200, series[1].Time);
|
||||
Assert.Equal(22.0, series[1].Close);
|
||||
Assert.Equal(300, series[2].Time);
|
||||
Assert.Equal(32.0, series[2].Close);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Count_ReturnsCorrectValue()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
|
||||
Assert.Empty(series);
|
||||
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
Assert.Single(series);
|
||||
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
Assert.Equal(2, series.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetEnumerator_IteratesAllBars()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
series.Add(300, 30, 35, 25, 32, 300);
|
||||
|
||||
var list = series.ToList();
|
||||
|
||||
Assert.Equal(3, list.Count);
|
||||
Assert.Equal(10.0, list[0].Open);
|
||||
Assert.Equal(22.0, list[1].Close);
|
||||
Assert.Equal(32.0, list[2].Close);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetEnumerator_NonGeneric_Works()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100, isNew: true);
|
||||
series.Add(200, 20, 25, 15, 22, 200, isNew: true);
|
||||
|
||||
var list = new List<object>();
|
||||
|
||||
#pragma warning disable S4158
|
||||
foreach (var item in (IEnumerable)series)
|
||||
{
|
||||
list.Add(item);
|
||||
}
|
||||
|
||||
Assert.Equal(2, list.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_Event_IsRaisedOnAdd()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
TBar? received = null;
|
||||
series.Pub += (object? sender, in TBarEventArgs args) => received = args.Value;
|
||||
|
||||
var barToAdd = new TBar(100, 10, 15, 5, 12, 100);
|
||||
series.Add(barToAdd, isNew: true);
|
||||
|
||||
Assert.NotNull(received);
|
||||
Assert.Equal(100, received.Value.Time);
|
||||
Assert.Equal(12.0, received.Value.Close);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_Event_IsRaisedOnUpdate()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
TBar? received = null;
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Pub += (object? sender, in TBarEventArgs args) => received = args.Value;
|
||||
|
||||
series.Add(100, 10, 18, 5, 15, 150, isNew: false);
|
||||
|
||||
Assert.NotNull(received);
|
||||
Assert.Equal(15.0, received.Value.Close);
|
||||
Assert.Equal(18.0, received.Value.High);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubSeries_ShareSameTimeArray()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
|
||||
Assert.Equal(series.Open.Times[0], series.Close.Times[0]);
|
||||
Assert.Equal(series.High.Times[1], series.Volume.Times[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_MultipleBars_MaintainsOrder()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
series.Add(300, 30, 35, 25, 32, 300);
|
||||
|
||||
Assert.Equal(3, series.Count);
|
||||
Assert.Equal(100, series[0].Time);
|
||||
Assert.Equal(200, series[1].Time);
|
||||
Assert.Equal(300, series[2].Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithEnumerables_MismatchedLengths_ThrowsArgumentException()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
long[] times = [100, 200, 300];
|
||||
double[] opens = [10, 20]; // Mismatched length
|
||||
double[] highs = [15, 25, 35];
|
||||
double[] lows = [5, 15, 25];
|
||||
double[] closes = [12, 22, 32];
|
||||
double[] volumes = [100, 200, 300];
|
||||
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
series.Add(times, opens, highs, lows, closes, volumes));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_OutOfBounds_ThrowsException()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => _ = series[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_NegativeIndex_ThrowsException()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
|
||||
const int invalidIndex = -1;
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => _ = series[invalidIndex]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_BeyondCount_ThrowsException()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => _ = series[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithNaN_PreservesNaN()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
var bar = new TBar(DateTime.UtcNow.Ticks, double.NaN, 110, 90, 105, 1000);
|
||||
|
||||
series.Add(bar, isNew: true);
|
||||
|
||||
Assert.True(double.IsNaN(series.Last.Open));
|
||||
Assert.True(double.IsNaN(series.Open.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithInfinity_PreservesInfinity()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
var bar = new TBar(DateTime.UtcNow.Ticks, 100, double.PositiveInfinity, 90, 105, 1000);
|
||||
|
||||
series.Add(bar, isNew: true);
|
||||
|
||||
Assert.True(double.IsPositiveInfinity(series.Last.High));
|
||||
Assert.True(double.IsPositiveInfinity(series.High.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubSeries_EmptySeries_HaveZeroCount()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
|
||||
Assert.Empty(series.Open);
|
||||
Assert.Empty(series.High);
|
||||
Assert.Empty(series.Low);
|
||||
Assert.Empty(series.Close);
|
||||
Assert.Empty(series.Volume);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubSeries_ValuesSpan_ReturnsCorrectData()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
|
||||
ReadOnlySpan<double> closeValues = series.Close.Values;
|
||||
|
||||
Assert.Equal(2, closeValues.Length);
|
||||
Assert.Equal(12.0, closeValues[0]);
|
||||
Assert.Equal(22.0, closeValues[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubSeries_TimesSpan_ReturnsCorrectData()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
|
||||
ReadOnlySpan<long> times = series.Close.Times;
|
||||
|
||||
Assert.Equal(2, times.Length);
|
||||
Assert.Equal(100, times[0]);
|
||||
Assert.Equal(200, times[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_EventArgs_ContainsIsNewFlag()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
bool? receivedIsNew = null;
|
||||
series.Pub += (object? sender, in TBarEventArgs args) => receivedIsNew = args.IsNew;
|
||||
|
||||
series.Add(new TBar(100, 10, 15, 5, 12, 100), isNew: true);
|
||||
|
||||
Assert.True(receivedIsNew);
|
||||
|
||||
series.Add(new TBar(100, 10, 18, 5, 15, 150), isNew: false);
|
||||
|
||||
Assert.False(receivedIsNew);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithEnumerables_EmptyArrays_AddsNothing()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
var empty = Array.Empty<long>();
|
||||
var emptyD = Array.Empty<double>();
|
||||
|
||||
series.Add(empty, emptyD, emptyD, emptyD, emptyD, emptyD);
|
||||
|
||||
Assert.Empty(series);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithCapacity_DoesNotAffectCount()
|
||||
{
|
||||
var series = new TBarSeries(1000);
|
||||
|
||||
Assert.Empty(series);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetEnumerator_ExplicitGenericInterface_Works()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
series.Add(300, 30, 35, 25, 32, 300);
|
||||
|
||||
// Explicitly call IEnumerable<TBar>.GetEnumerator() through interface cast
|
||||
IEnumerable<TBar> genericEnumerable = series;
|
||||
using var enumerator = genericEnumerable.GetEnumerator();
|
||||
|
||||
var closes = new List<double>();
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
closes.Add(enumerator.Current.Close);
|
||||
}
|
||||
|
||||
Assert.Equal(3, closes.Count);
|
||||
Assert.Equal(12.0, closes[0]);
|
||||
Assert.Equal(22.0, closes[1]);
|
||||
Assert.Equal(32.0, closes[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetEnumerator_ExplicitNonGenericInterface_Works()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
|
||||
// Explicitly call IEnumerable.GetEnumerator() through interface cast
|
||||
IEnumerable nonGenericEnumerable = series;
|
||||
var enumerator = nonGenericEnumerable.GetEnumerator();
|
||||
|
||||
var closes = new List<double>();
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
var bar = (TBar)enumerator.Current;
|
||||
closes.Add(bar.Close);
|
||||
}
|
||||
|
||||
Assert.Equal(2, closes.Count);
|
||||
Assert.Equal(12.0, closes[0]);
|
||||
Assert.Equal(22.0, closes[1]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
# TBarSeries: OHLCV Data Container
|
||||
|
||||
## What It Does
|
||||
|
||||
`TBarSeries` is a high-performance collection of OHLCV bars. It is the primary data structure for managing historical and real-time market data in QuanTAlib. It uses a **Structure of Arrays (SoA)** layout to optimize memory access and enable efficient SIMD operations across individual price components.
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
A naive implementation of a bar series would be a `List<TBar>`. However, this is inefficient for technical analysis. Most indicators only need one component at a time (e.g., SMA uses Close prices). Iterating over a `List<TBar>` to get Close prices loads unnecessary Open, High, Low, and Volume data into the CPU cache, wasting bandwidth.
|
||||
|
||||
`TBarSeries` solves this by storing each component in its own contiguous array. This allows:
|
||||
|
||||
* **Component Views**: You can access `Close` prices as a `TSeries` without copying data.
|
||||
* **Cache Efficiency**: Iterating over `Close` prices loads *only* Close prices.
|
||||
* **Unified Time**: All component series share a single Time array, ensuring synchronization.
|
||||
|
||||
## How It Works
|
||||
|
||||
Internally, `TBarSeries` maintains six parallel lists:
|
||||
|
||||
1. `_t` (Time)
|
||||
2. `_o` (Open)
|
||||
3. `_h` (High)
|
||||
4. `_l` (Low)
|
||||
5. `_c` (Close)
|
||||
6. `_v` (Volume)
|
||||
|
||||
It exposes these internal lists as `TSeries` properties (`Open`, `High`, `Low`, `Close`, `Volume`), which act as read-only views into the master data.
|
||||
|
||||
## Structure
|
||||
|
||||
### Definition
|
||||
|
||||
```csharp
|
||||
public class TBarSeries : IReadOnlyList<TBar>
|
||||
{
|
||||
// Component Views (TSeries)
|
||||
public TSeries Open { get; }
|
||||
public TSeries High { get; }
|
||||
public TSeries Low { get; }
|
||||
public TSeries Close { get; }
|
||||
public TSeries Volume { get; }
|
||||
|
||||
// Aliases
|
||||
public TSeries O => Open;
|
||||
public TSeries H => High;
|
||||
public TSeries L => Low;
|
||||
public TSeries C => Close;
|
||||
public TSeries V => Volume;
|
||||
}
|
||||
```
|
||||
|
||||
### Core Methods
|
||||
|
||||
| Method | Description |
|
||||
| ------ | ------ |
|
||||
| `Add(TBar bar, bool isNew)` | Adds a bar or updates the last one. |
|
||||
| `Add(DateTime time, double o, double h, double l, double c, double v)` | Adds raw values directly. |
|
||||
| `Count` | Returns the number of bars. |
|
||||
| `Last` | Returns the most recent `TBar`. |
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating and Populating
|
||||
|
||||
```csharp
|
||||
var bars = new TBarSeries();
|
||||
|
||||
// Add a new bar
|
||||
bars.Add(new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000));
|
||||
|
||||
// Add raw values
|
||||
bars.Add(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
```
|
||||
|
||||
### Accessing Data
|
||||
|
||||
```csharp
|
||||
// Get the last full bar
|
||||
TBar lastBar = bars.Last;
|
||||
|
||||
// Get the Close series (Zero-Copy)
|
||||
TSeries closes = bars.Close;
|
||||
|
||||
// Calculate SMA on Close prices
|
||||
var sma = new Sma(14);
|
||||
var result = sma.Calculate(bars.Close);
|
||||
```
|
||||
|
||||
### Streaming Updates
|
||||
|
||||
```csharp
|
||||
// New minute starts
|
||||
bars.Add(newBar, isNew: true);
|
||||
|
||||
// Price updates within the same minute
|
||||
bars.Add(updatedBar, isNew: false); // Updates the last bar in place
|
||||
```
|
||||
|
||||
## Performance Profile
|
||||
|
||||
* **Memory Layout**: SoA (Structure of Arrays).
|
||||
* **Component Access**: Zero-copy `TSeries` views.
|
||||
* **Iteration**: Cache-friendly for single-component analysis.
|
||||
|
||||
## Integration
|
||||
|
||||
`TBarSeries` is the standard input for multi-input indicators (like ATR, ADX) and the primary data source for trading strategies.
|
||||
|
||||
* **Indicators**: Can be passed to indicators that require full bar data.
|
||||
* **Strategies**: Provides the historical context needed for signal generation.
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
* **Shared Storage**: The `TSeries` views (`Open`, `Close`, etc.) do not own their data; they point to the internal lists of the `TBarSeries`. This means modifying the `TBarSeries` automatically updates all views.
|
||||
* **Synchronization**: Because all views share the same `_t` (Time) list, they are guaranteed to be perfectly synchronized.
|
||||
|
||||
## References
|
||||
|
||||
* [Structure of Arrays (SoA)](https://en.wikipedia.org/wiki/AOS_and_SOA)
|
||||
* [Data Locality](https://gameprogrammingpatterns.com/data-locality.html)
|
||||
@@ -0,0 +1,388 @@
|
||||
using System.Collections;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Performance-focused event args for TBar updates.
|
||||
/// Implemented as struct to avoid heap allocations in high-frequency event dispatch.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
public readonly struct TBarEventArgs : IEquatable<TBarEventArgs>
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// High-performance enumerator for TBarSeries.
|
||||
/// </summary>
|
||||
public struct TBarSeriesEnumerator : IEnumerator<TBar>, IEquatable<TBarSeriesEnumerator>
|
||||
{
|
||||
private readonly List<long> _t;
|
||||
private readonly List<double> _o;
|
||||
private readonly List<double> _h;
|
||||
private readonly List<double> _l;
|
||||
private readonly List<double> _c;
|
||||
private readonly List<double> _v;
|
||||
private readonly int _count;
|
||||
private int _index;
|
||||
private TBar _current;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal TBarSeriesEnumerator(List<long> t, List<double> o, List<double> h, List<double> l, List<double> c, List<double> 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<TBar>
|
||||
{
|
||||
#pragma warning disable MA0016 // Prefer using collection abstraction instead of implementation
|
||||
private readonly List<long> _t;
|
||||
private readonly List<double> _o;
|
||||
private readonly List<double> _h;
|
||||
private readonly List<double> _l;
|
||||
private readonly List<double> _c;
|
||||
private readonly List<double> _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<long>(capacity);
|
||||
_o = new List<double>(capacity);
|
||||
_h = new List<double>(capacity);
|
||||
_l = new List<double>(capacity);
|
||||
_c = new List<double>(capacity);
|
||||
_v = new List<double>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the last bar without allocating a new TBar on failure.
|
||||
/// Returns true if successful; false if the series is empty.
|
||||
/// </summary>
|
||||
[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; }
|
||||
|
||||
/// <summary>
|
||||
/// Direct access to the underlying Time array as a Span.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<long> Times
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => CollectionsMarshal.AsSpan(_t);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direct access to the underlying Open array as a Span for SIMD operations.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<double> OpenValues
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => CollectionsMarshal.AsSpan(_o);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direct access to the underlying High array as a Span for SIMD operations.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<double> HighValues
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => CollectionsMarshal.AsSpan(_h);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direct access to the underlying Low array as a Span for SIMD operations.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<double> LowValues
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => CollectionsMarshal.AsSpan(_l);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direct access to the underlying Close array as a Span for SIMD operations.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<double> CloseValues
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => CollectionsMarshal.AsSpan(_c);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direct access to the underlying Volume array as a Span for SIMD operations.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<double> 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);
|
||||
|
||||
public void Add(IEnumerable<long> t, IEnumerable<double> o, IEnumerable<double> h, IEnumerable<double> l, IEnumerable<double> c, IEnumerable<double> 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]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Zero-allocation bulk add using ReadOnlySpan parameters.
|
||||
/// Does not fire Pub events for each bar (use for initial data loading).
|
||||
/// </summary>
|
||||
/// <param name="t">Timestamps as ticks</param>
|
||||
/// <param name="o">Open prices</param>
|
||||
/// <param name="h">High prices</param>
|
||||
/// <param name="l">Low prices</param>
|
||||
/// <param name="c">Close prices</param>
|
||||
/// <param name="v">Volume values</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void AddRange(ReadOnlySpan<long> t, ReadOnlySpan<double> o, ReadOnlySpan<double> h, ReadOnlySpan<double> l, ReadOnlySpan<double> c, ReadOnlySpan<double> 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));
|
||||
|
||||
// Pre-allocate capacity to avoid repeated resizing
|
||||
int newCapacity = _c.Count + len;
|
||||
if (_t.Capacity < newCapacity)
|
||||
{
|
||||
_t.Capacity = newCapacity;
|
||||
_o.Capacity = newCapacity;
|
||||
_h.Capacity = newCapacity;
|
||||
_l.Capacity = newCapacity;
|
||||
_c.Capacity = newCapacity;
|
||||
_v.Capacity = newCapacity;
|
||||
}
|
||||
|
||||
// Bulk add without event firing (for initial data loading)
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
_t.Add(t[i]);
|
||||
_o.Add(o[i]);
|
||||
_h.Add(h[i]);
|
||||
_l.Add(l[i]);
|
||||
_c.Add(c[i]);
|
||||
_v.Add(v[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Zero-allocation bulk add using ReadOnlySpan of TBar structs.
|
||||
/// Does not fire Pub events for each bar (use for initial data loading).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void AddRange(ReadOnlySpan<TBar> bars)
|
||||
{
|
||||
int len = bars.Length;
|
||||
if (len == 0) return;
|
||||
|
||||
// Pre-allocate capacity to avoid repeated resizing
|
||||
int newCapacity = _c.Count + len;
|
||||
if (_t.Capacity < newCapacity)
|
||||
{
|
||||
_t.Capacity = newCapacity;
|
||||
_o.Capacity = newCapacity;
|
||||
_h.Capacity = newCapacity;
|
||||
_l.Capacity = newCapacity;
|
||||
_c.Capacity = newCapacity;
|
||||
_v.Capacity = newCapacity;
|
||||
}
|
||||
|
||||
// Bulk add without event firing (for initial data loading)
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
ref readonly TBar bar = ref bars[i];
|
||||
_t.Add(bar.Time);
|
||||
_o.Add(bar.Open);
|
||||
_h.Add(bar.High);
|
||||
_l.Add(bar.Low);
|
||||
_c.Add(bar.Close);
|
||||
_v.Add(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<TBar> IEnumerable<TBar>.GetEnumerator() => GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
Reference in New Issue
Block a user