mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-14 00:28:05 +00:00
updates from mac
This commit is contained in:
@@ -1,249 +1,249 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class SimdExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void SumSIMD_EmptySpan_ReturnsZero()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
Assert.Equal(0.0, span.SumSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumSIMD_SingleElement_ReturnsElement()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(42.5, span.SumSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumSIMD_MultipleElements_ReturnsCorrectSum()
|
||||
{
|
||||
double[] data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(55.0, span.SumSIMD(), precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumSIMD_LargeArray_ReturnsCorrectSum()
|
||||
{
|
||||
double[] data = new double[1000];
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
data[i] = i + 1.0;
|
||||
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
double expected = 1000.0 * 1001.0 / 2.0; // Sum of 1..1000
|
||||
Assert.Equal(expected, span.SumSIMD(), precision: 8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinSIMD_EmptySpan_ReturnsNaN()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
Assert.True(double.IsNaN(span.MinSIMD()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinSIMD_SingleElement_ReturnsElement()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(42.5, span.MinSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinSIMD_MultipleElements_ReturnsMinimum()
|
||||
{
|
||||
double[] data = [5.0, 2.0, 8.0, 1.0, 9.0, 3.0, 7.0, 4.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(1.0, span.MinSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaxSIMD_EmptySpan_ReturnsNaN()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
Assert.True(double.IsNaN(span.MaxSIMD()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaxSIMD_SingleElement_ReturnsElement()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(42.5, span.MaxSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaxSIMD_MultipleElements_ReturnsMaximum()
|
||||
{
|
||||
double[] data = [5.0, 2.0, 8.0, 1.0, 9.0, 3.0, 7.0, 4.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(9.0, span.MaxSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AverageSIMD_EmptySpan_ReturnsNaN()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
Assert.True(double.IsNaN(span.AverageSIMD()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AverageSIMD_MultipleElements_ReturnsCorrectAverage()
|
||||
{
|
||||
double[] data = [1.0, 2.0, 3.0, 4.0, 5.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(3.0, span.AverageSIMD(), precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceSIMD_LessThanTwoElements_ReturnsNaN()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(double.IsNaN(span.VarianceSIMD()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceSIMD_MultipleElements_ReturnsCorrectVariance()
|
||||
{
|
||||
double[] data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
|
||||
// Expected variance: 4.571428... (sample variance)
|
||||
double variance = span.VarianceSIMD();
|
||||
Assert.True(Math.Abs(variance - 4.571428) < 0.0001);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StdDevSIMD_MultipleElements_ReturnsCorrectStdDev()
|
||||
{
|
||||
double[] data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
|
||||
// Expected std dev: sqrt(4.571428) ≈ 2.138
|
||||
double stdDev = span.StdDevSIMD();
|
||||
Assert.True(Math.Abs(stdDev - 2.138) < 0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinMaxSIMD_EmptySpan_ReturnsBothNaN()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
var (min, max) = span.MinMaxSIMD();
|
||||
Assert.True(double.IsNaN(min));
|
||||
Assert.True(double.IsNaN(max));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinMaxSIMD_SingleElement_ReturnsSameValue()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
var (min, max) = span.MinMaxSIMD();
|
||||
Assert.Equal(42.5, min);
|
||||
Assert.Equal(42.5, max);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinMaxSIMD_MultipleElements_ReturnsCorrectMinMax()
|
||||
{
|
||||
double[] data = [5.0, 2.0, 8.0, 1.0, 9.0, 3.0, 7.0, 4.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
var (min, max) = span.MinMaxSIMD();
|
||||
Assert.Equal(1.0, min);
|
||||
Assert.Equal(9.0, max);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SIMD_WorksWithTSeriesValues()
|
||||
{
|
||||
var series = new TSeries(100);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
series.Add(DateTime.UtcNow.Ticks + i, i + 1.0);
|
||||
}
|
||||
|
||||
var values = series.Values;
|
||||
|
||||
double sum = values.SumSIMD();
|
||||
double avg = values.AverageSIMD();
|
||||
double min = values.MinSIMD();
|
||||
double max = values.MaxSIMD();
|
||||
var (minAlt, maxAlt) = values.MinMaxSIMD();
|
||||
|
||||
Assert.Equal(5050.0, sum, precision: 8); // Sum of 1..100
|
||||
Assert.Equal(50.5, avg, precision: 8);
|
||||
Assert.Equal(1.0, min);
|
||||
Assert.Equal(100.0, max);
|
||||
Assert.Equal(min, minAlt);
|
||||
Assert.Equal(max, maxAlt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SIMD_WorksWithTBarSeriesClose()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
var bars = gbm.Fetch(1000, startTime, interval);
|
||||
|
||||
var closeValues = bars.Close.Values;
|
||||
|
||||
double sum = closeValues.SumSIMD();
|
||||
double avg = closeValues.AverageSIMD();
|
||||
double min = closeValues.MinSIMD();
|
||||
double max = closeValues.MaxSIMD();
|
||||
|
||||
Assert.True(sum > 0);
|
||||
Assert.True(avg > 0);
|
||||
Assert.True(min > 0);
|
||||
Assert.True(max > min);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SIMD_PerformanceTest_LargeDataset()
|
||||
{
|
||||
// Generate large dataset
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
var bars = gbm.Fetch(10000, startTime, interval);
|
||||
var closeValues = bars.Close.Values;
|
||||
|
||||
// Warm up
|
||||
_ = closeValues.SumSIMD();
|
||||
|
||||
// Test SIMD operations
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
double sum = closeValues.SumSIMD();
|
||||
double avg = closeValues.AverageSIMD();
|
||||
double min = closeValues.MinSIMD();
|
||||
double max = closeValues.MaxSIMD();
|
||||
var (minAlt, maxAlt) = closeValues.MinMaxSIMD();
|
||||
double variance = closeValues.VarianceSIMD();
|
||||
double stdDev = closeValues.StdDevSIMD();
|
||||
|
||||
sw.Stop();
|
||||
|
||||
// Verify results are valid
|
||||
Assert.True(sum > 0);
|
||||
Assert.True(avg > 0);
|
||||
Assert.True(min > 0);
|
||||
Assert.True(max > min);
|
||||
Assert.True(variance > 0);
|
||||
Assert.True(stdDev > 0);
|
||||
|
||||
// Performance should be sub-millisecond for 10k elements
|
||||
Assert.True(sw.ElapsedMilliseconds < 10,
|
||||
$"SIMD operations took {sw.ElapsedMilliseconds}ms, expected < 10ms");
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class SimdExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void SumSIMD_EmptySpan_ReturnsZero()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
Assert.Equal(0.0, span.SumSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumSIMD_SingleElement_ReturnsElement()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(42.5, span.SumSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumSIMD_MultipleElements_ReturnsCorrectSum()
|
||||
{
|
||||
double[] data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(55.0, span.SumSIMD(), precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumSIMD_LargeArray_ReturnsCorrectSum()
|
||||
{
|
||||
double[] data = new double[1000];
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
data[i] = i + 1.0;
|
||||
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
double expected = 1000.0 * 1001.0 / 2.0; // Sum of 1..1000
|
||||
Assert.Equal(expected, span.SumSIMD(), precision: 8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinSIMD_EmptySpan_ReturnsNaN()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
Assert.True(double.IsNaN(span.MinSIMD()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinSIMD_SingleElement_ReturnsElement()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(42.5, span.MinSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinSIMD_MultipleElements_ReturnsMinimum()
|
||||
{
|
||||
double[] data = [5.0, 2.0, 8.0, 1.0, 9.0, 3.0, 7.0, 4.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(1.0, span.MinSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaxSIMD_EmptySpan_ReturnsNaN()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
Assert.True(double.IsNaN(span.MaxSIMD()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaxSIMD_SingleElement_ReturnsElement()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(42.5, span.MaxSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaxSIMD_MultipleElements_ReturnsMaximum()
|
||||
{
|
||||
double[] data = [5.0, 2.0, 8.0, 1.0, 9.0, 3.0, 7.0, 4.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(9.0, span.MaxSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AverageSIMD_EmptySpan_ReturnsNaN()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
Assert.True(double.IsNaN(span.AverageSIMD()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AverageSIMD_MultipleElements_ReturnsCorrectAverage()
|
||||
{
|
||||
double[] data = [1.0, 2.0, 3.0, 4.0, 5.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(3.0, span.AverageSIMD(), precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceSIMD_LessThanTwoElements_ReturnsNaN()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(double.IsNaN(span.VarianceSIMD()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceSIMD_MultipleElements_ReturnsCorrectVariance()
|
||||
{
|
||||
double[] data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
|
||||
// Expected variance: 4.571428... (sample variance)
|
||||
double variance = span.VarianceSIMD();
|
||||
Assert.True(Math.Abs(variance - 4.571428) < 0.0001);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StdDevSIMD_MultipleElements_ReturnsCorrectStdDev()
|
||||
{
|
||||
double[] data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
|
||||
// Expected std dev: sqrt(4.571428) ≈ 2.138
|
||||
double stdDev = span.StdDevSIMD();
|
||||
Assert.True(Math.Abs(stdDev - 2.138) < 0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinMaxSIMD_EmptySpan_ReturnsBothNaN()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
var (min, max) = span.MinMaxSIMD();
|
||||
Assert.True(double.IsNaN(min));
|
||||
Assert.True(double.IsNaN(max));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinMaxSIMD_SingleElement_ReturnsSameValue()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
var (min, max) = span.MinMaxSIMD();
|
||||
Assert.Equal(42.5, min);
|
||||
Assert.Equal(42.5, max);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinMaxSIMD_MultipleElements_ReturnsCorrectMinMax()
|
||||
{
|
||||
double[] data = [5.0, 2.0, 8.0, 1.0, 9.0, 3.0, 7.0, 4.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
var (min, max) = span.MinMaxSIMD();
|
||||
Assert.Equal(1.0, min);
|
||||
Assert.Equal(9.0, max);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SIMD_WorksWithTSeriesValues()
|
||||
{
|
||||
var series = new TSeries(100);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
series.Add(DateTime.UtcNow.Ticks + i, i + 1.0);
|
||||
}
|
||||
|
||||
var values = series.Values;
|
||||
|
||||
double sum = values.SumSIMD();
|
||||
double avg = values.AverageSIMD();
|
||||
double min = values.MinSIMD();
|
||||
double max = values.MaxSIMD();
|
||||
var (minAlt, maxAlt) = values.MinMaxSIMD();
|
||||
|
||||
Assert.Equal(5050.0, sum, precision: 8); // Sum of 1..100
|
||||
Assert.Equal(50.5, avg, precision: 8);
|
||||
Assert.Equal(1.0, min);
|
||||
Assert.Equal(100.0, max);
|
||||
Assert.Equal(min, minAlt);
|
||||
Assert.Equal(max, maxAlt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SIMD_WorksWithTBarSeriesClose()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
var bars = gbm.Fetch(1000, startTime, interval);
|
||||
|
||||
var closeValues = bars.Close.Values;
|
||||
|
||||
double sum = closeValues.SumSIMD();
|
||||
double avg = closeValues.AverageSIMD();
|
||||
double min = closeValues.MinSIMD();
|
||||
double max = closeValues.MaxSIMD();
|
||||
|
||||
Assert.True(sum > 0);
|
||||
Assert.True(avg > 0);
|
||||
Assert.True(min > 0);
|
||||
Assert.True(max > min);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SIMD_PerformanceTest_LargeDataset()
|
||||
{
|
||||
// Generate large dataset
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
var bars = gbm.Fetch(10000, startTime, interval);
|
||||
var closeValues = bars.Close.Values;
|
||||
|
||||
// Warm up
|
||||
_ = closeValues.SumSIMD();
|
||||
|
||||
// Test SIMD operations
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
double sum = closeValues.SumSIMD();
|
||||
double avg = closeValues.AverageSIMD();
|
||||
double min = closeValues.MinSIMD();
|
||||
double max = closeValues.MaxSIMD();
|
||||
var (minAlt, maxAlt) = closeValues.MinMaxSIMD();
|
||||
double variance = closeValues.VarianceSIMD();
|
||||
double stdDev = closeValues.StdDevSIMD();
|
||||
|
||||
sw.Stop();
|
||||
|
||||
// Verify results are valid
|
||||
Assert.True(sum > 0);
|
||||
Assert.True(avg > 0);
|
||||
Assert.True(min > 0);
|
||||
Assert.True(max > min);
|
||||
Assert.True(variance > 0);
|
||||
Assert.True(stdDev > 0);
|
||||
|
||||
// Performance should be sub-millisecond for 10k elements
|
||||
Assert.True(sw.ElapsedMilliseconds < 10,
|
||||
$"SIMD operations took {sw.ElapsedMilliseconds}ms, expected < 10ms");
|
||||
}
|
||||
}
|
||||
|
||||
+280
-280
@@ -1,280 +1,280 @@
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// SIMD-accelerated extension methods for high-performance array operations.
|
||||
/// Uses Vector<T> for 4-8x speedup on supported hardware with automatic scalar fallback.
|
||||
/// </summary>
|
||||
public static class SimdExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculates sum using SIMD vectorization when available.
|
||||
/// 4-8x faster than scalar loop on AVX2/AVX-512 hardware.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double SumSIMD(this ReadOnlySpan<double> span)
|
||||
{
|
||||
if (span.IsEmpty) return 0.0;
|
||||
|
||||
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
||||
{
|
||||
Vector<double> sum = Vector<double>.Zero;
|
||||
int vectorSize = Vector<double>.Count;
|
||||
int i = 0;
|
||||
|
||||
// Process in vector chunks
|
||||
for (; i <= span.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
var vector = new Vector<double>(span.Slice(i, vectorSize));
|
||||
sum += vector;
|
||||
}
|
||||
|
||||
// Horizontal sum of vector
|
||||
double result = 0.0;
|
||||
for (int j = 0; j < vectorSize; j++)
|
||||
result += sum[j];
|
||||
|
||||
// Process remaining elements
|
||||
for (; i < span.Length; i++)
|
||||
result += span[i];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Scalar fallback
|
||||
double scalar = 0.0;
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
scalar += span[i];
|
||||
return scalar;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates minimum value using SIMD vectorization when available.
|
||||
/// 4-6x faster than scalar loop on AVX2/AVX-512 hardware.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double MinSIMD(this ReadOnlySpan<double> span)
|
||||
{
|
||||
if (span.IsEmpty) return double.NaN;
|
||||
if (span.Length == 1) return span[0];
|
||||
|
||||
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
||||
{
|
||||
int vectorSize = Vector<double>.Count;
|
||||
var minVec = new Vector<double>(span.Slice(0, vectorSize));
|
||||
int i = vectorSize;
|
||||
|
||||
// Process in vector chunks
|
||||
for (; i <= span.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
var vector = new Vector<double>(span.Slice(i, vectorSize));
|
||||
minVec = Vector.Min(minVec, vector);
|
||||
}
|
||||
|
||||
// Find minimum within vector
|
||||
double result = minVec[0];
|
||||
for (int j = 1; j < vectorSize; j++)
|
||||
{
|
||||
if (minVec[j] < result)
|
||||
result = minVec[j];
|
||||
}
|
||||
|
||||
// Process remaining elements
|
||||
for (; i < span.Length; i++)
|
||||
{
|
||||
if (span[i] < result)
|
||||
result = span[i];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Scalar fallback
|
||||
double min = span[0];
|
||||
for (int i = 1; i < span.Length; i++)
|
||||
{
|
||||
if (span[i] < min)
|
||||
min = span[i];
|
||||
}
|
||||
return min;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates maximum value using SIMD vectorization when available.
|
||||
/// 4-6x faster than scalar loop on AVX2/AVX-512 hardware.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double MaxSIMD(this ReadOnlySpan<double> span)
|
||||
{
|
||||
if (span.IsEmpty) return double.NaN;
|
||||
if (span.Length == 1) return span[0];
|
||||
|
||||
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
||||
{
|
||||
int vectorSize = Vector<double>.Count;
|
||||
var maxVec = new Vector<double>(span.Slice(0, vectorSize));
|
||||
int i = vectorSize;
|
||||
|
||||
// Process in vector chunks
|
||||
for (; i <= span.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
var vector = new Vector<double>(span.Slice(i, vectorSize));
|
||||
maxVec = Vector.Max(maxVec, vector);
|
||||
}
|
||||
|
||||
// Find maximum within vector
|
||||
double result = maxVec[0];
|
||||
for (int j = 1; j < vectorSize; j++)
|
||||
{
|
||||
if (maxVec[j] > result)
|
||||
result = maxVec[j];
|
||||
}
|
||||
|
||||
// Process remaining elements
|
||||
for (; i < span.Length; i++)
|
||||
{
|
||||
if (span[i] > result)
|
||||
result = span[i];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Scalar fallback
|
||||
double max = span[0];
|
||||
for (int i = 1; i < span.Length; i++)
|
||||
{
|
||||
if (span[i] > max)
|
||||
max = span[i];
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates average using SIMD vectorization when available.
|
||||
/// 4-8x faster than scalar loop on AVX2/AVX-512 hardware.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double AverageSIMD(this ReadOnlySpan<double> span)
|
||||
{
|
||||
if (span.IsEmpty) return double.NaN;
|
||||
return span.SumSIMD() / span.Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates variance using SIMD vectorization (Welford's online algorithm adapted).
|
||||
/// More numerically stable than naive two-pass algorithm.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double VarianceSIMD(this ReadOnlySpan<double> span, double? mean = null)
|
||||
{
|
||||
if (span.Length < 2) return double.NaN;
|
||||
|
||||
double m = mean ?? span.AverageSIMD();
|
||||
|
||||
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
||||
{
|
||||
var meanVec = new Vector<double>(m);
|
||||
Vector<double> sumSq = Vector<double>.Zero;
|
||||
int vectorSize = Vector<double>.Count;
|
||||
int i = 0;
|
||||
|
||||
// Process in vector chunks
|
||||
for (; i <= span.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
var vector = new Vector<double>(span.Slice(i, vectorSize));
|
||||
var diff = vector - meanVec;
|
||||
sumSq += diff * diff;
|
||||
}
|
||||
|
||||
// Horizontal sum of vector
|
||||
double result = 0.0;
|
||||
for (int j = 0; j < vectorSize; j++)
|
||||
result += sumSq[j];
|
||||
|
||||
// Process remaining elements
|
||||
for (; i < span.Length; i++)
|
||||
{
|
||||
double diff = span[i] - m;
|
||||
result += diff * diff;
|
||||
}
|
||||
|
||||
return result / (span.Length - 1);
|
||||
}
|
||||
|
||||
// Scalar fallback
|
||||
double sumSquares = 0.0;
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
{
|
||||
double diff = span[i] - m;
|
||||
sumSquares += diff * diff;
|
||||
}
|
||||
return sumSquares / (span.Length - 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates standard deviation using SIMD vectorization.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double StdDevSIMD(this ReadOnlySpan<double> span, double? mean = null)
|
||||
{
|
||||
return Math.Sqrt(span.VarianceSIMD(mean));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds both min and max in a single pass using SIMD vectorization.
|
||||
/// More efficient than calling MinSIMD and MaxSIMD separately.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static (double Min, double Max) MinMaxSIMD(this ReadOnlySpan<double> span)
|
||||
{
|
||||
if (span.IsEmpty) return (double.NaN, double.NaN);
|
||||
if (span.Length == 1) return (span[0], span[0]);
|
||||
|
||||
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
||||
{
|
||||
int vectorSize = Vector<double>.Count;
|
||||
var minVec = new Vector<double>(span.Slice(0, vectorSize));
|
||||
var maxVec = minVec;
|
||||
int i = vectorSize;
|
||||
|
||||
// Process in vector chunks
|
||||
for (; i <= span.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
var vector = new Vector<double>(span.Slice(i, vectorSize));
|
||||
minVec = Vector.Min(minVec, vector);
|
||||
maxVec = Vector.Max(maxVec, vector);
|
||||
}
|
||||
|
||||
// Find min/max within vectors
|
||||
double min = minVec[0];
|
||||
double max = maxVec[0];
|
||||
for (int j = 1; j < vectorSize; j++)
|
||||
{
|
||||
if (minVec[j] < min) min = minVec[j];
|
||||
if (maxVec[j] > max) max = maxVec[j];
|
||||
}
|
||||
|
||||
// Process remaining elements
|
||||
for (; i < span.Length; i++)
|
||||
{
|
||||
if (span[i] < min) min = span[i];
|
||||
if (span[i] > max) max = span[i];
|
||||
}
|
||||
|
||||
return (min, max);
|
||||
}
|
||||
|
||||
// Scalar fallback
|
||||
double scalarMin = span[0];
|
||||
double scalarMax = span[0];
|
||||
for (int i = 1; i < span.Length; i++)
|
||||
{
|
||||
if (span[i] < scalarMin) scalarMin = span[i];
|
||||
if (span[i] > scalarMax) scalarMax = span[i];
|
||||
}
|
||||
return (scalarMin, scalarMax);
|
||||
}
|
||||
}
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// SIMD-accelerated extension methods for high-performance array operations.
|
||||
/// Uses Vector<T> for 4-8x speedup on supported hardware with automatic scalar fallback.
|
||||
/// </summary>
|
||||
public static class SimdExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculates sum using SIMD vectorization when available.
|
||||
/// 4-8x faster than scalar loop on AVX2/AVX-512 hardware.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double SumSIMD(this ReadOnlySpan<double> span)
|
||||
{
|
||||
if (span.IsEmpty) return 0.0;
|
||||
|
||||
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
||||
{
|
||||
Vector<double> sum = Vector<double>.Zero;
|
||||
int vectorSize = Vector<double>.Count;
|
||||
int i = 0;
|
||||
|
||||
// Process in vector chunks
|
||||
for (; i <= span.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
var vector = new Vector<double>(span.Slice(i, vectorSize));
|
||||
sum += vector;
|
||||
}
|
||||
|
||||
// Horizontal sum of vector
|
||||
double result = 0.0;
|
||||
for (int j = 0; j < vectorSize; j++)
|
||||
result += sum[j];
|
||||
|
||||
// Process remaining elements
|
||||
for (; i < span.Length; i++)
|
||||
result += span[i];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Scalar fallback
|
||||
double scalar = 0.0;
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
scalar += span[i];
|
||||
return scalar;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates minimum value using SIMD vectorization when available.
|
||||
/// 4-6x faster than scalar loop on AVX2/AVX-512 hardware.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double MinSIMD(this ReadOnlySpan<double> span)
|
||||
{
|
||||
if (span.IsEmpty) return double.NaN;
|
||||
if (span.Length == 1) return span[0];
|
||||
|
||||
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
||||
{
|
||||
int vectorSize = Vector<double>.Count;
|
||||
var minVec = new Vector<double>(span.Slice(0, vectorSize));
|
||||
int i = vectorSize;
|
||||
|
||||
// Process in vector chunks
|
||||
for (; i <= span.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
var vector = new Vector<double>(span.Slice(i, vectorSize));
|
||||
minVec = Vector.Min(minVec, vector);
|
||||
}
|
||||
|
||||
// Find minimum within vector
|
||||
double result = minVec[0];
|
||||
for (int j = 1; j < vectorSize; j++)
|
||||
{
|
||||
if (minVec[j] < result)
|
||||
result = minVec[j];
|
||||
}
|
||||
|
||||
// Process remaining elements
|
||||
for (; i < span.Length; i++)
|
||||
{
|
||||
if (span[i] < result)
|
||||
result = span[i];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Scalar fallback
|
||||
double min = span[0];
|
||||
for (int i = 1; i < span.Length; i++)
|
||||
{
|
||||
if (span[i] < min)
|
||||
min = span[i];
|
||||
}
|
||||
return min;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates maximum value using SIMD vectorization when available.
|
||||
/// 4-6x faster than scalar loop on AVX2/AVX-512 hardware.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double MaxSIMD(this ReadOnlySpan<double> span)
|
||||
{
|
||||
if (span.IsEmpty) return double.NaN;
|
||||
if (span.Length == 1) return span[0];
|
||||
|
||||
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
||||
{
|
||||
int vectorSize = Vector<double>.Count;
|
||||
var maxVec = new Vector<double>(span.Slice(0, vectorSize));
|
||||
int i = vectorSize;
|
||||
|
||||
// Process in vector chunks
|
||||
for (; i <= span.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
var vector = new Vector<double>(span.Slice(i, vectorSize));
|
||||
maxVec = Vector.Max(maxVec, vector);
|
||||
}
|
||||
|
||||
// Find maximum within vector
|
||||
double result = maxVec[0];
|
||||
for (int j = 1; j < vectorSize; j++)
|
||||
{
|
||||
if (maxVec[j] > result)
|
||||
result = maxVec[j];
|
||||
}
|
||||
|
||||
// Process remaining elements
|
||||
for (; i < span.Length; i++)
|
||||
{
|
||||
if (span[i] > result)
|
||||
result = span[i];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Scalar fallback
|
||||
double max = span[0];
|
||||
for (int i = 1; i < span.Length; i++)
|
||||
{
|
||||
if (span[i] > max)
|
||||
max = span[i];
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates average using SIMD vectorization when available.
|
||||
/// 4-8x faster than scalar loop on AVX2/AVX-512 hardware.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double AverageSIMD(this ReadOnlySpan<double> span)
|
||||
{
|
||||
if (span.IsEmpty) return double.NaN;
|
||||
return span.SumSIMD() / span.Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates variance using SIMD vectorization (Welford's online algorithm adapted).
|
||||
/// More numerically stable than naive two-pass algorithm.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double VarianceSIMD(this ReadOnlySpan<double> span, double? mean = null)
|
||||
{
|
||||
if (span.Length < 2) return double.NaN;
|
||||
|
||||
double m = mean ?? span.AverageSIMD();
|
||||
|
||||
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
||||
{
|
||||
var meanVec = new Vector<double>(m);
|
||||
Vector<double> sumSq = Vector<double>.Zero;
|
||||
int vectorSize = Vector<double>.Count;
|
||||
int i = 0;
|
||||
|
||||
// Process in vector chunks
|
||||
for (; i <= span.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
var vector = new Vector<double>(span.Slice(i, vectorSize));
|
||||
var diff = vector - meanVec;
|
||||
sumSq += diff * diff;
|
||||
}
|
||||
|
||||
// Horizontal sum of vector
|
||||
double result = 0.0;
|
||||
for (int j = 0; j < vectorSize; j++)
|
||||
result += sumSq[j];
|
||||
|
||||
// Process remaining elements
|
||||
for (; i < span.Length; i++)
|
||||
{
|
||||
double diff = span[i] - m;
|
||||
result += diff * diff;
|
||||
}
|
||||
|
||||
return result / (span.Length - 1);
|
||||
}
|
||||
|
||||
// Scalar fallback
|
||||
double sumSquares = 0.0;
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
{
|
||||
double diff = span[i] - m;
|
||||
sumSquares += diff * diff;
|
||||
}
|
||||
return sumSquares / (span.Length - 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates standard deviation using SIMD vectorization.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double StdDevSIMD(this ReadOnlySpan<double> span, double? mean = null)
|
||||
{
|
||||
return Math.Sqrt(span.VarianceSIMD(mean));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds both min and max in a single pass using SIMD vectorization.
|
||||
/// More efficient than calling MinSIMD and MaxSIMD separately.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static (double Min, double Max) MinMaxSIMD(this ReadOnlySpan<double> span)
|
||||
{
|
||||
if (span.IsEmpty) return (double.NaN, double.NaN);
|
||||
if (span.Length == 1) return (span[0], span[0]);
|
||||
|
||||
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
||||
{
|
||||
int vectorSize = Vector<double>.Count;
|
||||
var minVec = new Vector<double>(span.Slice(0, vectorSize));
|
||||
var maxVec = minVec;
|
||||
int i = vectorSize;
|
||||
|
||||
// Process in vector chunks
|
||||
for (; i <= span.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
var vector = new Vector<double>(span.Slice(i, vectorSize));
|
||||
minVec = Vector.Min(minVec, vector);
|
||||
maxVec = Vector.Max(maxVec, vector);
|
||||
}
|
||||
|
||||
// Find min/max within vectors
|
||||
double min = minVec[0];
|
||||
double max = maxVec[0];
|
||||
for (int j = 1; j < vectorSize; j++)
|
||||
{
|
||||
if (minVec[j] < min) min = minVec[j];
|
||||
if (maxVec[j] > max) max = maxVec[j];
|
||||
}
|
||||
|
||||
// Process remaining elements
|
||||
for (; i < span.Length; i++)
|
||||
{
|
||||
if (span[i] < min) min = span[i];
|
||||
if (span[i] > max) max = span[i];
|
||||
}
|
||||
|
||||
return (min, max);
|
||||
}
|
||||
|
||||
// Scalar fallback
|
||||
double scalarMin = span[0];
|
||||
double scalarMax = span[0];
|
||||
for (int i = 1; i < span.Length; i++)
|
||||
{
|
||||
if (span[i] < scalarMin) scalarMin = span[i];
|
||||
if (span[i] > scalarMax) scalarMax = span[i];
|
||||
}
|
||||
return (scalarMin, scalarMax);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,43 @@
|
||||
# SimdExtensions Class
|
||||
|
||||
`SimdExtensions` provides high-performance, SIMD-accelerated extension methods for `ReadOnlySpan<double>`. It leverages .NET's `Vector<T>` to achieve 4-8x speedups on supported hardware (AVX2, AVX-512) while automatically falling back to scalar implementations on older hardware.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Hardware Acceleration**: Uses CPU vector registers to process multiple elements in parallel.
|
||||
- **Automatic Fallback**: Gracefully handles non-SIMD hardware or small arrays.
|
||||
- **Zero-Allocation**: Operates directly on spans without creating new arrays.
|
||||
- **Aggressive Inlining**: Methods are marked for inlining to minimize call overhead.
|
||||
|
||||
## Available Methods
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `SumSIMD()` | Calculates the sum of elements. |
|
||||
| `MinSIMD()` | Finds the minimum value. |
|
||||
| `MaxSIMD()` | Finds the maximum value. |
|
||||
| `MinMaxSIMD()` | Finds both min and max in a single pass (more efficient than separate calls). |
|
||||
| `AverageSIMD()` | Calculates the arithmetic mean. |
|
||||
| `VarianceSIMD()` | Calculates the sample variance. |
|
||||
| `StdDevSIMD()` | Calculates the sample standard deviation. |
|
||||
|
||||
## Performance
|
||||
|
||||
On modern CPUs (e.g., Intel Core i7/i9, AMD Ryzen), these methods typically outperform standard LINQ or scalar loops by a factor of 4 to 8 for large arrays.
|
||||
|
||||
## Usage
|
||||
|
||||
```csharp
|
||||
using QuanTAlib;
|
||||
|
||||
double[] data = { 1.0, 2.0, 3.0, 4.0, 5.0, ... };
|
||||
ReadOnlySpan<double> span = data;
|
||||
|
||||
// Calculate sum
|
||||
double sum = span.SumSIMD();
|
||||
|
||||
// Calculate min and max in one pass
|
||||
var (min, max) = span.MinMaxSIMD();
|
||||
|
||||
// Calculate standard deviation
|
||||
double stdDev = span.StdDevSIMD();
|
||||
# SimdExtensions Class
|
||||
|
||||
`SimdExtensions` provides high-performance, SIMD-accelerated extension methods for `ReadOnlySpan<double>`. It leverages .NET's `Vector<T>` to achieve 4-8x speedups on supported hardware (AVX2, AVX-512) while automatically falling back to scalar implementations on older hardware.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Hardware Acceleration**: Uses CPU vector registers to process multiple elements in parallel.
|
||||
- **Automatic Fallback**: Gracefully handles non-SIMD hardware or small arrays.
|
||||
- **Zero-Allocation**: Operates directly on spans without creating new arrays.
|
||||
- **Aggressive Inlining**: Methods are marked for inlining to minimize call overhead.
|
||||
|
||||
## Available Methods
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `SumSIMD()` | Calculates the sum of elements. |
|
||||
| `MinSIMD()` | Finds the minimum value. |
|
||||
| `MaxSIMD()` | Finds the maximum value. |
|
||||
| `MinMaxSIMD()` | Finds both min and max in a single pass (more efficient than separate calls). |
|
||||
| `AverageSIMD()` | Calculates the arithmetic mean. |
|
||||
| `VarianceSIMD()` | Calculates the sample variance. |
|
||||
| `StdDevSIMD()` | Calculates the sample standard deviation. |
|
||||
|
||||
## Performance
|
||||
|
||||
On modern CPUs (e.g., Intel Core i7/i9, AMD Ryzen), these methods typically outperform standard LINQ or scalar loops by a factor of 4 to 8 for large arrays.
|
||||
|
||||
## Usage
|
||||
|
||||
```csharp
|
||||
using QuanTAlib;
|
||||
|
||||
double[] data = { 1.0, 2.0, 3.0, 4.0, 5.0, ... };
|
||||
ReadOnlySpan<double> span = data;
|
||||
|
||||
// Calculate sum
|
||||
double sum = span.SumSIMD();
|
||||
|
||||
// Calculate min and max in one pass
|
||||
var (min, max) = span.MinMaxSIMD();
|
||||
|
||||
// Calculate standard deviation
|
||||
double stdDev = span.StdDevSIMD();
|
||||
|
||||
@@ -1,74 +1,74 @@
|
||||
#!meta
|
||||
|
||||
{"kernelInfo":{"defaultKernelName":"csharp","items":[{"aliases":[],"languageName":"csharp","name":"csharp"}]}}
|
||||
|
||||
#!csharp
|
||||
|
||||
// Reference the library
|
||||
#r "..\..\bin\QuanTAlib.dll"
|
||||
|
||||
using QuanTAlib;
|
||||
|
||||
// 1. Creating a TBar
|
||||
// TBar represents a single OHLCV bar (Open, High, Low, Close, Volume)
|
||||
// It is an immutable struct optimized for memory and performance
|
||||
|
||||
long now = DateTime.UtcNow.Ticks;
|
||||
var bar = new TBar(now, 100.0, 105.0, 95.0, 102.0, 1000.0);
|
||||
|
||||
Console.WriteLine($"Created TBar: {bar}");
|
||||
Console.WriteLine($"Time: {bar.AsDateTime}");
|
||||
Console.WriteLine($"Open: {bar.Open}");
|
||||
Console.WriteLine($"High: {bar.High}");
|
||||
Console.WriteLine($"Low: {bar.Low}");
|
||||
Console.WriteLine($"Close: {bar.Close}");
|
||||
Console.WriteLine($"Volume: {bar.Volume}");
|
||||
|
||||
#!csharp
|
||||
|
||||
// 2. Computed Properties
|
||||
// TBar provides on-demand calculation of common price averages
|
||||
// These are calculated when accessed, saving storage space
|
||||
|
||||
Console.WriteLine($"HL2 (High+Low)/2: {bar.HL2}");
|
||||
Console.WriteLine($"OC2 (Open+Close)/2: {bar.OC2}");
|
||||
Console.WriteLine($"OHL3 (Open+High+Low)/3: {bar.OHL3}");
|
||||
Console.WriteLine($"HLC3 (High+Low+Close)/3: {bar.HLC3}");
|
||||
Console.WriteLine($"OHLC4 (Open+High+Low+Close)/4: {bar.OHLC4}");
|
||||
Console.WriteLine($"HLCC4 (High+Low+Close+Close)/4: {bar.HLCC4}");
|
||||
|
||||
#!csharp
|
||||
|
||||
// 3. TValue Accessors
|
||||
// You can efficiently access individual components as TValue (Time-Value pair)
|
||||
// This is useful when you need to treat a specific price component as a time series point
|
||||
|
||||
Console.WriteLine($"Open TValue: {bar.O}");
|
||||
Console.WriteLine($"High TValue: {bar.H}");
|
||||
Console.WriteLine($"Low TValue: {bar.L}");
|
||||
Console.WriteLine($"Close TValue: {bar.C}");
|
||||
Console.WriteLine($"Volume TValue: {bar.V}");
|
||||
|
||||
#!csharp
|
||||
|
||||
// 4. Implicit Conversions
|
||||
// TBar supports implicit conversions to double (Close price), TValue (Close), and DateTime
|
||||
|
||||
double closePrice = bar;
|
||||
TValue value = bar;
|
||||
DateTime dt = bar;
|
||||
|
||||
Console.WriteLine($"Implicit double (Close): {closePrice}");
|
||||
Console.WriteLine($"Implicit TValue (Close): {value}");
|
||||
Console.WriteLine($"Implicit DateTime: {dt}");
|
||||
|
||||
#!csharp
|
||||
|
||||
// 5. Equality and Immutability
|
||||
// Being a struct, TBar has value semantics
|
||||
|
||||
var bar2 = new TBar(now, 100.0, 105.0, 95.0, 102.0, 1000.0);
|
||||
var bar3 = new TBar(now, 101.0, 106.0, 96.0, 103.0, 1100.0);
|
||||
|
||||
Console.WriteLine($"bar equals bar2? {bar == bar2}"); // True, same values
|
||||
Console.WriteLine($"bar equals bar3? {bar == bar3}"); // False, different values
|
||||
#!meta
|
||||
|
||||
{"kernelInfo":{"defaultKernelName":"csharp","items":[{"aliases":[],"languageName":"csharp","name":"csharp"}]}}
|
||||
|
||||
#!csharp
|
||||
|
||||
// Reference the library
|
||||
#r "..\..\bin\QuanTAlib.dll"
|
||||
|
||||
using QuanTAlib;
|
||||
|
||||
// 1. Creating a TBar
|
||||
// TBar represents a single OHLCV bar (Open, High, Low, Close, Volume)
|
||||
// It is an immutable struct optimized for memory and performance
|
||||
|
||||
long now = DateTime.UtcNow.Ticks;
|
||||
var bar = new TBar(now, 100.0, 105.0, 95.0, 102.0, 1000.0);
|
||||
|
||||
Console.WriteLine($"Created TBar: {bar}");
|
||||
Console.WriteLine($"Time: {bar.AsDateTime}");
|
||||
Console.WriteLine($"Open: {bar.Open}");
|
||||
Console.WriteLine($"High: {bar.High}");
|
||||
Console.WriteLine($"Low: {bar.Low}");
|
||||
Console.WriteLine($"Close: {bar.Close}");
|
||||
Console.WriteLine($"Volume: {bar.Volume}");
|
||||
|
||||
#!csharp
|
||||
|
||||
// 2. Computed Properties
|
||||
// TBar provides on-demand calculation of common price averages
|
||||
// These are calculated when accessed, saving storage space
|
||||
|
||||
Console.WriteLine($"HL2 (High+Low)/2: {bar.HL2}");
|
||||
Console.WriteLine($"OC2 (Open+Close)/2: {bar.OC2}");
|
||||
Console.WriteLine($"OHL3 (Open+High+Low)/3: {bar.OHL3}");
|
||||
Console.WriteLine($"HLC3 (High+Low+Close)/3: {bar.HLC3}");
|
||||
Console.WriteLine($"OHLC4 (Open+High+Low+Close)/4: {bar.OHLC4}");
|
||||
Console.WriteLine($"HLCC4 (High+Low+Close+Close)/4: {bar.HLCC4}");
|
||||
|
||||
#!csharp
|
||||
|
||||
// 3. TValue Accessors
|
||||
// You can efficiently access individual components as TValue (Time-Value pair)
|
||||
// This is useful when you need to treat a specific price component as a time series point
|
||||
|
||||
Console.WriteLine($"Open TValue: {bar.O}");
|
||||
Console.WriteLine($"High TValue: {bar.H}");
|
||||
Console.WriteLine($"Low TValue: {bar.L}");
|
||||
Console.WriteLine($"Close TValue: {bar.C}");
|
||||
Console.WriteLine($"Volume TValue: {bar.V}");
|
||||
|
||||
#!csharp
|
||||
|
||||
// 4. Implicit Conversions
|
||||
// TBar supports implicit conversions to double (Close price), TValue (Close), and DateTime
|
||||
|
||||
double closePrice = bar;
|
||||
TValue value = bar;
|
||||
DateTime dt = bar;
|
||||
|
||||
Console.WriteLine($"Implicit double (Close): {closePrice}");
|
||||
Console.WriteLine($"Implicit TValue (Close): {value}");
|
||||
Console.WriteLine($"Implicit DateTime: {dt}");
|
||||
|
||||
#!csharp
|
||||
|
||||
// 5. Equality and Immutability
|
||||
// Being a struct, TBar has value semantics
|
||||
|
||||
var bar2 = new TBar(now, 100.0, 105.0, 95.0, 102.0, 1000.0);
|
||||
var bar3 = new TBar(now, 101.0, 106.0, 96.0, 103.0, 1100.0);
|
||||
|
||||
Console.WriteLine($"bar equals bar2? {bar == bar2}"); // True, same values
|
||||
Console.WriteLine($"bar equals bar3? {bar == bar3}"); // False, different values
|
||||
|
||||
+76
-76
@@ -1,76 +1,76 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests
|
||||
{
|
||||
public class TBarTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_SetsPropertiesCorrectly()
|
||||
{
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
double open = 100;
|
||||
double high = 110;
|
||||
double low = 90;
|
||||
double close = 105;
|
||||
double volume = 1000;
|
||||
|
||||
var bar = new TBar(time, open, high, low, close, volume);
|
||||
|
||||
Assert.Equal(time, bar.Time);
|
||||
Assert.Equal(open, bar.Open);
|
||||
Assert.Equal(high, bar.High);
|
||||
Assert.Equal(low, bar.Low);
|
||||
Assert.Equal(close, bar.Close);
|
||||
Assert.Equal(volume, bar.Volume);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HL2_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, 100, 110, 90, 105, 1000);
|
||||
Assert.Equal(100.0, bar.HL2); // (110 + 90) / 2
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OHL3_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, 100, 110, 90, 105, 1000);
|
||||
Assert.Equal(100.0, bar.OHL3); // (100 + 110 + 90) / 3
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HLC3_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, 100, 110, 90, 100, 1000);
|
||||
Assert.Equal(100.0, bar.HLC3); // (110 + 90 + 100) / 3
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OHLC4_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, 100, 110, 90, 100, 1000);
|
||||
Assert.Equal(100.0, bar.OHLC4); // (100 + 110 + 90 + 100) / 4
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HLCC4_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, 100, 110, 90, 100, 1000);
|
||||
Assert.Equal(100.0, bar.HLCC4); // (110 + 90 + 100 + 100) / 4
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImplicitConversion_ToTValue_ReturnsClosePriceWithTime()
|
||||
{
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
var bar = new TBar(time, 100, 110, 90, 105, 1000);
|
||||
|
||||
TValue tv = bar;
|
||||
|
||||
Assert.Equal(time, tv.Time);
|
||||
Assert.Equal(105.0, tv.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests
|
||||
{
|
||||
public class TBarTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_SetsPropertiesCorrectly()
|
||||
{
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
double open = 100;
|
||||
double high = 110;
|
||||
double low = 90;
|
||||
double close = 105;
|
||||
double volume = 1000;
|
||||
|
||||
var bar = new TBar(time, open, high, low, close, volume);
|
||||
|
||||
Assert.Equal(time, bar.Time);
|
||||
Assert.Equal(open, bar.Open);
|
||||
Assert.Equal(high, bar.High);
|
||||
Assert.Equal(low, bar.Low);
|
||||
Assert.Equal(close, bar.Close);
|
||||
Assert.Equal(volume, bar.Volume);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HL2_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, 100, 110, 90, 105, 1000);
|
||||
Assert.Equal(100.0, bar.HL2); // (110 + 90) / 2
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OHL3_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, 100, 110, 90, 105, 1000);
|
||||
Assert.Equal(100.0, bar.OHL3); // (100 + 110 + 90) / 3
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HLC3_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, 100, 110, 90, 100, 1000);
|
||||
Assert.Equal(100.0, bar.HLC3); // (110 + 90 + 100) / 3
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OHLC4_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, 100, 110, 90, 100, 1000);
|
||||
Assert.Equal(100.0, bar.OHLC4); // (100 + 110 + 90 + 100) / 4
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HLCC4_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, 100, 110, 90, 100, 1000);
|
||||
Assert.Equal(100.0, bar.HLCC4); // (110 + 90 + 100 + 100) / 4
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImplicitConversion_ToTValue_ReturnsClosePriceWithTime()
|
||||
{
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
var bar = new TBar(time, 100, 110, 90, 105, 1000);
|
||||
|
||||
TValue tv = bar;
|
||||
|
||||
Assert.Equal(time, tv.Time);
|
||||
Assert.Equal(105.0, tv.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+68
-68
@@ -1,68 +1,68 @@
|
||||
# TBar Struct
|
||||
|
||||
`TBar` is a lightweight, immutable struct representing a single OHLCV (Open, High, Low, Close, Volume) bar. It is designed for high-performance financial data processing with minimal memory overhead.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Memory Efficient**: Pure data type occupying exactly 48 bytes (1 `long` + 5 `double`s).
|
||||
- **Immutable**: Thread-safe by design.
|
||||
- **Zero-Copy Conversions**: Efficiently converts to `TValue` for individual price components (Open, High, Low, Close, Volume).
|
||||
- **Computed Properties**: Provides on-demand calculation of common price averages (HL2, HLC3, etc.) without storage overhead.
|
||||
- **SIMD Compatible**: Layout is optimized for potential vectorization in collection types.
|
||||
|
||||
## Structure Definition
|
||||
|
||||
```csharp
|
||||
public readonly struct TBar : IEquatable<TBar>
|
||||
{
|
||||
public readonly long Time; // Unix ticks
|
||||
public readonly double Open;
|
||||
public readonly double High;
|
||||
public readonly double Low;
|
||||
public readonly double Close;
|
||||
public readonly double Volume;
|
||||
}
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `Time` | `long` | Timestamp in ticks. |
|
||||
| `Open` | `double` | Opening price. |
|
||||
| `High` | `double` | Highest price. |
|
||||
| `Low` | `double` | Lowest price. |
|
||||
| `Close` | `double` | Closing price. |
|
||||
| `Volume` | `double` | Traded volume. |
|
||||
| `AsDateTime` | `DateTime` | `Time` converted to UTC DateTime. |
|
||||
|
||||
### Computed Averages
|
||||
These properties are calculated on the fly:
|
||||
- `HL2`: (High + Low) / 2
|
||||
- `OC2`: (Open + Close) / 2
|
||||
- `OHL3`: (Open + High + Low) / 3
|
||||
- `HLC3`: (High + Low + Close) / 3
|
||||
- `OHLC4`: (Open + High + Low + Close) / 4
|
||||
- `HLCC4`: (High + Low + Close + Close) / 4
|
||||
|
||||
### TValue Accessors
|
||||
Efficiently access components as `TValue` (Time-Value pair):
|
||||
- `O`: (Time, Open)
|
||||
- `H`: (Time, High)
|
||||
- `L`: (Time, Low)
|
||||
- `C`: (Time, Close)
|
||||
- `V`: (Time, Volume)
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating a TBar
|
||||
```csharp
|
||||
long now = DateTime.UtcNow.Ticks;
|
||||
var bar = new TBar(now, 100.0, 105.0, 95.0, 102.0, 1000.0);
|
||||
```
|
||||
|
||||
### Implicit Conversions
|
||||
```csharp
|
||||
double closePrice = bar; // Implicitly converts to Close price
|
||||
TValue value = bar; // Implicitly converts to (Time, Close)
|
||||
DateTime dt = bar; // Implicitly converts to DateTime
|
||||
# TBar Struct
|
||||
|
||||
`TBar` is a lightweight, immutable struct representing a single OHLCV (Open, High, Low, Close, Volume) bar. It is designed for high-performance financial data processing with minimal memory overhead.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Memory Efficient**: Pure data type occupying exactly 48 bytes (1 `long` + 5 `double`s).
|
||||
- **Immutable**: Thread-safe by design.
|
||||
- **Zero-Copy Conversions**: Efficiently converts to `TValue` for individual price components (Open, High, Low, Close, Volume).
|
||||
- **Computed Properties**: Provides on-demand calculation of common price averages (HL2, HLC3, etc.) without storage overhead.
|
||||
- **SIMD Compatible**: Layout is optimized for potential vectorization in collection types.
|
||||
|
||||
## Structure Definition
|
||||
|
||||
```csharp
|
||||
public readonly struct TBar : IEquatable<TBar>
|
||||
{
|
||||
public readonly long Time; // Unix ticks
|
||||
public readonly double Open;
|
||||
public readonly double High;
|
||||
public readonly double Low;
|
||||
public readonly double Close;
|
||||
public readonly double Volume;
|
||||
}
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `Time` | `long` | Timestamp in ticks. |
|
||||
| `Open` | `double` | Opening price. |
|
||||
| `High` | `double` | Highest price. |
|
||||
| `Low` | `double` | Lowest price. |
|
||||
| `Close` | `double` | Closing price. |
|
||||
| `Volume` | `double` | Traded volume. |
|
||||
| `AsDateTime` | `DateTime` | `Time` converted to UTC DateTime. |
|
||||
|
||||
### Computed Averages
|
||||
These properties are calculated on the fly:
|
||||
- `HL2`: (High + Low) / 2
|
||||
- `OC2`: (Open + Close) / 2
|
||||
- `OHL3`: (Open + High + Low) / 3
|
||||
- `HLC3`: (High + Low + Close) / 3
|
||||
- `OHLC4`: (Open + High + Low + Close) / 4
|
||||
- `HLCC4`: (High + Low + Close + Close) / 4
|
||||
|
||||
### TValue Accessors
|
||||
Efficiently access components as `TValue` (Time-Value pair):
|
||||
- `O`: (Time, Open)
|
||||
- `H`: (Time, High)
|
||||
- `L`: (Time, Low)
|
||||
- `C`: (Time, Close)
|
||||
- `V`: (Time, Volume)
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating a TBar
|
||||
```csharp
|
||||
long now = DateTime.UtcNow.Ticks;
|
||||
var bar = new TBar(now, 100.0, 105.0, 95.0, 102.0, 1000.0);
|
||||
```
|
||||
|
||||
### Implicit Conversions
|
||||
```csharp
|
||||
double closePrice = bar; // Implicitly converts to Close price
|
||||
TValue value = bar; // Implicitly converts to (Time, Close)
|
||||
DateTime dt = bar; // Implicitly converts to DateTime
|
||||
|
||||
+83
-83
@@ -1,83 +1,83 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// A lightweight struct representing an OHLCV bar.
|
||||
/// Pure data type: 48 bytes (long + 5 doubles).
|
||||
/// </summary>
|
||||
[SkipLocalsInit]
|
||||
public readonly struct TBar : IEquatable<TBar>
|
||||
{
|
||||
public readonly long Time;
|
||||
public readonly double Open;
|
||||
public readonly double High;
|
||||
public readonly double Low;
|
||||
public readonly double Close;
|
||||
public readonly double Volume;
|
||||
|
||||
public DateTime AsDateTime => new(Time, DateTimeKind.Utc);
|
||||
|
||||
// TValue conversions (Zero-copy / lightweight creation)
|
||||
public TValue O { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Time, Open); }
|
||||
public TValue H { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Time, High); }
|
||||
public TValue L { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Time, Low); }
|
||||
public TValue C { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Time, Close); }
|
||||
public TValue V { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Time, Volume); }
|
||||
|
||||
// Computed properties (calculated on demand, no storage overhead)
|
||||
public double HL2 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low) * 0.5; }
|
||||
public double OC2 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + Close) * 0.5; }
|
||||
public double OHL3 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + High + Low) / 3.0; }
|
||||
public double HLC3 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low + Close) / 3.0; }
|
||||
public double OHLC4 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + High + Low + Close) * 0.25; }
|
||||
public double HLCC4 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low + Close + Close) * 0.25; }
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TBar(long time, double open, double high, double low, double close, double volume)
|
||||
{
|
||||
Time = time;
|
||||
Open = open;
|
||||
High = high;
|
||||
Low = low;
|
||||
Close = close;
|
||||
Volume = volume;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TBar(DateTime time, double open, double high, double low, double close, double volume)
|
||||
{
|
||||
Time = time.Ticks;
|
||||
Open = open;
|
||||
High = high;
|
||||
Low = low;
|
||||
Close = close;
|
||||
Volume = volume;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator double(TBar bar) => bar.Close;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator TValue(TBar bar) => new(bar.Time, bar.Close);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator DateTime(TBar bar) => new(bar.Time, DateTimeKind.Utc);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override string ToString() => $"[{AsDateTime:yyyy-MM-dd HH:mm:ss}: O={Open:F2}, H={High:F2}, L={Low:F2}, C={Close:F2}, V={Volume:F2}]";
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool Equals(TBar other) =>
|
||||
Time == other.Time &&
|
||||
Open == other.Open &&
|
||||
High == other.High &&
|
||||
Low == other.Low &&
|
||||
Close == other.Close &&
|
||||
Volume == other.Volume;
|
||||
|
||||
public override bool Equals(object? obj) => obj is TBar other && Equals(other);
|
||||
public override int GetHashCode() => HashCode.Combine(Time, Open, High, Low, Close, Volume);
|
||||
public static bool operator ==(TBar left, TBar right) => left.Equals(right);
|
||||
public static bool operator !=(TBar left, TBar right) => !left.Equals(right);
|
||||
}
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// A lightweight struct representing an OHLCV bar.
|
||||
/// Pure data type: 48 bytes (long + 5 doubles).
|
||||
/// </summary>
|
||||
[SkipLocalsInit]
|
||||
public readonly struct TBar : IEquatable<TBar>
|
||||
{
|
||||
public readonly long Time;
|
||||
public readonly double Open;
|
||||
public readonly double High;
|
||||
public readonly double Low;
|
||||
public readonly double Close;
|
||||
public readonly double Volume;
|
||||
|
||||
public DateTime AsDateTime => new(Time, DateTimeKind.Utc);
|
||||
|
||||
// TValue conversions (Zero-copy / lightweight creation)
|
||||
public TValue O { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Time, Open); }
|
||||
public TValue H { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Time, High); }
|
||||
public TValue L { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Time, Low); }
|
||||
public TValue C { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Time, Close); }
|
||||
public TValue V { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Time, Volume); }
|
||||
|
||||
// Computed properties (calculated on demand, no storage overhead)
|
||||
public double HL2 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low) * 0.5; }
|
||||
public double OC2 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + Close) * 0.5; }
|
||||
public double OHL3 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + High + Low) / 3.0; }
|
||||
public double HLC3 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low + Close) / 3.0; }
|
||||
public double OHLC4 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + High + Low + Close) * 0.25; }
|
||||
public double HLCC4 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low + Close + Close) * 0.25; }
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TBar(long time, double open, double high, double low, double close, double volume)
|
||||
{
|
||||
Time = time;
|
||||
Open = open;
|
||||
High = high;
|
||||
Low = low;
|
||||
Close = close;
|
||||
Volume = volume;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TBar(DateTime time, double open, double high, double low, double close, double volume)
|
||||
{
|
||||
Time = time.Ticks;
|
||||
Open = open;
|
||||
High = high;
|
||||
Low = low;
|
||||
Close = close;
|
||||
Volume = volume;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator double(TBar bar) => bar.Close;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator TValue(TBar bar) => new(bar.Time, bar.Close);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator DateTime(TBar bar) => new(bar.Time, DateTimeKind.Utc);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override string ToString() => $"[{AsDateTime:yyyy-MM-dd HH:mm:ss}: O={Open:F2}, H={High:F2}, L={Low:F2}, C={Close:F2}, V={Volume:F2}]";
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool Equals(TBar other) =>
|
||||
Time == other.Time &&
|
||||
Open == other.Open &&
|
||||
High == other.High &&
|
||||
Low == other.Low &&
|
||||
Close == other.Close &&
|
||||
Volume == other.Volume;
|
||||
|
||||
public override bool Equals(object? obj) => obj is TBar other && Equals(other);
|
||||
public override int GetHashCode() => HashCode.Combine(Time, Open, High, Low, Close, Volume);
|
||||
public static bool operator ==(TBar left, TBar right) => left.Equals(right);
|
||||
public static bool operator !=(TBar left, TBar right) => !left.Equals(right);
|
||||
}
|
||||
|
||||
@@ -1,82 +1,82 @@
|
||||
#!meta
|
||||
|
||||
{"kernelInfo":{"defaultKernelName":"csharp","items":[{"aliases":[],"languageName":"csharp","name":"csharp"}]}}
|
||||
|
||||
#!csharp
|
||||
|
||||
// Reference the library
|
||||
#r "..\..\bin\QuanTAlib.dll"
|
||||
|
||||
using QuanTAlib;
|
||||
|
||||
// 1. Creating a TBarSeries
|
||||
// TBarSeries is a collection of bars stored in Structure of Arrays (SoA) format
|
||||
// This layout is optimized for performance and SIMD operations
|
||||
|
||||
var bars = new TBarSeries();
|
||||
long now = DateTime.UtcNow.Ticks;
|
||||
|
||||
// Add a new bar
|
||||
var bar1 = new TBar(now, 100, 105, 95, 102, 1000);
|
||||
bars.Add(bar1, isNew: true);
|
||||
|
||||
Console.WriteLine($"Added Bar 1: Count={bars.Count}");
|
||||
Console.WriteLine($"Last Close: {bars.Last.Close}");
|
||||
|
||||
#!csharp
|
||||
|
||||
// 2. Streaming Updates
|
||||
// TBarSeries supports updating the last bar in place
|
||||
// This is crucial for real-time feeds where the current bar changes until it closes
|
||||
|
||||
// Update the bar (e.g. price changed within the same minute)
|
||||
var bar1Update = new TBar(now, 100, 106, 95, 104, 1500);
|
||||
bars.Add(bar1Update, isNew: false);
|
||||
|
||||
Console.WriteLine($"Updated Bar 1: Count={bars.Count} (Count should not increase)");
|
||||
Console.WriteLine($"Last Close: {bars.Last.Close}");
|
||||
Console.WriteLine($"Last High: {bars.Last.High}");
|
||||
|
||||
#!csharp
|
||||
|
||||
// 3. Zero-Copy Views
|
||||
// You can access individual components (Open, High, Low, Close, Volume) as TSeries
|
||||
// These views share the underlying memory, so no copying is involved
|
||||
|
||||
Console.WriteLine($"Bars Count: {bars.Count}");
|
||||
Console.WriteLine($"Close Series Count: {bars.Close.Count}");
|
||||
Console.WriteLine($"Close Series Last: {bars.Close.Last.Value}");
|
||||
|
||||
// Verify view updates automatically
|
||||
Console.WriteLine("\nAdding new bar...");
|
||||
bars.Add(now + TimeSpan.TicksPerMinute, 104, 108, 103, 107, 2000, isNew: true);
|
||||
|
||||
Console.WriteLine($"Bars Count: {bars.Count}");
|
||||
Console.WriteLine($"Close Series Count: {bars.Close.Count} (Should match Bars Count)");
|
||||
Console.WriteLine($"Close Series Last: {bars.Close.Last.Value} (Should be 107)");
|
||||
|
||||
#!csharp
|
||||
|
||||
// 4. Aliases and Direct Access
|
||||
// TBarSeries provides short aliases (O, H, L, C, V) and direct access properties
|
||||
|
||||
Console.WriteLine($"Alias Access (C.Last): {bars.C.Last.Value}");
|
||||
Console.WriteLine($"Direct Last Access (LastClose): {bars.LastClose}");
|
||||
Console.WriteLine($"Direct Last Time (LastTime): {new DateTime(bars.LastTime)}");
|
||||
|
||||
#!csharp
|
||||
|
||||
// 5. Iteration
|
||||
// You can iterate over the bars or individual series
|
||||
|
||||
Console.WriteLine("\nIterating over bars:");
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
Console.WriteLine($" {bar}");
|
||||
}
|
||||
|
||||
Console.WriteLine("\nIterating over Close prices:");
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
Console.WriteLine($" Bar {i}: Close={bars.Close[i].Value}");
|
||||
}
|
||||
#!meta
|
||||
|
||||
{"kernelInfo":{"defaultKernelName":"csharp","items":[{"aliases":[],"languageName":"csharp","name":"csharp"}]}}
|
||||
|
||||
#!csharp
|
||||
|
||||
// Reference the library
|
||||
#r "..\..\bin\QuanTAlib.dll"
|
||||
|
||||
using QuanTAlib;
|
||||
|
||||
// 1. Creating a TBarSeries
|
||||
// TBarSeries is a collection of bars stored in Structure of Arrays (SoA) format
|
||||
// This layout is optimized for performance and SIMD operations
|
||||
|
||||
var bars = new TBarSeries();
|
||||
long now = DateTime.UtcNow.Ticks;
|
||||
|
||||
// Add a new bar
|
||||
var bar1 = new TBar(now, 100, 105, 95, 102, 1000);
|
||||
bars.Add(bar1, isNew: true);
|
||||
|
||||
Console.WriteLine($"Added Bar 1: Count={bars.Count}");
|
||||
Console.WriteLine($"Last Close: {bars.Last.Close}");
|
||||
|
||||
#!csharp
|
||||
|
||||
// 2. Streaming Updates
|
||||
// TBarSeries supports updating the last bar in place
|
||||
// This is crucial for real-time feeds where the current bar changes until it closes
|
||||
|
||||
// Update the bar (e.g. price changed within the same minute)
|
||||
var bar1Update = new TBar(now, 100, 106, 95, 104, 1500);
|
||||
bars.Add(bar1Update, isNew: false);
|
||||
|
||||
Console.WriteLine($"Updated Bar 1: Count={bars.Count} (Count should not increase)");
|
||||
Console.WriteLine($"Last Close: {bars.Last.Close}");
|
||||
Console.WriteLine($"Last High: {bars.Last.High}");
|
||||
|
||||
#!csharp
|
||||
|
||||
// 3. Zero-Copy Views
|
||||
// You can access individual components (Open, High, Low, Close, Volume) as TSeries
|
||||
// These views share the underlying memory, so no copying is involved
|
||||
|
||||
Console.WriteLine($"Bars Count: {bars.Count}");
|
||||
Console.WriteLine($"Close Series Count: {bars.Close.Count}");
|
||||
Console.WriteLine($"Close Series Last: {bars.Close.Last.Value}");
|
||||
|
||||
// Verify view updates automatically
|
||||
Console.WriteLine("\nAdding new bar...");
|
||||
bars.Add(now + TimeSpan.TicksPerMinute, 104, 108, 103, 107, 2000, isNew: true);
|
||||
|
||||
Console.WriteLine($"Bars Count: {bars.Count}");
|
||||
Console.WriteLine($"Close Series Count: {bars.Close.Count} (Should match Bars Count)");
|
||||
Console.WriteLine($"Close Series Last: {bars.Close.Last.Value} (Should be 107)");
|
||||
|
||||
#!csharp
|
||||
|
||||
// 4. Aliases and Direct Access
|
||||
// TBarSeries provides short aliases (O, H, L, C, V) and direct access properties
|
||||
|
||||
Console.WriteLine($"Alias Access (C.Last): {bars.C.Last.Value}");
|
||||
Console.WriteLine($"Direct Last Access (LastClose): {bars.LastClose}");
|
||||
Console.WriteLine($"Direct Last Time (LastTime): {new DateTime(bars.LastTime)}");
|
||||
|
||||
#!csharp
|
||||
|
||||
// 5. Iteration
|
||||
// You can iterate over the bars or individual series
|
||||
|
||||
Console.WriteLine("\nIterating over bars:");
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
Console.WriteLine($" {bar}");
|
||||
}
|
||||
|
||||
Console.WriteLine("\nIterating over Close prices:");
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
Console.WriteLine($" Bar {i}: Close={bars.Close[i].Value}");
|
||||
}
|
||||
|
||||
@@ -1,58 +1,58 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests
|
||||
{
|
||||
public class TBarSeriesTests
|
||||
{
|
||||
[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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests
|
||||
{
|
||||
public class TBarSeriesTests
|
||||
{
|
||||
[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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,70 +1,70 @@
|
||||
# TBarSeries Class
|
||||
|
||||
`TBarSeries` is a high-performance collection of OHLCV bars implemented using a Structure of Arrays (SoA) layout. This design optimizes memory access patterns and enables efficient SIMD operations while providing convenient object-oriented views.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Structure of Arrays (SoA)**: Stores Time, Open, High, Low, Close, and Volume in separate contiguous arrays rather than an array of structs. This improves cache locality for operations that only need specific components (e.g., calculating SMA on Close prices).
|
||||
- **Zero-Copy Views**: Exposes `TSeries` properties (`Open`, `High`, `Low`, `Close`, `Volume`) that view the underlying data without copying.
|
||||
- **Streaming Support**: Efficiently handles real-time data updates with `Add(bar, isNew: false)`.
|
||||
- **Memory Efficient**: Minimizes object overhead by using shared internal lists.
|
||||
|
||||
## Class Definition
|
||||
|
||||
```csharp
|
||||
public class TBarSeries : IReadOnlyList<TBar>
|
||||
{
|
||||
// Views
|
||||
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 = true)` | Adds a new bar or updates the last one. |
|
||||
| `Add(DateTime time, double o, double h, double l, double c, double v, bool isNew)` | 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
|
||||
long now = DateTime.UtcNow.Ticks;
|
||||
bars.Add(new TBar(now, 100, 105, 95, 102, 1000), isNew: true);
|
||||
|
||||
// Update the last bar (e.g., real-time feed update)
|
||||
bars.Add(new TBar(now, 100, 106, 95, 104, 1500), isNew: false);
|
||||
```
|
||||
|
||||
### Accessing Data
|
||||
```csharp
|
||||
// Access entire bar
|
||||
TBar lastBar = bars.Last;
|
||||
|
||||
// Access specific component series (Zero-Copy)
|
||||
TSeries closes = bars.Close;
|
||||
double lastClose = closes.Last.Value;
|
||||
|
||||
// Access via indexer
|
||||
TBar firstBar = bars[0];
|
||||
```
|
||||
|
||||
### Performance Note
|
||||
Because `TBarSeries` uses SoA layout, iterating over a single component (like `Close` prices) is extremely cache-efficient. The CPU prefetcher can load contiguous doubles without loading the interleaved Open, High, Low, or Volume data.
|
||||
# TBarSeries Class
|
||||
|
||||
`TBarSeries` is a high-performance collection of OHLCV bars implemented using a Structure of Arrays (SoA) layout. This design optimizes memory access patterns and enables efficient SIMD operations while providing convenient object-oriented views.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Structure of Arrays (SoA)**: Stores Time, Open, High, Low, Close, and Volume in separate contiguous arrays rather than an array of structs. This improves cache locality for operations that only need specific components (e.g., calculating SMA on Close prices).
|
||||
- **Zero-Copy Views**: Exposes `TSeries` properties (`Open`, `High`, `Low`, `Close`, `Volume`) that view the underlying data without copying.
|
||||
- **Streaming Support**: Efficiently handles real-time data updates with `Add(bar, isNew: false)`.
|
||||
- **Memory Efficient**: Minimizes object overhead by using shared internal lists.
|
||||
|
||||
## Class Definition
|
||||
|
||||
```csharp
|
||||
public class TBarSeries : IReadOnlyList<TBar>
|
||||
{
|
||||
// Views
|
||||
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 = true)` | Adds a new bar or updates the last one. |
|
||||
| `Add(DateTime time, double o, double h, double l, double c, double v, bool isNew)` | 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
|
||||
long now = DateTime.UtcNow.Ticks;
|
||||
bars.Add(new TBar(now, 100, 105, 95, 102, 1000), isNew: true);
|
||||
|
||||
// Update the last bar (e.g., real-time feed update)
|
||||
bars.Add(new TBar(now, 100, 106, 95, 104, 1500), isNew: false);
|
||||
```
|
||||
|
||||
### Accessing Data
|
||||
```csharp
|
||||
// Access entire bar
|
||||
TBar lastBar = bars.Last;
|
||||
|
||||
// Access specific component series (Zero-Copy)
|
||||
TSeries closes = bars.Close;
|
||||
double lastClose = closes.Last.Value;
|
||||
|
||||
// Access via indexer
|
||||
TBar firstBar = bars[0];
|
||||
```
|
||||
|
||||
### Performance Note
|
||||
Because `TBarSeries` uses SoA layout, iterating over a single component (like `Close` prices) is extremely cache-efficient. The CPU prefetcher can load contiguous doubles without loading the interleaved Open, High, Low, or Volume data.
|
||||
|
||||
+147
-147
@@ -1,147 +1,147 @@
|
||||
using System.Collections;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// A high-performance OHLCV time series implementation using Structure of Arrays (SoA) layout.
|
||||
/// Stores Time, Open, High, Low, Close, Volume in separate contiguous arrays for SIMD efficiency.
|
||||
/// Exposes TSeries views for each component that share the underlying Time array.
|
||||
/// </summary>
|
||||
public class TBarSeries : IReadOnlyList<TBar>
|
||||
{
|
||||
// Internal storage: SoA layout
|
||||
protected readonly List<long> _t = new();
|
||||
protected readonly List<double> _o = new();
|
||||
protected readonly List<double> _h = new();
|
||||
protected readonly List<double> _l = new();
|
||||
protected readonly List<double> _c = new();
|
||||
protected readonly List<double> _v = new();
|
||||
|
||||
public string Name { get; set; } = "Bar";
|
||||
public event Action<TBar>? Pub;
|
||||
|
||||
// Public properties are Views into the main data
|
||||
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()
|
||||
{
|
||||
// Initialize views sharing the same Time list but different Value lists
|
||||
Open = new TSeries(_t, _o) { Name = "Open" };
|
||||
High = new TSeries(_t, _h) { Name = "High" };
|
||||
Low = new TSeries(_t, _l) { Name = "Low" };
|
||||
Close = new TSeries(_t, _c) { Name = "Close" };
|
||||
Volume = new TSeries(_t, _v) { Name = "Volume" };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with capacity hint to avoid List growth overhead.
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
// Initialize views sharing the same Time list but different Value lists
|
||||
Open = new TSeries(_t, _o) { Name = "Open" };
|
||||
High = new TSeries(_t, _h) { Name = "High" };
|
||||
Low = new TSeries(_t, _l) { Name = "Low" };
|
||||
Close = new TSeries(_t, _c) { Name = "Close" };
|
||||
Volume = new TSeries(_t, _v) { Name = "Volume" };
|
||||
}
|
||||
|
||||
public int Count
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _c.Count;
|
||||
}
|
||||
|
||||
public TBar this[int index]
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => new(_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;
|
||||
}
|
||||
|
||||
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; }
|
||||
|
||||
[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(bar);
|
||||
}
|
||||
|
||||
[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)
|
||||
{
|
||||
_t.AddRange(t);
|
||||
_o.AddRange(o);
|
||||
_h.AddRange(h);
|
||||
_l.AddRange(l);
|
||||
_c.AddRange(c);
|
||||
_v.AddRange(v);
|
||||
}
|
||||
|
||||
public IEnumerator<TBar> GetEnumerator()
|
||||
{
|
||||
for (int i = 0; i < _c.Count; i++)
|
||||
{
|
||||
yield return new TBar(_t[i], _o[i], _h[i], _l[i], _c[i], _v[i]);
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
using System.Collections;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// A high-performance OHLCV time series implementation using Structure of Arrays (SoA) layout.
|
||||
/// Stores Time, Open, High, Low, Close, Volume in separate contiguous arrays for SIMD efficiency.
|
||||
/// Exposes TSeries views for each component that share the underlying Time array.
|
||||
/// </summary>
|
||||
public class TBarSeries : IReadOnlyList<TBar>
|
||||
{
|
||||
// Internal storage: SoA layout
|
||||
protected readonly List<long> _t = new();
|
||||
protected readonly List<double> _o = new();
|
||||
protected readonly List<double> _h = new();
|
||||
protected readonly List<double> _l = new();
|
||||
protected readonly List<double> _c = new();
|
||||
protected readonly List<double> _v = new();
|
||||
|
||||
public string Name { get; set; } = "Bar";
|
||||
public event Action<TBar>? Pub;
|
||||
|
||||
// Public properties are Views into the main data
|
||||
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()
|
||||
{
|
||||
// Initialize views sharing the same Time list but different Value lists
|
||||
Open = new TSeries(_t, _o) { Name = "Open" };
|
||||
High = new TSeries(_t, _h) { Name = "High" };
|
||||
Low = new TSeries(_t, _l) { Name = "Low" };
|
||||
Close = new TSeries(_t, _c) { Name = "Close" };
|
||||
Volume = new TSeries(_t, _v) { Name = "Volume" };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with capacity hint to avoid List growth overhead.
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
// Initialize views sharing the same Time list but different Value lists
|
||||
Open = new TSeries(_t, _o) { Name = "Open" };
|
||||
High = new TSeries(_t, _h) { Name = "High" };
|
||||
Low = new TSeries(_t, _l) { Name = "Low" };
|
||||
Close = new TSeries(_t, _c) { Name = "Close" };
|
||||
Volume = new TSeries(_t, _v) { Name = "Volume" };
|
||||
}
|
||||
|
||||
public int Count
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _c.Count;
|
||||
}
|
||||
|
||||
public TBar this[int index]
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => new(_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;
|
||||
}
|
||||
|
||||
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; }
|
||||
|
||||
[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(bar);
|
||||
}
|
||||
|
||||
[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)
|
||||
{
|
||||
_t.AddRange(t);
|
||||
_o.AddRange(o);
|
||||
_h.AddRange(h);
|
||||
_l.AddRange(l);
|
||||
_c.AddRange(c);
|
||||
_v.AddRange(v);
|
||||
}
|
||||
|
||||
public IEnumerator<TBar> GetEnumerator()
|
||||
{
|
||||
for (int i = 0; i < _c.Count; i++)
|
||||
{
|
||||
yield return new TBar(_t[i], _o[i], _h[i], _l[i], _c[i], _v[i]);
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
|
||||
@@ -1,49 +1,49 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests
|
||||
{
|
||||
public class TSeriesTests
|
||||
{
|
||||
[Fact]
|
||||
public void Add_NewValue_IncreasesCount()
|
||||
{
|
||||
var series = new TSeries();
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
|
||||
series.Add(time, 10.0, isNew: true);
|
||||
|
||||
Assert.Single(series);
|
||||
Assert.Equal(10.0, series.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_UpdateValue_DoesNotIncreaseCount()
|
||||
{
|
||||
var series = new TSeries();
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
|
||||
series.Add(time, 10.0, isNew: true);
|
||||
series.Add(time, 11.0, isNew: false);
|
||||
|
||||
Assert.Single(series);
|
||||
Assert.Equal(11.0, series.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_MultipleValues_MaintainsOrder()
|
||||
{
|
||||
var series = new TSeries();
|
||||
long t0 = DateTime.UtcNow.Ticks;
|
||||
long t1 = t0 + TimeSpan.TicksPerMinute;
|
||||
|
||||
series.Add(t0, 10.0, isNew: true);
|
||||
series.Add(t1, 20.0, isNew: true);
|
||||
|
||||
Assert.Equal(2, series.Count);
|
||||
Assert.Equal(10.0, series[0].Value);
|
||||
Assert.Equal(20.0, series[1].Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests
|
||||
{
|
||||
public class TSeriesTests
|
||||
{
|
||||
[Fact]
|
||||
public void Add_NewValue_IncreasesCount()
|
||||
{
|
||||
var series = new TSeries();
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
|
||||
series.Add(time, 10.0, isNew: true);
|
||||
|
||||
Assert.Single(series);
|
||||
Assert.Equal(10.0, series.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_UpdateValue_DoesNotIncreaseCount()
|
||||
{
|
||||
var series = new TSeries();
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
|
||||
series.Add(time, 10.0, isNew: true);
|
||||
series.Add(time, 11.0, isNew: false);
|
||||
|
||||
Assert.Single(series);
|
||||
Assert.Equal(11.0, series.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_MultipleValues_MaintainsOrder()
|
||||
{
|
||||
var series = new TSeries();
|
||||
long t0 = DateTime.UtcNow.Ticks;
|
||||
long t1 = t0 + TimeSpan.TicksPerMinute;
|
||||
|
||||
series.Add(t0, 10.0, isNew: true);
|
||||
series.Add(t1, 20.0, isNew: true);
|
||||
|
||||
Assert.Equal(2, series.Count);
|
||||
Assert.Equal(10.0, series[0].Value);
|
||||
Assert.Equal(20.0, series[1].Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+56
-56
@@ -1,56 +1,56 @@
|
||||
# TSeries: Time Series Data
|
||||
|
||||
## Overview
|
||||
|
||||
`TSeries` is a high-performance container for time-series data. Unlike a standard `List<TValue>`, it uses a **Structure of Arrays (SoA)** layout internally. This means it stores timestamps and values in separate contiguous arrays (`List<long>` and `List<double>`).
|
||||
|
||||
This layout is critical for performance because it allows:
|
||||
1. **SIMD Optimization**: The `Values` property returns a `ReadOnlySpan<double>` that can be directly processed by CPU vector instructions (AVX/SSE).
|
||||
2. **Cache Locality**: Iterating over values doesn't load timestamps into the CPU cache, and vice versa.
|
||||
|
||||
## Structure
|
||||
|
||||
```csharp
|
||||
public class TSeries : IReadOnlyList<TValue>
|
||||
{
|
||||
// Internal SoA storage
|
||||
protected readonly List<long> _t;
|
||||
protected readonly List<double> _v;
|
||||
|
||||
// Public accessors
|
||||
public ReadOnlySpan<double> Values => ...; // Zero-copy access
|
||||
public ReadOnlySpan<long> Times => ...; // Zero-copy access
|
||||
|
||||
public TValue Last { get; }
|
||||
public int Count { get; }
|
||||
}
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
* **SoA Layout**: Optimized for numerical computing and SIMD.
|
||||
* **Zero-Copy Access**: `Values` and `Times` properties expose internal storage as Spans without copying.
|
||||
* **Streaming Support**: The `Add` method supports `isNew` parameter to handle intra-bar updates (replacing the last value instead of appending).
|
||||
* **Event Publishing**: Optional `Pub` event for reactive pipelines.
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating and Adding Data
|
||||
```csharp
|
||||
var series = new TSeries();
|
||||
series.Add(DateTime.Now, 100.0); // isNew=true by default
|
||||
```
|
||||
|
||||
### Streaming Updates
|
||||
```csharp
|
||||
// New bar
|
||||
series.Add(time, 100.0, isNew: true);
|
||||
|
||||
// Update current bar (e.g. price change within same minute)
|
||||
series.Add(time, 101.0, isNew: false);
|
||||
```
|
||||
|
||||
### SIMD Processing
|
||||
```csharp
|
||||
// Calculate average using SIMD
|
||||
double avg = series.Values.AverageSIMD();
|
||||
# TSeries: Time Series Data
|
||||
|
||||
## Overview
|
||||
|
||||
`TSeries` is a high-performance container for time-series data. Unlike a standard `List<TValue>`, it uses a **Structure of Arrays (SoA)** layout internally. This means it stores timestamps and values in separate contiguous arrays (`List<long>` and `List<double>`).
|
||||
|
||||
This layout is critical for performance because it allows:
|
||||
1. **SIMD Optimization**: The `Values` property returns a `ReadOnlySpan<double>` that can be directly processed by CPU vector instructions (AVX/SSE).
|
||||
2. **Cache Locality**: Iterating over values doesn't load timestamps into the CPU cache, and vice versa.
|
||||
|
||||
## Structure
|
||||
|
||||
```csharp
|
||||
public class TSeries : IReadOnlyList<TValue>
|
||||
{
|
||||
// Internal SoA storage
|
||||
protected readonly List<long> _t;
|
||||
protected readonly List<double> _v;
|
||||
|
||||
// Public accessors
|
||||
public ReadOnlySpan<double> Values => ...; // Zero-copy access
|
||||
public ReadOnlySpan<long> Times => ...; // Zero-copy access
|
||||
|
||||
public TValue Last { get; }
|
||||
public int Count { get; }
|
||||
}
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
* **SoA Layout**: Optimized for numerical computing and SIMD.
|
||||
* **Zero-Copy Access**: `Values` and `Times` properties expose internal storage as Spans without copying.
|
||||
* **Streaming Support**: The `Add` method supports `isNew` parameter to handle intra-bar updates (replacing the last value instead of appending).
|
||||
* **Event Publishing**: Optional `Pub` event for reactive pipelines.
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating and Adding Data
|
||||
```csharp
|
||||
var series = new TSeries();
|
||||
series.Add(DateTime.Now, 100.0); // isNew=true by default
|
||||
```
|
||||
|
||||
### Streaming Updates
|
||||
```csharp
|
||||
// New bar
|
||||
series.Add(time, 100.0, isNew: true);
|
||||
|
||||
// Update current bar (e.g. price change within same minute)
|
||||
series.Add(time, 101.0, isNew: false);
|
||||
```
|
||||
|
||||
### SIMD Processing
|
||||
```csharp
|
||||
// Calculate average using SIMD
|
||||
double avg = series.Values.AverageSIMD();
|
||||
|
||||
+146
-146
@@ -1,146 +1,146 @@
|
||||
using System.Collections;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// A high-performance time series implementation using Structure of Arrays (SoA) layout.
|
||||
/// Stores Time (long) and Value (double) in separate contiguous arrays for SIMD efficiency.
|
||||
/// Supports "New Bar" vs "Update Last" streaming semantics.
|
||||
/// </summary>
|
||||
public class TSeries : IReadOnlyList<TValue>
|
||||
{
|
||||
// Internal storage: SoA layout
|
||||
// We use List<T> for dynamic sizing but access internal arrays via CollectionsMarshal for speed
|
||||
protected readonly List<long> _t;
|
||||
protected readonly List<double> _v;
|
||||
|
||||
public string Name { get; set; } = "Data";
|
||||
|
||||
// Event optimization: Use Action<TValue> to avoid EventArgs allocation
|
||||
// Note: Events are generally discouraged in the hot path of this high-perf design,
|
||||
// but kept for compatibility/chaining.
|
||||
public event Action<TValue>? Pub;
|
||||
|
||||
public TSeries()
|
||||
{
|
||||
_t = new List<long>();
|
||||
_v = new List<double>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with capacity hint to avoid List growth overhead.
|
||||
/// </summary>
|
||||
public TSeries(int capacity)
|
||||
{
|
||||
_t = new List<long>(capacity);
|
||||
_v = new List<double>(capacity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for wrapping existing lists (e.g. from TBarSeries).
|
||||
/// </summary>
|
||||
public TSeries(List<long> time, List<double> values)
|
||||
{
|
||||
_t = time;
|
||||
_v = values;
|
||||
}
|
||||
|
||||
public int Count
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _v.Count;
|
||||
}
|
||||
|
||||
public TValue this[int index]
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => new(_t[index], _v[index]);
|
||||
}
|
||||
|
||||
public TValue Last
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _v.Count > 0 ? new(_t[^1], _v[^1]) : default;
|
||||
}
|
||||
|
||||
public double LastValue
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _v.Count > 0 ? _v[^1] : double.NaN;
|
||||
}
|
||||
|
||||
public long LastTime
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _t.Count > 0 ? _t[^1] : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direct access to the underlying Value array as a Span for SIMD operations.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<double> Values
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => CollectionsMarshal.AsSpan(_v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direct access to the underlying Time array as a Span.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<long> Times
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => CollectionsMarshal.AsSpan(_t);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public virtual void Add(TValue value, bool isNew)
|
||||
{
|
||||
if (isNew || _v.Count == 0)
|
||||
{
|
||||
_t.Add(value.Time);
|
||||
_v.Add(value.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update last bar
|
||||
int lastIdx = _v.Count - 1;
|
||||
_t[lastIdx] = value.Time;
|
||||
_v[lastIdx] = value.Value;
|
||||
}
|
||||
Pub?.Invoke(value);
|
||||
}
|
||||
|
||||
// Overload for backward compatibility (assumes isNew=true)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public virtual void Add(TValue value) => Add(value, true);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Add(long time, double value, bool isNew = true) => Add(new TValue(time, value), isNew);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Add(DateTime time, double value, bool isNew = true) => Add(new TValue(time.Ticks, value), isNew);
|
||||
|
||||
public void Add(IEnumerable<double> values)
|
||||
{
|
||||
long t = DateTime.UtcNow.Ticks;
|
||||
foreach (var v in values)
|
||||
{
|
||||
Add(new TValue(t, v), isNew: true);
|
||||
t += TimeSpan.TicksPerMinute; // Dummy time increment
|
||||
}
|
||||
}
|
||||
|
||||
// IEnumerable implementation
|
||||
public IEnumerator<TValue> GetEnumerator()
|
||||
{
|
||||
for (int i = 0; i < _v.Count; i++)
|
||||
{
|
||||
yield return new TValue(_t[i], _v[i]);
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
using System.Collections;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// A high-performance time series implementation using Structure of Arrays (SoA) layout.
|
||||
/// Stores Time (long) and Value (double) in separate contiguous arrays for SIMD efficiency.
|
||||
/// Supports "New Bar" vs "Update Last" streaming semantics.
|
||||
/// </summary>
|
||||
public class TSeries : IReadOnlyList<TValue>
|
||||
{
|
||||
// Internal storage: SoA layout
|
||||
// We use List<T> for dynamic sizing but access internal arrays via CollectionsMarshal for speed
|
||||
protected readonly List<long> _t;
|
||||
protected readonly List<double> _v;
|
||||
|
||||
public string Name { get; set; } = "Data";
|
||||
|
||||
// Event optimization: Use Action<TValue> to avoid EventArgs allocation
|
||||
// Note: Events are generally discouraged in the hot path of this high-perf design,
|
||||
// but kept for compatibility/chaining.
|
||||
public event Action<TValue>? Pub;
|
||||
|
||||
public TSeries()
|
||||
{
|
||||
_t = new List<long>();
|
||||
_v = new List<double>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with capacity hint to avoid List growth overhead.
|
||||
/// </summary>
|
||||
public TSeries(int capacity)
|
||||
{
|
||||
_t = new List<long>(capacity);
|
||||
_v = new List<double>(capacity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for wrapping existing lists (e.g. from TBarSeries).
|
||||
/// </summary>
|
||||
public TSeries(List<long> time, List<double> values)
|
||||
{
|
||||
_t = time;
|
||||
_v = values;
|
||||
}
|
||||
|
||||
public int Count
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _v.Count;
|
||||
}
|
||||
|
||||
public TValue this[int index]
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => new(_t[index], _v[index]);
|
||||
}
|
||||
|
||||
public TValue Last
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _v.Count > 0 ? new(_t[^1], _v[^1]) : default;
|
||||
}
|
||||
|
||||
public double LastValue
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _v.Count > 0 ? _v[^1] : double.NaN;
|
||||
}
|
||||
|
||||
public long LastTime
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _t.Count > 0 ? _t[^1] : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direct access to the underlying Value array as a Span for SIMD operations.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<double> Values
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => CollectionsMarshal.AsSpan(_v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direct access to the underlying Time array as a Span.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<long> Times
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => CollectionsMarshal.AsSpan(_t);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public virtual void Add(TValue value, bool isNew)
|
||||
{
|
||||
if (isNew || _v.Count == 0)
|
||||
{
|
||||
_t.Add(value.Time);
|
||||
_v.Add(value.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update last bar
|
||||
int lastIdx = _v.Count - 1;
|
||||
_t[lastIdx] = value.Time;
|
||||
_v[lastIdx] = value.Value;
|
||||
}
|
||||
Pub?.Invoke(value);
|
||||
}
|
||||
|
||||
// Overload for backward compatibility (assumes isNew=true)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public virtual void Add(TValue value) => Add(value, true);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Add(long time, double value, bool isNew = true) => Add(new TValue(time, value), isNew);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Add(DateTime time, double value, bool isNew = true) => Add(new TValue(time.Ticks, value), isNew);
|
||||
|
||||
public void Add(IEnumerable<double> values)
|
||||
{
|
||||
long t = DateTime.UtcNow.Ticks;
|
||||
foreach (var v in values)
|
||||
{
|
||||
Add(new TValue(t, v), isNew: true);
|
||||
t += TimeSpan.TicksPerMinute; // Dummy time increment
|
||||
}
|
||||
}
|
||||
|
||||
// IEnumerable implementation
|
||||
public IEnumerator<TValue> GetEnumerator()
|
||||
{
|
||||
for (int i = 0; i < _v.Count; i++)
|
||||
{
|
||||
yield return new TValue(_t[i], _v[i]);
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
|
||||
@@ -1,51 +1,51 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests
|
||||
{
|
||||
public class TValueTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_SetsPropertiesCorrectly()
|
||||
{
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
double value = 123.45;
|
||||
|
||||
var tValue = new TValue(time, value);
|
||||
|
||||
Assert.Equal(time, tValue.Time);
|
||||
Assert.Equal(value, tValue.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AsDateTime_ReturnsCorrectDateTime()
|
||||
{
|
||||
DateTime dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
long ticks = dt.Ticks;
|
||||
var tValue = new TValue(ticks, 100.0);
|
||||
|
||||
Assert.Equal(dt, tValue.AsDateTime);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_FormatsCorrectly()
|
||||
{
|
||||
DateTime dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
var tValue = new TValue(dt.Ticks, 123.456);
|
||||
|
||||
string result = tValue.ToString();
|
||||
|
||||
Assert.Contains(dt.ToString("yyyy-MM-dd HH:mm:ss"), result);
|
||||
Assert.Contains("123.46", result); // Default formatting usually 2 decimals or similar
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImplicitConversion_ToDouble()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, 42.0);
|
||||
double val = tValue;
|
||||
Assert.Equal(42.0, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests
|
||||
{
|
||||
public class TValueTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_SetsPropertiesCorrectly()
|
||||
{
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
double value = 123.45;
|
||||
|
||||
var tValue = new TValue(time, value);
|
||||
|
||||
Assert.Equal(time, tValue.Time);
|
||||
Assert.Equal(value, tValue.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AsDateTime_ReturnsCorrectDateTime()
|
||||
{
|
||||
DateTime dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
long ticks = dt.Ticks;
|
||||
var tValue = new TValue(ticks, 100.0);
|
||||
|
||||
Assert.Equal(dt, tValue.AsDateTime);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_FormatsCorrectly()
|
||||
{
|
||||
DateTime dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
var tValue = new TValue(dt.Ticks, 123.456);
|
||||
|
||||
string result = tValue.ToString();
|
||||
|
||||
Assert.Contains(dt.ToString("yyyy-MM-dd HH:mm:ss"), result);
|
||||
Assert.Contains("123.46", result); // Default formatting usually 2 decimals or similar
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImplicitConversion_ToDouble()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, 42.0);
|
||||
double val = tValue;
|
||||
Assert.Equal(42.0, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+37
-37
@@ -1,37 +1,37 @@
|
||||
# TValue: Time-Value Pair
|
||||
|
||||
## Overview
|
||||
|
||||
`TValue` is the fundamental building block of QuanTAlib. It represents a single data point in a time series, consisting of a timestamp and a double-precision floating-point value.
|
||||
|
||||
It is implemented as a lightweight `readonly struct` to ensure immutability and high performance (stack allocation, no GC overhead).
|
||||
|
||||
## Structure
|
||||
|
||||
```csharp
|
||||
public readonly struct TValue
|
||||
{
|
||||
public readonly long Time; // Ticks (UTC)
|
||||
public readonly double Value; // Data value
|
||||
public readonly bool IsNew; // Metadata for streaming (optional usage)
|
||||
}
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
* **Lightweight**: 24 bytes (long + double + bool + padding).
|
||||
* **Immutable**: Thread-safe by design.
|
||||
* **Implicit Conversions**: Can be implicitly converted to `double` (returns Value) and `DateTime` (returns Time).
|
||||
* **Performance**: Designed for high-frequency trading and large dataset processing.
|
||||
|
||||
## Usage
|
||||
|
||||
`TValue` is used throughout the library for:
|
||||
* Input to indicators (`Update(TValue)`).
|
||||
* Output from indicators (`Value` property).
|
||||
* Elements in `TSeries`.
|
||||
|
||||
## Constructors
|
||||
|
||||
* `new TValue(long time, double value, bool isNew = true)`
|
||||
* `new TValue(DateTime time, double value, bool isNew = true)`
|
||||
# TValue: Time-Value Pair
|
||||
|
||||
## Overview
|
||||
|
||||
`TValue` is the fundamental building block of QuanTAlib. It represents a single data point in a time series, consisting of a timestamp and a double-precision floating-point value.
|
||||
|
||||
It is implemented as a lightweight `readonly struct` to ensure immutability and high performance (stack allocation, no GC overhead).
|
||||
|
||||
## Structure
|
||||
|
||||
```csharp
|
||||
public readonly struct TValue
|
||||
{
|
||||
public readonly long Time; // Ticks (UTC)
|
||||
public readonly double Value; // Data value
|
||||
public readonly bool IsNew; // Metadata for streaming (optional usage)
|
||||
}
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
* **Lightweight**: 24 bytes (long + double + bool + padding).
|
||||
* **Immutable**: Thread-safe by design.
|
||||
* **Implicit Conversions**: Can be implicitly converted to `double` (returns Value) and `DateTime` (returns Time).
|
||||
* **Performance**: Designed for high-frequency trading and large dataset processing.
|
||||
|
||||
## Usage
|
||||
|
||||
`TValue` is used throughout the library for:
|
||||
* Input to indicators (`Update(TValue)`).
|
||||
* Output from indicators (`Value` property).
|
||||
* Elements in `TSeries`.
|
||||
|
||||
## Constructors
|
||||
|
||||
* `new TValue(long time, double value, bool isNew = true)`
|
||||
* `new TValue(DateTime time, double value, bool isNew = true)`
|
||||
|
||||
+57
-57
@@ -1,57 +1,57 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// A lightweight struct representing a time-value pair.
|
||||
/// Pure data type: 16 bytes (long + double).
|
||||
/// </summary>
|
||||
[SkipLocalsInit]
|
||||
public readonly struct TValue : IEquatable<TValue>
|
||||
{
|
||||
/// <summary>
|
||||
/// Time in ticks (UTC).
|
||||
/// </summary>
|
||||
public readonly long Time;
|
||||
|
||||
/// <summary>
|
||||
/// The value.
|
||||
/// </summary>
|
||||
public readonly double Value;
|
||||
|
||||
/// <summary>
|
||||
/// Convenience property to get DateTime from Ticks.
|
||||
/// </summary>
|
||||
public DateTime AsDateTime => new(Time, DateTimeKind.Utc);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue(long time, double value)
|
||||
{
|
||||
Time = time;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue(DateTime time, double value)
|
||||
{
|
||||
Time = time.Ticks;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator double(TValue tv) => tv.Value;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator DateTime(TValue tv) => new(tv.Time, DateTimeKind.Utc);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override string ToString() => $"[{AsDateTime:yyyy-MM-dd HH:mm:ss}, {Value:F2}]";
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool Equals(TValue other) => Time == other.Time && Value == other.Value;
|
||||
|
||||
public override bool Equals(object? obj) => obj is TValue other && Equals(other);
|
||||
public override int GetHashCode() => HashCode.Combine(Time, Value);
|
||||
public static bool operator ==(TValue left, TValue right) => left.Equals(right);
|
||||
public static bool operator !=(TValue left, TValue right) => !left.Equals(right);
|
||||
}
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// A lightweight struct representing a time-value pair.
|
||||
/// Pure data type: 16 bytes (long + double).
|
||||
/// </summary>
|
||||
[SkipLocalsInit]
|
||||
public readonly struct TValue : IEquatable<TValue>
|
||||
{
|
||||
/// <summary>
|
||||
/// Time in ticks (UTC).
|
||||
/// </summary>
|
||||
public readonly long Time;
|
||||
|
||||
/// <summary>
|
||||
/// The value.
|
||||
/// </summary>
|
||||
public readonly double Value;
|
||||
|
||||
/// <summary>
|
||||
/// Convenience property to get DateTime from Ticks.
|
||||
/// </summary>
|
||||
public DateTime AsDateTime => new(Time, DateTimeKind.Utc);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue(long time, double value)
|
||||
{
|
||||
Time = time;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue(DateTime time, double value)
|
||||
{
|
||||
Time = time.Ticks;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator double(TValue tv) => tv.Value;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator DateTime(TValue tv) => new(tv.Time, DateTimeKind.Utc);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override string ToString() => $"[{AsDateTime:yyyy-MM-dd HH:mm:ss}, {Value:F2}]";
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool Equals(TValue other) => Time == other.Time && Value == other.Value;
|
||||
|
||||
public override bool Equals(object? obj) => obj is TValue other && Equals(other);
|
||||
public override int GetHashCode() => HashCode.Combine(Time, Value);
|
||||
public static bool operator ==(TValue left, TValue right) => left.Equals(right);
|
||||
public static bool operator !=(TValue left, TValue right) => !left.Equals(right);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user