Files
QuanTAlib/lib/trends/sma/Sma.cs
T
Miha Kralj 5c3b3fbab4 Refactor indicators to support optional time step in Prime method
- Updated the Prime method signature in multiple indicators (Jma, Kama, Lsma, Mama, Mgdi, Pwma, Rma, Sma, Ssf, Super, T3, Tema, Trima, Usf, Vidya, Wma, Atr) to accept an optional TimeSpan parameter for improved flexibility.
- Added unit tests for Lsma to verify Dispose functionality, ensuring proper unsubscription from the source and thread safety.
- Enhanced Mama and Wma classes to handle non-finite inputs gracefully and added checks for valid parameters in constructors.
- Introduced additional tests for T3 to validate constructor behavior with invalid volume factors.
- Ensured all indicators maintain consistent behavior when handling edge cases, such as empty buffers and non-finite values.
2025-12-28 15:14:07 -08:00

603 lines
20 KiB
C#

using System;
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>
/// SMA: Simple Moving Average
/// </summary>
/// <remarks>
/// SMA calculates the arithmetic mean of the last n values.
/// Uses a RingBuffer for storage and manual running sum for O(1) complexity per update.
///
/// Calculation:
/// SMA = (P_n + P_(n-1) + ... + P_1) / n
///
/// O(1) update:
/// S_new = S_old - oldest + newest
/// SMA = S_new / n
///
/// IsHot:
/// Becomes true when the buffer is full (period samples processed).
/// </remarks>
[SkipLocalsInit]
public sealed class Sma : AbstractBase
{
private readonly int _period;
private readonly RingBuffer _buffer;
private readonly TValuePublishedHandler _handler;
[StructLayout(LayoutKind.Auto)]
private record struct State(double Sum, double LastInput, double LastValidValue, int TickCount);
private State _state;
private State _p_state;
private const int ResyncInterval = 1000;
/// <summary>
/// Creates SMA with specified period.
/// </summary>
/// <param name="period">Number of values to average (must be > 0)</param>
public Sma(int period)
{
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
_period = period;
_buffer = new RingBuffer(period);
Name = $"Sma({period})";
WarmupPeriod = period;
_handler = Handle;
}
public Sma(ITValuePublisher source, int period) : this(period)
{
source.Pub += _handler;
}
public Sma(TSeries source, int period) : this(period)
{
Prime(source.Values);
if (source.Count > 0)
{
Last = new TValue(source.LastTime, Last.Value);
}
source.Pub += _handler;
}
private void Handle(object? sender, TValueEventArgs e) => Update(e.Value, e.IsNew);
/////////////////////////////////////////////////////////////////////////////////////////////////
// Mode B: Streaming (Stateful)
/////////////////////////////////////////////////////////////////////////////////////////////////
/// <summary>
/// True if the SMA has enough data to produce valid results.
/// SMA is "hot" when the buffer is full (has received at least 'period' values).
/// </summary>
public override bool IsHot => _buffer.IsFull;
/////////////////////////////////////////////////////////////////////////////////////////////////
// Mode C: Priming (The Bridge)
/////////////////////////////////////////////////////////////////////////////////////////////////
/// <summary>
/// Initializes the indicator state using the provided history.
/// Efficiently processes only the last 'Period' values required to sync the buffer.
/// </summary>
/// <param name="source">Historical data (only the last 'period' is actually needed)</param>
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
if (source.Length == 0) return;
// Reset state
_buffer.Clear();
_state = default;
_p_state = default;
// We only need the last 'period' values to fully restore state
// If history is shorter than period, we take it all.
int warmupLength = Math.Min(source.Length, WarmupPeriod);
int startIndex = source.Length - warmupLength;
// 1. Seed the LastValidValue (crucial for NaN handling)
// We must look backwards from start of our warmup window to find a valid predecessor
_state.LastValidValue = double.NaN;
for (int i = startIndex - 1; i >= 0; i--)
{
if (double.IsFinite(source[i]))
{
_state.LastValidValue = source[i];
break;
}
}
// If we didn't find a valid value in history, try finding one inside the warmup window
if (double.IsNaN(_state.LastValidValue))
{
for (int i = startIndex; i < source.Length; i++)
{
if (double.IsFinite(source[i]))
{
_state.LastValidValue = source[i];
break;
}
}
}
// 2. Feed the RingBuffer and State
for (int i = startIndex; i < source.Length; i++)
{
double val = GetValidValue(source[i]);
UpdateState(val);
_state.LastInput = val;
}
// 3. Finalize State
// Calculate the initial "Last" value so the indicator is ready to be read immediately
double result = _buffer.Count > 0 ? _state.Sum / _buffer.Count : double.NaN;
// Note: We can't infer accurate Time from a simple Span<double>,
// so we leave 'Last' with default time or user updates it on next Tick.
Last = new TValue(DateTime.MinValue, result);
// Backup state for the next update cycle
_p_state = _state;
}
/// <summary>
/// Gets a valid input value, using last-value substitution for non-finite inputs.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetValidValue(double input)
{
if (double.IsFinite(input))
{
_state.LastValidValue = input;
return input;
}
return _state.LastValidValue;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void UpdateState(double val)
{
double removedValue = _buffer.Count == _buffer.Capacity ? _buffer.Oldest : 0.0;
_state.Sum = _state.Sum - removedValue + val;
_buffer.Add(val);
_state.TickCount++;
if (_buffer.IsFull && _state.TickCount >= ResyncInterval)
{
_state.TickCount = 0;
_state.Sum = _buffer.RecalculateSum();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
double val = GetValidValue(input.Value);
UpdateState(val);
_state.LastInput = val;
_p_state = _state;
}
else
{
_state = _p_state;
double val = GetValidValue(input.Value);
_state.Sum = _state.Sum - _state.LastInput + val;
_buffer.UpdateNewest(val);
}
double result = _buffer.Count > 0 ? _state.Sum / _buffer.Count : double.NaN;
Last = new TValue(input.Time, result);
PubEvent(Last, isNew);
return Last;
}
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
int len = source.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);
Batch(source.Values, vSpan, _period);
source.Times.CopyTo(tSpan);
Prime(source.Values);
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v);
}
/////////////////////////////////////////////////////////////////////////////////////////////////
// Mode A: Batch (Stateless)
/////////////////////////////////////////////////////////////////////////////////////////////////
/// <summary>
/// Calculates SMA for the entire series using a new instance.
/// </summary>
/// <param name="source">Input series</param>
/// <param name="period">SMA period</param>
/// <returns>SMA series</returns>
public static TSeries Batch(TSeries source, int period)
{
var sma = new Sma(period);
return sma.Update(source);
}
/// <summary>
/// Calculates SMA in-place, writing results to pre-allocated output span.
/// Zero-allocation method for maximum performance.
/// Uses stackalloc circular buffer for NaN-safe sliding window calculation.
/// Automatically uses SIMD acceleration for large, clean datasets.
/// </summary>
/// <param name="source">Input values</param>
/// <param name="output">Output span (must be same length as source)</param>
/// <param name="period">SMA period (must be > 0)</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period)
{
if (source.Length != output.Length)
throw new ArgumentException("Source and output must have the same length", nameof(output));
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
int len = source.Length;
if (len == 0) return;
// Try SIMD path for large, clean datasets
// Requirements: SIMD support, large enough dataset, no NaN values
const int SimdThreshold = 256;
if (len >= SimdThreshold && !source.ContainsNonFinite())
{
if (Avx512F.IsSupported)
{
CalculateAvx512Core(source, output, period);
return;
}
if (Avx2.IsSupported)
{
CalculateAvx2Core(source, output, period);
return;
}
if (AdvSimd.Arm64.IsSupported)
{
CalculateNeonCore(source, output, period);
return;
}
}
// Scalar path with NaN handling
CalculateScalarCore(source, output, period);
}
/// <summary>
/// Runs a high-performance SIMD batch calculation on history and returns
/// a "Hot" Sma instance ready to process the next tick immediately.
/// </summary>
/// <param name="source">Historical time series</param>
/// <param name="period">SMA Period</param>
/// <returns>A tuple containing the full calculation results and the hot indicator instance</returns>
public static (TSeries Results, Sma Indicator) Calculate(TSeries source, int period)
{
var sma = new Sma(period);
TSeries results = sma.Update(source);
return (results, sma);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void CalculateScalarCore(ReadOnlySpan<double> source, Span<double> output, int period)
{
int len = source.Length;
const int StackAllocThreshold = 256;
Span<double> buffer = period <= StackAllocThreshold
? stackalloc double[period]
: new double[period];
double sum = 0;
double lastValid = double.NaN;
// Find first valid value to seed lastValid
for (int k = 0; k < len; k++)
{
if (double.IsFinite(source[k]))
{
lastValid = source[k];
break;
}
}
int bufferIndex = 0;
int i = 0;
int warmupEnd = Math.Min(period, len);
for (; i < warmupEnd; i++)
{
double val = source[i];
if (double.IsFinite(val))
lastValid = val;
else
val = lastValid;
sum += val;
buffer[i] = val;
output[i] = sum / (i + 1);
}
int tickCount = 0;
for (; i < len; i++)
{
double val = source[i];
if (double.IsFinite(val))
lastValid = val;
else
val = lastValid;
sum = sum - buffer[bufferIndex] + val;
buffer[bufferIndex] = val;
bufferIndex++;
if (bufferIndex >= period)
bufferIndex = 0;
output[i] = sum / period;
tickCount++;
if (tickCount >= ResyncInterval)
{
tickCount = 0;
double recalcSum = 0;
for (int k = 0; k < period; k++)
{
recalcSum += buffer[k];
}
sum = recalcSum;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private static void CalculateAvx512Core(ReadOnlySpan<double> source, Span<double> output, int period)
{
int len = source.Length;
const int VectorWidth = 8;
ref double srcRef = ref MemoryMarshal.GetReference(source);
ref double outRef = ref MemoryMarshal.GetReference(output);
double invPeriod = 1.0 / period;
int warmupEnd = Math.Min(period, len);
double sum = 0;
for (int i = 0; i < warmupEnd; i++)
{
sum += Unsafe.Add(ref srcRef, i);
Unsafe.Add(ref outRef, i) = sum / (i + 1);
}
if (len <= period)
return;
var vInvPeriod = Vector512.Create(invPeriod);
int simdEnd = period + ((len - period) / VectorWidth) * VectorWidth;
int tickCount = 0;
for (int i = period; i < simdEnd; i += VectorWidth)
{
var vNew = Vector512.LoadUnsafe(ref Unsafe.Add(ref srcRef, i));
var vOld = Vector512.LoadUnsafe(ref Unsafe.Add(ref srcRef, i - period));
var vDelta = Avx512F.Subtract(vNew, vOld);
// Prefix sum of Delta
var vShift1 = Vector512.Create(0.0, vDelta.GetElement(0), vDelta.GetElement(1), vDelta.GetElement(2), vDelta.GetElement(3), vDelta.GetElement(4), vDelta.GetElement(5), vDelta.GetElement(6));
var vP1 = Avx512F.Add(vDelta, vShift1);
var vShift2 = Vector512.Create(0.0, 0.0, vP1.GetElement(0), vP1.GetElement(1), vP1.GetElement(2), vP1.GetElement(3), vP1.GetElement(4), vP1.GetElement(5));
var vP2 = Avx512F.Add(vP1, vShift2);
var vShift4 = Vector512.Create(0.0, 0.0, 0.0, 0.0, vP2.GetElement(0), vP2.GetElement(1), vP2.GetElement(2), vP2.GetElement(3));
var vP4 = Avx512F.Add(vP2, vShift4);
var vSumPrev = Vector512.Create(sum);
var vSums = Avx512F.Add(vSumPrev, vP4);
var vResult = Avx512F.Multiply(vSums, vInvPeriod);
Vector512.StoreUnsafe(vResult, ref Unsafe.Add(ref outRef, i));
sum = vSums.GetElement(7);
tickCount += VectorWidth;
if (tickCount >= ResyncInterval)
{
tickCount = 0;
int lastIdx = i + VectorWidth - 1;
double recalcSum = 0;
for (int k = 0; k < period; k++)
{
recalcSum += Unsafe.Add(ref srcRef, lastIdx - k);
}
sum = recalcSum;
}
}
for (int i = simdEnd; i < len; i++)
{
sum = sum - Unsafe.Add(ref srcRef, i - period) + Unsafe.Add(ref srcRef, i);
Unsafe.Add(ref outRef, i) = sum * invPeriod;
}
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private static void CalculateAvx2Core(ReadOnlySpan<double> source, Span<double> output, int period)
{
int len = source.Length;
const int VectorWidth = 4;
ref double srcRef = ref MemoryMarshal.GetReference(source);
ref double outRef = ref MemoryMarshal.GetReference(output);
double invPeriod = 1.0 / period;
int warmupEnd = Math.Min(period, len);
double sum = 0;
for (int i = 0; i < warmupEnd; i++)
{
sum += Unsafe.Add(ref srcRef, i);
Unsafe.Add(ref outRef, i) = sum / (i + 1);
}
if (len <= period)
return;
var vInvPeriod = Vector256.Create(invPeriod);
var vZero = Vector256<double>.Zero;
int simdEnd = period + ((len - period) / VectorWidth) * VectorWidth;
int tickCount = 0;
for (int i = period; i < simdEnd; i += VectorWidth)
{
var vNew = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, i));
var vOld = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, i - period));
var vDelta = Avx.Subtract(vNew, vOld);
var vShift1 = Avx2.Permute4x64(vDelta.AsUInt64(), 0b_10_01_00_00).AsDouble(); // skipcq: CS-R1131
vShift1 = Avx.Blend(vZero, vShift1, 0b_1110);
var vP1 = Avx.Add(vDelta, vShift1);
var vShift2 = Avx2.Permute4x64(vP1.AsUInt64(), 0b_01_00_00_00).AsDouble(); // skipcq: CS-R1131
vShift2 = Avx.Blend(vZero, vShift2, 0b_1100);
var vP2 = Avx.Add(vP1, vShift2);
var vSumPrev = Vector256.Create(sum);
var vSums = Avx.Add(vSumPrev, vP2);
var vResult = Avx.Multiply(vSums, vInvPeriod);
Vector256.StoreUnsafe(vResult, ref Unsafe.Add(ref outRef, i));
sum = vSums.GetElement(3);
tickCount += VectorWidth;
if (tickCount >= ResyncInterval)
{
tickCount = 0;
int lastIdx = i + VectorWidth - 1;
double recalcSum = 0;
for (int k = 0; k < period; k++)
{
recalcSum += Unsafe.Add(ref srcRef, lastIdx - k);
}
sum = recalcSum;
}
}
for (int i = simdEnd; i < len; i++)
{
sum = sum - Unsafe.Add(ref srcRef, i - period) + Unsafe.Add(ref srcRef, i);
Unsafe.Add(ref outRef, i) = sum * invPeriod;
}
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private static void CalculateNeonCore(ReadOnlySpan<double> source, Span<double> output, int period)
{
int len = source.Length;
const int VectorWidth = 2;
ref double srcRef = ref MemoryMarshal.GetReference(source);
ref double outRef = ref MemoryMarshal.GetReference(output);
double invPeriod = 1.0 / period;
int warmupEnd = Math.Min(period, len);
double sum = 0;
for (int i = 0; i < warmupEnd; i++)
{
sum += Unsafe.Add(ref srcRef, i);
Unsafe.Add(ref outRef, i) = sum / (i + 1);
}
if (len <= period)
return;
var vInvPeriod = Vector128.Create(invPeriod);
int simdEnd = period + ((len - period) / VectorWidth) * VectorWidth;
int tickCount = 0;
for (int i = period; i < simdEnd; i += VectorWidth)
{
var vNew = Vector128.LoadUnsafe(ref Unsafe.Add(ref srcRef, i));
var vOld = Vector128.LoadUnsafe(ref Unsafe.Add(ref srcRef, i - period));
var vDelta = AdvSimd.Arm64.Subtract(vNew, vOld);
// Prefix sum of Delta: [d0, d0+d1]
double d0 = vDelta.GetElement(0);
double d1 = vDelta.GetElement(1);
double ps0 = sum + d0;
double ps1 = ps0 + d1;
var vSums = Vector128.Create(ps0, ps1);
var vResult = AdvSimd.Arm64.Multiply(vSums, vInvPeriod);
Vector128.StoreUnsafe(vResult, ref Unsafe.Add(ref outRef, i));
sum = ps1;
tickCount += VectorWidth;
if (tickCount >= ResyncInterval)
{
tickCount = 0;
int lastIdx = i + VectorWidth - 1;
double recalcSum = 0;
for (int k = 0; k < period; k++)
{
recalcSum += Unsafe.Add(ref srcRef, lastIdx - k);
}
sum = recalcSum;
}
}
for (int i = simdEnd; i < len; i++)
{
sum = sum - Unsafe.Add(ref srcRef, i - period) + Unsafe.Add(ref srcRef, i);
Unsafe.Add(ref outRef, i) = sum * invPeriod;
}
}
/// <summary>
/// Resets the SMA state.
/// </summary>
public override void Reset()
{
_buffer.Clear();
_state = default;
_p_state = default;
Last = default;
}
}