mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-19 02:58:05 +00:00
SIMD Refactor: Merge simd-dev into dev (#55)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) <aider@aider.chat> Co-authored-by: Warp <agent@warp.dev>
This commit is contained in:
co-authored by
Claude Opus 4.5
aider
Warp
parent
5bcdf8d614
commit
86fe32a682
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Abstract base class for all indicators.
|
||||
/// Enforces a consistent contract for State, Name, WarmupPeriod, and core methods.
|
||||
/// </summary>
|
||||
public abstract class AbstractBase : ITValuePublisher, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Display name for the indicator.
|
||||
/// </summary>
|
||||
public string Name { get; protected init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Number of periods before the indicator is considered "hot" (valid).
|
||||
/// </summary>
|
||||
public int WarmupPeriod { get; protected init; }
|
||||
|
||||
/// <summary>
|
||||
/// Current value of the indicator.
|
||||
/// </summary>
|
||||
public TValue Last { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the indicator has enough data to produce valid results.
|
||||
/// </summary>
|
||||
public abstract bool IsHot { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Event triggered when a new TValue is available.
|
||||
/// </summary>
|
||||
public event TValuePublishedHandler? Pub;
|
||||
|
||||
/// <summary>
|
||||
/// Helper to invoke the Pub event.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected void PubEvent(TValue value, bool isNew = true)
|
||||
{
|
||||
Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the indicator state using the provided history.
|
||||
/// </summary>
|
||||
/// <param name="source">Historical data</param>
|
||||
/// <param name="step">Time interval between values (default: 1 second)</param>
|
||||
public abstract void Prime(ReadOnlySpan<double> source, TimeSpan? step = null);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a single value.
|
||||
/// </summary>
|
||||
/// <param name="input">Input value</param>
|
||||
/// <param name="isNew">True if this is a new bar, False if it's an update to the last bar</param>
|
||||
/// <returns>Updated value</returns>
|
||||
public abstract TValue Update(TValue input, bool isNew = true);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a series of values.
|
||||
/// </summary>
|
||||
/// <param name="source">Input series</param>
|
||||
/// <returns>Series of calculated values</returns>
|
||||
public abstract TSeries Update(TSeries source);
|
||||
|
||||
/// <summary>
|
||||
/// Resets the indicator to its initial state.
|
||||
/// </summary>
|
||||
public abstract void Reset();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Delegate for bi-input batch calculation methods.
|
||||
/// This custom delegate is required because Action<T1,T2,T3,T4> cannot accept
|
||||
/// ref struct types (Span, ReadOnlySpan) as generic parameters in .NET 8.0.
|
||||
/// </summary>
|
||||
/// <param name="actual">Actual values span</param>
|
||||
/// <param name="predicted">Predicted values span</param>
|
||||
/// <param name="output">Output span for results</param>
|
||||
/// <param name="period">Calculation period</param>
|
||||
public delegate void BiInputBatchDelegate(
|
||||
ReadOnlySpan<double> actual,
|
||||
ReadOnlySpan<double> predicted,
|
||||
Span<double> output,
|
||||
int period);
|
||||
|
||||
/// <summary>
|
||||
/// Abstract base class for bi-input indicators (indicators that require two inputs like error metrics).
|
||||
/// Provides common infrastructure for RingBuffer-based sliding window calculations with O(1) updates.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This base class eliminates code duplication across error indicators (MAE, MSE, RMSE, MAPE, etc.)
|
||||
/// by providing:
|
||||
/// - Common state management with bar correction (isNew semantics)
|
||||
/// - RingBuffer-based sliding window with running sum
|
||||
/// - Periodic resync for floating-point drift correction
|
||||
/// - NaN/Infinity handling with last-valid-value substitution
|
||||
/// - Template Method pattern: subclasses only implement ComputeError and optionally PostProcess
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public abstract class BiInputIndicatorBase : AbstractBase
|
||||
{
|
||||
protected readonly RingBuffer _buffer;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
protected record struct BiInputState(double Sum, double LastValidActual, double LastValidPredicted, int TickCount);
|
||||
|
||||
protected BiInputState _state;
|
||||
protected BiInputState _p_state;
|
||||
|
||||
protected const int ResyncInterval = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a bi-input indicator with specified period.
|
||||
/// </summary>
|
||||
/// <param name="period">Number of values to average (must be > 0)</param>
|
||||
/// <param name="name">Indicator name</param>
|
||||
protected BiInputIndicatorBase(int period, string name)
|
||||
{
|
||||
if (period <= 0)
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
|
||||
_buffer = new RingBuffer(period);
|
||||
Name = name;
|
||||
WarmupPeriod = period;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if the indicator has enough data to produce valid results.
|
||||
/// </summary>
|
||||
public override bool IsHot => _buffer.IsFull;
|
||||
|
||||
/// <summary>
|
||||
/// Period of the indicator.
|
||||
/// </summary>
|
||||
public int Period => _buffer.Capacity;
|
||||
|
||||
/// <summary>
|
||||
/// Computes the error value from actual and predicted values.
|
||||
/// Subclasses implement this to define their specific error computation.
|
||||
/// </summary>
|
||||
/// <param name="actual">Actual value</param>
|
||||
/// <param name="predicted">Predicted value</param>
|
||||
/// <returns>Error value to be averaged</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected abstract double ComputeError(double actual, double predicted);
|
||||
|
||||
/// <summary>
|
||||
/// Optional post-processing of the mean result.
|
||||
/// Default implementation returns the mean unchanged.
|
||||
/// Override for indicators like RMSE that need sqrt of mean.
|
||||
/// </summary>
|
||||
/// <param name="mean">The mean of error values</param>
|
||||
/// <returns>Post-processed result</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected virtual double PostProcess(double mean) => mean;
|
||||
|
||||
/// <summary>
|
||||
/// Sanitizes input value, substituting last valid value for NaN/Infinity.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double SanitizeActual(double value)
|
||||
{
|
||||
if (double.IsFinite(value))
|
||||
{
|
||||
_state.LastValidActual = value;
|
||||
return value;
|
||||
}
|
||||
return double.IsFinite(_state.LastValidActual) ? _state.LastValidActual : 0.0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sanitizes predicted value, substituting last valid value for NaN/Infinity.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double SanitizePredicted(double value)
|
||||
{
|
||||
if (double.IsFinite(value))
|
||||
{
|
||||
_state.LastValidPredicted = value;
|
||||
return value;
|
||||
}
|
||||
return double.IsFinite(_state.LastValidPredicted) ? _state.LastValidPredicted : 0.0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value to be removed from the running sum (oldest value or 0 if buffer not full).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double GetRemovedValue() =>
|
||||
_buffer.Count == _buffer.Capacity ? _buffer.Oldest : 0.0;
|
||||
|
||||
/// <summary>
|
||||
/// Processes a new bar update.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void ProcessNewBar(double error)
|
||||
{
|
||||
_p_state = _state;
|
||||
// Snapshot buffer state BEFORE Add so Restore can undo it
|
||||
_buffer.Snapshot();
|
||||
_state.Sum = _state.Sum - GetRemovedValue() + error;
|
||||
_buffer.Add(error);
|
||||
_state.TickCount++;
|
||||
|
||||
if (_buffer.IsFull && _state.TickCount >= ResyncInterval)
|
||||
{
|
||||
_state.TickCount = 0;
|
||||
_state.Sum = _buffer.RecalculateSum();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes a bar correction (same bar update).
|
||||
/// Uses O(1) differential update: restores buffer and scalar state, then applies the new error.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void ProcessBarCorrection(double error)
|
||||
{
|
||||
// Restore scalar state
|
||||
_state = _p_state;
|
||||
// Restore buffer to state before last Add (undoes the Add completely)
|
||||
_buffer.Restore();
|
||||
// Now add the new correction value (this overwrites the same slot)
|
||||
_buffer.Add(error);
|
||||
// Update sum from buffer (Add already updated it correctly)
|
||||
_state.Sum = _buffer.Sum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with new actual and predicted values.
|
||||
/// </summary>
|
||||
/// <param name="actual">Actual value</param>
|
||||
/// <param name="predicted">Predicted value</param>
|
||||
/// <param name="isNew">Whether this is a new bar</param>
|
||||
/// <returns>The calculated indicator value</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TValue actual, TValue predicted, bool isNew = true)
|
||||
{
|
||||
double actualVal = SanitizeActual(actual.Value);
|
||||
double predictedVal = SanitizePredicted(predicted.Value);
|
||||
double error = ComputeError(actualVal, predictedVal);
|
||||
|
||||
if (isNew)
|
||||
ProcessNewBar(error);
|
||||
else
|
||||
ProcessBarCorrection(error);
|
||||
|
||||
double mean = _buffer.Count > 0 ? _state.Sum / _buffer.Count : error;
|
||||
double result = PostProcess(mean);
|
||||
|
||||
Last = new TValue(actual.Time, result);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with raw double values.
|
||||
/// Uses DateTime.MinValue as a sentinel timestamp for performance in high-frequency scenarios.
|
||||
/// For time-sensitive applications, use Update(TValue, TValue, bool) with explicit timestamps.
|
||||
/// </summary>
|
||||
/// <param name="actual">Actual value</param>
|
||||
/// <param name="predicted">Predicted value</param>
|
||||
/// <param name="isNew">Whether this is a new bar</param>
|
||||
/// <returns>The calculated indicator value (with DateTime.MinValue as timestamp)</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(double actual, double predicted, bool isNew = true)
|
||||
{
|
||||
return Update(new TValue(DateTime.MinValue, actual), new TValue(DateTime.MinValue, predicted), isNew);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Single-input Update is not supported for bi-input indicators.
|
||||
/// </summary>
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
throw new NotSupportedException($"{Name} requires two inputs. Use Update(actual, predicted).");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Single-series Update is not supported for bi-input indicators.
|
||||
/// </summary>
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
throw new NotSupportedException($"{Name} requires two inputs. Use Calculate(actualSeries, predictedSeries, period).");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Single-series Prime is not supported for bi-input indicators.
|
||||
/// </summary>
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
throw new NotSupportedException($"{Name} requires two inputs.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the indicator state.
|
||||
/// </summary>
|
||||
public override void Reset()
|
||||
{
|
||||
_buffer.Clear();
|
||||
_state = default;
|
||||
_p_state = default;
|
||||
Last = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method for subclasses to implement static Calculate with TSeries.
|
||||
/// </summary>
|
||||
protected static TSeries CalculateImpl(
|
||||
TSeries actual,
|
||||
TSeries predicted,
|
||||
int period,
|
||||
BiInputBatchDelegate batchMethod)
|
||||
{
|
||||
if (actual.Count != predicted.Count)
|
||||
throw new ArgumentException("Actual and predicted series must have the same length", nameof(predicted));
|
||||
|
||||
int len = actual.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
var tSpan = CollectionsMarshal.AsSpan(t);
|
||||
var vSpan = CollectionsMarshal.AsSpan(v);
|
||||
|
||||
batchMethod(actual.Values, predicted.Values, vSpan, period);
|
||||
actual.Times.CopyTo(tSpan);
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Common validation for Batch methods.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected static void ValidateBatchInputs(
|
||||
ReadOnlySpan<double> actual,
|
||||
ReadOnlySpan<double> predicted,
|
||||
Span<double> output,
|
||||
int period)
|
||||
{
|
||||
if (actual.Length != predicted.Length || actual.Length != output.Length)
|
||||
throw new ArgumentException("All spans must have the same length", nameof(output));
|
||||
if (period <= 0)
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,893 @@
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class RingBufferTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_ValidCapacity_CreatesBuffer()
|
||||
{
|
||||
var buffer = new RingBuffer(10);
|
||||
|
||||
Assert.Equal(10, buffer.Capacity);
|
||||
Assert.Equal(0, buffer.Count);
|
||||
Assert.False(buffer.IsFull);
|
||||
Assert.Equal(0, buffer.Sum);
|
||||
Assert.Equal(0, buffer.Average);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ZeroCapacity_ThrowsArgumentException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new RingBuffer(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativeCapacity_ThrowsArgumentException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new RingBuffer(-1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_SingleValue_UpdatesState()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
|
||||
Assert.Equal(1, buffer.Count);
|
||||
Assert.Equal(10.0, buffer.Sum);
|
||||
Assert.Equal(10.0, buffer.Average);
|
||||
Assert.Equal(10.0, buffer.Newest);
|
||||
Assert.Equal(10.0, buffer.Oldest);
|
||||
Assert.False(buffer.IsFull);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_MultipleValues_UpdatesState()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
Assert.Equal(3, buffer.Count);
|
||||
Assert.Equal(60.0, buffer.Sum);
|
||||
Assert.Equal(20.0, buffer.Average);
|
||||
Assert.Equal(30.0, buffer.Newest);
|
||||
Assert.Equal(10.0, buffer.Oldest);
|
||||
Assert.False(buffer.IsFull);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_FillBuffer_BecomesFullAndWraps()
|
||||
{
|
||||
var buffer = new RingBuffer(3);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
Assert.Equal(3, buffer.Count);
|
||||
Assert.True(buffer.IsFull);
|
||||
Assert.Equal(60.0, buffer.Sum);
|
||||
Assert.Equal(20.0, buffer.Average);
|
||||
|
||||
// Add one more - should remove 10.0
|
||||
double removed = buffer.Add(40.0);
|
||||
|
||||
Assert.Equal(10.0, removed);
|
||||
Assert.Equal(3, buffer.Count);
|
||||
Assert.True(buffer.IsFull);
|
||||
Assert.Equal(90.0, buffer.Sum); // 20 + 30 + 40
|
||||
Assert.Equal(30.0, buffer.Average);
|
||||
Assert.Equal(40.0, buffer.Newest);
|
||||
Assert.Equal(20.0, buffer.Oldest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_MultipleWraps_MaintainsCorrectState()
|
||||
{
|
||||
var buffer = new RingBuffer(3);
|
||||
|
||||
// Fill and wrap multiple times
|
||||
for (int i = 1; i <= 10; i++)
|
||||
{
|
||||
buffer.Add(i * 10.0);
|
||||
}
|
||||
|
||||
// Should contain: 80, 90, 100
|
||||
Assert.Equal(3, buffer.Count);
|
||||
Assert.True(buffer.IsFull);
|
||||
Assert.Equal(270.0, buffer.Sum); // 80 + 90 + 100
|
||||
Assert.Equal(90.0, buffer.Average);
|
||||
Assert.Equal(100.0, buffer.Newest);
|
||||
Assert.Equal(80.0, buffer.Oldest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateNewest_ModifiesLastValue()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
Assert.Equal(60.0, buffer.Sum);
|
||||
|
||||
buffer.UpdateNewest(35.0);
|
||||
|
||||
Assert.Equal(65.0, buffer.Sum); // 10 + 20 + 35
|
||||
Assert.Equal(35.0, buffer.Newest);
|
||||
Assert.Equal(3, buffer.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateNewest_EmptyBuffer_DoesNothing()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.UpdateNewest(100.0); // Should not throw
|
||||
|
||||
Assert.Equal(0, buffer.Count);
|
||||
Assert.Equal(0, buffer.Sum);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_AccessesCorrectValues()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
// Index 0 = oldest, Index 2 = newest
|
||||
Assert.Equal(10.0, buffer[0]);
|
||||
Assert.Equal(20.0, buffer[1]);
|
||||
Assert.Equal(30.0, buffer[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_AfterWrap_AccessesCorrectValues()
|
||||
{
|
||||
var buffer = new RingBuffer(3);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
buffer.Add(40.0); // Wraps, removes 10
|
||||
|
||||
// Should contain: 20, 30, 40
|
||||
Assert.Equal(20.0, buffer[0]);
|
||||
Assert.Equal(30.0, buffer[1]);
|
||||
Assert.Equal(40.0, buffer[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_OutOfRange_ThrowsException()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
|
||||
// Valid indices are 0 and 1 (2 elements)
|
||||
// Index 2 should throw ArgumentOutOfRangeException
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => _ = buffer[2]);
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => _ = buffer[10]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_ResetsState()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
buffer.Clear();
|
||||
|
||||
Assert.Equal(0, buffer.Count);
|
||||
Assert.Equal(0, buffer.Sum);
|
||||
Assert.Equal(0, buffer.Average);
|
||||
Assert.False(buffer.IsFull);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_AllowsReuse()
|
||||
{
|
||||
var buffer = new RingBuffer(3);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
buffer.Clear();
|
||||
|
||||
buffer.Add(100.0);
|
||||
|
||||
Assert.Equal(1, buffer.Count);
|
||||
Assert.Equal(100.0, buffer.Sum);
|
||||
Assert.Equal(100.0, buffer.Newest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clone_CreatesIndependentCopy()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
var clone = buffer.Clone();
|
||||
|
||||
// Verify clone has same state
|
||||
Assert.Equal(buffer.Count, clone.Count);
|
||||
Assert.Equal(buffer.Sum, clone.Sum);
|
||||
Assert.Equal(buffer.Newest, clone.Newest);
|
||||
Assert.Equal(buffer.Oldest, clone.Oldest);
|
||||
|
||||
// Modify original - clone should be unaffected
|
||||
buffer.Add(40.0);
|
||||
|
||||
Assert.Equal(4, buffer.Count);
|
||||
Assert.Equal(3, clone.Count);
|
||||
Assert.Equal(100.0, buffer.Sum);
|
||||
Assert.Equal(60.0, clone.Sum);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CopyFrom_CopiesState()
|
||||
{
|
||||
var source = new RingBuffer(5);
|
||||
var target = new RingBuffer(5);
|
||||
|
||||
source.Add(10.0);
|
||||
source.Add(20.0);
|
||||
source.Add(30.0);
|
||||
|
||||
target.Add(100.0); // Different initial state
|
||||
|
||||
target.CopyFrom(source);
|
||||
|
||||
Assert.Equal(source.Count, target.Count);
|
||||
Assert.Equal(source.Sum, target.Sum);
|
||||
Assert.Equal(source.Newest, target.Newest);
|
||||
Assert.Equal(source.Oldest, target.Oldest);
|
||||
|
||||
// Verify independence after copy
|
||||
source.Add(40.0);
|
||||
Assert.NotEqual(source.Sum, target.Sum);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CopyFrom_DifferentCapacity_ThrowsException()
|
||||
{
|
||||
var source = new RingBuffer(5);
|
||||
var target = new RingBuffer(10);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => target.CopyFrom(source));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSpan_ReturnsChronologicalOrder()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
var span = buffer.GetSpan();
|
||||
|
||||
Assert.Equal(3, span.Length);
|
||||
Assert.Equal(10.0, span[0]);
|
||||
Assert.Equal(20.0, span[1]);
|
||||
Assert.Equal(30.0, span[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSpan_AfterWrap_ReturnsChronologicalOrder()
|
||||
{
|
||||
var buffer = new RingBuffer(3);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
buffer.Add(40.0);
|
||||
buffer.Add(50.0);
|
||||
|
||||
var span = buffer.GetSpan();
|
||||
|
||||
// Should be: 30, 40, 50
|
||||
Assert.Equal(3, span.Length);
|
||||
Assert.Equal(30.0, span[0]);
|
||||
Assert.Equal(40.0, span[1]);
|
||||
Assert.Equal(50.0, span[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSpan_EmptyBuffer_ReturnsEmpty()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
var span = buffer.GetSpan();
|
||||
|
||||
Assert.True(span.IsEmpty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Min_ReturnsMinimumValue()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(30.0);
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(50.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(40.0);
|
||||
|
||||
Assert.Equal(10.0, buffer.Min());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Max_ReturnsMaximumValue()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(30.0);
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(50.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(40.0);
|
||||
|
||||
Assert.Equal(50.0, buffer.Max());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Min_EmptyBuffer_ReturnsNaN()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
Assert.True(double.IsNaN(buffer.Min()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Max_EmptyBuffer_ReturnsNaN()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
Assert.True(double.IsNaN(buffer.Max()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Enumerator_IteratesInChronologicalOrder()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
List<double> values = [];
|
||||
foreach (var v in buffer)
|
||||
{
|
||||
values.Add(v);
|
||||
}
|
||||
|
||||
Assert.Equal(3, values.Count);
|
||||
Assert.Equal(10.0, values[0]);
|
||||
Assert.Equal(20.0, values[1]);
|
||||
Assert.Equal(30.0, values[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Enumerator_AfterWrap_IteratesInChronologicalOrder()
|
||||
{
|
||||
var buffer = new RingBuffer(3);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
buffer.Add(40.0);
|
||||
buffer.Add(50.0);
|
||||
|
||||
List<double> values = [];
|
||||
foreach (var v in buffer)
|
||||
{
|
||||
values.Add(v);
|
||||
}
|
||||
|
||||
// Should be: 30, 40, 50
|
||||
Assert.Equal(3, values.Count);
|
||||
Assert.Equal(30.0, values[0]);
|
||||
Assert.Equal(40.0, values[1]);
|
||||
Assert.Equal(50.0, values[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithIsNew_WorksCorrectly()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0, isNew: true);
|
||||
buffer.Add(20.0, isNew: true);
|
||||
buffer.Add(25.0, isNew: false); // Should update 20.0 to 25.0
|
||||
|
||||
Assert.Equal(2, buffer.Count);
|
||||
Assert.Equal(25.0, buffer.Newest);
|
||||
Assert.Equal(35.0, buffer.Sum); // 10 + 25
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_WithIndexType_SupportsFromEnd()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
Assert.Equal(30.0, buffer[^1]); // Newest
|
||||
Assert.Equal(20.0, buffer[^2]);
|
||||
Assert.Equal(10.0, buffer[^3]); // Oldest
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Newest_EmptyBuffer_ReturnsNaN()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
Assert.True(double.IsNaN(buffer.Newest));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Oldest_EmptyBuffer_ReturnsNaN()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
Assert.True(double.IsNaN(buffer.Oldest));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Average_EmptyBuffer_ReturnsZero()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
Assert.Equal(0, buffer.Average);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetInternalSpan_ReturnsFullBuffer()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
|
||||
var span = buffer.GetInternalSpan();
|
||||
|
||||
Assert.Equal(5, span.Length); // Full capacity, not count
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToArray_AfterWrap_ReturnsChronologicalOrder()
|
||||
{
|
||||
var buffer = new RingBuffer(3);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
buffer.Add(40.0); // Wraps
|
||||
|
||||
var arr = buffer.ToArray();
|
||||
|
||||
Assert.Equal(3, arr.Length);
|
||||
Assert.Equal(20.0, arr[0]);
|
||||
Assert.Equal(30.0, arr[1]);
|
||||
Assert.Equal(40.0, arr[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CopyTo_AfterWrap_CopiesInChronologicalOrder()
|
||||
{
|
||||
var buffer = new RingBuffer(3);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
buffer.Add(40.0); // Wraps
|
||||
|
||||
var dest = new double[5];
|
||||
buffer.CopyTo(dest, 1);
|
||||
|
||||
Assert.Equal(0, dest[0]); // Untouched
|
||||
Assert.Equal(20.0, dest[1]);
|
||||
Assert.Equal(30.0, dest[2]);
|
||||
Assert.Equal(40.0, dest[3]);
|
||||
Assert.Equal(0, dest[4]); // Untouched
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_Set_UpdatesValueAndSum()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
Assert.Equal(60.0, buffer.Sum);
|
||||
|
||||
buffer[1] = 25.0; // Change 20.0 to 25.0
|
||||
|
||||
Assert.Equal(65.0, buffer.Sum);
|
||||
Assert.Equal(25.0, buffer[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_SetFromEnd_UpdatesValueAndSum()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
buffer[^1] = 35.0; // Change newest (30.0) to 35.0
|
||||
|
||||
Assert.Equal(65.0, buffer.Sum);
|
||||
Assert.Equal(35.0, buffer[^1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Min_LargeBuffer_UsesSimd()
|
||||
{
|
||||
var buffer = new RingBuffer(100);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
buffer.Add(i + 1); // 1 to 100
|
||||
}
|
||||
|
||||
Assert.Equal(1.0, buffer.Min());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Max_LargeBuffer_UsesSimd()
|
||||
{
|
||||
var buffer = new RingBuffer(100);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
buffer.Add(i + 1); // 1 to 100
|
||||
}
|
||||
|
||||
Assert.Equal(100.0, buffer.Max());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToArray_EmptyBuffer_ReturnsEmpty()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
var arr = buffer.ToArray();
|
||||
|
||||
Assert.Empty(arr);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CopyTo_EmptyBuffer_DoesNothing()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
double[] dest = [1.0, 2.0, 3.0];
|
||||
|
||||
buffer.CopyTo(dest, 0);
|
||||
|
||||
Assert.Equal(1.0, dest[0]);
|
||||
Assert.Equal(2.0, dest[1]);
|
||||
Assert.Equal(3.0, dest[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InternalBuffer_ReturnsSpan()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
|
||||
var span = buffer.InternalBuffer;
|
||||
|
||||
Assert.Equal(5, span.Length);
|
||||
Assert.Equal(10.0, span[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Enumerator_Reset_AllowsReIteration()
|
||||
{
|
||||
var buffer = new RingBuffer(3);
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
|
||||
var enumerator = buffer.GetEnumerator();
|
||||
|
||||
// First iteration
|
||||
Assert.True(enumerator.MoveNext());
|
||||
Assert.Equal(10.0, enumerator.Current);
|
||||
Assert.True(enumerator.MoveNext());
|
||||
Assert.Equal(20.0, enumerator.Current);
|
||||
Assert.False(enumerator.MoveNext());
|
||||
|
||||
// Reset and iterate again
|
||||
enumerator.Reset();
|
||||
Assert.True(enumerator.MoveNext());
|
||||
Assert.Equal(10.0, enumerator.Current);
|
||||
|
||||
enumerator.Dispose(); // Coverage for Dispose
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IEnumerable_GetEnumerator_Works()
|
||||
{
|
||||
var buffer = new RingBuffer(3);
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
|
||||
IEnumerable<double> enumerable = buffer;
|
||||
List<double> values = [];
|
||||
foreach (var v in enumerable)
|
||||
{
|
||||
values.Add(v);
|
||||
}
|
||||
|
||||
Assert.Equal(2, values.Count);
|
||||
Assert.Equal(10.0, values[0]);
|
||||
Assert.Equal(20.0, values[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IEnumerable_NonGeneric_GetEnumerator_Works()
|
||||
{
|
||||
var buffer = new RingBuffer(3);
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
|
||||
IEnumerable enumerable = buffer;
|
||||
List<double> values = [];
|
||||
foreach (var v in enumerable)
|
||||
{
|
||||
values.Add((double)v);
|
||||
}
|
||||
|
||||
Assert.Equal(2, values.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_Set_AfterWrap_UpdatesCorrectly()
|
||||
{
|
||||
var buffer = new RingBuffer(3);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
buffer.Add(40.0); // Wraps - now has 20, 30, 40
|
||||
|
||||
buffer[0] = 25.0; // Change oldest (20.0) to 25.0
|
||||
|
||||
Assert.Equal(95.0, buffer.Sum); // 25 + 30 + 40
|
||||
Assert.Equal(25.0, buffer[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithIsNew_EmptyBuffer_AddsValue()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0, isNew: false); // isNew=false but buffer empty, should still add
|
||||
|
||||
Assert.Equal(1, buffer.Count);
|
||||
Assert.Equal(10.0, buffer.Newest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BarCorrection_Workflow()
|
||||
{
|
||||
// Simulate bar correction (isNew=false) workflow
|
||||
var buffer = new RingBuffer(3);
|
||||
var backup = new RingBuffer(3);
|
||||
|
||||
// Add values as new bars
|
||||
buffer.Add(10.0);
|
||||
backup.CopyFrom(buffer);
|
||||
|
||||
buffer.Add(20.0);
|
||||
backup.CopyFrom(buffer);
|
||||
|
||||
buffer.Add(30.0);
|
||||
backup.CopyFrom(buffer);
|
||||
|
||||
double avgBeforeCorrection = buffer.Average;
|
||||
|
||||
// Simulate correction (isNew=false)
|
||||
buffer.CopyFrom(backup); // Restore previous state
|
||||
buffer.UpdateNewest(35.0); // Update with corrected value
|
||||
|
||||
// Average should reflect the correction
|
||||
Assert.Equal(21.666666666666668, buffer.Average, 1e-10);
|
||||
Assert.NotEqual(avgBeforeCorrection, buffer.Average);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Snapshot_CapturesCurrentState()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
buffer.Snapshot();
|
||||
|
||||
// Modify buffer after snapshot
|
||||
buffer.Add(40.0);
|
||||
|
||||
Assert.Equal(4, buffer.Count);
|
||||
Assert.Equal(100.0, buffer.Sum); // 10 + 20 + 30 + 40
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Restore_ReturnsToSnapshotState()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
buffer.Snapshot();
|
||||
double sumBeforeModification = buffer.Sum;
|
||||
int countBeforeModification = buffer.Count;
|
||||
|
||||
// Modify buffer after snapshot
|
||||
buffer.Add(40.0);
|
||||
Assert.Equal(4, buffer.Count);
|
||||
|
||||
// Restore to snapshot state
|
||||
buffer.Restore();
|
||||
|
||||
Assert.Equal(countBeforeModification, buffer.Count);
|
||||
Assert.Equal(sumBeforeModification, buffer.Sum);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Snapshot_Restore_WithWrapping()
|
||||
{
|
||||
var buffer = new RingBuffer(3);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
buffer.Snapshot();
|
||||
|
||||
// Add value that causes wrap
|
||||
buffer.Add(40.0);
|
||||
Assert.Equal(90.0, buffer.Sum); // 20 + 30 + 40
|
||||
|
||||
buffer.Restore();
|
||||
|
||||
Assert.Equal(60.0, buffer.Sum); // 10 + 20 + 30
|
||||
Assert.Equal(30.0, buffer.Newest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecalculateSum_CorrectsDrift()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
double recalculated = buffer.RecalculateSum();
|
||||
|
||||
Assert.Equal(60.0, recalculated);
|
||||
Assert.Equal(60.0, buffer.Sum);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecalculateSum_AfterMultipleOperations()
|
||||
{
|
||||
var buffer = new RingBuffer(3);
|
||||
|
||||
// Simulate many operations that could accumulate floating-point drift
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
buffer.Add(i * 0.1);
|
||||
}
|
||||
|
||||
double recalculated = buffer.RecalculateSum();
|
||||
|
||||
// Should be equal (or very close) since we're using exact values
|
||||
Assert.Equal(recalculated, buffer.Sum);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StartIndex_EmptyBuffer_ReturnsZero()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
Assert.Equal(0, buffer.StartIndex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StartIndex_PartiallyFilled_ReturnsZero()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
|
||||
Assert.Equal(0, buffer.StartIndex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StartIndex_FullBuffer_ReturnsHead()
|
||||
{
|
||||
var buffer = new RingBuffer(3);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
buffer.Add(40.0); // Wraps
|
||||
|
||||
// StartIndex should point to oldest element
|
||||
Assert.True(buffer.StartIndex >= 0 && buffer.StartIndex < buffer.Capacity);
|
||||
Assert.Equal(20.0, buffer.Oldest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_NegativeIndexViaFromEnd_ThrowsWhenOutOfBounds()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
// ^4 when count=3 should throw
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => _ = buffer[^4]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CopyTo_InsufficientDestinationBuffer_Behavior()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
buffer.Add(30.0);
|
||||
|
||||
var dest = new double[2]; // Too small
|
||||
|
||||
// This will throw IndexOutOfRangeException since we're copying 3 elements to size-2 array
|
||||
Assert.Throws<ArgumentException>(() => buffer.CopyTo(dest, 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CopyTo_StartIndexOutOfRange_Behavior()
|
||||
{
|
||||
var buffer = new RingBuffer(5);
|
||||
|
||||
buffer.Add(10.0);
|
||||
buffer.Add(20.0);
|
||||
|
||||
var dest = new double[5];
|
||||
|
||||
// Starting at index 4 with 2 elements should fail
|
||||
Assert.Throws<ArgumentException>(() => buffer.CopyTo(dest, 4));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,642 @@
|
||||
using System.Collections;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// A high-performance circular buffer for double values optimized for SIMD operations.
|
||||
/// Uses pinned memory and maintains running sum for O(1) average calculations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Key characteristics:
|
||||
/// - Fixed capacity set at construction
|
||||
/// - Pinned memory for SIMD compatibility
|
||||
/// - O(1) Add and Sum operations via running sum
|
||||
/// - SIMD-accelerated Min/Max operations
|
||||
/// - Direct span access when buffer is contiguous
|
||||
/// - Thread-unsafe for maximum performance
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class RingBuffer : IEnumerable<double>
|
||||
{
|
||||
private readonly double[] _buffer;
|
||||
private int _head;
|
||||
private int _count;
|
||||
private double _sum;
|
||||
|
||||
private int _savedHead;
|
||||
private int _savedCount;
|
||||
private double _savedSum;
|
||||
private double _savedValue;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new RingBuffer with the specified capacity.
|
||||
/// Uses pinned memory for SIMD compatibility.
|
||||
/// </summary>
|
||||
/// <param name="capacity">Maximum number of elements (must be > 0)</param>
|
||||
public RingBuffer(int capacity)
|
||||
{
|
||||
if (capacity <= 0)
|
||||
throw new ArgumentException("Capacity must be greater than 0", nameof(capacity));
|
||||
|
||||
Capacity = capacity;
|
||||
_buffer = GC.AllocateArray<double>(capacity, pinned: true);
|
||||
_head = 0;
|
||||
_count = 0;
|
||||
_sum = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of elements the buffer can hold.
|
||||
/// </summary>
|
||||
public int Capacity { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Current number of elements in the buffer.
|
||||
/// </summary>
|
||||
public int Count
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if the buffer is full (Count == Capacity).
|
||||
/// </summary>
|
||||
public bool IsFull
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _count == Capacity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Running sum of all elements in the buffer.
|
||||
/// O(1) operation using maintained running sum.
|
||||
/// </summary>
|
||||
public double Sum
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _sum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recalculates the sum by iterating over all elements.
|
||||
/// Useful for correcting floating-point drift after many updates.
|
||||
/// Uses GetSequencedSpans to avoid allocation when buffer wraps.
|
||||
/// </summary>
|
||||
public double RecalculateSum()
|
||||
{
|
||||
double sum = 0;
|
||||
GetSequencedSpans(out var first, out var second);
|
||||
|
||||
for (int i = 0; i < first.Length; i++)
|
||||
{
|
||||
sum += first[i];
|
||||
}
|
||||
for (int i = 0; i < second.Length; i++)
|
||||
{
|
||||
sum += second[i];
|
||||
}
|
||||
|
||||
_sum = sum;
|
||||
return sum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Average of all elements in the buffer.
|
||||
/// Returns 0 if buffer is empty.
|
||||
/// </summary>
|
||||
public double Average
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _count > 0 ? _sum / _count : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the newest (most recently added) value.
|
||||
/// Returns double.NaN if buffer is empty.
|
||||
/// </summary>
|
||||
public double Newest
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get
|
||||
{
|
||||
if (_count == 0) return double.NaN;
|
||||
int idx = (_head - 1 + Capacity) % Capacity;
|
||||
return _buffer[idx];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the oldest value in the buffer.
|
||||
/// Returns double.NaN if buffer is empty.
|
||||
/// </summary>
|
||||
public double Oldest
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get
|
||||
{
|
||||
if (_count == 0) return double.NaN;
|
||||
int start = _count == Capacity ? _head : 0;
|
||||
return _buffer[start];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the index in the internal buffer where the oldest element is located.
|
||||
/// </summary>
|
||||
public int StartIndex
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _count == Capacity ? _head : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a read-only span over the internal buffer array for direct SIMD access.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<double> InternalBuffer
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _buffer.AsSpan();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a value to the buffer.
|
||||
/// If full, the oldest value is overwritten and its value is subtracted from the sum.
|
||||
/// Returns the value that was removed (0 if buffer was not full).
|
||||
/// </summary>
|
||||
/// <param name="value">Value to add</param>
|
||||
/// <returns>The removed oldest value, or 0 if buffer was not full</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public double Add(double value)
|
||||
{
|
||||
double removed = 0;
|
||||
|
||||
if (_count == Capacity)
|
||||
{
|
||||
removed = _buffer[_head];
|
||||
_sum = Math.FusedMultiplyAdd(-1.0, removed, _sum + value);
|
||||
}
|
||||
else
|
||||
{
|
||||
_count++;
|
||||
_sum += value;
|
||||
}
|
||||
|
||||
_buffer[_head] = value;
|
||||
_head = (_head + 1) % Capacity;
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a value with support for bar correction semantics.
|
||||
/// </summary>
|
||||
/// <param name="value">Value to add</param>
|
||||
/// <param name="isNew">True for new bar, false for update to current bar</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Add(double value, bool isNew)
|
||||
{
|
||||
if (isNew || _count == 0)
|
||||
{
|
||||
Add(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateNewest(value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the newest (most recently added) value.
|
||||
/// This is used for bar correction (isNew=false semantics).
|
||||
/// </summary>
|
||||
/// <param name="value">New value to replace the newest</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void UpdateNewest(double value)
|
||||
{
|
||||
if (_count == 0) return;
|
||||
|
||||
int idx = (_head - 1 + Capacity) % Capacity;
|
||||
double oldValue = _buffer[idx];
|
||||
_sum = Math.FusedMultiplyAdd(-1.0, oldValue, _sum + value);
|
||||
_buffer[idx] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets element at the specified index (0 = oldest, Count-1 = newest).
|
||||
/// Supports negative indexing via Index type.
|
||||
/// </summary>
|
||||
public double this[Index index]
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => GetAt(index);
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
set => SetAt(index, value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double GetAt(Index index)
|
||||
{
|
||||
int actualIndex = index.IsFromEnd ? _count - index.Value : index.Value;
|
||||
#pragma warning disable S3236 // Caller information arguments should not be provided explicitly - intentionally using cleaner parameter name
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)actualIndex, (uint)_count, nameof(index));
|
||||
#pragma warning restore S3236
|
||||
|
||||
int start = _count == Capacity ? _head : 0;
|
||||
int bufferIdx = (start + actualIndex) % Capacity;
|
||||
return _buffer[bufferIdx];
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void SetAt(Index index, double value)
|
||||
{
|
||||
int actualIndex = index.IsFromEnd ? _count - index.Value : index.Value;
|
||||
#pragma warning disable S3236 // Caller information arguments should not be provided explicitly - intentionally using cleaner parameter name
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)actualIndex, (uint)_count, nameof(index));
|
||||
#pragma warning restore S3236
|
||||
|
||||
int start = _count == Capacity ? _head : 0;
|
||||
int bufferIdx = (start + actualIndex) % Capacity;
|
||||
|
||||
double oldValue = _buffer[bufferIdx];
|
||||
_sum = Math.FusedMultiplyAdd(-1.0, oldValue, _sum + value);
|
||||
_buffer[bufferIdx] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a span over the buffer contents.
|
||||
/// If buffer is contiguous, returns direct span (SIMD-friendly).
|
||||
/// If wrapped, returns span over a copy.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b> Allocation Warning:</b> When the buffer wraps around (i.e., when data spans
|
||||
/// from the end of the internal array back to the beginning), this method allocates a new
|
||||
/// array via <see cref="ToArray"/> to return contiguous data. For allocation-free iteration
|
||||
/// over wrapped buffers, use <see cref="GetSequencedSpans"/> instead.</para>
|
||||
/// </remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ReadOnlySpan<double> GetSpan()
|
||||
{
|
||||
if (_count == 0) return ReadOnlySpan<double>.Empty;
|
||||
|
||||
int start = _count == Capacity ? _head : 0;
|
||||
|
||||
if (start + _count <= Capacity)
|
||||
{
|
||||
return new ReadOnlySpan<double>(_buffer, start, _count);
|
||||
}
|
||||
|
||||
return new ReadOnlySpan<double>(ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a span over the entire internal buffer (for advanced SIMD use).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ReadOnlySpan<double> GetInternalSpan() => _buffer.AsSpan();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the two sequential spans that make up the buffer contents in chronological order (Oldest to Newest).
|
||||
/// <param name="first">The first segment of data.</param>
|
||||
/// <param name="second">The second segment of data (empty if buffer is contiguous).</param>
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void GetSequencedSpans(out ReadOnlySpan<double> first, out ReadOnlySpan<double> second)
|
||||
{
|
||||
if (_count == 0)
|
||||
{
|
||||
first = default;
|
||||
second = default;
|
||||
return;
|
||||
}
|
||||
|
||||
int start = _count == Capacity ? _head : 0;
|
||||
int firstLen = Math.Min(_count, Capacity - start);
|
||||
|
||||
first = new ReadOnlySpan<double>(_buffer, start, firstLen);
|
||||
|
||||
second = _count > firstLen
|
||||
? new ReadOnlySpan<double>(_buffer, 0, _count - firstLen)
|
||||
: default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the maximum value in the buffer using SIMD acceleration.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public double Max()
|
||||
{
|
||||
if (_count == 0) return double.NaN;
|
||||
return MaxSimd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the minimum value in the buffer using SIMD acceleration.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public double Min()
|
||||
{
|
||||
if (_count == 0) return double.NaN;
|
||||
return MinSimd();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private double MaxSimd()
|
||||
{
|
||||
GetSequencedSpans(out var first, out var second);
|
||||
var vectorSize = Vector<double>.Count;
|
||||
var maxVector = new Vector<double>(double.MinValue);
|
||||
double max = double.MinValue;
|
||||
|
||||
// Process first span with SIMD
|
||||
int i = 0;
|
||||
if (first.Length >= vectorSize)
|
||||
{
|
||||
ref double firstRef = ref MemoryMarshal.GetReference(first);
|
||||
for (; i <= first.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
maxVector = Vector.Max(maxVector, Unsafe.As<double, Vector<double>>(ref Unsafe.Add(ref firstRef, i)));
|
||||
}
|
||||
}
|
||||
// Scalar remainder of first span
|
||||
for (; i < first.Length; i++)
|
||||
{
|
||||
max = Math.Max(max, first[i]);
|
||||
}
|
||||
|
||||
// Process second span with SIMD (if wrapped)
|
||||
i = 0;
|
||||
if (second.Length >= vectorSize)
|
||||
{
|
||||
ref double secondRef = ref MemoryMarshal.GetReference(second);
|
||||
for (; i <= second.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
maxVector = Vector.Max(maxVector, Unsafe.As<double, Vector<double>>(ref Unsafe.Add(ref secondRef, i)));
|
||||
}
|
||||
}
|
||||
// Scalar remainder of second span
|
||||
for (; i < second.Length; i++)
|
||||
{
|
||||
max = Math.Max(max, second[i]);
|
||||
}
|
||||
|
||||
// Reduce vector to scalar
|
||||
for (int j = 0; j < vectorSize; j++)
|
||||
{
|
||||
max = Math.Max(max, maxVector[j]);
|
||||
}
|
||||
|
||||
return max;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private double MinSimd()
|
||||
{
|
||||
GetSequencedSpans(out var first, out var second);
|
||||
var vectorSize = Vector<double>.Count;
|
||||
var minVector = new Vector<double>(double.MaxValue);
|
||||
double min = double.MaxValue;
|
||||
|
||||
// Process first span with SIMD
|
||||
int i = 0;
|
||||
if (first.Length >= vectorSize)
|
||||
{
|
||||
ref double firstRef = ref MemoryMarshal.GetReference(first);
|
||||
for (; i <= first.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
minVector = Vector.Min(minVector, Unsafe.As<double, Vector<double>>(ref Unsafe.Add(ref firstRef, i)));
|
||||
}
|
||||
}
|
||||
// Scalar remainder of first span
|
||||
for (; i < first.Length; i++)
|
||||
{
|
||||
min = Math.Min(min, first[i]);
|
||||
}
|
||||
|
||||
// Process second span with SIMD (if wrapped)
|
||||
i = 0;
|
||||
if (second.Length >= vectorSize)
|
||||
{
|
||||
ref double secondRef = ref MemoryMarshal.GetReference(second);
|
||||
for (; i <= second.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
minVector = Vector.Min(minVector, Unsafe.As<double, Vector<double>>(ref Unsafe.Add(ref secondRef, i)));
|
||||
}
|
||||
}
|
||||
// Scalar remainder of second span
|
||||
for (; i < second.Length; i++)
|
||||
{
|
||||
min = Math.Min(min, second[i]);
|
||||
}
|
||||
|
||||
// Reduce vector to scalar
|
||||
for (int j = 0; j < vectorSize; j++)
|
||||
{
|
||||
min = Math.Min(min, minVector[j]);
|
||||
}
|
||||
|
||||
return min;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all elements from the buffer.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
Array.Clear(_buffer, 0, _buffer.Length);
|
||||
_head = 0;
|
||||
_count = 0;
|
||||
_sum = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the buffer elements to a new array in chronological order.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public double[] ToArray()
|
||||
{
|
||||
if (_count == 0) return Array.Empty<double>();
|
||||
|
||||
double[] array = new double[_count];
|
||||
CopyTo(array, 0);
|
||||
return array;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies elements to destination array starting at destinationIndex.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void CopyTo(double[] destination, int destinationIndex)
|
||||
{
|
||||
if (_count == 0) return;
|
||||
|
||||
int start = _count == Capacity ? _head : 0;
|
||||
|
||||
if (start + _count <= Capacity)
|
||||
{
|
||||
Array.Copy(_buffer, start, destination, destinationIndex, _count);
|
||||
}
|
||||
else
|
||||
{
|
||||
int firstPartLength = Capacity - start;
|
||||
Array.Copy(_buffer, start, destination, destinationIndex, firstPartLength);
|
||||
Array.Copy(_buffer, 0, destination, destinationIndex + firstPartLength, _count - firstPartLength);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies elements to a destination span in chronological order.
|
||||
/// Destination must have at least Count elements.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void CopyTo(Span<double> destination)
|
||||
{
|
||||
if (_count == 0) return;
|
||||
|
||||
int start = _count == Capacity ? _head : 0;
|
||||
|
||||
if (start + _count <= Capacity)
|
||||
{
|
||||
_buffer.AsSpan(start, _count).CopyTo(destination);
|
||||
}
|
||||
else
|
||||
{
|
||||
int firstPartLength = Capacity - start;
|
||||
_buffer.AsSpan(start, firstPartLength).CopyTo(destination);
|
||||
_buffer.AsSpan(0, _count - firstPartLength).CopyTo(destination.Slice(firstPartLength));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a copy of the current state for bar correction support.
|
||||
/// </summary>
|
||||
public RingBuffer Clone()
|
||||
{
|
||||
var clone = new RingBuffer(Capacity);
|
||||
Array.Copy(_buffer, clone._buffer, Capacity);
|
||||
clone._head = _head;
|
||||
clone._count = _count;
|
||||
clone._sum = _sum;
|
||||
return clone;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies state from another RingBuffer.
|
||||
/// Both buffers must have the same capacity.
|
||||
/// </summary>
|
||||
public void CopyFrom(RingBuffer source)
|
||||
{
|
||||
if (source.Capacity != Capacity)
|
||||
throw new ArgumentException("Source buffer must have same capacity", nameof(source));
|
||||
|
||||
Array.Copy(source._buffer, _buffer, Capacity);
|
||||
_head = source._head;
|
||||
_count = source._count;
|
||||
_sum = source._sum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures the current state of the buffer.
|
||||
/// Must be called BEFORE adding a new value if you intend to Restore later.
|
||||
/// Saves the value at _head position (which will be overwritten by the next Add).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Snapshot()
|
||||
{
|
||||
_savedHead = _head;
|
||||
_savedCount = _count;
|
||||
_savedSum = _sum;
|
||||
// Save the value that will be overwritten by the next Add()
|
||||
// When buffer is full, Add() will overwrite _buffer[_head] (the oldest value)
|
||||
// When buffer is not full, _buffer[_head] is undefined but we save it anyway
|
||||
_savedValue = _buffer[_head];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores the buffer to the state captured by Snapshot.
|
||||
/// This restores the buffer to its state before the last Add() operation.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Restore()
|
||||
{
|
||||
_head = _savedHead;
|
||||
_count = _savedCount;
|
||||
_sum = _savedSum;
|
||||
// Restore the value at _head position that was saved before the Add()
|
||||
_buffer[_head] = _savedValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an enumerator that iterates through the buffer in chronological order.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Enumerator GetEnumerator() => new(this);
|
||||
|
||||
IEnumerator<double> IEnumerable<double>.GetEnumerator() => GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
/// <summary>
|
||||
/// High-performance enumerator for the RingBuffer.
|
||||
/// </summary>
|
||||
public struct Enumerator : IEnumerator<double>, IEquatable<Enumerator>
|
||||
{
|
||||
private readonly RingBuffer _buffer;
|
||||
private readonly int _start;
|
||||
private readonly int _count;
|
||||
private int _index;
|
||||
private double _current;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal Enumerator(RingBuffer buffer)
|
||||
{
|
||||
_buffer = buffer;
|
||||
_count = buffer._count;
|
||||
_start = buffer._count == buffer.Capacity ? buffer._head : 0;
|
||||
_index = -1;
|
||||
_current = 0.0;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (_index + 1 >= _count)
|
||||
return false;
|
||||
|
||||
_index++;
|
||||
int bufferIdx = (_start + _index) % _buffer.Capacity;
|
||||
_current = _buffer._buffer[bufferIdx];
|
||||
return true;
|
||||
}
|
||||
|
||||
public readonly double Current => _current;
|
||||
readonly object IEnumerator.Current => Current;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Reset()
|
||||
{
|
||||
_index = -1;
|
||||
_current = 0.0;
|
||||
}
|
||||
|
||||
public readonly void Dispose() { }
|
||||
|
||||
public readonly bool Equals(Enumerator other) =>
|
||||
ReferenceEquals(_buffer, other._buffer) &&
|
||||
_start == other._start &&
|
||||
_count == other._count &&
|
||||
_index == other._index &&
|
||||
_current.Equals(other._current);
|
||||
|
||||
public readonly override bool Equals(object? obj) =>
|
||||
obj is Enumerator other && Equals(other);
|
||||
|
||||
public readonly override int GetHashCode() =>
|
||||
HashCode.Combine(RuntimeHelpers.GetHashCode(_buffer), _start, _count, _index, _current);
|
||||
|
||||
public static bool operator ==(Enumerator left, Enumerator right) => left.Equals(right);
|
||||
public static bool operator !=(Enumerator left, Enumerator right) => !left.Equals(right);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,995 @@
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class SimdExtensionsTests
|
||||
{
|
||||
// ContainsNonFinite tests
|
||||
[Fact]
|
||||
public void ContainsNonFinite_EmptySpan_ReturnsFalse()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
Assert.False(span.ContainsNonFinite());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContainsNonFinite_AllFinite_ReturnsFalse()
|
||||
{
|
||||
double[] data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.False(span.ContainsNonFinite());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContainsNonFinite_ContainsNaN_ReturnsTrue()
|
||||
{
|
||||
double[] data = [1.0, 2.0, double.NaN, 4.0, 5.0, 6.0, 7.0, 8.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(span.ContainsNonFinite());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContainsNonFinite_ContainsPositiveInfinity_ReturnsTrue()
|
||||
{
|
||||
double[] data = [1.0, 2.0, 3.0, double.PositiveInfinity, 5.0, 6.0, 7.0, 8.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(span.ContainsNonFinite());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContainsNonFinite_ContainsNegativeInfinity_ReturnsTrue()
|
||||
{
|
||||
double[] data = [1.0, 2.0, 3.0, 4.0, double.NegativeInfinity, 6.0, 7.0, 8.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(span.ContainsNonFinite());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContainsNonFinite_NonFiniteInRemainder_ReturnsTrue()
|
||||
{
|
||||
double[] data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, double.NaN];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(span.ContainsNonFinite());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContainsNonFinite_SingleNaN_ReturnsTrue()
|
||||
{
|
||||
double[] data = [double.NaN];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(span.ContainsNonFinite());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContainsNonFinite_TwoElements_AllFinite_ReturnsFalse()
|
||||
{
|
||||
double[] data = [1.0, 2.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.False(span.ContainsNonFinite());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContainsNonFinite_TwoElements_OneNaN_ReturnsTrue()
|
||||
{
|
||||
double[] data = [1.0, double.NaN];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(span.ContainsNonFinite());
|
||||
}
|
||||
|
||||
// SumSIMD tests
|
||||
[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_TwoElements_ReturnsSum()
|
||||
{
|
||||
double[] data = [1.5, 2.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(4.0, 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);
|
||||
const double expected = 1000.0 * 1001.0 / 2.0;
|
||||
Assert.Equal(expected, span.SumSIMD(), precision: 8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumSIMD_ContainsNaN_ReturnsNaN()
|
||||
{
|
||||
double[] data = [1.0, 2.0, double.NaN, 4.0, 5.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(double.IsNaN(span.SumSIMD()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumSIMD_ContainsInfinity_ReturnsNaN()
|
||||
{
|
||||
double[] data = [1.0, 2.0, double.PositiveInfinity, 4.0, 5.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(double.IsNaN(span.SumSIMD()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumSIMD_NegativeValues_ReturnsCorrectSum()
|
||||
{
|
||||
double[] data = [-1.0, -2.0, -3.0, -4.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(-10.0, span.SumSIMD());
|
||||
}
|
||||
|
||||
// MinSIMD tests
|
||||
[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_TwoElements_ReturnsMinimum()
|
||||
{
|
||||
double[] data = [5.0, 2.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(2.0, 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 MinSIMD_ContainsNaN_ReturnsNaN()
|
||||
{
|
||||
double[] data = [5.0, 2.0, double.NaN, 1.0, 9.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(double.IsNaN(span.MinSIMD()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinSIMD_MinInRemainder_ReturnsCorrectMin()
|
||||
{
|
||||
double[] data = [5.0, 2.0, 8.0, 6.0, 9.0, 3.0, 7.0, 4.0, 0.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(0.5, span.MinSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinSIMD_NegativeValues_ReturnsMinimum()
|
||||
{
|
||||
double[] data = [-5.0, -2.0, -8.0, -1.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(-8.0, span.MinSIMD());
|
||||
}
|
||||
|
||||
// MaxSIMD tests
|
||||
[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_TwoElements_ReturnsMaximum()
|
||||
{
|
||||
double[] data = [5.0, 9.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(9.0, 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 MaxSIMD_ContainsNaN_ReturnsNaN()
|
||||
{
|
||||
double[] data = [5.0, 2.0, double.NaN, 1.0, 9.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(double.IsNaN(span.MaxSIMD()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaxSIMD_MaxInRemainder_ReturnsCorrectMax()
|
||||
{
|
||||
double[] data = [5.0, 2.0, 8.0, 6.0, 4.0, 3.0, 7.0, 1.0, 99.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(99.0, span.MaxSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaxSIMD_NegativeValues_ReturnsMaximum()
|
||||
{
|
||||
double[] data = [-5.0, -2.0, -8.0, -1.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(-1.0, span.MaxSIMD());
|
||||
}
|
||||
|
||||
// AverageSIMD tests
|
||||
[Fact]
|
||||
public void AverageSIMD_EmptySpan_ReturnsNaN()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
Assert.True(double.IsNaN(span.AverageSIMD()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AverageSIMD_TwoElements_ReturnsAverage()
|
||||
{
|
||||
double[] data = [2.0, 4.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(3.0, 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 AverageSIMD_ContainsNaN_ReturnsNaN()
|
||||
{
|
||||
double[] data = [1.0, double.NaN, 3.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(double.IsNaN(span.AverageSIMD()));
|
||||
}
|
||||
|
||||
// VarianceSIMD tests
|
||||
[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_EmptySpan_ReturnsNaN()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
Assert.True(double.IsNaN(span.VarianceSIMD()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceSIMD_TwoElements_ReturnsCorrect()
|
||||
{
|
||||
double[] data = [1.0, 3.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(2.0, span.VarianceSIMD(), precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceSIMD_ThreeElements_ReturnsCorrect()
|
||||
{
|
||||
double[] data = [1.0, 2.0, 3.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(1.0, span.VarianceSIMD(), precision: 10);
|
||||
}
|
||||
|
||||
[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);
|
||||
|
||||
double variance = span.VarianceSIMD();
|
||||
Assert.True(Math.Abs(variance - 4.571428) < 0.0001);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceSIMD_WithProvidedMean_UsesProvidedMean()
|
||||
{
|
||||
double[] data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
|
||||
double mean = 5.0;
|
||||
double variance = span.VarianceSIMD(mean);
|
||||
|
||||
Assert.True(variance > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceSIMD_ContainsNaN_ReturnsNaN()
|
||||
{
|
||||
double[] data = [2.0, double.NaN, 4.0, 5.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(double.IsNaN(span.VarianceSIMD()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceSIMD_WithNaNMean_ReturnsNaN()
|
||||
{
|
||||
double[] data = [2.0, 4.0, 4.0, 5.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(double.IsNaN(span.VarianceSIMD(double.NaN)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceSIMD_WithInfinityMean_ReturnsNaN()
|
||||
{
|
||||
double[] data = [2.0, 4.0, 4.0, 5.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(double.IsNaN(span.VarianceSIMD(double.PositiveInfinity)));
|
||||
}
|
||||
|
||||
// StdDevSIMD tests
|
||||
[Fact]
|
||||
public void StdDevSIMD_TwoElements_ReturnsCorrect()
|
||||
{
|
||||
double[] data = [1.0, 3.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(Math.Abs(span.StdDevSIMD() - 1.414) < 0.01);
|
||||
}
|
||||
|
||||
[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);
|
||||
|
||||
double stdDev = span.StdDevSIMD();
|
||||
Assert.True(Math.Abs(stdDev - 2.138) < 0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StdDevSIMD_WithProvidedMean_Works()
|
||||
{
|
||||
double[] data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
|
||||
double stdDev = span.StdDevSIMD(5.0);
|
||||
Assert.True(stdDev > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StdDevSIMD_ContainsNaN_ReturnsNaN()
|
||||
{
|
||||
double[] data = [2.0, double.NaN, 4.0, 5.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(double.IsNaN(span.StdDevSIMD()));
|
||||
}
|
||||
|
||||
// MinMaxSIMD tests
|
||||
[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_TwoElements_ReturnsCorrect()
|
||||
{
|
||||
double[] data = [5.0, 2.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
var (min, max) = span.MinMaxSIMD();
|
||||
Assert.Equal(2.0, min);
|
||||
Assert.Equal(5.0, 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 MinMaxSIMD_ContainsNaN_ReturnsBothNaN()
|
||||
{
|
||||
double[] data = [5.0, 2.0, double.NaN, 1.0, 9.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
var (min, max) = span.MinMaxSIMD();
|
||||
Assert.True(double.IsNaN(min));
|
||||
Assert.True(double.IsNaN(max));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinMaxSIMD_MinMaxInRemainder_ReturnsCorrect()
|
||||
{
|
||||
double[] data = [5.0, 2.0, 8.0, 6.0, 4.0, 3.0, 7.0, 5.0, 0.1, 99.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
var (min, max) = span.MinMaxSIMD();
|
||||
Assert.Equal(0.1, min);
|
||||
Assert.Equal(99.0, max);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinMaxSIMD_NegativeValues_ReturnsCorrect()
|
||||
{
|
||||
double[] data = [-5.0, -2.0, -8.0, -1.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
var (min, max) = span.MinMaxSIMD();
|
||||
Assert.Equal(-8.0, min);
|
||||
Assert.Equal(-1.0, max);
|
||||
}
|
||||
|
||||
// Add/Subtract tests
|
||||
[Fact]
|
||||
public void Add_SameLength_CorrectResult()
|
||||
{
|
||||
double[] left = [1.0, 2.0, 3.0, 4.0, 5.0];
|
||||
double[] right = [10.0, 20.0, 30.0, 40.0, 50.0];
|
||||
double[] result = new double[5];
|
||||
|
||||
SimdExtensions.Add(left, right, result);
|
||||
|
||||
Assert.Equal(11.0, result[0]);
|
||||
Assert.Equal(22.0, result[1]);
|
||||
Assert.Equal(33.0, result[2]);
|
||||
Assert.Equal(44.0, result[3]);
|
||||
Assert.Equal(55.0, result[4]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_DifferentLengths_ThrowsArgumentException()
|
||||
{
|
||||
double[] left = [1.0, 2.0];
|
||||
double[] right = [1.0];
|
||||
double[] result = new double[2];
|
||||
|
||||
Assert.Throws<ArgumentException>(() => SimdExtensions.Add(left, right, result));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Subtract_SameLength_CorrectResult()
|
||||
{
|
||||
double[] left = [10.0, 20.0, 30.0, 40.0, 50.0];
|
||||
double[] right = [1.0, 2.0, 3.0, 4.0, 5.0];
|
||||
double[] result = new double[5];
|
||||
|
||||
SimdExtensions.Subtract(left, right, result);
|
||||
|
||||
Assert.Equal(9.0, result[0]);
|
||||
Assert.Equal(18.0, result[1]);
|
||||
Assert.Equal(27.0, result[2]);
|
||||
Assert.Equal(36.0, result[3]);
|
||||
Assert.Equal(45.0, result[4]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Subtract_DifferentLengths_ThrowsArgumentException()
|
||||
{
|
||||
double[] left = [1.0, 2.0];
|
||||
double[] right = [1.0];
|
||||
double[] result = new double[2];
|
||||
|
||||
Assert.Throws<ArgumentException>(() => SimdExtensions.Subtract(left, right, result));
|
||||
}
|
||||
|
||||
// DotProduct tests
|
||||
[Fact]
|
||||
public void DotProduct_SameLength_CorrectResult()
|
||||
{
|
||||
double[] a = [1.0, 2.0, 3.0];
|
||||
double[] b = [4.0, 5.0, 6.0];
|
||||
// 1*4 + 2*5 + 3*6 = 4 + 10 + 18 = 32
|
||||
Assert.Equal(32.0, a.DotProduct(b));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DotProduct_DifferentLengths_ThrowsArgumentException()
|
||||
{
|
||||
double[] a = [1.0, 2.0];
|
||||
double[] b = [1.0];
|
||||
Assert.Throws<ArgumentException>(() => a.DotProduct(b));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DotProduct_EmptySpans_ReturnsZero()
|
||||
{
|
||||
double[] a = [];
|
||||
double[] b = [];
|
||||
Assert.Equal(0.0, a.DotProduct(b));
|
||||
}
|
||||
|
||||
// Integration tests
|
||||
[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);
|
||||
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()
|
||||
{
|
||||
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;
|
||||
|
||||
_ = closeValues.SumSIMD();
|
||||
|
||||
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();
|
||||
|
||||
Assert.True(sum > 0);
|
||||
Assert.True(avg > 0);
|
||||
Assert.True(min > 0);
|
||||
Assert.True(max > min);
|
||||
Assert.Equal(min, minAlt);
|
||||
Assert.Equal(max, maxAlt);
|
||||
Assert.True(variance > 0);
|
||||
Assert.True(stdDev > 0);
|
||||
|
||||
Assert.True(sw.ElapsedMilliseconds < 50,
|
||||
$"SIMD operations took {sw.ElapsedMilliseconds}ms, expected < 50ms");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SIMD_ScalarFallback_SmallArray()
|
||||
{
|
||||
double[] data = [1.0, 2.0, 3.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
|
||||
Assert.Equal(6.0, span.SumSIMD());
|
||||
Assert.Equal(1.0, span.MinSIMD());
|
||||
Assert.Equal(3.0, span.MaxSIMD());
|
||||
Assert.Equal(2.0, span.AverageSIMD());
|
||||
|
||||
var (min, max) = span.MinMaxSIMD();
|
||||
Assert.Equal(1.0, min);
|
||||
Assert.Equal(3.0, max);
|
||||
}
|
||||
}
|
||||
|
||||
// Tests for internal scalar implementations
|
||||
public class SimdScalarFallbackTests
|
||||
{
|
||||
[Fact]
|
||||
public void ContainsNonFiniteScalar_AllFinite_ReturnsFalse()
|
||||
{
|
||||
double[] data = [1.0, 2.0, 3.0, 4.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.False(SimdExtensions.ContainsNonFiniteScalar(span));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContainsNonFiniteScalar_ContainsNaN_ReturnsTrue()
|
||||
{
|
||||
double[] data = [1.0, double.NaN, 3.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(SimdExtensions.ContainsNonFiniteScalar(span));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContainsNonFiniteScalar_ContainsInfinity_ReturnsTrue()
|
||||
{
|
||||
double[] data = [1.0, double.PositiveInfinity, 3.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(SimdExtensions.ContainsNonFiniteScalar(span));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContainsNonFiniteScalar_Empty_ReturnsFalse()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
Assert.False(SimdExtensions.ContainsNonFiniteScalar(span));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumScalar_MultipleElements_ReturnsCorrectSum()
|
||||
{
|
||||
double[] data = [1.0, 2.0, 3.0, 4.0, 5.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(15.0, SimdExtensions.SumScalar(span));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumScalar_NegativeValues_ReturnsCorrectSum()
|
||||
{
|
||||
double[] data = [-1.0, -2.0, 3.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(0.0, SimdExtensions.SumScalar(span));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumScalar_Empty_ReturnsZero()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
Assert.Equal(0.0, SimdExtensions.SumScalar(span));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinScalar_MultipleElements_ReturnsMinimum()
|
||||
{
|
||||
double[] data = [5.0, 2.0, 8.0, 1.0, 9.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(1.0, SimdExtensions.MinScalar(span));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinScalar_NegativeValues_ReturnsMinimum()
|
||||
{
|
||||
double[] data = [-5.0, -2.0, -8.0, -1.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(-8.0, SimdExtensions.MinScalar(span));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinScalar_SingleElement_ReturnsElement()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(42.5, SimdExtensions.MinScalar(span));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaxScalar_MultipleElements_ReturnsMaximum()
|
||||
{
|
||||
double[] data = [5.0, 2.0, 8.0, 1.0, 9.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(9.0, SimdExtensions.MaxScalar(span));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaxScalar_NegativeValues_ReturnsMaximum()
|
||||
{
|
||||
double[] data = [-5.0, -2.0, -8.0, -1.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(-1.0, SimdExtensions.MaxScalar(span));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaxScalar_SingleElement_ReturnsElement()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(42.5, SimdExtensions.MaxScalar(span));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceScalar_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);
|
||||
double mean = 5.0;
|
||||
double variance = SimdExtensions.VarianceScalar(span, mean);
|
||||
Assert.True(Math.Abs(variance - 4.571428) < 0.0001);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceScalar_TwoElements_ReturnsCorrectVariance()
|
||||
{
|
||||
double[] data = [1.0, 3.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
double mean = 2.0;
|
||||
Assert.Equal(2.0, SimdExtensions.VarianceScalar(span, mean), precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinMaxScalar_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) = SimdExtensions.MinMaxScalar(span);
|
||||
Assert.Equal(1.0, min);
|
||||
Assert.Equal(9.0, max);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinMaxScalar_NegativeValues_ReturnsCorrectMinMax()
|
||||
{
|
||||
double[] data = [-5.0, -2.0, -8.0, -1.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
var (min, max) = SimdExtensions.MinMaxScalar(span);
|
||||
Assert.Equal(-8.0, min);
|
||||
Assert.Equal(-1.0, max);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinMaxScalar_SingleElement_ReturnsSameValue()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
var (min, max) = SimdExtensions.MinMaxScalar(span);
|
||||
Assert.Equal(42.5, min);
|
||||
Assert.Equal(42.5, max);
|
||||
}
|
||||
|
||||
// Additional edge case tests
|
||||
[Fact]
|
||||
public void DotProduct_ContainsNaN_PropagatesNaN()
|
||||
{
|
||||
double[] a = [1.0, double.NaN, 3.0];
|
||||
double[] b = [4.0, 5.0, 6.0];
|
||||
double result = a.DotProduct(b);
|
||||
Assert.True(double.IsNaN(result));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DotProduct_ContainsInfinity_PropagatesCorrectly()
|
||||
{
|
||||
double[] a = [1.0, double.PositiveInfinity, 3.0];
|
||||
double[] b = [4.0, 5.0, 6.0];
|
||||
double result = a.DotProduct(b);
|
||||
Assert.True(double.IsPositiveInfinity(result));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_ContainsNaN_PropagatesNaN()
|
||||
{
|
||||
double[] left = [1.0, double.NaN, 3.0];
|
||||
double[] right = [4.0, 5.0, 6.0];
|
||||
double[] result = new double[3];
|
||||
|
||||
SimdExtensions.Add(left, right, result);
|
||||
|
||||
Assert.Equal(5.0, result[0]);
|
||||
Assert.True(double.IsNaN(result[1]));
|
||||
Assert.Equal(9.0, result[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Subtract_ContainsNaN_PropagatesNaN()
|
||||
{
|
||||
double[] left = [10.0, double.NaN, 30.0];
|
||||
double[] right = [1.0, 2.0, 3.0];
|
||||
double[] result = new double[3];
|
||||
|
||||
SimdExtensions.Subtract(left, right, result);
|
||||
|
||||
Assert.Equal(9.0, result[0]);
|
||||
Assert.True(double.IsNaN(result[1]));
|
||||
Assert.Equal(27.0, result[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContainsNonFinite_NegativeInfinityAtStart_ReturnsTrue()
|
||||
{
|
||||
double[] data = [double.NegativeInfinity, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(span.ContainsNonFinite());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContainsNonFinite_NegativeInfinityAtEnd_ReturnsTrue()
|
||||
{
|
||||
double[] data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, double.NegativeInfinity];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(span.ContainsNonFinite());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceSIMD_SingleElement_ReturnsNaN()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(double.IsNaN(span.VarianceSIMD()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StdDevSIMD_SingleElement_ReturnsNaN()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(double.IsNaN(span.StdDevSIMD()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StdDevSIMD_EmptySpan_ReturnsNaN()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
Assert.True(double.IsNaN(span.StdDevSIMD()));
|
||||
}
|
||||
|
||||
[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 AverageSIMD_SingleElement_ReturnsElement()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(42.5, span.AverageSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DotProduct_SingleElement_ReturnsProduct()
|
||||
{
|
||||
double[] a = [3.0];
|
||||
double[] b = [4.0];
|
||||
Assert.Equal(12.0, a.DotProduct(b));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DotProduct_TwoElements_ReturnsCorrect()
|
||||
{
|
||||
double[] a = [2.0, 3.0];
|
||||
double[] b = [4.0, 5.0];
|
||||
// 2*4 + 3*5 = 8 + 15 = 23
|
||||
Assert.Equal(23.0, a.DotProduct(b));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_SingleElement_Works()
|
||||
{
|
||||
double[] left = [5.0];
|
||||
double[] right = [3.0];
|
||||
double[] result = new double[1];
|
||||
|
||||
SimdExtensions.Add(left, right, result);
|
||||
|
||||
Assert.Equal(8.0, result[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Subtract_SingleElement_Works()
|
||||
{
|
||||
double[] left = [5.0];
|
||||
double[] right = [3.0];
|
||||
double[] result = new double[1];
|
||||
|
||||
SimdExtensions.Subtract(left, right, result);
|
||||
|
||||
Assert.Equal(2.0, result[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_EmptyArrays_Works()
|
||||
{
|
||||
double[] left = [];
|
||||
double[] right = [];
|
||||
double[] result = [];
|
||||
|
||||
SimdExtensions.Add(left, right, result); // Should not throw
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Subtract_EmptyArrays_Works()
|
||||
{
|
||||
double[] left = [];
|
||||
double[] right = [];
|
||||
double[] result = [];
|
||||
|
||||
SimdExtensions.Subtract(left, right, result); // Should not throw
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_ResultTooSmall_ThrowsArgumentException()
|
||||
{
|
||||
double[] left = [1.0, 2.0, 3.0];
|
||||
double[] right = [4.0, 5.0, 6.0];
|
||||
double[] result = new double[2]; // Too small
|
||||
|
||||
Assert.Throws<ArgumentException>(() => SimdExtensions.Add(left, right, result));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Subtract_ResultTooSmall_ThrowsArgumentException()
|
||||
{
|
||||
double[] left = [1.0, 2.0, 3.0];
|
||||
double[] right = [4.0, 5.0, 6.0];
|
||||
double[] result = new double[2]; // Too small
|
||||
|
||||
Assert.Throws<ArgumentException>(() => SimdExtensions.Subtract(left, right, result));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,760 @@
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Intrinsics;
|
||||
using System.Runtime.Intrinsics.Arm;
|
||||
using System.Runtime.Intrinsics.X86;
|
||||
|
||||
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
|
||||
{
|
||||
// Internal scalar implementations for testability
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static bool ContainsNonFiniteScalar(ReadOnlySpan<double> span)
|
||||
{
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
{
|
||||
if (!double.IsFinite(span[i]))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static double SumScalar(ReadOnlySpan<double> span)
|
||||
{
|
||||
double scalar = 0.0;
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
scalar += span[i];
|
||||
return scalar;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static double MinScalar(ReadOnlySpan<double> span)
|
||||
{
|
||||
if (span.Length == 0)
|
||||
throw new ArgumentException("Span must not be empty", nameof(span));
|
||||
|
||||
double min = span[0];
|
||||
for (int i = 1; i < span.Length; i++)
|
||||
{
|
||||
if (span[i] < min)
|
||||
min = span[i];
|
||||
}
|
||||
return min;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static double MaxScalar(ReadOnlySpan<double> span)
|
||||
{
|
||||
if (span.Length == 0)
|
||||
throw new ArgumentException("Span must not be empty", nameof(span));
|
||||
|
||||
double max = span[0];
|
||||
for (int i = 1; i < span.Length; i++)
|
||||
{
|
||||
if (span[i] > max)
|
||||
max = span[i];
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static double VarianceScalar(ReadOnlySpan<double> span, double mean)
|
||||
{
|
||||
// Match VarianceSIMD behavior: return 0.0 for length <= 1 to avoid divide-by-zero
|
||||
if (span.Length <= 1)
|
||||
return 0.0;
|
||||
|
||||
double sumSquares = 0.0;
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
{
|
||||
double diff = span[i] - mean;
|
||||
sumSquares += diff * diff;
|
||||
}
|
||||
return sumSquares / (span.Length - 1);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static (double Min, double Max) MinMaxScalar(ReadOnlySpan<double> span)
|
||||
{
|
||||
if (span.Length == 0)
|
||||
throw new ArgumentException("Span must not be empty", nameof(span));
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if span contains any non-finite values (NaN or Infinity).
|
||||
/// Returns true if any non-finite value is found.
|
||||
/// Uses SIMD: NaN detected via v != v (NaN is the only value where this is true),
|
||||
/// Infinity detected via |v| > MaxValue comparison.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool ContainsNonFinite(this ReadOnlySpan<double> span)
|
||||
{
|
||||
if (span.IsEmpty) return false;
|
||||
|
||||
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
||||
{
|
||||
int vectorSize = Vector<double>.Count;
|
||||
int i = 0;
|
||||
var maxValue = new Vector<double>(double.MaxValue);
|
||||
|
||||
for (; i <= span.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
var vector = new Vector<double>(span.Slice(i, vectorSize));
|
||||
|
||||
// NaN check: NaN != NaN, so Vector.Equals(v, v) will be false for NaN lanes
|
||||
var nanCheck = Vector.Equals(vector, vector);
|
||||
if (!nanCheck.Equals(Vector<long>.AllBitsSet))
|
||||
return true;
|
||||
|
||||
// Infinity check: |v| > MaxValue (Infinity has magnitude > MaxValue)
|
||||
var absVec = Vector.Abs(vector);
|
||||
var infCheck = Vector.GreaterThan(absVec, maxValue);
|
||||
if (!infCheck.Equals(Vector<long>.Zero))
|
||||
return true;
|
||||
}
|
||||
|
||||
for (; i < span.Length; i++)
|
||||
{
|
||||
if (!double.IsFinite(span[i]))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return ContainsNonFiniteScalar(span);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates sum using SIMD vectorization when available.
|
||||
/// 4-8x faster than scalar loop on AVX2/AVX-512 hardware.
|
||||
/// Returns NaN if any input value is non-finite.
|
||||
/// Uses lazy non-finite check: computes sum first, then validates result.
|
||||
/// If result is non-finite, falls back to explicit check.
|
||||
/// </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;
|
||||
|
||||
for (; i <= span.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
var vector = new Vector<double>(span.Slice(i, vectorSize));
|
||||
sum += vector;
|
||||
}
|
||||
|
||||
double result = 0.0;
|
||||
for (int j = 0; j < vectorSize; j++)
|
||||
result += sum[j];
|
||||
|
||||
for (; i < span.Length; i++)
|
||||
result += span[i];
|
||||
|
||||
// Lazy check: if result is non-finite AND input contained non-finite values, return NaN
|
||||
// NaN + anything = NaN, Inf + anything finite = Inf
|
||||
// If result is infinite from overflow (no input NaN/Inf), return as-is
|
||||
if (!double.IsFinite(result) && span.ContainsNonFinite())
|
||||
return double.NaN;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Scalar path with lazy check
|
||||
double scalarSum = SumScalar(span);
|
||||
return !double.IsFinite(scalarSum) && span.ContainsNonFinite() ? double.NaN : scalarSum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates minimum value using SIMD vectorization when available.
|
||||
/// 4-6x faster than scalar loop on AVX2/AVX-512 hardware.
|
||||
/// Returns NaN if any input value is non-finite.
|
||||
/// </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];
|
||||
|
||||
// Guard against non-finite inputs
|
||||
if (span.ContainsNonFinite()) return double.NaN;
|
||||
|
||||
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
||||
{
|
||||
int vectorSize = Vector<double>.Count;
|
||||
var minVec = new Vector<double>(span[..vectorSize]);
|
||||
int i = vectorSize;
|
||||
|
||||
for (; i <= span.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
var vector = new Vector<double>(span.Slice(i, vectorSize));
|
||||
minVec = Vector.Min(minVec, vector);
|
||||
}
|
||||
|
||||
double result = minVec[0];
|
||||
for (int j = 1; j < vectorSize; j++)
|
||||
{
|
||||
if (minVec[j] < result)
|
||||
result = minVec[j];
|
||||
}
|
||||
|
||||
for (; i < span.Length; i++)
|
||||
{
|
||||
if (span[i] < result)
|
||||
result = span[i];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return MinScalar(span);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates maximum value using SIMD vectorization when available.
|
||||
/// 4-6x faster than scalar loop on AVX2/AVX-512 hardware.
|
||||
/// Returns NaN if any input value is non-finite.
|
||||
/// </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];
|
||||
|
||||
// Guard against non-finite inputs
|
||||
if (span.ContainsNonFinite()) return double.NaN;
|
||||
|
||||
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
||||
{
|
||||
int vectorSize = Vector<double>.Count;
|
||||
var maxVec = new Vector<double>(span[..vectorSize]);
|
||||
int i = vectorSize;
|
||||
|
||||
for (; i <= span.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
var vector = new Vector<double>(span.Slice(i, vectorSize));
|
||||
maxVec = Vector.Max(maxVec, vector);
|
||||
}
|
||||
|
||||
double result = maxVec[0];
|
||||
for (int j = 1; j < vectorSize; j++)
|
||||
{
|
||||
if (maxVec[j] > result)
|
||||
result = maxVec[j];
|
||||
}
|
||||
|
||||
for (; i < span.Length; i++)
|
||||
{
|
||||
if (span[i] > result)
|
||||
result = span[i];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return MaxScalar(span);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates average using SIMD vectorization when available.
|
||||
/// 4-8x faster than scalar loop on AVX2/AVX-512 hardware.
|
||||
/// Returns NaN if any input value is non-finite.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double AverageSIMD(this ReadOnlySpan<double> span)
|
||||
{
|
||||
if (span.IsEmpty) return double.NaN;
|
||||
// SumSIMD already guards against non-finite, which will propagate NaN
|
||||
return span.SumSIMD() / span.Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates variance using a two-pass SIMD variant that computes the mean first (via AverageSIMD) and then sums squared differences to produce variance.
|
||||
/// Note that this is not the single-pass Welford algorithm.
|
||||
/// Returns NaN if any input value is non-finite or if mean is non-finite.
|
||||
/// Caches non-finite check result: when mean is not provided, AverageSIMD -> SumSIMD already validates;
|
||||
/// when mean IS provided, we need explicit check only once.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double VarianceSIMD(this ReadOnlySpan<double> span, double? mean = null)
|
||||
{
|
||||
if (span.Length < 2) return double.NaN;
|
||||
|
||||
double m;
|
||||
if (mean.HasValue)
|
||||
{
|
||||
// Mean provided externally - need explicit non-finite check
|
||||
if (span.ContainsNonFinite()) return double.NaN;
|
||||
m = mean.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
// AverageSIMD -> SumSIMD already performs lazy non-finite check
|
||||
// If input has NaN, SumSIMD returns NaN, which propagates here
|
||||
m = span.AverageSIMD();
|
||||
}
|
||||
|
||||
// If mean is NaN (from input NaN or explicit NaN mean), return NaN
|
||||
if (!double.IsFinite(m)) return double.NaN;
|
||||
|
||||
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;
|
||||
|
||||
for (; i <= span.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
var vector = new Vector<double>(span.Slice(i, vectorSize));
|
||||
var diff = vector - meanVec;
|
||||
sumSq += diff * diff;
|
||||
}
|
||||
|
||||
double result = 0.0;
|
||||
for (int j = 0; j < vectorSize; j++)
|
||||
result += sumSq[j];
|
||||
|
||||
for (; i < span.Length; i++)
|
||||
{
|
||||
double diff = span[i] - m;
|
||||
result += diff * diff;
|
||||
}
|
||||
|
||||
return result / (span.Length - 1);
|
||||
}
|
||||
|
||||
return VarianceScalar(span, m);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates standard deviation using SIMD vectorization.
|
||||
/// Returns NaN if any input value is non-finite or if mean is non-finite.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double StdDevSIMD(this ReadOnlySpan<double> span, double? mean = null)
|
||||
{
|
||||
// VarianceSIMD already guards against non-finite, which will propagate NaN through Sqrt
|
||||
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.
|
||||
/// Returns (NaN, NaN) if any input value is non-finite.
|
||||
/// </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]);
|
||||
|
||||
// Guard against non-finite inputs
|
||||
if (span.ContainsNonFinite()) return (double.NaN, double.NaN);
|
||||
|
||||
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
||||
{
|
||||
int vectorSize = Vector<double>.Count;
|
||||
var minVec = new Vector<double>(span[..vectorSize]);
|
||||
var maxVec = minVec;
|
||||
int i = vectorSize;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
for (; i < span.Length; i++)
|
||||
{
|
||||
if (span[i] < min) min = span[i];
|
||||
if (span[i] > max) max = span[i];
|
||||
}
|
||||
|
||||
return (min, max);
|
||||
}
|
||||
|
||||
return MinMaxScalar(span);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Element-wise addition of two spans using SIMD.
|
||||
/// result[i] = left[i] + right[i]
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Add(ReadOnlySpan<double> left, ReadOnlySpan<double> right, Span<double> result)
|
||||
{
|
||||
if (left.Length != right.Length || left.Length != result.Length)
|
||||
throw new ArgumentException("All spans must have the same length", nameof(result));
|
||||
|
||||
int i = 0;
|
||||
if (Vector.IsHardwareAccelerated && left.Length >= Vector<double>.Count)
|
||||
{
|
||||
int vectorSize = Vector<double>.Count;
|
||||
for (; i <= left.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
var vLeft = new Vector<double>(left.Slice(i, vectorSize));
|
||||
var vRight = new Vector<double>(right.Slice(i, vectorSize));
|
||||
(vLeft + vRight).CopyTo(result.Slice(i, vectorSize));
|
||||
}
|
||||
}
|
||||
|
||||
for (; i < left.Length; i++)
|
||||
{
|
||||
result[i] = left[i] + right[i];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales all elements in a span by a scalar value using SIMD.
|
||||
/// result[i] = source[i] * scalar
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Scale(ReadOnlySpan<double> source, double scalar, Span<double> result)
|
||||
{
|
||||
if (source.Length != result.Length)
|
||||
throw new ArgumentException("Source and result spans must have the same length", nameof(result));
|
||||
|
||||
int i = 0;
|
||||
if (Vector.IsHardwareAccelerated && source.Length >= Vector<double>.Count)
|
||||
{
|
||||
int vectorSize = Vector<double>.Count;
|
||||
var scalarVec = new Vector<double>(scalar);
|
||||
for (; i <= source.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
var vSource = new Vector<double>(source.Slice(i, vectorSize));
|
||||
(vSource * scalarVec).CopyTo(result.Slice(i, vectorSize));
|
||||
}
|
||||
}
|
||||
|
||||
for (; i < source.Length; i++)
|
||||
{
|
||||
result[i] = source[i] * scalar;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Element-wise subtraction of two spans using SIMD.
|
||||
/// result[i] = left[i] - right[i]
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Subtract(ReadOnlySpan<double> left, ReadOnlySpan<double> right, Span<double> result)
|
||||
{
|
||||
if (left.Length != right.Length || left.Length != result.Length)
|
||||
throw new ArgumentException("All spans must have the same length", nameof(result));
|
||||
|
||||
int i = 0;
|
||||
if (Vector.IsHardwareAccelerated && left.Length >= Vector<double>.Count)
|
||||
{
|
||||
int vectorSize = Vector<double>.Count;
|
||||
for (; i <= left.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
var vLeft = new Vector<double>(left.Slice(i, vectorSize));
|
||||
var vRight = new Vector<double>(right.Slice(i, vectorSize));
|
||||
(vLeft - vRight).CopyTo(result.Slice(i, vectorSize));
|
||||
}
|
||||
}
|
||||
|
||||
for (; i < left.Length; i++)
|
||||
{
|
||||
result[i] = left[i] - right[i];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the dot product of two spans using SIMD intrinsics.
|
||||
/// Supports AVX512, AVX2, and NEON (ARM64).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double DotProduct(this ReadOnlySpan<double> a, ReadOnlySpan<double> b)
|
||||
{
|
||||
if (a.Length != b.Length)
|
||||
throw new ArgumentException("Spans must have equal length", nameof(b));
|
||||
|
||||
if (a.IsEmpty) return 0.0;
|
||||
|
||||
int len = a.Length;
|
||||
|
||||
// Fast path for very small kernels (avoid SIMD overhead)
|
||||
if (len <= 3)
|
||||
{
|
||||
ref double aRef = ref MemoryMarshal.GetReference(a);
|
||||
ref double bRef = ref MemoryMarshal.GetReference(b);
|
||||
|
||||
double sum = aRef * bRef;
|
||||
if (len > 1) sum += Unsafe.Add(ref aRef, 1) * Unsafe.Add(ref bRef, 1);
|
||||
if (len > 2) sum += Unsafe.Add(ref aRef, 2) * Unsafe.Add(ref bRef, 2);
|
||||
return sum;
|
||||
}
|
||||
|
||||
if (Avx512F.IsSupported)
|
||||
return DotProductAvx512(a, b);
|
||||
|
||||
if (Avx2.IsSupported)
|
||||
return DotProductAvx2(a, b);
|
||||
|
||||
if (AdvSimd.Arm64.IsSupported)
|
||||
return DotProductNeon(a, b);
|
||||
|
||||
double s1 = 0, s2 = 0, s3 = 0, s4 = 0;
|
||||
ref double ar = ref MemoryMarshal.GetReference(a);
|
||||
ref double br = ref MemoryMarshal.GetReference(b);
|
||||
|
||||
int i = 0;
|
||||
// Unroll scalar loop with 4 accumulators to break dependency chains
|
||||
for (; i <= len - 4; i += 4)
|
||||
{
|
||||
s1 += Unsafe.Add(ref ar, i) * Unsafe.Add(ref br, i);
|
||||
s2 += Unsafe.Add(ref ar, i + 1) * Unsafe.Add(ref br, i + 1);
|
||||
s3 += Unsafe.Add(ref ar, i + 2) * Unsafe.Add(ref br, i + 2);
|
||||
s4 += Unsafe.Add(ref ar, i + 3) * Unsafe.Add(ref br, i + 3);
|
||||
}
|
||||
|
||||
double s = s1 + s2 + s3 + s4;
|
||||
|
||||
for (; i < len; i++)
|
||||
{
|
||||
s += Unsafe.Add(ref ar, i) * Unsafe.Add(ref br, i);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double DotProductAvx512(ReadOnlySpan<double> a, ReadOnlySpan<double> b)
|
||||
{
|
||||
int len = a.Length;
|
||||
int i = 0;
|
||||
Vector512<double> vSum = Vector512<double>.Zero;
|
||||
Vector512<double> vSum2 = Vector512<double>.Zero;
|
||||
Vector512<double> vSum3 = Vector512<double>.Zero;
|
||||
Vector512<double> vSum4 = Vector512<double>.Zero;
|
||||
|
||||
ref double aRef = ref MemoryMarshal.GetReference(a);
|
||||
ref double bRef = ref MemoryMarshal.GetReference(b);
|
||||
|
||||
// Unroll loop: Process 32 doubles (4 vectors) at a time
|
||||
if (len >= 32)
|
||||
{
|
||||
for (; i <= len - 32; i += 32)
|
||||
{
|
||||
var va1 = Vector512.LoadUnsafe(ref Unsafe.Add(ref aRef, i));
|
||||
var vb1 = Vector512.LoadUnsafe(ref Unsafe.Add(ref bRef, i));
|
||||
|
||||
var va2 = Vector512.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 8));
|
||||
var vb2 = Vector512.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 8));
|
||||
|
||||
var va3 = Vector512.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 16));
|
||||
var vb3 = Vector512.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 16));
|
||||
|
||||
var va4 = Vector512.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 24));
|
||||
var vb4 = Vector512.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 24));
|
||||
|
||||
vSum = Avx512F.FusedMultiplyAdd(va1, vb1, vSum);
|
||||
vSum2 = Avx512F.FusedMultiplyAdd(va2, vb2, vSum2);
|
||||
vSum3 = Avx512F.FusedMultiplyAdd(va3, vb3, vSum3);
|
||||
vSum4 = Avx512F.FusedMultiplyAdd(va4, vb4, vSum4);
|
||||
}
|
||||
}
|
||||
|
||||
// Process remaining vectors (8 doubles at a time)
|
||||
for (; i <= len - 8; i += 8)
|
||||
{
|
||||
var va = Vector512.LoadUnsafe(ref Unsafe.Add(ref aRef, i));
|
||||
var vb = Vector512.LoadUnsafe(ref Unsafe.Add(ref bRef, i));
|
||||
vSum = Avx512F.FusedMultiplyAdd(va, vb, vSum);
|
||||
}
|
||||
|
||||
// Combine accumulators
|
||||
vSum = Avx512F.Add(vSum, vSum2);
|
||||
vSum3 = Avx512F.Add(vSum3, vSum4);
|
||||
vSum = Avx512F.Add(vSum, vSum3);
|
||||
|
||||
Vector256<double> v256 = Avx.Add(vSum.GetLower(), vSum.GetUpper());
|
||||
Vector128<double> lower = v256.GetLower();
|
||||
Vector128<double> upper = v256.GetUpper();
|
||||
Vector128<double> combined = Sse2.Add(lower, upper);
|
||||
double sum = combined.GetElement(0) + combined.GetElement(1);
|
||||
|
||||
// Scalar remainder
|
||||
for (; i < len; i++)
|
||||
{
|
||||
sum += Unsafe.Add(ref aRef, i) * Unsafe.Add(ref bRef, i);
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double DotProductAvx2(ReadOnlySpan<double> a, ReadOnlySpan<double> b)
|
||||
{
|
||||
int len = a.Length;
|
||||
int i = 0;
|
||||
Vector256<double> vSum = Vector256<double>.Zero;
|
||||
Vector256<double> vSum2 = Vector256<double>.Zero;
|
||||
Vector256<double> vSum3 = Vector256<double>.Zero;
|
||||
Vector256<double> vSum4 = Vector256<double>.Zero;
|
||||
|
||||
ref double aRef = ref MemoryMarshal.GetReference(a);
|
||||
ref double bRef = ref MemoryMarshal.GetReference(b);
|
||||
|
||||
// Unroll loop: Process 16 doubles (4 vectors) at a time
|
||||
if (len >= 16)
|
||||
{
|
||||
for (; i <= len - 16; i += 16)
|
||||
{
|
||||
var va1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i));
|
||||
var vb1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i));
|
||||
|
||||
var va2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 4));
|
||||
var vb2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 4));
|
||||
|
||||
var va3 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 8));
|
||||
var vb3 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 8));
|
||||
|
||||
var va4 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 12));
|
||||
var vb4 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 12));
|
||||
|
||||
if (Fma.IsSupported)
|
||||
{
|
||||
vSum = Fma.MultiplyAdd(va1, vb1, vSum);
|
||||
vSum2 = Fma.MultiplyAdd(va2, vb2, vSum2);
|
||||
vSum3 = Fma.MultiplyAdd(va3, vb3, vSum3);
|
||||
vSum4 = Fma.MultiplyAdd(va4, vb4, vSum4);
|
||||
}
|
||||
else
|
||||
{
|
||||
vSum = Avx.Add(vSum, Avx.Multiply(va1, vb1));
|
||||
vSum2 = Avx.Add(vSum2, Avx.Multiply(va2, vb2));
|
||||
vSum3 = Avx.Add(vSum3, Avx.Multiply(va3, vb3));
|
||||
vSum4 = Avx.Add(vSum4, Avx.Multiply(va4, vb4));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process remaining vectors (4 doubles at a time)
|
||||
for (; i <= len - 4; i += 4)
|
||||
{
|
||||
var va = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i));
|
||||
var vb = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i));
|
||||
|
||||
vSum = Fma.IsSupported
|
||||
? Fma.MultiplyAdd(va, vb, vSum)
|
||||
: Avx.Add(vSum, Avx.Multiply(va, vb));
|
||||
}
|
||||
|
||||
// Combine accumulators
|
||||
vSum = Avx.Add(vSum, vSum2);
|
||||
vSum3 = Avx.Add(vSum3, vSum4);
|
||||
vSum = Avx.Add(vSum, vSum3);
|
||||
|
||||
// Horizontal sum
|
||||
Vector128<double> lower = vSum.GetLower();
|
||||
Vector128<double> upper = vSum.GetUpper();
|
||||
Vector128<double> combined = Sse2.Add(lower, upper);
|
||||
double sum = combined.GetElement(0) + combined.GetElement(1);
|
||||
|
||||
// Process remaining elements (scalar)
|
||||
for (; i < len; i++)
|
||||
{
|
||||
sum += Unsafe.Add(ref aRef, i) * Unsafe.Add(ref bRef, i);
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double DotProductNeon(ReadOnlySpan<double> a, ReadOnlySpan<double> b)
|
||||
{
|
||||
int len = a.Length;
|
||||
int i = 0;
|
||||
Vector128<double> vSum = Vector128<double>.Zero;
|
||||
Vector128<double> vSum2 = Vector128<double>.Zero;
|
||||
Vector128<double> vSum3 = Vector128<double>.Zero;
|
||||
Vector128<double> vSum4 = Vector128<double>.Zero;
|
||||
|
||||
ref double aRef = ref MemoryMarshal.GetReference(a);
|
||||
ref double bRef = ref MemoryMarshal.GetReference(b);
|
||||
|
||||
// Unroll loop: Process 8 doubles (4 vectors) at a time
|
||||
if (len >= 8)
|
||||
{
|
||||
for (; i <= len - 8; i += 8)
|
||||
{
|
||||
var va1 = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i));
|
||||
var vb1 = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i));
|
||||
|
||||
var va2 = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 2));
|
||||
var vb2 = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 2));
|
||||
|
||||
var va3 = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 4));
|
||||
var vb3 = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 4));
|
||||
|
||||
var va4 = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 6));
|
||||
var vb4 = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 6));
|
||||
|
||||
// NEON has FMA on ARM64
|
||||
// Since we are inside DotProductNeon which is guarded by AdvSimd.Arm64.IsSupported,
|
||||
// we can assume Arm64 support.
|
||||
vSum = AdvSimd.Arm64.FusedMultiplyAdd(vSum, va1, vb1);
|
||||
vSum2 = AdvSimd.Arm64.FusedMultiplyAdd(vSum2, va2, vb2);
|
||||
vSum3 = AdvSimd.Arm64.FusedMultiplyAdd(vSum3, va3, vb3);
|
||||
vSum4 = AdvSimd.Arm64.FusedMultiplyAdd(vSum4, va4, vb4);
|
||||
}
|
||||
}
|
||||
|
||||
// Process remaining vectors (2 doubles at a time)
|
||||
for (; i <= len - 2; i += 2)
|
||||
{
|
||||
var va = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i));
|
||||
var vb = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i));
|
||||
|
||||
vSum = AdvSimd.Arm64.FusedMultiplyAdd(vSum, va, vb);
|
||||
}
|
||||
|
||||
// Combine accumulators
|
||||
vSum = AdvSimd.Arm64.Add(vSum, vSum2);
|
||||
vSum3 = AdvSimd.Arm64.Add(vSum3, vSum4);
|
||||
vSum = AdvSimd.Arm64.Add(vSum, vSum3);
|
||||
|
||||
// Horizontal sum (NEON has pairwise add)
|
||||
double sum = AdvSimd.Arm64.AddPairwiseScalar(vSum).ToScalar();
|
||||
|
||||
// Scalar remainder (0-1 elements)
|
||||
for (; i < len; i++)
|
||||
{
|
||||
sum += Unsafe.Add(ref aRef, i) * Unsafe.Add(ref bRef, i);
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
# 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 |
|
||||
| ------ | ------ |
|
||||
| `ContainsNonFinite()` | Checks if span contains any non-finite values (NaN or Infinity). |
|
||||
| `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. |
|
||||
| `DotProduct()` | Calculates the dot product of two spans. |
|
||||
|
||||
## 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();
|
||||
|
||||
// Check for valid data
|
||||
bool hasInvalid = span.ContainsNonFinite();
|
||||
|
||||
// Calculate dot product
|
||||
double dot = span.DotProduct(otherSpan);
|
||||
```
|
||||
@@ -1,61 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
|
||||
[SkipLocalsInit]
|
||||
public readonly record struct TBar(DateTime Time, double Open, double High, double Low, double Close, double Volume, bool IsNew = true) : ITBar
|
||||
{
|
||||
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 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 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) { }
|
||||
|
||||
[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) { }
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator double(TBar bar) => bar.Close;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator DateTime(TBar tv) => tv.Time;
|
||||
|
||||
[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}]";
|
||||
}
|
||||
@@ -0,0 +1,500 @@
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class TBarTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_SetsPropertiesCorrectly()
|
||||
{
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
const double open = 100;
|
||||
const double high = 110;
|
||||
const double low = 90;
|
||||
const double close = 105;
|
||||
const 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 Constructor_WithDateTime_SetsPropertiesCorrectly()
|
||||
{
|
||||
var dateTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc);
|
||||
const double open = 100;
|
||||
const double high = 110;
|
||||
const double low = 90;
|
||||
const double close = 105;
|
||||
const double volume = 1000;
|
||||
|
||||
var bar = new TBar(dateTime, open, high, low, close, volume);
|
||||
|
||||
Assert.Equal(dateTime.Ticks, 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 AsDateTime_ReturnsCorrectDateTime()
|
||||
{
|
||||
var dateTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc);
|
||||
var bar = new TBar(dateTime, 100, 110, 90, 105, 1000);
|
||||
|
||||
Assert.Equal(dateTime, bar.AsDateTime);
|
||||
Assert.Equal(DateTimeKind.Utc, bar.AsDateTime.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void O_Property_ReturnsTValueWithOpenPrice()
|
||||
{
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
var bar = new TBar(time, 100, 110, 90, 105, 1000);
|
||||
|
||||
TValue o = bar.O;
|
||||
|
||||
Assert.Equal(time, o.Time);
|
||||
Assert.Equal(100.0, o.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void H_Property_ReturnsTValueWithHighPrice()
|
||||
{
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
var bar = new TBar(time, 100, 110, 90, 105, 1000);
|
||||
|
||||
TValue h = bar.H;
|
||||
|
||||
Assert.Equal(time, h.Time);
|
||||
Assert.Equal(110.0, h.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void L_Property_ReturnsTValueWithLowPrice()
|
||||
{
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
var bar = new TBar(time, 100, 110, 90, 105, 1000);
|
||||
|
||||
TValue l = bar.L;
|
||||
|
||||
Assert.Equal(time, l.Time);
|
||||
Assert.Equal(90.0, l.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void C_Property_ReturnsTValueWithClosePrice()
|
||||
{
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
var bar = new TBar(time, 100, 110, 90, 105, 1000);
|
||||
|
||||
TValue c = bar.C;
|
||||
|
||||
Assert.Equal(time, c.Time);
|
||||
Assert.Equal(105.0, c.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void V_Property_ReturnsTValueWithVolume()
|
||||
{
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
var bar = new TBar(time, 100, 110, 90, 105, 1000);
|
||||
|
||||
TValue v = bar.V;
|
||||
|
||||
Assert.Equal(time, v.Time);
|
||||
Assert.Equal(1000.0, v.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HL2_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, 100, 110, 90, 105, 1000);
|
||||
Assert.Equal(100.0, bar.HL2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OC2_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, 100, 110, 90, 104, 1000);
|
||||
Assert.Equal(102.0, bar.OC2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OHL3_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, 100, 110, 90, 105, 1000);
|
||||
Assert.Equal(100.0, bar.OHL3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HLC3_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, 100, 110, 90, 100, 1000);
|
||||
Assert.Equal(100.0, bar.HLC3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OHLC4_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, 100, 110, 90, 100, 1000);
|
||||
Assert.Equal(100.0, bar.OHLC4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HLCC4_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, 100, 110, 90, 100, 1000);
|
||||
Assert.Equal(100.0, bar.HLCC4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImplicitConversion_ToDouble_ReturnsClosePrice()
|
||||
{
|
||||
var bar = new TBar(0, 100, 110, 90, 105, 1000);
|
||||
double closePrice = bar;
|
||||
Assert.Equal(105.0, closePrice);
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImplicitConversion_ToDateTime_ReturnsCorrectDateTime()
|
||||
{
|
||||
var dateTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc);
|
||||
var bar = new TBar(dateTime, 100, 110, 90, 105, 1000);
|
||||
|
||||
DateTime result = bar;
|
||||
|
||||
Assert.Equal(dateTime, result);
|
||||
Assert.Equal(DateTimeKind.Utc, result.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_ReturnsFormattedString()
|
||||
{
|
||||
var dateTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc);
|
||||
var bar = new TBar(dateTime, 100.5, 110.25, 90.75, 105.0, 1000.0);
|
||||
|
||||
string result = bar.ToString();
|
||||
|
||||
Assert.Contains("2024-06-15", result, StringComparison.Ordinal);
|
||||
Assert.Contains("10:30:00", result, StringComparison.Ordinal);
|
||||
Assert.Contains("O=100.50", result, StringComparison.Ordinal);
|
||||
Assert.Contains("H=110.25", result, StringComparison.Ordinal);
|
||||
Assert.Contains("L=90.75", result, StringComparison.Ordinal);
|
||||
Assert.Contains("C=105.00", result, StringComparison.Ordinal);
|
||||
Assert.Contains("V=1000.00", result, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_TBar_SameBars_ReturnsTrue()
|
||||
{
|
||||
var bar1 = new TBar(12345, 100, 110, 90, 105, 1000);
|
||||
var bar2 = new TBar(12345, 100, 110, 90, 105, 1000);
|
||||
|
||||
Assert.True(bar1.Equals(bar2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_TBar_DifferentTime_ReturnsFalse()
|
||||
{
|
||||
var bar1 = new TBar(12345, 100, 110, 90, 105, 1000);
|
||||
var bar2 = new TBar(12346, 100, 110, 90, 105, 1000);
|
||||
|
||||
Assert.False(bar1.Equals(bar2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_TBar_DifferentOpen_ReturnsFalse()
|
||||
{
|
||||
var bar1 = new TBar(12345, 100, 110, 90, 105, 1000);
|
||||
var bar2 = new TBar(12345, 101, 110, 90, 105, 1000);
|
||||
|
||||
Assert.False(bar1.Equals(bar2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_TBar_DifferentHigh_ReturnsFalse()
|
||||
{
|
||||
var bar1 = new TBar(12345, 100, 110, 90, 105, 1000);
|
||||
var bar2 = new TBar(12345, 100, 111, 90, 105, 1000);
|
||||
|
||||
Assert.False(bar1.Equals(bar2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_TBar_DifferentLow_ReturnsFalse()
|
||||
{
|
||||
var bar1 = new TBar(12345, 100, 110, 90, 105, 1000);
|
||||
var bar2 = new TBar(12345, 100, 110, 91, 105, 1000);
|
||||
|
||||
Assert.False(bar1.Equals(bar2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_TBar_DifferentClose_ReturnsFalse()
|
||||
{
|
||||
var bar1 = new TBar(12345, 100, 110, 90, 105, 1000);
|
||||
var bar2 = new TBar(12345, 100, 110, 90, 106, 1000);
|
||||
|
||||
Assert.False(bar1.Equals(bar2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_TBar_DifferentVolume_ReturnsFalse()
|
||||
{
|
||||
var bar1 = new TBar(12345, 100, 110, 90, 105, 1000);
|
||||
var bar2 = new TBar(12345, 100, 110, 90, 105, 1001);
|
||||
|
||||
Assert.False(bar1.Equals(bar2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_Object_SameTBar_ReturnsTrue()
|
||||
{
|
||||
var bar1 = new TBar(12345, 100, 110, 90, 105, 1000);
|
||||
object bar2 = new TBar(12345, 100, 110, 90, 105, 1000);
|
||||
|
||||
Assert.True(bar1.Equals(bar2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_Object_DifferentType_ReturnsFalse()
|
||||
{
|
||||
var bar = new TBar(12345, 100, 110, 90, 105, 1000);
|
||||
object other = "not a TBar";
|
||||
|
||||
Assert.False(bar.Equals(other));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_Object_Null_ReturnsFalse()
|
||||
{
|
||||
var bar = new TBar(12345, 100, 110, 90, 105, 1000);
|
||||
|
||||
Assert.False(bar.Equals(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_SameBars_ReturnsSameHashCode()
|
||||
{
|
||||
var bar1 = new TBar(12345, 100, 110, 90, 105, 1000);
|
||||
var bar2 = new TBar(12345, 100, 110, 90, 105, 1000);
|
||||
|
||||
Assert.Equal(bar1.GetHashCode(), bar2.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_DifferentBars_ReturnsDifferentHashCode()
|
||||
{
|
||||
var bar1 = new TBar(12345, 100, 110, 90, 105, 1000);
|
||||
var bar2 = new TBar(12346, 100, 110, 90, 105, 1000);
|
||||
|
||||
Assert.NotEqual(bar1.GetHashCode(), bar2.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EqualityOperator_SameBars_ReturnsTrue()
|
||||
{
|
||||
var bar1 = new TBar(12345, 100, 110, 90, 105, 1000);
|
||||
var bar2 = new TBar(12345, 100, 110, 90, 105, 1000);
|
||||
|
||||
Assert.True(bar1 == bar2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EqualityOperator_DifferentBars_ReturnsFalse()
|
||||
{
|
||||
var bar1 = new TBar(12345, 100, 110, 90, 105, 1000);
|
||||
var bar2 = new TBar(12346, 100, 110, 90, 105, 1000);
|
||||
|
||||
Assert.False(bar1 == bar2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InequalityOperator_SameBars_ReturnsFalse()
|
||||
{
|
||||
var bar1 = new TBar(12345, 100, 110, 90, 105, 1000);
|
||||
var bar2 = new TBar(12345, 100, 110, 90, 105, 1000);
|
||||
|
||||
Assert.False(bar1 != bar2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InequalityOperator_DifferentBars_ReturnsTrue()
|
||||
{
|
||||
var bar1 = new TBar(12345, 100, 110, 90, 105, 1000);
|
||||
var bar2 = new TBar(12346, 100, 110, 90, 105, 1000);
|
||||
|
||||
Assert.True(bar1 != bar2);
|
||||
}
|
||||
|
||||
// Additional edge case tests
|
||||
[Fact]
|
||||
public void Constructor_WithLocalDateTime_ConvertsToUtc()
|
||||
{
|
||||
var localDateTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Local);
|
||||
var bar = new TBar(localDateTime, 100, 110, 90, 105, 1000);
|
||||
|
||||
// AsDateTime should return UTC
|
||||
Assert.Equal(DateTimeKind.Utc, bar.AsDateTime.Kind);
|
||||
Assert.Equal(localDateTime.ToUniversalTime().Ticks, bar.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithUnspecifiedDateTime_ConvertsToUtc()
|
||||
{
|
||||
var unspecifiedDateTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Unspecified);
|
||||
var bar = new TBar(unspecifiedDateTime, 100, 110, 90, 105, 1000);
|
||||
|
||||
// Should be converted to UTC
|
||||
Assert.Equal(DateTimeKind.Utc, bar.AsDateTime.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultTBar_HasZeroValues()
|
||||
{
|
||||
var bar = default(TBar);
|
||||
|
||||
Assert.Equal(0, bar.Time);
|
||||
Assert.Equal(0.0, bar.Open);
|
||||
Assert.Equal(0.0, bar.High);
|
||||
Assert.Equal(0.0, bar.Low);
|
||||
Assert.Equal(0.0, bar.Close);
|
||||
Assert.Equal(0.0, bar.Volume);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TBar_WithNaN_HandlesGracefully()
|
||||
{
|
||||
var bar = new TBar(12345, double.NaN, 110, 90, 105, 1000);
|
||||
|
||||
Assert.True(double.IsNaN(bar.Open));
|
||||
Assert.True(double.IsNaN(bar.O.Value));
|
||||
Assert.True(double.IsNaN(bar.OHL3)); // Uses Open
|
||||
Assert.True(double.IsNaN(bar.OC2)); // Uses Open
|
||||
Assert.True(double.IsNaN(bar.OHLC4)); // Uses Open
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TBar_WithInfinity_HandlesGracefully()
|
||||
{
|
||||
var bar = new TBar(12345, 100, double.PositiveInfinity, 90, 105, 1000);
|
||||
|
||||
Assert.True(double.IsPositiveInfinity(bar.High));
|
||||
Assert.True(double.IsPositiveInfinity(bar.H.Value));
|
||||
Assert.True(double.IsPositiveInfinity(bar.HL2)); // Uses High
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TBar_WithMaxValue_HandlesGracefully()
|
||||
{
|
||||
var bar = new TBar(12345, double.MaxValue, double.MaxValue, double.MinValue, 105, 1000);
|
||||
|
||||
Assert.Equal(double.MaxValue, bar.Open);
|
||||
Assert.Equal(double.MaxValue, bar.High);
|
||||
Assert.Equal(double.MinValue, bar.Low);
|
||||
// HL2 calculation with extreme values
|
||||
Assert.True(double.IsFinite(bar.HL2) || double.IsInfinity(bar.HL2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TBar_WithEpsilon_HandlesGracefully()
|
||||
{
|
||||
var bar = new TBar(12345, double.Epsilon, double.Epsilon, double.Epsilon, double.Epsilon, double.Epsilon);
|
||||
|
||||
Assert.Equal(double.Epsilon, bar.Open);
|
||||
Assert.Equal(double.Epsilon, bar.Close);
|
||||
Assert.True(bar.HL2 > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HL2_WithNegativeValues_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, -100, -90, -110, -95, 1000);
|
||||
|
||||
Assert.Equal(-100.0, bar.HL2); // (-90 + -110) / 2
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OHLC4_WithNegativeValues_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, -100, -90, -110, -100, 1000);
|
||||
|
||||
Assert.Equal(-100.0, bar.OHLC4); // (-100 + -90 + -110 + -100) / 4
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImplicitConversion_ToTValue_PreservesTimeAndClose()
|
||||
{
|
||||
const long time = 12_345_678_901_234_567;
|
||||
var bar = new TBar(time, 100, 110, 90, 105.5, 1000);
|
||||
|
||||
TValue tv = bar;
|
||||
|
||||
Assert.Equal(time, tv.Time);
|
||||
Assert.Equal(105.5, tv.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_WithNaN_DoesNotThrow()
|
||||
{
|
||||
var bar = new TBar(DateTime.UtcNow.Ticks, double.NaN, double.NaN, double.NaN, double.NaN, double.NaN);
|
||||
|
||||
string result = bar.ToString();
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains("NaN", result, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void O_H_L_C_V_AllHaveSameTime()
|
||||
{
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
var bar = new TBar(time, 100, 110, 90, 105, 1000);
|
||||
|
||||
Assert.Equal(time, bar.O.Time);
|
||||
Assert.Equal(time, bar.H.Time);
|
||||
Assert.Equal(time, bar.L.Time);
|
||||
Assert.Equal(time, bar.C.Time);
|
||||
Assert.Equal(time, bar.V.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HLCC4_DoubleWeightsClose()
|
||||
{
|
||||
// HLCC4 = (High + Low + Close + Close) / 4
|
||||
var bar = new TBar(0, 100, 120, 80, 100, 1000);
|
||||
|
||||
// (120 + 80 + 100 + 100) / 4 = 400 / 4 = 100
|
||||
Assert.Equal(100.0, bar.HLCC4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OHL3_ExcludesClose()
|
||||
{
|
||||
// OHL3 = (Open + High + Low) / 3
|
||||
var bar = new TBar(0, 90, 120, 60, 999, 1000);
|
||||
|
||||
// (90 + 120 + 60) / 3 = 270 / 3 = 90
|
||||
Assert.Equal(90.0, bar.OHL3);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
# TBar: OHLCV Bar Struct
|
||||
|
||||
## What It Does
|
||||
|
||||
`TBar` is a lightweight, immutable struct representing a single OHLCV (Open, High, Low, Close, Volume) bar. It serves as the fundamental unit for price data in QuanTAlib, designed to hold market data with minimal memory overhead while providing convenient accessors for common price derivations.
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
Financial data processing often involves millions of bars. Storing these as classes would create massive GC pressure and memory fragmentation. `TBar` is designed as a **pure data struct** to ensure:
|
||||
|
||||
* **Compactness**: Occupies exactly 48 bytes (1 `long` + 5 `double`s), fitting efficiently in memory.
|
||||
* **Immutability**: Thread-safe by default; values cannot change once created.
|
||||
* **Zero-Cost Abstractions**: Computed properties (like `HL2`) are calculated on-demand, requiring no extra storage.
|
||||
|
||||
## How It Works
|
||||
|
||||
`TBar` is a `readonly record struct` that stores:
|
||||
|
||||
* **Time**: Timestamp in ticks.
|
||||
* **Open, High, Low, Close**: Price components.
|
||||
* **Volume**: Traded volume.
|
||||
|
||||
It includes implicit conversions to `double` (defaulting to Close price) and `TValue` (Time + Close), allowing it to be used interchangeably with simpler types in many contexts.
|
||||
|
||||
## Structure
|
||||
|
||||
### Definition
|
||||
|
||||
```csharp
|
||||
public readonly record struct TBar(long Time, double Open, double High, double Low, double Close, double Volume);
|
||||
```
|
||||
|
||||
### Core Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| ------ | ------ | ------ |
|
||||
| `Time` | `long` | Timestamp in ticks (UTC). |
|
||||
| `Open` | `double` | Opening price. |
|
||||
| `High` | `double` | Highest price. |
|
||||
| `Low` | `double` | Lowest price. |
|
||||
| `Close` | `double` | Closing price. |
|
||||
| `Volume` | `double` | Traded volume. |
|
||||
|
||||
### Computed Properties (Zero-Storage)
|
||||
|
||||
| Property | Formula | Description |
|
||||
| ------ | ------ | ------ |
|
||||
| `HL2` | `(H + L) / 2` | Median Price. |
|
||||
| `OC2` | `(O + C) / 2` | Midpoint Price. |
|
||||
| `OHL3` | `(O + H + L) / 3` | Typical Price (Variant). |
|
||||
| `HLC3` | `(H + L + C) / 3` | Typical Price. |
|
||||
| `OHLC4` | `(O + H + L + C) / 4` | Weighted Close. |
|
||||
| `HLCC4` | `(H + L + 2C) / 4` | Weighted Close (Variant). |
|
||||
|
||||
### TValue Accessors
|
||||
|
||||
Efficiently extracts components as `TValue` pairs:
|
||||
|
||||
* `O`, `H`, `L`, `C`, `V`
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating a Bar
|
||||
|
||||
```csharp
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
```
|
||||
|
||||
### Implicit Conversions
|
||||
|
||||
```csharp
|
||||
TBar bar = ...;
|
||||
|
||||
// Treat as double (uses Close price)
|
||||
double price = bar;
|
||||
|
||||
// Treat as TValue (Time + Close)
|
||||
TValue tv = bar;
|
||||
|
||||
// Treat as DateTime
|
||||
DateTime dt = bar;
|
||||
```
|
||||
|
||||
### Using Computed Properties
|
||||
|
||||
```csharp
|
||||
// Calculate Typical Price on the fly
|
||||
double typical = bar.HLC3;
|
||||
```
|
||||
|
||||
## Performance Profile
|
||||
|
||||
* **Memory**: 48 bytes per instance.
|
||||
* **Allocation**: 0 bytes (Stack allocated).
|
||||
* **Access**: Direct field access (no property overhead).
|
||||
|
||||
## Integration
|
||||
|
||||
`TBar` is the primary input for:
|
||||
|
||||
* **TBarSeries**: A collection of bars.
|
||||
* **Indicators**: Some indicators (like ATR) require full `TBar` input rather than just a single value.
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
* **SkipLocalsInit**: Marked with `[SkipLocalsInit]` for performance in tight loops.
|
||||
* **AggressiveInlining**: All computed properties are inlined to ensure they are as fast as writing the formula manually.
|
||||
|
||||
## References
|
||||
|
||||
* [OHLC Chart](https://en.wikipedia.org/wiki/Open-high-low-close_chart)
|
||||
* [C# Record Structs](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record)
|
||||
@@ -0,0 +1,48 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// A lightweight struct representing an OHLCV bar.
|
||||
/// Pure data type: 48 bytes (long + 5 doubles).
|
||||
/// </summary>
|
||||
[SkipLocalsInit]
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
public readonly record struct TBar(long Time, double Open, double High, double Low, double Close, 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(DateTime time, double open, double high, double low, double close, double volume)
|
||||
: this(time.Kind == DateTimeKind.Utc ? time.Ticks : time.ToUniversalTime().Ticks, open, high, low, close, 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}]";
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public delegate void BarSignal(object source, in TBarEventArgs args);
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class TBarEventArgs : EventArgs
|
||||
{
|
||||
public readonly TBar Bar;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TBarEventArgs(TBar bar) => Bar = bar;
|
||||
}
|
||||
|
||||
[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 TSeries Open { get; init; }
|
||||
public TSeries High { get; init; }
|
||||
public TSeries Low { get; init; }
|
||||
public TSeries Close { get; init; }
|
||||
public TSeries Volume { get; init; }
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TBarSeries(object source) : this()
|
||||
{
|
||||
var pubEvent = source.GetType().GetEvent("Pub");
|
||||
pubEvent?.AddEventHandler(source, new BarSignal(Sub));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public new virtual void Add(TBar bar)
|
||||
{
|
||||
if (bar.IsNew || base.Count == 0)
|
||||
{
|
||||
base.Add(bar);
|
||||
}
|
||||
else
|
||||
{
|
||||
this[^1] = bar;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
[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));
|
||||
|
||||
[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));
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Add(TBarSeries series)
|
||||
{
|
||||
if (series == this)
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,601 @@
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class TBarSeriesTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_Default_CreatesEmptySeries()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
|
||||
Assert.Empty(series);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithCapacity_CreatesEmptySeries()
|
||||
{
|
||||
var series = new TBarSeries(100);
|
||||
|
||||
Assert.Empty(series);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Name_DefaultValue_IsBar()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
|
||||
Assert.Equal("Bar", series.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Name_CanBeSet()
|
||||
{
|
||||
var series = new TBarSeries { Name = "TestBars" };
|
||||
|
||||
Assert.Equal("TestBars", series.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_NewBar_IncreasesCount()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
var bar = new TBar(DateTime.UtcNow.Ticks, 100, 110, 90, 105, 1000);
|
||||
|
||||
series.Add(bar, isNew: true);
|
||||
|
||||
Assert.Single(series);
|
||||
Assert.Equal(105.0, series.Last.Close);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_UpdateBar_DoesNotIncreaseCount()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
var bar1 = new TBar(time, 100, 110, 90, 105, 1000);
|
||||
var bar2 = new TBar(time, 100, 112, 90, 108, 1200);
|
||||
|
||||
series.Add(bar1, isNew: true);
|
||||
series.Add(bar2, isNew: false);
|
||||
|
||||
Assert.Single(series);
|
||||
Assert.Equal(108.0, series.Last.Close);
|
||||
Assert.Equal(112.0, series.Last.High);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_UpdateOnEmptySeries_AddsNewBar()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
var bar = new TBar(DateTime.UtcNow.Ticks, 100, 110, 90, 105, 1000);
|
||||
|
||||
series.Add(bar, isNew: false);
|
||||
|
||||
Assert.Single(series);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithLongTime_AddsBar()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
|
||||
series.Add(time, 100, 110, 90, 105, 1000, isNew: true);
|
||||
|
||||
Assert.Single(series);
|
||||
Assert.Equal(time, series.Last.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithDateTime_AddsBar()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
var dt = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc);
|
||||
|
||||
series.Add(dt, 100, 110, 90, 105, 1000, isNew: true);
|
||||
|
||||
Assert.Single(series);
|
||||
Assert.Equal(dt.Ticks, series.Last.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithEnumerables_AddsMultipleBars()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
long[] times = [100, 200, 300];
|
||||
double[] opens = [10, 20, 30];
|
||||
double[] highs = [15, 25, 35];
|
||||
double[] lows = [5, 15, 25];
|
||||
double[] closes = [12, 22, 32];
|
||||
double[] volumes = [100, 200, 300];
|
||||
|
||||
series.Add(times, opens, highs, lows, closes, volumes);
|
||||
|
||||
Assert.Equal(3, series.Count);
|
||||
Assert.Equal(10, series[0].Open);
|
||||
Assert.Equal(32, series[2].Close);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubSeries_AreUpdated()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
var bar = new TBar(DateTime.UtcNow.Ticks, 100, 110, 90, 105, 1000);
|
||||
|
||||
series.Add(bar, isNew: true);
|
||||
|
||||
Assert.Single(series.Open);
|
||||
Assert.Single(series.High);
|
||||
Assert.Single(series.Low);
|
||||
Assert.Single(series.Close);
|
||||
Assert.Single(series.Volume);
|
||||
|
||||
Assert.Equal(100.0, series.Open.Last.Value);
|
||||
Assert.Equal(110.0, series.High.Last.Value);
|
||||
Assert.Equal(90.0, series.Low.Last.Value);
|
||||
Assert.Equal(105.0, series.Close.Last.Value);
|
||||
Assert.Equal(1000.0, series.Volume.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubSeries_Aliases_Work()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
var bar = new TBar(DateTime.UtcNow.Ticks, 100, 110, 90, 105, 1000);
|
||||
series.Add(bar, isNew: true);
|
||||
|
||||
Assert.Same(series.Open, series.O);
|
||||
Assert.Same(series.High, series.H);
|
||||
Assert.Same(series.Low, series.L);
|
||||
Assert.Same(series.Close, series.C);
|
||||
Assert.Same(series.Volume, series.V);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubSeries_HaveCorrectNames()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
|
||||
Assert.Equal("Open", series.Open.Name);
|
||||
Assert.Equal("High", series.High.Name);
|
||||
Assert.Equal("Low", series.Low.Name);
|
||||
Assert.Equal("Close", series.Close.Name);
|
||||
Assert.Equal("Volume", series.Volume.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Last_EmptySeries_ReturnsDefault()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
|
||||
var last = series.Last;
|
||||
|
||||
Assert.Equal(0, last.Time);
|
||||
Assert.Equal(0.0, last.Open);
|
||||
Assert.Equal(0.0, last.Close);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Last_NonEmptySeries_ReturnsLastBar()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
|
||||
var last = series.Last;
|
||||
|
||||
Assert.Equal(200, last.Time);
|
||||
Assert.Equal(22.0, last.Close);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LastTime_EmptySeries_ReturnsZero()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
Assert.Equal(0, series.LastTime);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LastTime_NonEmptySeries_ReturnsLastTime()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
|
||||
Assert.Equal(200, series.LastTime);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LastOpen_EmptySeries_ReturnsNaN()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
Assert.True(double.IsNaN(series.LastOpen));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LastOpen_NonEmptySeries_ReturnsLastOpen()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
|
||||
Assert.Equal(20.0, series.LastOpen);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LastHigh_EmptySeries_ReturnsNaN()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
Assert.True(double.IsNaN(series.LastHigh));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LastHigh_NonEmptySeries_ReturnsLastHigh()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
|
||||
Assert.Equal(25.0, series.LastHigh);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LastLow_EmptySeries_ReturnsNaN()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
Assert.True(double.IsNaN(series.LastLow));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LastLow_NonEmptySeries_ReturnsLastLow()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
|
||||
Assert.Equal(15.0, series.LastLow);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LastClose_EmptySeries_ReturnsNaN()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
Assert.True(double.IsNaN(series.LastClose));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LastClose_NonEmptySeries_ReturnsLastClose()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
|
||||
Assert.Equal(22.0, series.LastClose);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LastVolume_EmptySeries_ReturnsNaN()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
Assert.True(double.IsNaN(series.LastVolume));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LastVolume_NonEmptySeries_ReturnsLastVolume()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
|
||||
Assert.Equal(200.0, series.LastVolume);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_ReturnsCorrectBar()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
series.Add(300, 30, 35, 25, 32, 300);
|
||||
|
||||
Assert.Equal(100, series[0].Time);
|
||||
Assert.Equal(10.0, series[0].Open);
|
||||
Assert.Equal(200, series[1].Time);
|
||||
Assert.Equal(22.0, series[1].Close);
|
||||
Assert.Equal(300, series[2].Time);
|
||||
Assert.Equal(32.0, series[2].Close);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Count_ReturnsCorrectValue()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
|
||||
Assert.Empty(series);
|
||||
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
Assert.Single(series);
|
||||
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
Assert.Equal(2, series.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetEnumerator_IteratesAllBars()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
series.Add(300, 30, 35, 25, 32, 300);
|
||||
|
||||
var list = series.ToList();
|
||||
|
||||
Assert.Equal(3, list.Count);
|
||||
Assert.Equal(10.0, list[0].Open);
|
||||
Assert.Equal(22.0, list[1].Close);
|
||||
Assert.Equal(32.0, list[2].Close);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetEnumerator_NonGeneric_Works()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100, isNew: true);
|
||||
series.Add(200, 20, 25, 15, 22, 200, isNew: true);
|
||||
|
||||
var list = new List<object>();
|
||||
|
||||
#pragma warning disable S4158
|
||||
foreach (var item in (IEnumerable)series)
|
||||
{
|
||||
list.Add(item);
|
||||
}
|
||||
|
||||
Assert.Equal(2, list.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_Event_IsRaisedOnAdd()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
TBar? received = null;
|
||||
series.Pub += (object? sender, in TBarEventArgs args) => received = args.Value;
|
||||
|
||||
var barToAdd = new TBar(100, 10, 15, 5, 12, 100);
|
||||
series.Add(barToAdd, isNew: true);
|
||||
|
||||
Assert.NotNull(received);
|
||||
Assert.Equal(100, received.Value.Time);
|
||||
Assert.Equal(12.0, received.Value.Close);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_Event_IsRaisedOnUpdate()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
TBar? received = null;
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Pub += (object? sender, in TBarEventArgs args) => received = args.Value;
|
||||
|
||||
series.Add(100, 10, 18, 5, 15, 150, isNew: false);
|
||||
|
||||
Assert.NotNull(received);
|
||||
Assert.Equal(15.0, received.Value.Close);
|
||||
Assert.Equal(18.0, received.Value.High);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubSeries_ShareSameTimeArray()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
|
||||
Assert.Equal(series.Open.Times[0], series.Close.Times[0]);
|
||||
Assert.Equal(series.High.Times[1], series.Volume.Times[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_MultipleBars_MaintainsOrder()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
series.Add(300, 30, 35, 25, 32, 300);
|
||||
|
||||
Assert.Equal(3, series.Count);
|
||||
Assert.Equal(100, series[0].Time);
|
||||
Assert.Equal(200, series[1].Time);
|
||||
Assert.Equal(300, series[2].Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithEnumerables_MismatchedLengths_ThrowsArgumentException()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
long[] times = [100, 200, 300];
|
||||
double[] opens = [10, 20]; // Mismatched length
|
||||
double[] highs = [15, 25, 35];
|
||||
double[] lows = [5, 15, 25];
|
||||
double[] closes = [12, 22, 32];
|
||||
double[] volumes = [100, 200, 300];
|
||||
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
series.Add(times, opens, highs, lows, closes, volumes));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_OutOfBounds_ThrowsException()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => _ = series[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_NegativeIndex_ThrowsException()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
|
||||
const int invalidIndex = -1;
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => _ = series[invalidIndex]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_BeyondCount_ThrowsException()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => _ = series[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithNaN_PreservesNaN()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
var bar = new TBar(DateTime.UtcNow.Ticks, double.NaN, 110, 90, 105, 1000);
|
||||
|
||||
series.Add(bar, isNew: true);
|
||||
|
||||
Assert.True(double.IsNaN(series.Last.Open));
|
||||
Assert.True(double.IsNaN(series.Open.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithInfinity_PreservesInfinity()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
var bar = new TBar(DateTime.UtcNow.Ticks, 100, double.PositiveInfinity, 90, 105, 1000);
|
||||
|
||||
series.Add(bar, isNew: true);
|
||||
|
||||
Assert.True(double.IsPositiveInfinity(series.Last.High));
|
||||
Assert.True(double.IsPositiveInfinity(series.High.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubSeries_EmptySeries_HaveZeroCount()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
|
||||
Assert.Empty(series.Open);
|
||||
Assert.Empty(series.High);
|
||||
Assert.Empty(series.Low);
|
||||
Assert.Empty(series.Close);
|
||||
Assert.Empty(series.Volume);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubSeries_ValuesSpan_ReturnsCorrectData()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
|
||||
ReadOnlySpan<double> closeValues = series.Close.Values;
|
||||
|
||||
Assert.Equal(2, closeValues.Length);
|
||||
Assert.Equal(12.0, closeValues[0]);
|
||||
Assert.Equal(22.0, closeValues[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubSeries_TimesSpan_ReturnsCorrectData()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
|
||||
ReadOnlySpan<long> times = series.Close.Times;
|
||||
|
||||
Assert.Equal(2, times.Length);
|
||||
Assert.Equal(100, times[0]);
|
||||
Assert.Equal(200, times[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_EventArgs_ContainsIsNewFlag()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
bool? receivedIsNew = null;
|
||||
series.Pub += (object? sender, in TBarEventArgs args) => receivedIsNew = args.IsNew;
|
||||
|
||||
series.Add(new TBar(100, 10, 15, 5, 12, 100), isNew: true);
|
||||
|
||||
Assert.True(receivedIsNew);
|
||||
|
||||
series.Add(new TBar(100, 10, 18, 5, 15, 150), isNew: false);
|
||||
|
||||
Assert.False(receivedIsNew);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithEnumerables_EmptyArrays_AddsNothing()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
var empty = Array.Empty<long>();
|
||||
var emptyD = Array.Empty<double>();
|
||||
|
||||
series.Add(empty, emptyD, emptyD, emptyD, emptyD, emptyD);
|
||||
|
||||
Assert.Empty(series);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithCapacity_DoesNotAffectCount()
|
||||
{
|
||||
var series = new TBarSeries(1000);
|
||||
|
||||
Assert.Empty(series);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetEnumerator_ExplicitGenericInterface_Works()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
series.Add(300, 30, 35, 25, 32, 300);
|
||||
|
||||
// Explicitly call IEnumerable<TBar>.GetEnumerator() through interface cast
|
||||
IEnumerable<TBar> genericEnumerable = series;
|
||||
using var enumerator = genericEnumerable.GetEnumerator();
|
||||
|
||||
var closes = new List<double>();
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
closes.Add(enumerator.Current.Close);
|
||||
}
|
||||
|
||||
Assert.Equal(3, closes.Count);
|
||||
Assert.Equal(12.0, closes[0]);
|
||||
Assert.Equal(22.0, closes[1]);
|
||||
Assert.Equal(32.0, closes[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetEnumerator_ExplicitNonGenericInterface_Works()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(100, 10, 15, 5, 12, 100);
|
||||
series.Add(200, 20, 25, 15, 22, 200);
|
||||
|
||||
// Explicitly call IEnumerable.GetEnumerator() through interface cast
|
||||
IEnumerable nonGenericEnumerable = series;
|
||||
var enumerator = nonGenericEnumerable.GetEnumerator();
|
||||
|
||||
var closes = new List<double>();
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
var bar = (TBar)enumerator.Current;
|
||||
closes.Add(bar.Close);
|
||||
}
|
||||
|
||||
Assert.Equal(2, closes.Count);
|
||||
Assert.Equal(12.0, closes[0]);
|
||||
Assert.Equal(22.0, closes[1]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
# TBarSeries: OHLCV Data Container
|
||||
|
||||
## What It Does
|
||||
|
||||
`TBarSeries` is a high-performance collection of OHLCV bars. It is the primary data structure for managing historical and real-time market data in QuanTAlib. It uses a **Structure of Arrays (SoA)** layout to optimize memory access and enable efficient SIMD operations across individual price components.
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
A naive implementation of a bar series would be a `List<TBar>`. However, this is inefficient for technical analysis. Most indicators only need one component at a time (e.g., SMA uses Close prices). Iterating over a `List<TBar>` to get Close prices loads unnecessary Open, High, Low, and Volume data into the CPU cache, wasting bandwidth.
|
||||
|
||||
`TBarSeries` solves this by storing each component in its own contiguous array. This allows:
|
||||
|
||||
* **Component Views**: You can access `Close` prices as a `TSeries` without copying data.
|
||||
* **Cache Efficiency**: Iterating over `Close` prices loads *only* Close prices.
|
||||
* **Unified Time**: All component series share a single Time array, ensuring synchronization.
|
||||
|
||||
## How It Works
|
||||
|
||||
Internally, `TBarSeries` maintains six parallel lists:
|
||||
|
||||
1. `_t` (Time)
|
||||
2. `_o` (Open)
|
||||
3. `_h` (High)
|
||||
4. `_l` (Low)
|
||||
5. `_c` (Close)
|
||||
6. `_v` (Volume)
|
||||
|
||||
It exposes these internal lists as `TSeries` properties (`Open`, `High`, `Low`, `Close`, `Volume`), which act as read-only views into the master data.
|
||||
|
||||
## Structure
|
||||
|
||||
### Definition
|
||||
|
||||
```csharp
|
||||
public class TBarSeries : IReadOnlyList<TBar>
|
||||
{
|
||||
// Component Views (TSeries)
|
||||
public TSeries Open { get; }
|
||||
public TSeries High { get; }
|
||||
public TSeries Low { get; }
|
||||
public TSeries Close { get; }
|
||||
public TSeries Volume { get; }
|
||||
|
||||
// Aliases
|
||||
public TSeries O => Open;
|
||||
public TSeries H => High;
|
||||
public TSeries L => Low;
|
||||
public TSeries C => Close;
|
||||
public TSeries V => Volume;
|
||||
}
|
||||
```
|
||||
|
||||
### Core Methods
|
||||
|
||||
| Method | Description |
|
||||
| ------ | ------ |
|
||||
| `Add(TBar bar, bool isNew)` | Adds a bar or updates the last one. |
|
||||
| `Add(DateTime time, double o, double h, double l, double c, double v)` | Adds raw values directly. |
|
||||
| `Count` | Returns the number of bars. |
|
||||
| `Last` | Returns the most recent `TBar`. |
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating and Populating
|
||||
|
||||
```csharp
|
||||
var bars = new TBarSeries();
|
||||
|
||||
// Add a new bar
|
||||
bars.Add(new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000));
|
||||
|
||||
// Add raw values
|
||||
bars.Add(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
```
|
||||
|
||||
### Accessing Data
|
||||
|
||||
```csharp
|
||||
// Get the last full bar
|
||||
TBar lastBar = bars.Last;
|
||||
|
||||
// Get the Close series (Zero-Copy)
|
||||
TSeries closes = bars.Close;
|
||||
|
||||
// Calculate SMA on Close prices
|
||||
var sma = new Sma(14);
|
||||
var result = sma.Calculate(bars.Close);
|
||||
```
|
||||
|
||||
### Streaming Updates
|
||||
|
||||
```csharp
|
||||
// New minute starts
|
||||
bars.Add(newBar, isNew: true);
|
||||
|
||||
// Price updates within the same minute
|
||||
bars.Add(updatedBar, isNew: false); // Updates the last bar in place
|
||||
```
|
||||
|
||||
## Performance Profile
|
||||
|
||||
* **Memory Layout**: SoA (Structure of Arrays).
|
||||
* **Component Access**: Zero-copy `TSeries` views.
|
||||
* **Iteration**: Cache-friendly for single-component analysis.
|
||||
|
||||
## Integration
|
||||
|
||||
`TBarSeries` is the standard input for multi-input indicators (like ATR, ADX) and the primary data source for trading strategies.
|
||||
|
||||
* **Indicators**: Can be passed to indicators that require full bar data.
|
||||
* **Strategies**: Provides the historical context needed for signal generation.
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
* **Shared Storage**: The `TSeries` views (`Open`, `Close`, etc.) do not own their data; they point to the internal lists of the `TBarSeries`. This means modifying the `TBarSeries` automatically updates all views.
|
||||
* **Synchronization**: Because all views share the same `_t` (Time) list, they are guaranteed to be perfectly synchronized.
|
||||
|
||||
## References
|
||||
|
||||
* [Structure of Arrays (SoA)](https://en.wikipedia.org/wiki/AOS_and_SOA)
|
||||
* [Data Locality](https://gameprogrammingpatterns.com/data-locality.html)
|
||||
@@ -0,0 +1,388 @@
|
||||
using System.Collections;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Performance-focused event args for TBar updates.
|
||||
/// Implemented as struct to avoid heap allocations in high-frequency event dispatch.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
public readonly struct TBarEventArgs : IEquatable<TBarEventArgs>
|
||||
{
|
||||
public TBar Value { get; init; }
|
||||
public bool IsNew { get; init; }
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool Equals(TBarEventArgs other) =>
|
||||
Value.Equals(other.Value) && IsNew == other.IsNew;
|
||||
|
||||
public override bool Equals(object? obj) =>
|
||||
obj is TBarEventArgs other && Equals(other);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
HashCode.Combine(Value, IsNew);
|
||||
|
||||
public static bool operator ==(TBarEventArgs left, TBarEventArgs right) =>
|
||||
left.Equals(right);
|
||||
|
||||
public static bool operator !=(TBarEventArgs left, TBarEventArgs right) =>
|
||||
!left.Equals(right);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// High-performance enumerator for TBarSeries.
|
||||
/// </summary>
|
||||
public struct TBarSeriesEnumerator : IEnumerator<TBar>, IEquatable<TBarSeriesEnumerator>
|
||||
{
|
||||
private readonly List<long> _t;
|
||||
private readonly List<double> _o;
|
||||
private readonly List<double> _h;
|
||||
private readonly List<double> _l;
|
||||
private readonly List<double> _c;
|
||||
private readonly List<double> _v;
|
||||
private readonly int _count;
|
||||
private int _index;
|
||||
private TBar _current;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal TBarSeriesEnumerator(List<long> t, List<double> o, List<double> h, List<double> l, List<double> c, List<double> v)
|
||||
{
|
||||
_t = t;
|
||||
_o = o;
|
||||
_h = h;
|
||||
_l = l;
|
||||
_c = c;
|
||||
_v = v;
|
||||
_count = c.Count;
|
||||
_index = -1;
|
||||
_current = default;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (_index + 1 >= _count)
|
||||
return false;
|
||||
|
||||
_index++;
|
||||
_current = new TBar(_t[_index], _o[_index], _h[_index], _l[_index], _c[_index], _v[_index]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public readonly TBar Current => _current;
|
||||
readonly object IEnumerator.Current => Current;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Reset()
|
||||
{
|
||||
_index = -1;
|
||||
_current = default;
|
||||
}
|
||||
|
||||
public readonly void Dispose() { }
|
||||
|
||||
public readonly bool Equals(TBarSeriesEnumerator other) =>
|
||||
ReferenceEquals(_t, other._t) &&
|
||||
ReferenceEquals(_c, other._c) &&
|
||||
_count == other._count &&
|
||||
_index == other._index;
|
||||
|
||||
public override readonly bool Equals(object? obj) =>
|
||||
obj is TBarSeriesEnumerator other && Equals(other);
|
||||
|
||||
public override readonly int GetHashCode() =>
|
||||
HashCode.Combine(RuntimeHelpers.GetHashCode(_t), RuntimeHelpers.GetHashCode(_c), _count, _index);
|
||||
|
||||
public static bool operator ==(TBarSeriesEnumerator left, TBarSeriesEnumerator right) => left.Equals(right);
|
||||
public static bool operator !=(TBarSeriesEnumerator left, TBarSeriesEnumerator right) => !left.Equals(right);
|
||||
}
|
||||
|
||||
// Performance-focused event args struct; not derived from EventArgs by design.
|
||||
// We intentionally deviate from the standard EventArgs pattern here for perf.
|
||||
#pragma warning disable MA0046 // The second parameter must be of type 'System.EventArgs' or a derived type
|
||||
public delegate void TBarPublishedHandler(object? sender, in TBarEventArgs args);
|
||||
|
||||
public class TBarSeries : IReadOnlyList<TBar>
|
||||
{
|
||||
#pragma warning disable MA0016 // Prefer using collection abstraction instead of implementation
|
||||
private readonly List<long> _t;
|
||||
private readonly List<double> _o;
|
||||
private readonly List<double> _h;
|
||||
private readonly List<double> _l;
|
||||
private readonly List<double> _c;
|
||||
private readonly List<double> _v;
|
||||
#pragma warning restore MA0016
|
||||
|
||||
public string Name { get; set; } = "Bar";
|
||||
public event TBarPublishedHandler? Pub;
|
||||
#pragma warning restore MA0046
|
||||
|
||||
// Note: These views share underlying storage. Do not modify directly; use TBarSeries.Add() instead.
|
||||
public TSeries Open { get; }
|
||||
public TSeries High { get; }
|
||||
public TSeries Low { get; }
|
||||
public TSeries Close { get; }
|
||||
public TSeries Volume { get; }
|
||||
// Aliases for convenience
|
||||
public TSeries O => Open;
|
||||
public TSeries H => High;
|
||||
public TSeries L => Low;
|
||||
public TSeries C => Close;
|
||||
public TSeries V => Volume;
|
||||
|
||||
public TBarSeries() : this(0)
|
||||
{
|
||||
}
|
||||
|
||||
public TBarSeries(int capacity)
|
||||
{
|
||||
_t = new List<long>(capacity);
|
||||
_o = new List<double>(capacity);
|
||||
_h = new List<double>(capacity);
|
||||
_l = new List<double>(capacity);
|
||||
_c = new List<double>(capacity);
|
||||
_v = new List<double>(capacity);
|
||||
|
||||
Open = new TSeries(_t, _o, ShareStorageTag.Instance) { Name = "Open" };
|
||||
High = new TSeries(_t, _h, ShareStorageTag.Instance) { Name = "High" };
|
||||
Low = new TSeries(_t, _l, ShareStorageTag.Instance) { Name = "Low" };
|
||||
Close = new TSeries(_t, _c, ShareStorageTag.Instance) { Name = "Close" };
|
||||
Volume = new TSeries(_t, _v, ShareStorageTag.Instance) { Name = "Volume" };
|
||||
}
|
||||
|
||||
public int Count
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _c.Count;
|
||||
}
|
||||
|
||||
public TBar this[int index]
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => new TBar(_t[index], _o[index], _h[index], _l[index], _c[index], _v[index]);
|
||||
}
|
||||
|
||||
public TBar Last
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _c.Count > 0 ? new(_t[^1], _o[^1], _h[^1], _l[^1], _c[^1], _v[^1]) : default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the last bar without allocating a new TBar on failure.
|
||||
/// Returns true if successful; false if the series is empty.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryGetLast(out TBar bar)
|
||||
{
|
||||
if (_c.Count > 0)
|
||||
{
|
||||
bar = new TBar(_t[^1], _o[^1], _h[^1], _l[^1], _c[^1], _v[^1]);
|
||||
return true;
|
||||
}
|
||||
bar = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public long LastTime { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _t.Count > 0 ? _t[^1] : 0; }
|
||||
public double LastOpen { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _o.Count > 0 ? _o[^1] : double.NaN; }
|
||||
public double LastHigh { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _h.Count > 0 ? _h[^1] : double.NaN; }
|
||||
public double LastLow { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _l.Count > 0 ? _l[^1] : double.NaN; }
|
||||
public double LastClose { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _c.Count > 0 ? _c[^1] : double.NaN; }
|
||||
public double LastVolume { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _v.Count > 0 ? _v[^1] : double.NaN; }
|
||||
|
||||
/// <summary>
|
||||
/// Direct access to the underlying Time array as a Span.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<long> Times
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => CollectionsMarshal.AsSpan(_t);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direct access to the underlying Open array as a Span for SIMD operations.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<double> OpenValues
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => CollectionsMarshal.AsSpan(_o);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direct access to the underlying High array as a Span for SIMD operations.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<double> HighValues
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => CollectionsMarshal.AsSpan(_h);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direct access to the underlying Low array as a Span for SIMD operations.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<double> LowValues
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => CollectionsMarshal.AsSpan(_l);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direct access to the underlying Close array as a Span for SIMD operations.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<double> CloseValues
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => CollectionsMarshal.AsSpan(_c);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direct access to the underlying Volume array as a Span for SIMD operations.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<double> VolumeValues
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => CollectionsMarshal.AsSpan(_v);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Add(TBar bar, bool isNew = true)
|
||||
{
|
||||
if (isNew || _c.Count == 0)
|
||||
{
|
||||
_t.Add(bar.Time);
|
||||
_o.Add(bar.Open);
|
||||
_h.Add(bar.High);
|
||||
_l.Add(bar.Low);
|
||||
_c.Add(bar.Close);
|
||||
_v.Add(bar.Volume);
|
||||
}
|
||||
else
|
||||
{
|
||||
int lastIdx = _c.Count - 1;
|
||||
_t[lastIdx] = bar.Time;
|
||||
_o[lastIdx] = bar.Open;
|
||||
_h[lastIdx] = bar.High;
|
||||
_l[lastIdx] = bar.Low;
|
||||
_c[lastIdx] = bar.Close;
|
||||
_v[lastIdx] = bar.Volume;
|
||||
}
|
||||
|
||||
Pub?.Invoke(this, new TBarEventArgs { Value = bar, IsNew = isNew });
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Add(long time, double open, double high, double low, double close, double volume, bool isNew = true) =>
|
||||
Add(new TBar(time, open, high, low, close, volume), isNew);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Add(DateTime time, double open, double high, double low, double close, double volume, bool isNew = true) =>
|
||||
Add(new TBar(time.Ticks, open, high, low, close, volume), isNew);
|
||||
|
||||
public void Add(IEnumerable<long> t, IEnumerable<double> o, IEnumerable<double> h, IEnumerable<double> l, IEnumerable<double> c, IEnumerable<double> v)
|
||||
{
|
||||
var tArr = t as long[] ?? t.ToArray();
|
||||
var oArr = o as double[] ?? o.ToArray();
|
||||
var hArr = h as double[] ?? h.ToArray();
|
||||
var lArr = l as double[] ?? l.ToArray();
|
||||
var cArr = c as double[] ?? c.ToArray();
|
||||
var vArr = v as double[] ?? v.ToArray();
|
||||
|
||||
if (tArr.Length != oArr.Length || oArr.Length != hArr.Length ||
|
||||
hArr.Length != lArr.Length || lArr.Length != cArr.Length ||
|
||||
cArr.Length != vArr.Length)
|
||||
{
|
||||
throw new ArgumentException("All arrays must have the same length", nameof(t));
|
||||
}
|
||||
|
||||
for (int i = 0; i < tArr.Length; i++)
|
||||
{
|
||||
Add(tArr[i], oArr[i], hArr[i], lArr[i], cArr[i], vArr[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Zero-allocation bulk add using ReadOnlySpan parameters.
|
||||
/// Does not fire Pub events for each bar (use for initial data loading).
|
||||
/// </summary>
|
||||
/// <param name="t">Timestamps as ticks</param>
|
||||
/// <param name="o">Open prices</param>
|
||||
/// <param name="h">High prices</param>
|
||||
/// <param name="l">Low prices</param>
|
||||
/// <param name="c">Close prices</param>
|
||||
/// <param name="v">Volume values</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void AddRange(ReadOnlySpan<long> t, ReadOnlySpan<double> o, ReadOnlySpan<double> h, ReadOnlySpan<double> l, ReadOnlySpan<double> c, ReadOnlySpan<double> v)
|
||||
{
|
||||
int len = t.Length;
|
||||
if (o.Length != len || h.Length != len || l.Length != len || c.Length != len || v.Length != len)
|
||||
throw new ArgumentException("All spans must have the same length", nameof(t));
|
||||
|
||||
// Pre-allocate capacity to avoid repeated resizing
|
||||
int newCapacity = _c.Count + len;
|
||||
if (_t.Capacity < newCapacity)
|
||||
{
|
||||
_t.Capacity = newCapacity;
|
||||
_o.Capacity = newCapacity;
|
||||
_h.Capacity = newCapacity;
|
||||
_l.Capacity = newCapacity;
|
||||
_c.Capacity = newCapacity;
|
||||
_v.Capacity = newCapacity;
|
||||
}
|
||||
|
||||
// Bulk add without event firing (for initial data loading)
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
_t.Add(t[i]);
|
||||
_o.Add(o[i]);
|
||||
_h.Add(h[i]);
|
||||
_l.Add(l[i]);
|
||||
_c.Add(c[i]);
|
||||
_v.Add(v[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Zero-allocation bulk add using ReadOnlySpan of TBar structs.
|
||||
/// Does not fire Pub events for each bar (use for initial data loading).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void AddRange(ReadOnlySpan<TBar> bars)
|
||||
{
|
||||
int len = bars.Length;
|
||||
if (len == 0) return;
|
||||
|
||||
// Pre-allocate capacity to avoid repeated resizing
|
||||
int newCapacity = _c.Count + len;
|
||||
if (_t.Capacity < newCapacity)
|
||||
{
|
||||
_t.Capacity = newCapacity;
|
||||
_o.Capacity = newCapacity;
|
||||
_h.Capacity = newCapacity;
|
||||
_l.Capacity = newCapacity;
|
||||
_c.Capacity = newCapacity;
|
||||
_v.Capacity = newCapacity;
|
||||
}
|
||||
|
||||
// Bulk add without event firing (for initial data loading)
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
ref readonly TBar bar = ref bars[i];
|
||||
_t.Add(bar.Time);
|
||||
_o.Add(bar.Open);
|
||||
_h.Add(bar.High);
|
||||
_l.Add(bar.Low);
|
||||
_c.Add(bar.Close);
|
||||
_v.Add(bar.Volume);
|
||||
}
|
||||
}
|
||||
|
||||
// IEnumerable implementation with struct enumerator for zero-allocation iteration
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TBarSeriesEnumerator GetEnumerator() => new(_t, _o, _h, _l, _c, _v);
|
||||
|
||||
IEnumerator<TBar> IEnumerable<TBar>.GetEnumerator() => GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public delegate void ValueSignal(object source, in ValueEventArgs args);
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class ValueEventArgs : EventArgs
|
||||
{
|
||||
public readonly TValue Tick;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ValueEventArgs(TValue value) => Tick = value;
|
||||
}
|
||||
|
||||
[SkipLocalsInit]
|
||||
public class TSeries : List<TValue>
|
||||
{
|
||||
private static readonly TValue Default = new(DateTime.MinValue, double.NaN);
|
||||
|
||||
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; }
|
||||
|
||||
/// <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.
|
||||
/// </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()
|
||||
{
|
||||
Name = "Data";
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TSeries(object source) : this()
|
||||
{
|
||||
var pubEvent = source.GetType().GetEvent("Pub");
|
||||
if (pubEvent != null)
|
||||
{
|
||||
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);
|
||||
}
|
||||
else
|
||||
{
|
||||
this[^1] = tick;
|
||||
}
|
||||
Pub?.Invoke(this, new ValueEventArgs(tick));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public virtual void Add(DateTime Time, double Value, bool IsNew = true, bool IsHot = true) =>
|
||||
Add(new TValue(Time, Value, IsNew, IsHot));
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public virtual void Add(double Value, bool IsNew = true, bool IsHot = true) =>
|
||||
Add(new TValue(DateTime.UtcNow, Value, IsNew, IsHot));
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
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++)
|
||||
{
|
||||
Add(startTime, valueList[i]);
|
||||
startTime = startTime.AddHours(1);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Add(TSeries series)
|
||||
{
|
||||
if (series == this)
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
@@ -0,0 +1,541 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class TValueEventArgsTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_WithValueAndIsNew_SetsPropertiesCorrectly()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, 123.45);
|
||||
const bool isNew = true;
|
||||
|
||||
var eventArgs = new TValueEventArgs { Value = tValue, IsNew = isNew };
|
||||
|
||||
Assert.Equal(tValue, eventArgs.Value);
|
||||
Assert.True(eventArgs.IsNew);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Default_SetsDefaultValues()
|
||||
{
|
||||
var eventArgs = new TValueEventArgs();
|
||||
|
||||
Assert.Equal(default(TValue), eventArgs.Value);
|
||||
Assert.False(eventArgs.IsNew);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNaNValue_PreservesNaN()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, double.NaN);
|
||||
|
||||
var eventArgs = new TValueEventArgs { Value = tValue, IsNew = false };
|
||||
|
||||
Assert.True(double.IsNaN(eventArgs.Value.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithInfinityValue_PreservesInfinity()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, double.PositiveInfinity);
|
||||
|
||||
var eventArgs = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
|
||||
Assert.True(double.IsPositiveInfinity(eventArgs.Value.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_SameValues_ReturnsTrue()
|
||||
{
|
||||
var tValue = new TValue(12345, 100.0);
|
||||
var args1 = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
var args2 = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
|
||||
Assert.True(args1.Equals(args2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_DifferentValue_ReturnsFalse()
|
||||
{
|
||||
var tValue1 = new TValue(12345, 100.0);
|
||||
var tValue2 = new TValue(12345, 101.0);
|
||||
var args1 = new TValueEventArgs { Value = tValue1, IsNew = true };
|
||||
var args2 = new TValueEventArgs { Value = tValue2, IsNew = true };
|
||||
|
||||
Assert.False(args1.Equals(args2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_DifferentTime_ReturnsFalse()
|
||||
{
|
||||
var tValue1 = new TValue(12345, 100.0);
|
||||
var tValue2 = new TValue(12346, 100.0);
|
||||
var args1 = new TValueEventArgs { Value = tValue1, IsNew = true };
|
||||
var args2 = new TValueEventArgs { Value = tValue2, IsNew = true };
|
||||
|
||||
Assert.False(args1.Equals(args2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_DifferentIsNew_ReturnsFalse()
|
||||
{
|
||||
var tValue = new TValue(12345, 100.0);
|
||||
var args1 = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
var args2 = new TValueEventArgs { Value = tValue, IsNew = false };
|
||||
|
||||
Assert.False(args1.Equals(args2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_Object_SameTValueEventArgs_ReturnsTrue()
|
||||
{
|
||||
var tValue = new TValue(12345, 100.0);
|
||||
var args1 = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
object args2 = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
|
||||
Assert.True(args1.Equals(args2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_Object_DifferentType_ReturnsFalse()
|
||||
{
|
||||
var args = new TValueEventArgs { Value = new TValue(12345, 100.0), IsNew = true };
|
||||
object other = "not a TValueEventArgs";
|
||||
|
||||
Assert.False(args.Equals(other));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_Object_Null_ReturnsFalse()
|
||||
{
|
||||
var args = new TValueEventArgs { Value = new TValue(12345, 100.0), IsNew = true };
|
||||
|
||||
Assert.False(args.Equals(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_SameValues_ReturnsSameHashCode()
|
||||
{
|
||||
var tValue = new TValue(12345, 100.0);
|
||||
var args1 = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
var args2 = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
|
||||
Assert.Equal(args1.GetHashCode(), args2.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_DifferentValues_ReturnsDifferentHashCode()
|
||||
{
|
||||
var tValue1 = new TValue(12345, 100.0);
|
||||
var tValue2 = new TValue(12346, 100.0);
|
||||
var args1 = new TValueEventArgs { Value = tValue1, IsNew = true };
|
||||
var args2 = new TValueEventArgs { Value = tValue2, IsNew = true };
|
||||
|
||||
Assert.NotEqual(args1.GetHashCode(), args2.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_DifferentIsNew_ReturnsDifferentHashCode()
|
||||
{
|
||||
var tValue = new TValue(12345, 100.0);
|
||||
var args1 = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
var args2 = new TValueEventArgs { Value = tValue, IsNew = false };
|
||||
|
||||
Assert.NotEqual(args1.GetHashCode(), args2.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EqualityOperator_SameValues_ReturnsTrue()
|
||||
{
|
||||
var tValue = new TValue(12345, 100.0);
|
||||
var args1 = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
var args2 = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
|
||||
Assert.True(args1 == args2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EqualityOperator_DifferentValues_ReturnsFalse()
|
||||
{
|
||||
var tValue1 = new TValue(12345, 100.0);
|
||||
var tValue2 = new TValue(12346, 100.0);
|
||||
var args1 = new TValueEventArgs { Value = tValue1, IsNew = true };
|
||||
var args2 = new TValueEventArgs { Value = tValue2, IsNew = true };
|
||||
|
||||
Assert.False(args1 == args2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InequalityOperator_SameValues_ReturnsFalse()
|
||||
{
|
||||
var tValue = new TValue(12345, 100.0);
|
||||
var args1 = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
var args2 = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
|
||||
Assert.False(args1 != args2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InequalityOperator_DifferentValues_ReturnsTrue()
|
||||
{
|
||||
var tValue1 = new TValue(12345, 100.0);
|
||||
var tValue2 = new TValue(12346, 100.0);
|
||||
var args1 = new TValueEventArgs { Value = tValue1, IsNew = true };
|
||||
var args2 = new TValueEventArgs { Value = tValue2, IsNew = true };
|
||||
|
||||
Assert.True(args1 != args2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_WithNaN_BothNaN_ReturnsTrue()
|
||||
{
|
||||
var tValue1 = new TValue(12345, double.NaN);
|
||||
var tValue2 = new TValue(12345, double.NaN);
|
||||
var args1 = new TValueEventArgs { Value = tValue1, IsNew = true };
|
||||
var args2 = new TValueEventArgs { Value = tValue2, IsNew = true };
|
||||
|
||||
Assert.True(args1.Equals(args2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_WithNaN_DoesNotThrow()
|
||||
{
|
||||
var tValue = new TValue(12345, double.NaN);
|
||||
var args = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
|
||||
var hash = args.GetHashCode();
|
||||
|
||||
Assert.True(hash != 0 || hash == 0); // Just verify it doesn't throw
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithZeroTime_Allowed()
|
||||
{
|
||||
var tValue = new TValue(0, 100.0);
|
||||
var args = new TValueEventArgs { Value = tValue, IsNew = false };
|
||||
|
||||
Assert.Equal(0, args.Value.Time);
|
||||
Assert.Equal(100.0, args.Value.Value);
|
||||
Assert.False(args.IsNew);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNegativeTime_Allowed()
|
||||
{
|
||||
var tValue = new TValue(-12345, 100.0);
|
||||
var args = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
|
||||
Assert.Equal(-12345, args.Value.Time);
|
||||
Assert.Equal(100.0, args.Value.Value);
|
||||
Assert.True(args.IsNew);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithMaxLongTime_Allowed()
|
||||
{
|
||||
var tValue = new TValue(long.MaxValue, 100.0);
|
||||
var args = new TValueEventArgs { Value = tValue, IsNew = false };
|
||||
|
||||
Assert.Equal(long.MaxValue, args.Value.Time);
|
||||
Assert.Equal(100.0, args.Value.Value);
|
||||
Assert.False(args.IsNew);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithMaxDoubleValue_Allowed()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, double.MaxValue);
|
||||
var args = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
|
||||
Assert.Equal(double.MaxValue, args.Value.Value);
|
||||
Assert.True(args.IsNew);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithMinDoubleValue_Allowed()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, double.MinValue);
|
||||
var args = new TValueEventArgs { Value = tValue, IsNew = false };
|
||||
|
||||
Assert.Equal(double.MinValue, args.Value.Value);
|
||||
Assert.False(args.IsNew);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithEpsilonValue_Allowed()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, double.Epsilon);
|
||||
var args = new TValueEventArgs { Value = tValue, IsNew = true };
|
||||
|
||||
Assert.Equal(double.Epsilon, args.Value.Value);
|
||||
Assert.True(args.IsNew);
|
||||
}
|
||||
}
|
||||
|
||||
public class TValuePublishedHandlerTests
|
||||
{
|
||||
private class MockPublisher : ITValuePublisher
|
||||
{
|
||||
#pragma warning disable CS0067 // Event is never used - intentional for delegate testing
|
||||
public event TValuePublishedHandler? Pub;
|
||||
#pragma warning restore CS0067
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Delegate_CanBeAssigned()
|
||||
{
|
||||
TValuePublishedHandler handler = (_, in _) => { };
|
||||
|
||||
Assert.NotNull(handler);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Delegate_CanBeInvoked()
|
||||
{
|
||||
bool wasCalled = false;
|
||||
TValuePublishedHandler handler = (object? sender, in TValueEventArgs args) => wasCalled = true;
|
||||
|
||||
var args = new TValueEventArgs { Value = new TValue(12345, 100.0), IsNew = true };
|
||||
handler(null, args);
|
||||
|
||||
Assert.True(wasCalled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Delegate_ReceivesCorrectArguments()
|
||||
{
|
||||
object? receivedSender = null;
|
||||
TValueEventArgs receivedArgs = default;
|
||||
|
||||
TValuePublishedHandler handler = (object? sender, in TValueEventArgs args) =>
|
||||
{
|
||||
receivedSender = sender;
|
||||
receivedArgs = args;
|
||||
};
|
||||
|
||||
var publisher = new MockPublisher();
|
||||
var expectedArgs = new TValueEventArgs { Value = new TValue(12345, 100.0), IsNew = true };
|
||||
|
||||
handler(publisher, expectedArgs);
|
||||
|
||||
Assert.Equal(publisher, receivedSender);
|
||||
Assert.Equal(expectedArgs, receivedArgs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Delegate_CanBeNull()
|
||||
{
|
||||
TValuePublishedHandler? handler = null;
|
||||
|
||||
var exception = Record.Exception(() => handler?.Invoke(null, new TValueEventArgs()));
|
||||
Assert.Null(exception);
|
||||
}
|
||||
}
|
||||
|
||||
public class ITValuePublisherTests
|
||||
{
|
||||
private class MockPublisher : ITValuePublisher
|
||||
{
|
||||
public event TValuePublishedHandler? Pub;
|
||||
|
||||
public void RaiseEvent(TValue value, bool isNew = true)
|
||||
{
|
||||
Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew });
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Interface_CanBeImplemented()
|
||||
{
|
||||
ITValuePublisher publisher = new MockPublisher();
|
||||
|
||||
Assert.NotNull(publisher);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_Event_CanBeSubscribed()
|
||||
{
|
||||
var publisher = new MockPublisher();
|
||||
bool eventRaised = false;
|
||||
|
||||
publisher.Pub += (object? sender, in TValueEventArgs args) => eventRaised = true;
|
||||
|
||||
publisher.RaiseEvent(new TValue(12345, 100.0));
|
||||
|
||||
Assert.True(eventRaised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_Event_CanBeUnsubscribed()
|
||||
{
|
||||
var publisher = new MockPublisher();
|
||||
bool eventRaised = false;
|
||||
|
||||
TValuePublishedHandler handler = (object? sender, in TValueEventArgs args) => eventRaised = true;
|
||||
publisher.Pub += handler;
|
||||
|
||||
publisher.RaiseEvent(new TValue(12345, 100.0));
|
||||
Assert.True(eventRaised);
|
||||
|
||||
eventRaised = false;
|
||||
publisher.Pub -= handler;
|
||||
|
||||
publisher.RaiseEvent(new TValue(12346, 101.0));
|
||||
Assert.False(eventRaised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_Event_MultipleSubscribers_AllReceiveEvent()
|
||||
{
|
||||
var publisher = new MockPublisher();
|
||||
bool event1Raised = false;
|
||||
bool event2Raised = false;
|
||||
|
||||
publisher.Pub += (object? sender, in TValueEventArgs args) => event1Raised = true;
|
||||
publisher.Pub += (object? sender, in TValueEventArgs args) => event2Raised = true;
|
||||
|
||||
publisher.RaiseEvent(new TValue(12345, 100.0));
|
||||
|
||||
Assert.True(event1Raised);
|
||||
Assert.True(event2Raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_Event_NoSubscribers_DoesNotThrow()
|
||||
{
|
||||
var publisher = new MockPublisher();
|
||||
|
||||
var exception = Record.Exception(() => publisher.RaiseEvent(new TValue(12345, 100.0)));
|
||||
Assert.Null(exception);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_Event_ReceivesCorrectSender()
|
||||
{
|
||||
var publisher = new MockPublisher();
|
||||
object? receivedSender = null;
|
||||
|
||||
publisher.Pub += (object? sender, in TValueEventArgs args) => receivedSender = sender;
|
||||
|
||||
publisher.RaiseEvent(new TValue(12345, 100.0));
|
||||
|
||||
Assert.Equal(publisher, receivedSender);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_Event_ReceivesCorrectEventArgs()
|
||||
{
|
||||
var publisher = new MockPublisher();
|
||||
TValueEventArgs receivedArgs = default;
|
||||
|
||||
publisher.Pub += (object? sender, in TValueEventArgs args) => receivedArgs = args;
|
||||
|
||||
var expectedValue = new TValue(12345, 100.0);
|
||||
publisher.RaiseEvent(expectedValue, isNew: true);
|
||||
|
||||
Assert.Equal(expectedValue, receivedArgs.Value);
|
||||
Assert.True(receivedArgs.IsNew);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_Event_IsNew_False_ReceivedCorrectly()
|
||||
{
|
||||
var publisher = new MockPublisher();
|
||||
TValueEventArgs receivedArgs = default;
|
||||
|
||||
publisher.Pub += (object? sender, in TValueEventArgs args) => receivedArgs = args;
|
||||
|
||||
var expectedValue = new TValue(12345, 100.0);
|
||||
publisher.RaiseEvent(expectedValue, isNew: false);
|
||||
|
||||
Assert.Equal(expectedValue, receivedArgs.Value);
|
||||
Assert.False(receivedArgs.IsNew);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_Event_HandlerThrows_ExceptionPropagates()
|
||||
{
|
||||
var publisher = new MockPublisher();
|
||||
|
||||
publisher.Pub += (object? sender, in TValueEventArgs args) =>
|
||||
{
|
||||
throw new InvalidOperationException("Test exception");
|
||||
};
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
publisher.RaiseEvent(new TValue(12345, 100.0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_Event_HandlerWithNaNValue_ReceivesNaN()
|
||||
{
|
||||
var publisher = new MockPublisher();
|
||||
double receivedValue = 0;
|
||||
|
||||
publisher.Pub += (object? sender, in TValueEventArgs args) => receivedValue = args.Value.Value;
|
||||
|
||||
publisher.RaiseEvent(new TValue(12345, double.NaN));
|
||||
|
||||
Assert.True(double.IsNaN(receivedValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_Event_HandlerWithInfinityValue_ReceivesInfinity()
|
||||
{
|
||||
var publisher = new MockPublisher();
|
||||
double receivedValue = 0;
|
||||
|
||||
publisher.Pub += (object? sender, in TValueEventArgs args) => receivedValue = args.Value.Value;
|
||||
|
||||
publisher.RaiseEvent(new TValue(12345, double.PositiveInfinity));
|
||||
|
||||
Assert.True(double.IsPositiveInfinity(receivedValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_Event_MultipleEvents_AllReceived()
|
||||
{
|
||||
var publisher = new MockPublisher();
|
||||
var receivedValues = new List<double>();
|
||||
|
||||
publisher.Pub += (object? sender, in TValueEventArgs args) => receivedValues.Add(args.Value.Value);
|
||||
|
||||
publisher.RaiseEvent(new TValue(12345, 100.0));
|
||||
publisher.RaiseEvent(new TValue(12346, 200.0));
|
||||
publisher.RaiseEvent(new TValue(12347, 300.0));
|
||||
|
||||
Assert.Equal(3, receivedValues.Count);
|
||||
Assert.Equal(100.0, receivedValues[0]);
|
||||
Assert.Equal(200.0, receivedValues[1]);
|
||||
Assert.Equal(300.0, receivedValues[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_Event_SubscribeUnsubscribeMultipleTimes_Works()
|
||||
{
|
||||
var publisher = new MockPublisher();
|
||||
int callCount = 0;
|
||||
|
||||
TValuePublishedHandler handler = (object? sender, in TValueEventArgs args) => callCount++;
|
||||
|
||||
// Subscribe multiple times
|
||||
publisher.Pub += handler;
|
||||
publisher.Pub += handler;
|
||||
|
||||
publisher.RaiseEvent(new TValue(12345, 100.0));
|
||||
Assert.Equal(2, callCount); // Called twice
|
||||
|
||||
callCount = 0;
|
||||
// Unsubscribe once
|
||||
publisher.Pub -= handler;
|
||||
|
||||
publisher.RaiseEvent(new TValue(12346, 200.0));
|
||||
Assert.Equal(1, callCount); // Called once
|
||||
|
||||
callCount = 0;
|
||||
// Unsubscribe remaining
|
||||
publisher.Pub -= handler;
|
||||
|
||||
publisher.RaiseEvent(new TValue(12347, 300.0));
|
||||
Assert.Equal(0, callCount); // Not called
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Performance-focused event args for TValue updates.
|
||||
/// Implemented as struct to avoid heap allocations in high-frequency event dispatch.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
public readonly struct TValueEventArgs : IEquatable<TValueEventArgs>
|
||||
{
|
||||
public TValue Value { get; init; }
|
||||
public bool IsNew { get; init; }
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool Equals(TValueEventArgs other) =>
|
||||
Value.Equals(other.Value) && IsNew == other.IsNew;
|
||||
|
||||
public override bool Equals(object? obj) =>
|
||||
obj is TValueEventArgs other && Equals(other);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override int GetHashCode() =>
|
||||
HashCode.Combine(Value, IsNew);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool operator ==(TValueEventArgs left, TValueEventArgs right) =>
|
||||
left.Equals(right);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool operator !=(TValueEventArgs left, TValueEventArgs right) =>
|
||||
!left.Equals(right);
|
||||
}
|
||||
|
||||
// Performance-focused event args struct; not derived from EventArgs by design.
|
||||
// We intentionally deviate from the standard EventArgs pattern here for perf.
|
||||
// MA0046 suppressed: struct-based args avoid heap allocations in high-frequency events.
|
||||
#pragma warning disable MA0046 // The second parameter must be of type 'System.EventArgs' or a derived type
|
||||
public delegate void TValuePublishedHandler(object? sender, in TValueEventArgs args);
|
||||
|
||||
/// <summary>
|
||||
/// Interface for objects that publish TValue updates.
|
||||
/// </summary>
|
||||
public interface ITValuePublisher
|
||||
{
|
||||
/// <summary>
|
||||
/// Event triggered when a new TValue is available.
|
||||
/// </summary>
|
||||
event TValuePublishedHandler? Pub;
|
||||
}
|
||||
#pragma warning restore MA0046
|
||||
@@ -0,0 +1,586 @@
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class TSeriesTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_Default_CreatesEmptySeries()
|
||||
{
|
||||
var series = new TSeries();
|
||||
|
||||
Assert.Empty(series);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithCapacity_CreatesEmptySeries()
|
||||
{
|
||||
var series = new TSeries(100);
|
||||
|
||||
Assert.Empty(series);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithLists_WrapsExistingData()
|
||||
{
|
||||
var times = new List<long> { 100, 200, 300 };
|
||||
var values = new List<double> { 1.0, 2.0, 3.0 };
|
||||
|
||||
var series = new TSeries(times, values);
|
||||
|
||||
Assert.Equal(3, series.Count);
|
||||
Assert.Equal(1.0, series[0].Value);
|
||||
Assert.Equal(2.0, series[1].Value);
|
||||
Assert.Equal(3.0, series[2].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Name_DefaultValue_IsData()
|
||||
{
|
||||
var series = new TSeries();
|
||||
|
||||
Assert.Equal("Data", series.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Name_CanBeSet()
|
||||
{
|
||||
var series = new TSeries { Name = "TestSeries" };
|
||||
|
||||
Assert.Equal("TestSeries", series.Name);
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_TValue_AddsNewItem()
|
||||
{
|
||||
var series = new TSeries();
|
||||
var tv = new TValue(DateTime.UtcNow.Ticks, 42.0);
|
||||
|
||||
series.Add(tv);
|
||||
|
||||
Assert.Single(series);
|
||||
Assert.Equal(42.0, series.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_TValueWithIsNew_AddsOrUpdates()
|
||||
{
|
||||
var series = new TSeries();
|
||||
var tv1 = new TValue(DateTime.UtcNow.Ticks, 42.0);
|
||||
var tv2 = new TValue(DateTime.UtcNow.Ticks, 43.0);
|
||||
|
||||
series.Add(tv1, isNew: true);
|
||||
series.Add(tv2, isNew: false);
|
||||
|
||||
Assert.Single(series);
|
||||
Assert.Equal(43.0, series.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithDateTime_AddsValue()
|
||||
{
|
||||
var series = new TSeries();
|
||||
var dt = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc);
|
||||
|
||||
series.Add(dt, 100.0, isNew: true);
|
||||
|
||||
Assert.Single(series);
|
||||
Assert.Equal(dt.Ticks, series.Last.Time);
|
||||
Assert.Equal(100.0, series.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_EnumerableDoubles_AddsAllValues()
|
||||
{
|
||||
var series = new TSeries();
|
||||
var values = new[] { 1.0, 2.0, 3.0, 4.0, 5.0 };
|
||||
|
||||
series.Add(values);
|
||||
|
||||
Assert.Equal(5, series.Count);
|
||||
Assert.Equal(1.0, series[0].Value);
|
||||
Assert.Equal(5.0, series[4].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_UpdateOnEmptySeries_AddsNewItem()
|
||||
{
|
||||
var series = new TSeries();
|
||||
var tv = new TValue(DateTime.UtcNow.Ticks, 42.0);
|
||||
|
||||
series.Add(tv, isNew: false);
|
||||
|
||||
Assert.Single(series);
|
||||
Assert.Equal(42.0, series.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Last_EmptySeries_ReturnsDefault()
|
||||
{
|
||||
var series = new TSeries();
|
||||
|
||||
var last = series.Last;
|
||||
|
||||
Assert.Equal(0, last.Time);
|
||||
Assert.Equal(0.0, last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Last_NonEmptySeries_ReturnsLastValue()
|
||||
{
|
||||
var series = new TSeries();
|
||||
series.Add(100, 10.0);
|
||||
series.Add(200, 20.0);
|
||||
|
||||
var last = series.Last;
|
||||
|
||||
Assert.Equal(200, last.Time);
|
||||
Assert.Equal(20.0, last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LastValue_EmptySeries_ReturnsNaN()
|
||||
{
|
||||
var series = new TSeries();
|
||||
|
||||
Assert.True(double.IsNaN(series.LastValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LastValue_NonEmptySeries_ReturnsLastValue()
|
||||
{
|
||||
var series = new TSeries();
|
||||
series.Add(100, 10.0);
|
||||
series.Add(200, 20.0);
|
||||
|
||||
Assert.Equal(20.0, series.LastValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LastTime_EmptySeries_ReturnsZero()
|
||||
{
|
||||
var series = new TSeries();
|
||||
|
||||
Assert.Equal(0, series.LastTime);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LastTime_NonEmptySeries_ReturnsLastTime()
|
||||
{
|
||||
var series = new TSeries();
|
||||
series.Add(100, 10.0);
|
||||
series.Add(200, 20.0);
|
||||
|
||||
Assert.Equal(200, series.LastTime);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Values_ReturnsReadOnlySpanOfValues()
|
||||
{
|
||||
var series = new TSeries();
|
||||
series.Add(100, 1.0);
|
||||
series.Add(200, 2.0);
|
||||
series.Add(300, 3.0);
|
||||
|
||||
ReadOnlySpan<double> values = series.Values;
|
||||
|
||||
Assert.Equal(3, values.Length);
|
||||
Assert.Equal(1.0, values[0]);
|
||||
Assert.Equal(2.0, values[1]);
|
||||
Assert.Equal(3.0, values[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Times_ReturnsReadOnlySpanOfTimes()
|
||||
{
|
||||
var series = new TSeries();
|
||||
series.Add(100, 1.0);
|
||||
series.Add(200, 2.0);
|
||||
series.Add(300, 3.0);
|
||||
|
||||
ReadOnlySpan<long> times = series.Times;
|
||||
|
||||
Assert.Equal(3, times.Length);
|
||||
Assert.Equal(100, times[0]);
|
||||
Assert.Equal(200, times[1]);
|
||||
Assert.Equal(300, times[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_ReturnsCorrectTValue()
|
||||
{
|
||||
var series = new TSeries();
|
||||
series.Add(100, 10.0);
|
||||
series.Add(200, 20.0);
|
||||
series.Add(300, 30.0);
|
||||
|
||||
Assert.Equal(100, series[0].Time);
|
||||
Assert.Equal(10.0, series[0].Value);
|
||||
Assert.Equal(200, series[1].Time);
|
||||
Assert.Equal(20.0, series[1].Value);
|
||||
Assert.Equal(300, series[2].Time);
|
||||
Assert.Equal(30.0, series[2].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetEnumerator_IteratesAllValues()
|
||||
{
|
||||
var series = new TSeries();
|
||||
series.Add(100, 1.0);
|
||||
series.Add(200, 2.0);
|
||||
series.Add(300, 3.0);
|
||||
|
||||
var list = series.ToList();
|
||||
|
||||
Assert.Equal(3, list.Count);
|
||||
Assert.Equal(1.0, list[0].Value);
|
||||
Assert.Equal(2.0, list[1].Value);
|
||||
Assert.Equal(3.0, list[2].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetEnumerator_NonGeneric_Works()
|
||||
{
|
||||
var series = new TSeries();
|
||||
series.Add(100, 1.0);
|
||||
series.Add(200, 2.0);
|
||||
|
||||
var list = new List<object>();
|
||||
IEnumerable enumerable = series;
|
||||
#pragma warning disable S4158
|
||||
foreach (var item in enumerable)
|
||||
{
|
||||
list.Add(item);
|
||||
}
|
||||
|
||||
Assert.Equal(2, series.Count);
|
||||
Assert.Equal(2, list.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_Event_IsRaisedOnAdd()
|
||||
{
|
||||
var series = new TSeries();
|
||||
TValue? received = null;
|
||||
series.Pub += (object? sender, in TValueEventArgs args) => received = args.Value;
|
||||
|
||||
series.Add(100, 42.0);
|
||||
|
||||
Assert.NotNull(received);
|
||||
Assert.Equal(100, received.Value.Time);
|
||||
Assert.Equal(42.0, received.Value.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_Event_IsRaisedOnUpdate()
|
||||
{
|
||||
var series = new TSeries();
|
||||
TValue? received = null;
|
||||
series.Add(100, 42.0);
|
||||
series.Pub += (object? sender, in TValueEventArgs args) => received = args.Value;
|
||||
|
||||
series.Add(100, 43.0, isNew: false);
|
||||
|
||||
Assert.NotNull(received);
|
||||
Assert.Equal(43.0, received.Value.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Count_ReturnsCorrectValue()
|
||||
{
|
||||
var series = new TSeries();
|
||||
|
||||
series.Add(100, 1.0);
|
||||
Assert.Single(series);
|
||||
|
||||
series.Add(200, 2.0);
|
||||
Assert.Equal(2, series.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithMismatchedLists_WrapsData()
|
||||
{
|
||||
// TSeries wraps the lists directly if they're List<T>, no length validation
|
||||
var times = new List<long> { 100, 200, 300 };
|
||||
var values = new List<double> { 1.0, 2.0 }; // Different length
|
||||
|
||||
var series = new TSeries(times, values);
|
||||
|
||||
// Count is based on values list
|
||||
Assert.Equal(2, series.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_OutOfBounds_ThrowsException()
|
||||
{
|
||||
var series = new TSeries();
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => _ = series[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_NegativeIndex_ThrowsException()
|
||||
{
|
||||
var series = new TSeries();
|
||||
series.Add(100, 1.0);
|
||||
|
||||
#pragma warning disable DS003 // Invalid index - intentional for testing exception
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => _ = series[-1]);
|
||||
#pragma warning restore DS003
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indexer_BeyondCount_ThrowsException()
|
||||
{
|
||||
var series = new TSeries();
|
||||
series.Add(100, 1.0);
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => _ = series[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Values_EmptySeries_ReturnsEmptySpan()
|
||||
{
|
||||
var series = new TSeries();
|
||||
|
||||
ReadOnlySpan<double> values = series.Values;
|
||||
|
||||
Assert.Equal(0, values.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Times_EmptySeries_ReturnsEmptySpan()
|
||||
{
|
||||
var series = new TSeries();
|
||||
|
||||
ReadOnlySpan<long> times = series.Times;
|
||||
|
||||
Assert.Equal(0, times.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithNaN_PreservesNaN()
|
||||
{
|
||||
var series = new TSeries();
|
||||
|
||||
series.Add(100, double.NaN);
|
||||
|
||||
Assert.True(double.IsNaN(series.Last.Value));
|
||||
Assert.True(double.IsNaN(series.LastValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithInfinity_PreservesInfinity()
|
||||
{
|
||||
var series = new TSeries();
|
||||
|
||||
series.Add(100, double.PositiveInfinity);
|
||||
|
||||
Assert.True(double.IsPositiveInfinity(series.Last.Value));
|
||||
Assert.True(double.IsPositiveInfinity(series.LastValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithNegativeInfinity_PreservesNegativeInfinity()
|
||||
{
|
||||
var series = new TSeries();
|
||||
|
||||
series.Add(100, double.NegativeInfinity);
|
||||
|
||||
Assert.True(double.IsNegativeInfinity(series.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_EnumerableDoubles_GeneratesIncreasingTimes()
|
||||
{
|
||||
var series = new TSeries();
|
||||
var values = new[] { 1.0, 2.0, 3.0 };
|
||||
|
||||
series.Add(values);
|
||||
|
||||
Assert.Equal(3, series.Count);
|
||||
// Times should be increasing by TicksPerMinute
|
||||
Assert.True(series[1].Time > series[0].Time);
|
||||
Assert.True(series[2].Time > series[1].Time);
|
||||
Assert.Equal(TimeSpan.TicksPerMinute, series[1].Time - series[0].Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_EnumerableDoubles_EmptyArray_AddsNothing()
|
||||
{
|
||||
var series = new TSeries();
|
||||
|
||||
series.Add(Array.Empty<double>());
|
||||
|
||||
Assert.Empty(series);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_EventArgs_ContainsIsNewFlag()
|
||||
{
|
||||
var series = new TSeries();
|
||||
bool? receivedIsNew = null;
|
||||
series.Pub += (object? sender, in TValueEventArgs args) => receivedIsNew = args.IsNew;
|
||||
|
||||
series.Add(new TValue(100, 42.0), isNew: true);
|
||||
|
||||
Assert.True(receivedIsNew);
|
||||
|
||||
series.Add(new TValue(100, 43.0), isNew: false);
|
||||
|
||||
Assert.False(receivedIsNew);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithCapacity_DoesNotAffectCount()
|
||||
{
|
||||
var series = new TSeries(1000);
|
||||
|
||||
Assert.Empty(series);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithDateTimeLocal_ConvertsToUtc()
|
||||
{
|
||||
var series = new TSeries();
|
||||
var localTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Local);
|
||||
|
||||
series.Add(localTime, 100.0);
|
||||
|
||||
// The stored time should be UTC
|
||||
var storedTime = new DateTime(series.Last.Time, DateTimeKind.Utc);
|
||||
Assert.Equal(DateTimeKind.Utc, storedTime.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_WithDateTimeUnspecified_TreatsAsLocal()
|
||||
{
|
||||
var series = new TSeries();
|
||||
var unspecifiedTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Unspecified);
|
||||
|
||||
series.Add(unspecifiedTime, 100.0);
|
||||
|
||||
Assert.Single(series);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Values_ModifyingUnderlyingList_ReflectsInSpan()
|
||||
{
|
||||
var series = new TSeries();
|
||||
series.Add(100, 1.0);
|
||||
series.Add(200, 2.0);
|
||||
|
||||
// Get the span
|
||||
ReadOnlySpan<double> values1 = series.Values;
|
||||
Assert.Equal(2, values1.Length);
|
||||
|
||||
// Add more data
|
||||
series.Add(300, 3.0);
|
||||
|
||||
// Get new span - should reflect the change
|
||||
ReadOnlySpan<double> values2 = series.Values;
|
||||
Assert.Equal(3, values2.Length);
|
||||
Assert.Equal(3.0, values2[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithReadOnlyLists_CopiesData()
|
||||
{
|
||||
// Using arrays which implement IReadOnlyList but aren't List<T>
|
||||
long[] times = [100, 200, 300];
|
||||
IReadOnlyList<double> values = [1.0, 2.0, 3.0];
|
||||
|
||||
var series = new TSeries(times, values);
|
||||
|
||||
Assert.Equal(3, series.Count);
|
||||
Assert.Equal(1.0, series[0].Value);
|
||||
Assert.Equal(3.0, series[2].Value);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void GetEnumerator_ExplicitGenericInterface_Works()
|
||||
{
|
||||
var series = new TSeries();
|
||||
series.Add(100, 1.0);
|
||||
series.Add(200, 2.0);
|
||||
series.Add(300, 3.0);
|
||||
|
||||
// Explicitly call IEnumerable<TValue>.GetEnumerator() through interface cast
|
||||
IEnumerable<TValue> genericEnumerable = series;
|
||||
using var enumerator = genericEnumerable.GetEnumerator();
|
||||
|
||||
var values = new List<double>();
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
values.Add(enumerator.Current.Value);
|
||||
}
|
||||
|
||||
Assert.Equal(3, values.Count);
|
||||
Assert.Equal(1.0, values[0]);
|
||||
Assert.Equal(2.0, values[1]);
|
||||
Assert.Equal(3.0, values[2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetEnumerator_ExplicitNonGenericInterface_Works()
|
||||
{
|
||||
var series = new TSeries();
|
||||
series.Add(100, 1.0);
|
||||
series.Add(200, 2.0);
|
||||
|
||||
// Explicitly call IEnumerable.GetEnumerator() through interface cast
|
||||
IEnumerable nonGenericEnumerable = series;
|
||||
var enumerator = nonGenericEnumerable.GetEnumerator();
|
||||
|
||||
var values = new List<double>();
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
var tv = (TValue)enumerator.Current;
|
||||
values.Add(tv.Value);
|
||||
}
|
||||
|
||||
Assert.Equal(2, values.Count);
|
||||
Assert.Equal(1.0, values[0]);
|
||||
Assert.Equal(2.0, values[1]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
# TSeries: Time Series Data Container
|
||||
|
||||
## What It Does
|
||||
|
||||
`TSeries` is a high-performance, memory-efficient container for time-series data. Unlike standard collections (like `List<TValue>`), it uses a **Structure of Arrays (SoA)** layout internally. This means it stores timestamps and values in separate contiguous arrays, optimizing memory access patterns for numerical processing and SIMD vectorization.
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
Standard object-oriented collections (Array of Structures - AoS) are cache-inefficient for numerical algorithms. When calculating a moving average, the CPU only needs the values, but an AoS layout forces it to load interleaved timestamps into the cache, wasting bandwidth.
|
||||
|
||||
`TSeries` solves this by decoupling time and value storage:
|
||||
|
||||
* **Cache Locality**: Iterating over values loads only values.
|
||||
* **SIMD Readiness**: The internal value array can be exposed directly as a `Span<double>` for AVX/SSE processing.
|
||||
* **Zero-Copy Views**: Data is accessed without defensive copying, ensuring maximum throughput.
|
||||
|
||||
## How It Works
|
||||
|
||||
`TSeries` maintains two parallel internal lists:
|
||||
|
||||
1. `List<long> _t`: Stores timestamps.
|
||||
2. `List<double> _v`: Stores values.
|
||||
|
||||
It implements `IReadOnlyList<TValue>`, allowing it to be treated as a standard collection of `TValue` structs when needed, but its true power lies in its column-oriented properties (`Values`, `Times`).
|
||||
|
||||
## Structure
|
||||
|
||||
### Definition
|
||||
|
||||
```csharp
|
||||
public class TSeries : IReadOnlyList<TValue>, ITValuePublisher
|
||||
```
|
||||
|
||||
### Core Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| ------ | ------ | ------ |
|
||||
| `Values` | `ReadOnlySpan<double>` | Direct access to the value array (SIMD-ready). |
|
||||
| `Times` | `ReadOnlySpan<long>` | Direct access to the timestamp array. |
|
||||
| `Last` | `TValue` | The most recent time-value pair. |
|
||||
| `Count` | `int` | Number of elements in the series. |
|
||||
| `Name` | `string` | Optional identifier for the series. |
|
||||
|
||||
### Events
|
||||
|
||||
| Event | Type | Description |
|
||||
| ------ | ------ | ------ |
|
||||
| `Pub` | `Action<TValue>` | Fired whenever a new value is added or updated. |
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating and Populating
|
||||
|
||||
```csharp
|
||||
var series = new TSeries();
|
||||
|
||||
// Add a new bar (isNew = true by default)
|
||||
series.Add(DateTime.UtcNow, 100.0);
|
||||
|
||||
// Add multiple values
|
||||
series.Add(new List<double> { 1.0, 2.0, 3.0 });
|
||||
```
|
||||
|
||||
### Streaming Updates (Real-time)
|
||||
|
||||
`TSeries` supports "bar updates" where the last value changes until the bar closes.
|
||||
|
||||
```csharp
|
||||
// New minute starts
|
||||
series.Add(time, 100.0, isNew: true);
|
||||
|
||||
// Price updates within the same minute
|
||||
series.Add(time, 101.0, isNew: false); // Overwrites last value
|
||||
series.Add(time, 102.0, isNew: false); // Overwrites last value
|
||||
```
|
||||
|
||||
### SIMD Processing
|
||||
|
||||
```csharp
|
||||
// Calculate average using SIMD (via Span)
|
||||
double sum = 0;
|
||||
foreach (var v in series.Values) { sum += v; } // Compiler vectorizes this
|
||||
```
|
||||
|
||||
### Reactive Subscription
|
||||
|
||||
```csharp
|
||||
series.Pub += (item) => Console.WriteLine($"New value: {item}");
|
||||
```
|
||||
|
||||
## Performance Profile
|
||||
|
||||
* **Memory Layout**: SoA (Structure of Arrays).
|
||||
* **Access Speed**: O(1) for random access.
|
||||
* **Iteration**: Cache-friendly linear scan.
|
||||
* **SIMD**: Fully supported via `Values` span.
|
||||
|
||||
## Integration
|
||||
|
||||
`TSeries` is the standard output format for all indicators in QuanTAlib.
|
||||
|
||||
* **Input**: Can be fed into indicators via `Update(TSeries)`.
|
||||
* **Output**: Indicators return `TSeries` from their `Calculate` methods.
|
||||
* **Visualization**: Easily mappable to charting libraries due to separate Time/Value arrays.
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
* **CollectionsMarshal**: Uses `CollectionsMarshal.AsSpan` to expose internal list storage as spans without copying. This is unsafe if the list is modified during span access, but provides maximum performance for single-threaded algorithms.
|
||||
* **Virtual Methods**: `Add` is virtual to allow derived classes (like `TBarSeries` components) to intercept updates if necessary.
|
||||
|
||||
## References
|
||||
|
||||
* [Data-Oriented Design](https://en.wikipedia.org/wiki/Data-oriented_design)
|
||||
* [SIMD in .NET](https://learn.microsoft.com/en-us/dotnet/standard/simd)
|
||||
@@ -0,0 +1,230 @@
|
||||
using System.Collections;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Tag type indicating that a TSeries constructor should share storage with the provided lists
|
||||
/// rather than making defensive copies. Used internally for synchronized sub-series.
|
||||
/// </summary>
|
||||
public readonly struct ShareStorageTag
|
||||
{
|
||||
/// <summary>
|
||||
/// Singleton instance for the share storage tag.
|
||||
/// </summary>
|
||||
public static readonly ShareStorageTag Instance = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// High-performance enumerator for TSeries.
|
||||
/// </summary>
|
||||
public struct TSeriesEnumerator : IEnumerator<TValue>, IEquatable<TSeriesEnumerator>
|
||||
{
|
||||
private readonly List<long> _t;
|
||||
private readonly List<double> _v;
|
||||
private readonly int _count;
|
||||
private int _index;
|
||||
private TValue _current;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal TSeriesEnumerator(List<long> t, List<double> v)
|
||||
{
|
||||
_t = t;
|
||||
_v = v;
|
||||
_count = v.Count;
|
||||
_index = -1;
|
||||
_current = default;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (_index + 1 >= _count)
|
||||
return false;
|
||||
|
||||
_index++;
|
||||
_current = new TValue(_t[_index], _v[_index]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public readonly TValue Current => _current;
|
||||
readonly object IEnumerator.Current => Current;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Reset()
|
||||
{
|
||||
_index = -1;
|
||||
_current = default;
|
||||
}
|
||||
|
||||
public readonly void Dispose() { }
|
||||
|
||||
public readonly bool Equals(TSeriesEnumerator other) =>
|
||||
ReferenceEquals(_t, other._t) &&
|
||||
ReferenceEquals(_v, other._v) &&
|
||||
_count == other._count &&
|
||||
_index == other._index;
|
||||
|
||||
public override readonly bool Equals(object? obj) =>
|
||||
obj is TSeriesEnumerator other && Equals(other);
|
||||
|
||||
public override readonly int GetHashCode() =>
|
||||
HashCode.Combine(RuntimeHelpers.GetHashCode(_t), RuntimeHelpers.GetHashCode(_v), _count, _index);
|
||||
|
||||
public static bool operator ==(TSeriesEnumerator left, TSeriesEnumerator right) => left.Equals(right);
|
||||
public static bool operator !=(TSeriesEnumerator left, TSeriesEnumerator right) => !left.Equals(right);
|
||||
}
|
||||
|
||||
/// <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>, ITValuePublisher
|
||||
{
|
||||
#pragma warning disable MA0016 // Prefer using collection abstraction instead of implementation
|
||||
protected readonly List<long> _t;
|
||||
protected readonly List<double> _v;
|
||||
#pragma warning restore MA0016
|
||||
|
||||
public string Name { get; set; } = "Data";
|
||||
|
||||
public event TValuePublishedHandler? Pub;
|
||||
|
||||
public TSeries() : this(0)
|
||||
{
|
||||
}
|
||||
|
||||
public TSeries(int capacity)
|
||||
{
|
||||
_t = new List<long>(capacity);
|
||||
_v = new List<double>(capacity);
|
||||
}
|
||||
|
||||
public TSeries(IReadOnlyList<long> time, IReadOnlyList<double> values)
|
||||
{
|
||||
// Always make defensive copies to prevent external mutation of internal state
|
||||
_t = [.. time];
|
||||
_v = [.. values];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal constructor for creating views that share underlying storage.
|
||||
/// Used by TBarSeries to create synchronized OHLCV sub-series.
|
||||
/// </summary>
|
||||
/// <param name="time">The time list to share (not copied).</param>
|
||||
/// <param name="values">The values list to share (not copied).</param>
|
||||
/// <param name="_">Tag type indicating intentional storage sharing.</param>
|
||||
internal TSeries(List<long> time, List<double> values, ShareStorageTag _)
|
||||
{
|
||||
// Direct assignment for intentional storage sharing (internal use only)
|
||||
_t = time;
|
||||
_v = values;
|
||||
}
|
||||
|
||||
public int Count
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _v.Count;
|
||||
}
|
||||
|
||||
public TValue this[int index]
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => new TValue(_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>
|
||||
/// <remarks>
|
||||
/// <para><b>Lifetime:</b> The returned span is only valid while no structural mutations
|
||||
/// (Add, Clear, etc.) occur on this TSeries. Structural changes invalidate the span.</para>
|
||||
/// <para><b>Mutation:</b> The span reflects the internal List storage; modifications
|
||||
/// to the series after obtaining the span may cause undefined behavior if the span is still in use.</para>
|
||||
/// </remarks>
|
||||
public ReadOnlySpan<double> Values
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => CollectionsMarshal.AsSpan(_v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direct access to the underlying Time array as a Span.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>Lifetime:</b> The returned span is only valid while no structural mutations
|
||||
/// (Add, Clear, etc.) occur on this TSeries. Structural changes invalidate the span.</para>
|
||||
/// <para><b>Mutation:</b> The span reflects the internal List storage; modifications
|
||||
/// to the series after obtaining the span may cause undefined behavior if the span is still in use.</para>
|
||||
/// </remarks>
|
||||
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
|
||||
{
|
||||
int lastIdx = _v.Count - 1;
|
||||
_t[lastIdx] = value.Time;
|
||||
_v[lastIdx] = value.Value;
|
||||
}
|
||||
|
||||
Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew });
|
||||
}
|
||||
|
||||
// Overload for backward compatibility (assumes isNew=true)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public virtual void Add(TValue value) => Add(value, isNew: 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, 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;
|
||||
}
|
||||
}
|
||||
|
||||
// IEnumerable implementation with struct enumerator for zero-allocation iteration
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TSeriesEnumerator GetEnumerator() => new(_t, _v);
|
||||
|
||||
IEnumerator<TValue> IEnumerable<TValue>.GetEnumerator() => GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public interface ITValue
|
||||
{
|
||||
DateTime Time { get; }
|
||||
double Value { get; }
|
||||
bool IsNew { get; }
|
||||
bool IsHot { get; }
|
||||
}
|
||||
|
||||
[SkipLocalsInit]
|
||||
public readonly record struct TValue(DateTime Time, double Value, bool IsNew = true, bool IsHot = true) : ITValue
|
||||
{
|
||||
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;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue() : this(DateTime.UtcNow, 0) { }
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue(double value, bool isNew = true, bool isHot = true)
|
||||
: this(DateTime.UtcNow, value, IsNew: isNew, IsHot: isHot) { }
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator double(TValue tv) => tv.Value;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator DateTime(TValue tv) => tv.Time;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator TValue(double value) => new TValue(DateTime.UtcNow, value);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override string ToString() => $"[{Time:yyyy-MM-dd HH:mm:ss}, {Value:F2}, IsNew: {IsNew}, IsHot: {IsHot}]";
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class TValueTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_WithLongTime_SetsPropertiesCorrectly()
|
||||
{
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
const double value = 123.45;
|
||||
|
||||
var tValue = new TValue(time, value);
|
||||
|
||||
Assert.Equal(time, tValue.Time);
|
||||
Assert.Equal(value, tValue.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithDateTime_SetsPropertiesCorrectly()
|
||||
{
|
||||
var dateTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc);
|
||||
double value = 123.45;
|
||||
|
||||
var tValue = new TValue(dateTime, value);
|
||||
|
||||
Assert.Equal(dateTime.Ticks, tValue.Time);
|
||||
Assert.Equal(value, tValue.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AsDateTime_ReturnsCorrectDateTime()
|
||||
{
|
||||
var dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
var tValue = new TValue(dt.Ticks, 100.0);
|
||||
|
||||
Assert.Equal(dt, tValue.AsDateTime);
|
||||
Assert.Equal(DateTimeKind.Utc, tValue.AsDateTime.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_FormatsCorrectly()
|
||||
{
|
||||
var 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("2023-01-01", result, StringComparison.Ordinal);
|
||||
Assert.Contains("12:00:00", result, StringComparison.Ordinal);
|
||||
Assert.Contains("123.46", result, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExplicitConversion_ToDouble_ReturnsValue()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, 42.0);
|
||||
|
||||
double val = (double)tValue;
|
||||
|
||||
Assert.Equal(42.0, val);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImplicitConversion_ToDateTime_ReturnsCorrectDateTime()
|
||||
{
|
||||
var dateTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc);
|
||||
var tValue = new TValue(dateTime.Ticks, 100.0);
|
||||
|
||||
DateTime result = tValue;
|
||||
|
||||
Assert.Equal(dateTime, result);
|
||||
Assert.Equal(DateTimeKind.Utc, result.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_TValue_SameValues_ReturnsTrue()
|
||||
{
|
||||
var tv1 = new TValue(12345, 100.0);
|
||||
var tv2 = new TValue(12345, 100.0);
|
||||
|
||||
Assert.True(tv1.Equals(tv2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_TValue_DifferentTime_ReturnsFalse()
|
||||
{
|
||||
var tv1 = new TValue(12345, 100.0);
|
||||
var tv2 = new TValue(12346, 100.0);
|
||||
|
||||
Assert.False(tv1.Equals(tv2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_TValue_DifferentValue_ReturnsFalse()
|
||||
{
|
||||
var tv1 = new TValue(12345, 100.0);
|
||||
var tv2 = new TValue(12345, 101.0);
|
||||
|
||||
Assert.False(tv1.Equals(tv2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_Object_SameTValue_ReturnsTrue()
|
||||
{
|
||||
var tv1 = new TValue(12345, 100.0);
|
||||
object tv2 = new TValue(12345, 100.0);
|
||||
|
||||
Assert.True(tv1.Equals(tv2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_Object_DifferentType_ReturnsFalse()
|
||||
{
|
||||
var tv = new TValue(12345, 100.0);
|
||||
object other = "not a TValue";
|
||||
|
||||
Assert.False(tv.Equals(other));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_Object_Null_ReturnsFalse()
|
||||
{
|
||||
var tv = new TValue(12345, 100.0);
|
||||
|
||||
Assert.False(tv.Equals(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_SameValues_ReturnsSameHashCode()
|
||||
{
|
||||
var tv1 = new TValue(12345, 100.0);
|
||||
var tv2 = new TValue(12345, 100.0);
|
||||
|
||||
Assert.Equal(tv1.GetHashCode(), tv2.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_DifferentValues_ReturnsDifferentHashCode()
|
||||
{
|
||||
var tv1 = new TValue(12345, 100.0);
|
||||
var tv2 = new TValue(12346, 100.0);
|
||||
|
||||
Assert.NotEqual(tv1.GetHashCode(), tv2.GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EqualityOperator_SameValues_ReturnsTrue()
|
||||
{
|
||||
var tv1 = new TValue(12345, 100.0);
|
||||
var tv2 = new TValue(12345, 100.0);
|
||||
|
||||
Assert.True(tv1 == tv2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EqualityOperator_DifferentValues_ReturnsFalse()
|
||||
{
|
||||
var tv1 = new TValue(12345, 100.0);
|
||||
var tv2 = new TValue(12346, 100.0);
|
||||
|
||||
Assert.False(tv1 == tv2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InequalityOperator_SameValues_ReturnsFalse()
|
||||
{
|
||||
var tv1 = new TValue(12345, 100.0);
|
||||
var tv2 = new TValue(12345, 100.0);
|
||||
|
||||
Assert.False(tv1 != tv2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InequalityOperator_DifferentValues_ReturnsTrue()
|
||||
{
|
||||
var tv1 = new TValue(12345, 100.0);
|
||||
var tv2 = new TValue(12346, 100.0);
|
||||
|
||||
Assert.True(tv1 != tv2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithDateTimeLocal_ConvertsToUtc()
|
||||
{
|
||||
var localTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Local);
|
||||
double value = 123.45;
|
||||
|
||||
var tValue = new TValue(localTime, value);
|
||||
|
||||
// Time should be stored as UTC ticks
|
||||
var expectedUtc = localTime.ToUniversalTime();
|
||||
Assert.Equal(expectedUtc.Ticks, tValue.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithDateTimeUnspecified_ConvertsToUtc()
|
||||
{
|
||||
var unspecifiedTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Unspecified);
|
||||
double value = 123.45;
|
||||
|
||||
var tValue = new TValue(unspecifiedTime, value);
|
||||
|
||||
// Unspecified is treated as local and converted to UTC
|
||||
var expectedUtc = unspecifiedTime.ToUniversalTime();
|
||||
Assert.Equal(expectedUtc.Ticks, tValue.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithDateTimeUtc_PreservesTicks()
|
||||
{
|
||||
var utcTime = new DateTime(2024, 6, 15, 10, 30, 0, DateTimeKind.Utc);
|
||||
double value = 123.45;
|
||||
|
||||
var tValue = new TValue(utcTime, value);
|
||||
|
||||
Assert.Equal(utcTime.Ticks, tValue.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Default_TValue_HasZeroTimeAndValue()
|
||||
{
|
||||
var defaultTValue = default(TValue);
|
||||
|
||||
Assert.Equal(0, defaultTValue.Time);
|
||||
Assert.Equal(0.0, defaultTValue.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNaN_PreservesNaN()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, double.NaN);
|
||||
|
||||
Assert.True(double.IsNaN(tValue.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithPositiveInfinity_PreservesInfinity()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, double.PositiveInfinity);
|
||||
|
||||
Assert.True(double.IsPositiveInfinity(tValue.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNegativeInfinity_PreservesInfinity()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, double.NegativeInfinity);
|
||||
|
||||
Assert.True(double.IsNegativeInfinity(tValue.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithMaxValue_PreservesMaxValue()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, double.MaxValue);
|
||||
|
||||
Assert.Equal(double.MaxValue, tValue.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithMinValue_PreservesMinValue()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, double.MinValue);
|
||||
|
||||
Assert.Equal(double.MinValue, tValue.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithEpsilon_PreservesEpsilon()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, double.Epsilon);
|
||||
|
||||
Assert.Equal(double.Epsilon, tValue.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExplicitConversion_ToDouble_WithNaN_ReturnsNaN()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, double.NaN);
|
||||
|
||||
double val = (double)tValue;
|
||||
|
||||
Assert.True(double.IsNaN(val));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_WithNaN_FormatsCorrectly()
|
||||
{
|
||||
var dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
var tValue = new TValue(dt.Ticks, double.NaN);
|
||||
|
||||
string result = tValue.ToString();
|
||||
|
||||
Assert.Contains("NaN", result, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_WithInfinity_FormatsCorrectly()
|
||||
{
|
||||
var dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
var tValue = new TValue(dt.Ticks, double.PositiveInfinity);
|
||||
|
||||
string result = tValue.ToString();
|
||||
|
||||
Assert.Contains("∞", result, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_WithNegativeValue_FormatsCorrectly()
|
||||
{
|
||||
var 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("-123.46", result, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AsDateTime_ReturnsUtcKind()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, 100.0);
|
||||
|
||||
Assert.Equal(DateTimeKind.Utc, tValue.AsDateTime.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Equals_WithNaN_BothNaN_ReturnsFalse()
|
||||
{
|
||||
// NaN != NaN in IEEE 754
|
||||
var tv1 = new TValue(12345, double.NaN);
|
||||
var tv2 = new TValue(12345, double.NaN);
|
||||
|
||||
// Record struct equality compares fields directly
|
||||
// double.NaN.Equals(double.NaN) returns true in .NET
|
||||
Assert.True(tv1.Equals(tv2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHashCode_WithNaN_DoesNotThrow()
|
||||
{
|
||||
var tv = new TValue(12345, double.NaN);
|
||||
|
||||
var hash = tv.GetHashCode();
|
||||
|
||||
Assert.True(hash != 0 || hash == 0); // Just verify it doesn't throw
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithZeroTime_Allowed()
|
||||
{
|
||||
var tValue = new TValue(0, 100.0);
|
||||
|
||||
Assert.Equal(0, tValue.Time);
|
||||
Assert.Equal(100.0, tValue.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNegativeTime_Allowed()
|
||||
{
|
||||
var tValue = new TValue(-12345, 100.0);
|
||||
|
||||
Assert.Equal(-12345, tValue.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithMaxLongTime_Allowed()
|
||||
{
|
||||
var tValue = new TValue(long.MaxValue, 100.0);
|
||||
|
||||
Assert.Equal(long.MaxValue, tValue.Time);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
# TValue: Time-Value Pair
|
||||
|
||||
## What It Does
|
||||
|
||||
`TValue` is the fundamental atomic unit of data in QuanTAlib. It represents a single point in a time series, consisting of a timestamp and a double-precision floating-point value. It serves as the standard input and output format for all indicators and data streams.
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
In high-frequency trading and quantitative analysis, memory allocation is a critical bottleneck. `TValue` is designed as a **lightweight, immutable struct** to ensure:
|
||||
|
||||
* **Zero Heap Allocation**: Being a struct, it lives on the stack or embedded in arrays, avoiding Garbage Collector (GC) pressure.
|
||||
* **Thread Safety**: Immutability guarantees safe concurrent access.
|
||||
* **Minimal Footprint**: Occupies exactly 16 bytes (8 bytes for `long` Time + 8 bytes for `double` Value), fitting efficiently in CPU cache lines.
|
||||
|
||||
## How It Works
|
||||
|
||||
`TValue` is implemented as a `readonly record struct`. It encapsulates:
|
||||
|
||||
* **Time**: A `long` representing ticks (UTC).
|
||||
* **Value**: A `double` representing the data magnitude.
|
||||
|
||||
It supports implicit conversions to `double` (extracting the value) and `DateTime` (extracting the time), making it syntactically fluid to use in calculations.
|
||||
|
||||
## Structure
|
||||
|
||||
### Definition
|
||||
|
||||
```csharp
|
||||
public readonly record struct TValue(long Time, double Value);
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| ------ | ------ | ------ |
|
||||
| `Time` | `long` | Timestamp in ticks (UTC). |
|
||||
| `Value` | `double` | The data value. |
|
||||
| `AsDateTime` | `DateTime` | Helper to view `Time` as a `DateTime` object. |
|
||||
|
||||
### Constructors
|
||||
|
||||
| Constructor | Description |
|
||||
| ------ | ------ |
|
||||
| `new TValue(long time, double value)` | Creates a TValue from raw ticks. |
|
||||
| `new TValue(DateTime time, double value)` | Creates a TValue from a DateTime object. |
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating TValues
|
||||
|
||||
```csharp
|
||||
// From DateTime
|
||||
var t1 = new TValue(DateTime.UtcNow, 100.5);
|
||||
|
||||
// From Ticks
|
||||
var t2 = new TValue(DateTime.UtcNow.Ticks, 100.5);
|
||||
```
|
||||
|
||||
### Implicit Conversions
|
||||
|
||||
```csharp
|
||||
TValue tv = new TValue(DateTime.UtcNow, 42.0);
|
||||
|
||||
// Implicitly converts to double
|
||||
double val = tv; // 42.0
|
||||
|
||||
// Implicitly converts to DateTime
|
||||
DateTime dt = tv; // DateTime object
|
||||
```
|
||||
|
||||
### String Representation
|
||||
|
||||
```csharp
|
||||
Console.WriteLine(tv); // Output: "[2024-01-01 12:00:00, 42.00]"
|
||||
```
|
||||
|
||||
## Performance Profile
|
||||
|
||||
* **Memory**: 16 bytes per instance.
|
||||
* **Allocation**: 0 bytes (Stack allocated).
|
||||
* **Copying**: Cheap (fits in two 64-bit registers).
|
||||
|
||||
## Integration
|
||||
|
||||
`TValue` is the primary currency of the library:
|
||||
|
||||
* **Indicators**: `Update(TValue input)` accepts it.
|
||||
* **Series**: `TSeries` stores collections of it.
|
||||
* **Events**: `ITValuePublisher` broadcasts it.
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
* **SkipLocalsInit**: The struct is marked with `[SkipLocalsInit]` to suppress zero-initialization of locals, squeezing out nanoseconds in tight loops.
|
||||
* **AggressiveInlining**: All accessors and operators are inlined to ensure zero abstraction penalty.
|
||||
|
||||
## References
|
||||
|
||||
* [Structure of Arrays (SoA)](https://en.wikipedia.org/wiki/AOS_and_SOA)
|
||||
* [C# Struct Performance](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/struct)
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// A lightweight struct representing a time-value pair.
|
||||
/// Pure data type: 16 bytes (long + double).
|
||||
/// </summary>
|
||||
[SkipLocalsInit]
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
public readonly record struct TValue(long Time, double Value)
|
||||
{
|
||||
public DateTime AsDateTime => new(Time, DateTimeKind.Utc);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue(DateTime time, double value)
|
||||
: this(time.Kind == DateTimeKind.Utc ? time.Ticks : time.ToUniversalTime().Ticks, value)
|
||||
{
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static explicit 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()
|
||||
{
|
||||
string valueStr = Value switch
|
||||
{
|
||||
double.PositiveInfinity => ((char)0x221E).ToString(),
|
||||
double.NegativeInfinity => "-" + (char)0x221E,
|
||||
_ when double.IsNaN(Value) => "NaN",
|
||||
_ => Value.ToString("F2"),
|
||||
};
|
||||
return $"[{AsDateTime:yyyy-MM-dd HH:mm:ss}, {valueStr}]";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user