first iteration

This commit is contained in:
Miha Kralj
2025-11-25 20:40:46 -08:00
parent b5881b9bb4
commit 33ffd3a37a
594 changed files with 117007 additions and 80111 deletions
-129
View File
@@ -1,129 +0,0 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// Provides a base implementation for financial indicators that work with bar data in the QuanTAlib library.
/// </summary>
/// <remarks>
/// This abstract class implements the iTValue interface and defines common properties
/// and methods used by inheriting indicator types. It handles the basic flow of
/// receiving bar data, performing calculations, and publishing results.
/// </remarks>
public abstract class AbstractBarBase : ITValue
{
public System.DateTime Time { get; set; }
public double Value { get; set; }
public bool IsNew { get; set; }
public bool IsHot { get; set; }
public TBar Input { get; set; }
public string Name { get; set; } = "";
public int WarmupPeriod { get; set; }
public TValue Tick => new(Time, Value, IsNew, IsHot);
public event ValueSignal Pub = delegate { };
protected int _index;
protected double _lastValidValue;
protected AbstractBarBase()
{
// Add parameters into constructor if needed
}
/// <summary>
/// Subscribes to bar data updates.
/// </summary>
/// <param name="source">The source of the bar data.</param>
/// <param name="args">The event arguments containing the bar data.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Sub(object source, in TBarEventArgs args) => Calc(args.Bar);
/// <summary>
/// Initializes the indicator's state.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual void Init()
{
_index = 0;
_lastValidValue = 0;
}
/// <summary>
/// Checks if the input value is valid (not NaN or Infinity).
/// </summary>
/// <param name="value">The value to check.</param>
/// <returns>True if the value is valid, false otherwise.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected static bool IsValidValue(double value)
{
return !double.IsNaN(value) && !double.IsInfinity(value);
}
/// <summary>
/// Creates a new TValue with the current state.
/// </summary>
/// <param name="value">The value to use.</param>
/// <returns>A new TValue instance.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected TValue CreateTValue(double value)
{
return new TValue(Time: Input.Time, Value: value, IsNew: Input.IsNew, IsHot: IsHot);
}
/// <summary>
/// Calculates the indicator value based on the input bar.
/// </summary>
/// <param name="input">The input bar data.</param>
/// <returns>A TValue containing the calculated result.</returns>
public virtual TValue Calc(TBar input)
{
Input = input;
if (!IsValidValue(input.Close))
{
return Process(CreateTValue(GetLastValid()));
}
Value = Calculation();
return Process(CreateTValue(Value));
}
/// <summary>
/// Retrieves the last valid calculated value.
/// </summary>
/// <returns>The last valid value of the indicator.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected virtual double GetLastValid()
{
return Value;
}
/// <summary>
/// Manages the state of the indicator based on whether a new bar is being processed.
/// </summary>
/// <param name="isNew">Indicates whether the current input is a new bar.</param>
protected abstract void ManageState(bool isNew);
/// <summary>
/// Performs the actual calculation of the indicator value.
/// </summary>
/// <returns>The calculated indicator value.</returns>
protected abstract double Calculation();
/// <summary>
/// Processes the calculated value, updates the indicator's own state,
/// and publishes the result through an event.
/// </summary>
/// <param name="value">The calculated TValue to process.</param>
/// <returns>The processed TValue.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected virtual TValue Process(TValue value)
{
Time = value.Time;
Value = value.Value;
IsNew = value.IsNew;
IsHot = value.IsHot;
Pub?.Invoke(this, new ValueEventArgs(value));
return value;
}
}
+280
View File
@@ -0,0 +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);
}
}
-159
View File
@@ -1,159 +0,0 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// Provides a base implementation for financial indicators in the QuanTAlib library.
/// </summary>
/// <remarks>
/// This abstract class implements the iTValue interface and defines common properties
/// and methods used by inheriting indicator types. It handles the basic flow of
/// receiving data, performing calculations, and publishing results.
/// </remarks>
public abstract class AbstractBase : ITValue
{
public System.DateTime Time { get; set; }
public double Value { get; set; }
public bool IsNew { get; set; }
public bool IsHot { get; set; }
public TValue Input { get; set; }
public TValue Input2 { get; set; }
public TBar BarInput { get; set; }
public TBar BarInput2 { get; set; }
public string Name { get; set; } = "";
public int WarmupPeriod { get; set; }
public TValue Tick => new(Time, Value, IsNew, IsHot);
public event ValueSignal Pub = delegate { };
protected int _index;
protected double _lastValidValue;
protected AbstractBase()
{
// Add parameters into constructor if needed
}
/// <summary>
/// Checks if the input value is valid (not NaN or Infinity).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected static bool IsValidValue(double value)
{
return !double.IsNaN(value) && !double.IsInfinity(value);
}
/// <summary>
/// Creates a new TValue with the current state.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected static TValue CreateTValue(System.DateTime time, double value, bool isNew, bool isHot = false)
{
return new TValue(time, value, isNew, isHot);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Sub(object source, in ValueEventArgs args) => Calc(args.Tick);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Sub(object source1, object source2, in ValueEventArgs args1, in ValueEventArgs args2) =>
Calc(args1.Tick, args2.Tick);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Sub(object source, in TBarEventArgs args) => Calc(args.Bar);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual void Init()
{
_index = 0;
_lastValidValue = 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual TValue Calc(TValue input)
{
Input = input;
Input2 = CreateTValue(input.Time, double.NaN, input.IsNew, input.IsHot);
return Process(input.Value, input.Time, input.IsNew);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual TValue Calc(double value, bool isNew)
{
Input = CreateTValue(Time, value, isNew);
Input2 = CreateTValue(Time, double.NaN, false);
return Process(Input.Value, Input.Time, Input.IsNew);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual TValue Calc(TBar barInput)
{
BarInput = barInput;
return Process(barInput.Close, barInput.Time, barInput.IsNew);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual TValue Calc(TValue input1, TValue input2)
{
Input = input1;
Input2 = input2;
return Process(input1.Value, input2.Value, input1.Time, input1.IsNew);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual TValue Calc(TBar input1, TBar input2)
{
BarInput = input1;
BarInput2 = input2;
return Process(input1.Close, input2.Close, input1.Time, input1.IsNew);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual TValue Calc(double value1, double value2)
{
var now = System.DateTime.Now;
Input = CreateTValue(now, value1, true, true);
Input2 = CreateTValue(now, value2, true, true);
return Process(value1, value2, now, true);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected virtual TValue Process(double value, System.DateTime time, bool isNew)
{
if (!IsValidValue(value))
{
return Process(CreateTValue(time, GetLastValid(), isNew, IsHot));
}
Value = Calculation();
return Process(CreateTValue(time, Value, isNew, IsHot));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected virtual TValue Process(double value1, double value2, System.DateTime time, bool isNew)
{
if (!IsValidValue(value1) || !IsValidValue(value2))
{
return Process(CreateTValue(time, GetLastValid(), isNew, IsHot));
}
Value = Calculation();
return Process(CreateTValue(time, Value, isNew, IsHot));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected virtual TValue Process(TValue value)
{
Time = value.Time;
Value = value.Value;
IsNew = value.IsNew;
IsHot = value.IsHot;
Pub?.Invoke(this, new ValueEventArgs(value));
return value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected virtual double GetLastValid()
{
return Value;
}
protected abstract void ManageState(bool isNew);
protected abstract double Calculation();
}
-443
View File
@@ -1,443 +0,0 @@
using System.Collections;
using System.Runtime.CompilerServices;
using System.Numerics;
namespace QuanTAlib;
/// <summary>
/// Represents a circular buffer of double values with fixed capacity.
/// </summary>
/// <remarks>
/// This class provides efficient operations for adding, accessing, and manipulating
/// a fixed-size buffer of double values. It uses SIMD operations for improved performance
/// on supported hardware.
/// </remarks>
[SkipLocalsInit]
public class CircularBuffer : IEnumerable<double>
{
private readonly double[] _buffer;
private readonly int _capacity;
private int _start = 0;
private int _size = 0;
/// <summary>
/// Gets the maximum number of elements that can be contained in the buffer.
/// </summary>
public int Capacity => _capacity;
/// <summary>
/// Gets the number of elements currently contained in the buffer.
/// </summary>
public int Count => _size;
/// <summary>
/// Initializes a new instance of the CircularBuffer class with the specified capacity.
/// </summary>
/// <param name="capacity">The maximum number of elements the buffer can hold.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public CircularBuffer(int capacity)
{
_capacity = capacity;
_buffer = GC.AllocateArray<double>(capacity, pinned: true);
}
/// <summary>
/// Adds an item to the buffer.
/// </summary>
/// <param name="item">The item to add to the buffer.</param>
/// <param name="isNew">Indicates whether the item is a new value or an update to the last added value.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(double item, bool isNew = true)
{
if (_size == 0 || isNew)
{
if (_size < _capacity)
{
_buffer[(_start + _size) % _capacity] = item;
_size++;
}
else
{
_buffer[_start] = item;
_start = (_start + 1) % _capacity;
}
}
else
{
_buffer[(_start + _size - 1) % _capacity] = item;
}
}
/// <summary>
/// Gets or sets the element at the specified index.
/// </summary>
/// <param name="index">The zero-based index of the element to get or set.</param>
/// <returns>The element at the specified index.</returns>
public double this[Index index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
int actualIndex = index.IsFromEnd ? _size - index.Value : index.Value;
actualIndex = Math.Clamp(actualIndex, 0, _size - 1);
return _buffer[(_start + actualIndex) % _capacity];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
set
{
int actualIndex = index.IsFromEnd ? _size - index.Value : index.Value;
actualIndex = Math.Clamp(actualIndex, 0, _size - 1);
_buffer[(_start + actualIndex) % _capacity] = value;
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowArgumentOutOfRangeException(string paramName)
{
throw new ArgumentOutOfRangeException(paramName, "Index is out of range.");
}
/// <summary>
/// Gets the newest (most recently added) element in the buffer.
/// </summary>
/// <returns>The newest element in the buffer.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double Newest()
{
if (_size == 0)
return 0;
return _buffer[(_start + _size - 1) % _capacity];
}
/// <summary>
/// Gets the oldest element in the buffer.
/// </summary>
/// <returns>The oldest element in the buffer.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double Oldest()
{
if (_size == 0)
ThrowInvalidOperationException();
return _buffer[_start];
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowInvalidOperationException()
{
throw new InvalidOperationException("Buffer is empty.");
}
/// <summary>
/// Returns an enumerator that iterates through the buffer.
/// </summary>
/// <returns>An enumerator for the buffer.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Enumerator GetEnumerator() => new(this);
IEnumerator<double> IEnumerable<double>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <summary>
/// Represents an enumerator for the CircularBuffer.
/// </summary>
public readonly struct Enumerator : IEnumerator<double>
{
private readonly CircularBuffer _buffer;
private readonly int _size;
private readonly int _index;
private readonly double _current;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal Enumerator(CircularBuffer buffer)
{
_buffer = buffer;
_size = buffer._size;
_index = -1;
_current = default;
}
/// <summary>
/// Advances the enumerator to the next element of the buffer.
/// </summary>
/// <returns>true if the enumerator was successfully advanced to the next element; false if the enumerator has passed the end of the collection.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
if (_index + 1 >= _size)
return false;
Unsafe.AsRef(in _index)++;
Unsafe.AsRef(in _current) = _buffer[_index];
return true;
}
/// <summary>
/// Gets the element in the buffer at the current position of the enumerator.
/// </summary>
public double Current => _current;
object IEnumerator.Current => Current;
/// <summary>
/// Sets the enumerator to its initial position, which is before the first element in the buffer.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset()
{
Unsafe.AsRef(in _index) = -1;
Unsafe.AsRef(in _current) = default;
}
/// <summary>
/// Disposes the enumerator.
/// </summary>
public void Dispose() { }
}
/// <summary>
/// Copies the elements of the buffer to an array, starting at a particular array index.
/// </summary>
/// <param name="destination">The one-dimensional array that is the destination of the elements copied from the buffer.</param>
/// <param name="destinationIndex">The zero-based index in array at which copying begins.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void CopyTo(double[] destination, int destinationIndex)
{
if (_size == 0)
return;
if (_start + _size <= _capacity)
{
Array.Copy(_buffer, _start, destination, destinationIndex, _size);
}
else
{
int firstPartLength = _capacity - _start;
Array.Copy(_buffer, _start, destination, destinationIndex, firstPartLength);
Array.Copy(_buffer, 0, destination, destinationIndex + firstPartLength, _size - firstPartLength);
}
}
/// <summary>
/// Returns a read-only span over the contents of the buffer.
/// </summary>
/// <returns>A read-only span over the buffer contents.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ReadOnlySpan<double> GetSpan()
{
if (_size == 0)
return ReadOnlySpan<double>.Empty;
if (_start + _size <= _capacity)
{
return new ReadOnlySpan<double>(_buffer, _start, _size);
}
return new ReadOnlySpan<double>(ToArray());
}
/// <summary>
/// Gets the internal buffer array.
/// </summary>
public double[] InternalBuffer => _buffer;
/// <summary>
/// Returns a read-only span over the entire internal buffer.
/// </summary>
/// <returns>A read-only span over the entire internal buffer.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ReadOnlySpan<double> GetInternalSpan() => _buffer.AsSpan();
/// <summary>
/// Removes all elements from the buffer.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Clear()
{
Array.Clear(_buffer, 0, _buffer.Length);
_start = 0;
_size = 0;
}
/// <summary>
/// Returns the maximum value in the buffer.
/// </summary>
/// <returns>The maximum value in the buffer.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double Max()
{
if (_size == 0)
ThrowInvalidOperationException();
return MaxSimd();
}
/// <summary>
/// Returns the minimum value in the buffer.
/// </summary>
/// <returns>The minimum value in the buffer.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double Min()
{
if (_size == 0)
ThrowInvalidOperationException();
return MinSimd();
}
/// <summary>
/// Computes the sum of all values in the buffer.
/// </summary>
/// <returns>The sum of all values in the buffer.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double Sum()
{
return SumSimd();
}
/// <summary>
/// Computes the average of all values in the buffer.
/// </summary>
/// <returns>The average of all values in the buffer.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double Average()
{
if (_size == 0)
ThrowInvalidOperationException();
return SumSimd() / _size;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private double MaxSimd()
{
var span = GetSpan();
var vectorSize = Vector<double>.Count;
var maxVector = new Vector<double>(double.MinValue);
int i = 0;
ref double spanRef = ref System.Runtime.InteropServices.MemoryMarshal.GetReference(span);
for (; i <= span.Length - vectorSize; i += vectorSize)
{
maxVector = Vector.Max(maxVector, Unsafe.As<double, Vector<double>>(ref Unsafe.Add(ref spanRef, i)));
}
double max = double.MinValue;
for (int j = 0; j < vectorSize; j++)
{
max = Math.Max(max, maxVector[j]);
}
for (; i < span.Length; i++)
{
max = Math.Max(max, span[i]);
}
return max;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private double MinSimd()
{
var span = GetSpan();
var vectorSize = Vector<double>.Count;
var minVector = new Vector<double>(double.MaxValue);
int i = 0;
ref double spanRef = ref System.Runtime.InteropServices.MemoryMarshal.GetReference(span);
for (; i <= span.Length - vectorSize; i += vectorSize)
{
minVector = Vector.Min(minVector, Unsafe.As<double, Vector<double>>(ref Unsafe.Add(ref spanRef, i)));
}
double min = double.MaxValue;
for (int j = 0; j < vectorSize; j++)
{
min = Math.Min(min, minVector[j]);
}
for (; i < span.Length; i++)
{
min = Math.Min(min, span[i]);
}
return min;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private double SumSimd()
{
var span = GetSpan();
var vectorSize = Vector<double>.Count;
var sumVector = Vector<double>.Zero;
int i = 0;
ref double spanRef = ref System.Runtime.InteropServices.MemoryMarshal.GetReference(span);
for (; i <= span.Length - vectorSize; i += vectorSize)
{
sumVector += Unsafe.As<double, Vector<double>>(ref Unsafe.Add(ref spanRef, i));
}
double sum = 0;
for (int j = 0; j < vectorSize; j++)
{
sum += sumVector[j];
}
for (; i < span.Length; i++)
{
sum += span[i];
}
return sum;
}
/// <summary>
/// Copies the buffer elements to a new array.
/// </summary>
/// <returns>An array containing copies of the buffer elements.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double[] ToArray()
{
double[] array = new double[_size];
CopyTo(array, 0);
return array;
}
/// <summary>
/// Performs a parallel operation on the buffer elements.
/// </summary>
/// <param name="operation">The operation to perform on each partition of the buffer.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ParallelOperation(Func<double[], int, int, double> operation)
{
const int MinimumPartitionSize = 1024;
if (_size < MinimumPartitionSize)
{
var span = GetSpan();
var array = span.ToArray();
operation(array, 0, array.Length);
return;
}
int partitionCount = Environment.ProcessorCount;
int partitionSize = _size / partitionCount;
if (partitionSize < MinimumPartitionSize)
{
partitionCount = Math.Max(1, _size / MinimumPartitionSize);
partitionSize = _size / partitionCount;
}
var buffer = ToArray();
var results = GC.AllocateUninitializedArray<double>(partitionCount);
Parallel.For(0, partitionCount, i =>
{
int start = i * partitionSize;
int length = (i == partitionCount - 1) ? _size - start : partitionSize;
results[i] = operation(buffer, start, length);
});
}
}
-97
View File
@@ -1,97 +0,0 @@
using Microsoft.DotNet.Interactive.Formatting;
using System.ComponentModel.DataAnnotations;
using System.Text;
namespace QuanTAlib;
public static class Formatters
{
const string smallfont = "smaller";
const string pad = "18";
public static void Initialize()
{
Formatter.Register<ITValue>((tick, writer) =>
{
var sb = new StringBuilder();
sb.Append("<table style='border-collapse: collapse; text-align: left;'><tr>");
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0; font-size: {smallfont};'>{tick.Time:yyyy-MM-dd HH:mm:ss}</td>");
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0;'>{tick.Value:F2}</td>");
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0;'>{(tick.IsHot ? "🔥" : "")}</td>");
sb.Append("</tr></table>");
writer.Write(sb.ToString());
}, HtmlFormatter.MimeType);
Formatter.Register<TSeries>((series, writer) =>
{
var sb = new StringBuilder();
sb.Append("<table style='border-collapse: collapse; text-align: right;'></tr>");
sb.Append($"<th style='padding-left: {pad}px; padding-right: {pad}px; text-align: left;'><b>{series.Name}</b></th>");
sb.Append($"<th style='padding-left: {pad}px; padding-right: {pad}px; font-size: {smallfont};'><i>Index</i></th>");
sb.Append($"<th style='padding-left: {pad}px; padding-right: {pad}px; font-size: {smallfont};'><i>Value</i></th>");
sb.Append("</tr>");
for (int i = 0; i < Math.Min(100, series.Count); i++)
{
TValue item = series[i];
sb.Append("<tr>");
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0; font-size: {smallfont};'>{item.Time:yyyy-MM-dd HH:mm:ss}</td>");
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0; font-size: {smallfont};'>{i}</td>");
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0;'>{item.Value:F2}</td>");
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0;'>{(item.IsHot ? "🔥" : "")}</td>");
sb.Append("</tr>");
}
sb.Append("</table>");
if (series.Count > 100)
{
sb.Append("<p>Showing first 100 items. Total items: " + series.Count + "</p>");
}
writer.Write(sb.ToString());
}, HtmlFormatter.MimeType);
Formatter.Register<TBar>((bar, writer) =>
{
var sb = new StringBuilder();
sb.Append("<table style='border-collapse: collapse; text-align: right;'><tr>");
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0; font-size: {smallfont};'>{bar.Time:yyyy-MM-dd HH:mm:ss}</td>");
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0;'>{bar.Open:F2}</td>");
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0;'>{bar.High:F2}</td>");
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0;'>{bar.Low:F2}</td>");
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0;'>{bar.Close:F2}</td>");
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0;'> {bar.Volume:F2}</td>");
sb.Append("</tr></table>");
writer.Write(sb.ToString());
}, HtmlFormatter.MimeType);
Formatter.Register<TBarSeries>((series, writer) =>
{
var sb = new StringBuilder();
sb.Append("<table style='border-collapse: collapse; text-align: right;'></tr>");
sb.Append($"<th style='padding-left: {pad}px; padding-right: {pad}px; text-align: left;'><b>{series.Name}</b></th>");
sb.Append($"<th style='padding-left: {pad}px; padding-right: {pad}px; font-size: {smallfont};'><i>Index</i></th>");
sb.Append($"<th style='padding-left: {pad}px; padding-right: {pad}px; font-size: {smallfont};'><i>Open</i></th>");
sb.Append($"<th style='padding-left: {pad}px; padding-right: {pad}px; font-size: {smallfont};'><i>High</i></th>");
sb.Append($"<th style='padding-left: {pad}px; padding-right: {pad}px; font-size: {smallfont};'><i>Low</i></th>");
sb.Append($"<th style='padding-left: {pad}px; padding-right: {pad}px; font-size: {smallfont};'><i>Close</i></th>");
sb.Append($"<th style='padding-left: {pad}px; padding-right: {pad}px; font-size: {smallfont};'><i>Volume</i></th>");
sb.Append("</tr>");
for (int i = 0; i < Math.Min(100, series.Count); i++)
{
TBar item = series[i];
sb.Append("<tr>");
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0; font-size: {smallfont};'>{item.Time:yyyy-MM-dd HH:mm:ss}</td>");
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0; font-size: {smallfont};'>{i}</td>");
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0;'>{item.Open:F2}</td>");
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0;'>{item.High:F2}</td>");
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0;'>{item.Low:F2}</td>");
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0;'>{item.Close:F2}</td>");
sb.Append($"<td style='padding-left: {pad}px; padding-right: {pad}px; line-height: 1.0;'>{item.Volume:F2}</td>");
sb.Append("</tr>");
}
sb.Append("</table>");
if (series.Count > 100)
{
sb.Append("<p>Showing first 100 items. Total items: " + series.Count + "</p>");
}
writer.Write(sb.ToString());
}, HtmlFormatter.MimeType);
}
}
+57 -38
View File
@@ -2,60 +2,79 @@ using System.Runtime.CompilerServices;
namespace QuanTAlib;
public interface ITBar
{
DateTime Time { get; }
double Open { get; }
double High { get; }
double Low { get; }
double Close { get; }
double Volume { get; }
bool IsNew { get; }
}
/// <summary>
/// A lightweight struct representing an OHLCV bar.
/// Pure data type: 48 bytes (long + 5 doubles).
/// </summary>
[SkipLocalsInit]
public readonly record struct TBar(DateTime Time, double Open, double High, double Low, double Close, double Volume, bool IsNew = true) : ITBar
public readonly struct TBar : IEquatable<TBar>
{
public DateTime Time { get; init; } = Time;
public double Open { get; init; } = Open;
public double High { get; init; } = High;
public double Low { get; init; } = Low;
public double Close { get; init; } = Close;
public double Volume { get; init; } = Volume;
public bool IsNew { get; init; } = IsNew;
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 => (High + Low) * 0.5;
public double OC2 => (Open + Close) * 0.5;
public double OHL3 => (Open + High + Low) / 3;
public double HLC3 => (High + Low + Close) / 3;
public double OHL3 => (Open + High + Low) / 3.0;
public double HLC3 => (High + Low + Close) / 3.0;
public double OHLC4 => (Open + High + Low + Close) * 0.25;
public double HLCC4 => (High + Low + Close + Close) * 0.25;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TBar() : this(DateTime.UtcNow, 0, 0, 0, 0, 0) { }
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(double Open, double High, double Low, double Close, double Volume, bool IsNew = true)
: this(DateTime.UtcNow, Open, High, Low, Close, Volume, IsNew) { }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TBar(double value)
: this(Time: DateTime.UtcNow, Open: value, High: value, Low: value, Close: value, Volume: value, IsNew: true) { }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TBar(TValue value)
: this(Time: value.Time, Open: value.Value, High: value.Value, Low: value.Value, Close: value.Value, Volume: value.Value, IsNew: value.IsNew) { }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TBar(TBar v)
: this(Time: v.Time, Open: v.Open, High: v.High, Low: v.Low, Close: v.Close, Volume: v.Volume, IsNew: true) { }
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 DateTime(TBar tv) => tv.Time;
public static implicit operator DateTime(TBar bar) => new(bar.Time, DateTimeKind.Utc);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override string ToString() => $"[{Time:yyyy-MM-dd HH:mm:ss}: O={Open:F2}, H={High:F2}, L={Low:F2}, C={Close:F2}, V={Volume:F2}]";
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);
}
+112 -75
View File
@@ -1,110 +1,147 @@
using System.Collections;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
public delegate void BarSignal(object source, in TBarEventArgs args);
[SkipLocalsInit]
public sealed class TBarEventArgs : EventArgs
/// <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>
{
public readonly TBar Bar;
// 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();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TBarEventArgs(TBar bar) => Bar = bar;
}
public string Name { get; set; } = "Bar";
public event Action<TBar>? Pub;
[SkipLocalsInit]
public class TBarSeries : List<TBar>
{
private static readonly TBar Default = new(DateTime.MinValue, double.NaN, double.NaN, double.NaN, double.NaN, double.NaN);
// 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; }
public TSeries Open { get; init; }
public TSeries High { get; init; }
public TSeries Low { get; init; }
public TSeries Close { get; init; }
public TSeries Volume { get; init; }
// Aliases for convenience
public TSeries O => Open;
public TSeries H => High;
public TSeries L => Low;
public TSeries C => Close;
public TSeries V => Volume;
public TBar Last => Count > 0 ? this[^1] : Default;
public TBar First => Count > 0 ? this[0] : Default;
public int Length => Count;
public string Name { get; set; }
public event BarSignal Pub = delegate { };
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TBarSeries()
{
Name = "Bar";
Open = new TSeries();
High = new TSeries();
Low = new TSeries();
Close = new TSeries();
Volume = new TSeries();
// 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" };
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TBarSeries(object source) : this()
/// <summary>
/// Constructor with capacity hint to avoid List growth overhead.
/// </summary>
public TBarSeries(int capacity)
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new BarSignal(Sub));
_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" };
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public new virtual void Add(TBar bar)
public int Count
{
if (bar.IsNew || base.Count == 0)
[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)
{
base.Add(bar);
_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
{
this[^1] = bar;
int lastIdx = _c.Count - 1;
_t[lastIdx] = bar.Time;
_o[lastIdx] = bar.Open;
_h[lastIdx] = bar.High;
_l[lastIdx] = bar.Low;
_c[lastIdx] = bar.Close;
_v[lastIdx] = bar.Volume;
}
Pub?.Invoke(this, new TBarEventArgs(bar));
Open.Add(bar.Time, bar.Open, IsNew: bar.IsNew, IsHot: true);
High.Add(bar.Time, bar.High, IsNew: bar.IsNew, IsHot: true);
Low.Add(bar.Time, bar.Low, IsNew: bar.IsNew, IsHot: true);
Close.Add(bar.Time, bar.Close, IsNew: bar.IsNew, IsHot: true);
Volume.Add(bar.Time, bar.Volume, IsNew: bar.IsNew, IsHot: true);
Pub?.Invoke(bar);
}
[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, Open, High, Low, Close, Volume, IsNew));
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(double Open, double High, double Low, double Close, double Volume, bool IsNew = true) =>
Add(new TBar(DateTime.Now, Open, High, Low, Close, Volume, IsNew));
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);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(TBarSeries series)
public void Add(IEnumerable<long> t, IEnumerable<double> o, IEnumerable<double> h, IEnumerable<double> l, IEnumerable<double> c, IEnumerable<double> v)
{
if (series == this)
_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++)
{
// If adding itself, create a copy to avoid modification during enumeration
var copy = new TBarSeries { Name = Name };
copy.AddRange(this);
AddRange(copy);
}
else
{
AddRange(series);
yield return new TBar(_t[i], _o[i], _h[i], _l[i], _c[i], _v[i]);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public new virtual void AddRange(IEnumerable<TBar> collection)
{
foreach (var item in collection)
{
Add(item);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Sub(object source, in TBarEventArgs args)
{
Add(args.Bar);
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
+106 -82
View File
@@ -1,122 +1,146 @@
using System.Collections;
using System.Runtime.CompilerServices;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
namespace QuanTAlib;
public delegate void ValueSignal(object source, in ValueEventArgs args);
[SkipLocalsInit]
public sealed class ValueEventArgs : EventArgs
/// <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>
{
public readonly TValue Tick;
// 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;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ValueEventArgs(TValue value) => Tick = value;
}
public string Name { get; set; } = "Data";
[SkipLocalsInit]
public class TSeries : List<TValue>
{
private static readonly TValue Default = new(DateTime.MinValue, double.NaN);
// 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 IEnumerable<DateTime> t => this.Select(item => item.t);
public IEnumerable<double> v => this.Select(item => item.v);
public TValue Last => Count > 0 ? this[^1] : Default;
public TValue First => Count > 0 ? this[0] : Default;
public int Length => Count;
public string Name { get; set; }
public TSeries()
{
_t = new List<long>();
_v = new List<double>();
}
/// <summary>
/// Event that publishes value updates to subscribers. This event is used in the pub/sub pattern
/// where TSeries instances can subscribe to updates from other data sources through the Sub method,
/// and publish their own updates to downstream subscribers.
/// Constructor with capacity hint to avoid List growth overhead.
/// </summary>
[SuppressMessage("Minor Code Smell", "S3264:Events should be invoked", Justification = "Event is invoked through delegate")]
public event ValueSignal Pub = delegate { };
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TSeries()
public TSeries(int capacity)
{
Name = "Data";
_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 TSeries(object source) : this()
public virtual void Add(TValue value, bool isNew)
{
var pubEvent = source.GetType().GetEvent("Pub");
if (pubEvent != null)
if (isNew || _v.Count == 0)
{
pubEvent.AddEventHandler(source, new ValueSignal(Sub));
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static explicit operator List<double>(TSeries series) => series.Select(item => item.Value).ToList();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static explicit operator double[](TSeries series) => series.Select(item => item.Value).ToArray();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public new virtual void Add(TValue tick)
{
if (tick.IsNew || base.Count == 0)
{
base.Add(tick);
_t.Add(value.Time);
_v.Add(value.Value);
}
else
{
this[^1] = tick;
// Update last bar
int lastIdx = _v.Count - 1;
_t[lastIdx] = value.Time;
_v[lastIdx] = value.Value;
}
Pub?.Invoke(this, new ValueEventArgs(tick));
Pub?.Invoke(value);
}
// Overload for backward compatibility (assumes isNew=true)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual void Add(DateTime Time, double Value, bool IsNew = true, bool IsHot = true) =>
Add(new TValue(Time, Value, IsNew, IsHot));
public virtual void Add(TValue value) => Add(value, true);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual void Add(double Value, bool IsNew = true, bool IsHot = true) =>
Add(new TValue(DateTime.UtcNow, Value, IsNew, IsHot));
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)
{
var valueList = values.ToList();
int count = valueList.Count;
DateTime startTime = DateTime.UtcNow - TimeSpan.FromHours(count);
for (int i = 0; i < count; i++)
long t = DateTime.UtcNow.Ticks;
foreach (var v in values)
{
Add(startTime, valueList[i]);
startTime = startTime.AddHours(1);
Add(new TValue(t, v), isNew: true);
t += TimeSpan.TicksPerMinute; // Dummy time increment
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(TSeries series)
// IEnumerable implementation
public IEnumerator<TValue> GetEnumerator()
{
if (series == this)
for (int i = 0; i < _v.Count; i++)
{
// If adding itself, create a copy to avoid modification during enumeration
var copy = new TSeries { Name = Name };
copy.AddRange(this);
AddRange(copy);
}
else
{
AddRange(series);
yield return new TValue(_t[i], _v[i]);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public new virtual void AddRange(IEnumerable<TValue> collection)
{
foreach (var item in collection)
{
Add(item);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Sub(object source, in ValueEventArgs args) => Add(args.Tick);
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
+37 -21
View File
@@ -2,40 +2,56 @@ using System.Runtime.CompilerServices;
namespace QuanTAlib;
public interface ITValue
{
DateTime Time { get; }
double Value { get; }
bool IsNew { get; }
bool IsHot { get; }
}
/// <summary>
/// A lightweight struct representing a time-value pair.
/// Pure data type: 16 bytes (long + double).
/// </summary>
[SkipLocalsInit]
public readonly record struct TValue(DateTime Time, double Value, bool IsNew = true, bool IsHot = true) : ITValue
public readonly struct TValue : IEquatable<TValue>
{
public DateTime Time { get; init; } = Time;
public double Value { get; init; } = Value;
public bool IsNew { get; init; } = IsNew;
public bool IsHot { get; init; } = IsHot;
public DateTime t => Time;
public double v => Value;
/// <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() : this(DateTime.UtcNow, 0) { }
public TValue(long time, double value)
{
Time = time;
Value = value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue(double value, bool isNew = true, bool isHot = true)
: this(DateTime.UtcNow, value, IsNew: isNew, IsHot: isHot) { }
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) => tv.Time;
public static implicit operator DateTime(TValue tv) => new(tv.Time, DateTimeKind.Utc);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator TValue(double value) => new TValue(DateTime.UtcNow, value);
public override string ToString() => $"[{AsDateTime:yyyy-MM-dd HH:mm:ss}, {Value:F2}]";
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override string ToString() => $"[{Time:yyyy-MM-dd HH:mm:ss}, {Value:F2}, IsNew: {IsNew}, IsHot: {IsHot}]";
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);
}