updates from mac

This commit is contained in:
Miha Kralj
2025-11-28 13:35:16 -08:00
parent 74b49d2bb4
commit acac3e610c
55 changed files with 126278 additions and 126081 deletions
+249 -249
View File
@@ -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
View File
@@ -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);
}
}
+43 -43
View File
@@ -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();
+74 -74
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
+82 -82
View File
@@ -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}");
}
+58 -58
View File
@@ -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);
}
}
}
+70 -70
View File
@@ -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
View File
@@ -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();
}
+49 -49
View File
@@ -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
View File
@@ -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
View File
@@ -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();
}
+51 -51
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}