refactoring

This commit is contained in:
Miha Kralj
2025-12-16 21:16:50 -08:00
parent a67ad65fa5
commit d277e08056
137 changed files with 5074 additions and 3178 deletions
+2 -2
View File
@@ -103,7 +103,7 @@
| HOMOD | Homodyne Discriminator Dominant Cycle | Cycles |
| HP | Hodrick-Prescott Filter | Trends |
| HPF | Ehlers Highpass Filter | Trends |
| HTIT | Ehlers Hilbert Transform Instantaneous Trend | Trends |
| [HTIT](trends/htit/Htit.md) | Ehlers Hilbert Transform Instantaneous Trend | Trends |
| HT_DCPERIOD | Ehlers Hilbert Transform Dominant Cycle Period | Cycles |
| HT_DCPHASE | Ehlers Hilbert Transform Dominant Cycle Phase | Cycles |
| HT_PHASOR | Ehlers Hilbert Transform Phasor Components | Cycles |
@@ -188,7 +188,7 @@
| PVO | Percentage Volume Oscillator | Volume |
| PVR | Price Volume Rank | Volume |
| PVT | Price Volume Trend | Volume |
| PWMA | Pascal Weighted MA | Trends |
| [PWMA](trends/pwma/Pwma.md) | Pascal Weighted MA | Trends |
| QEMA | Quadruple Exponential MA | Trends |
| QSTICK | Qstick Indicator | Momentum |
| QUANTILE | Quantile | Statistics |
+69
View File
@@ -0,0 +1,69 @@
using System;
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
{
/// <summary>
/// Display name for the indicator.
/// </summary>
public string Name { get; protected set; } = string.Empty;
/// <summary>
/// Number of periods before the indicator is considered "hot" (valid).
/// </summary>
public int WarmupPeriod { get; protected set; }
/// <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 Action<TValue>? Pub;
/// <summary>
/// Helper to invoke the Pub event.
/// </summary>
protected void PubEvent(TValue value)
{
Pub?.Invoke(value);
}
/// <summary>
/// Initializes the indicator state using the provided history.
/// </summary>
/// <param name="source">Historical data</param>
public abstract void Prime(ReadOnlySpan<double> source);
/// <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();
}
+26
View File
@@ -525,6 +525,32 @@ public class SimdExtensionsTests
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, SimdExtensions.DotProduct(a, b));
}
[Fact]
public void DotProduct_DifferentLengths_ThrowsArgumentException()
{
double[] a = [1.0, 2.0];
double[] b = [1.0];
Assert.Throws<ArgumentException>(() => SimdExtensions.DotProduct(a, b));
}
[Fact]
public void DotProduct_EmptySpans_ReturnsZero()
{
double[] a = [];
double[] b = [];
Assert.Equal(0.0, SimdExtensions.DotProduct(a, b));
}
// Integration tests
[Fact]
public void SIMD_WorksWithTSeriesValues()
File diff suppressed because it is too large Load Diff
+83 -83
View File
@@ -1,83 +1,83 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// A lightweight struct representing an OHLCV bar.
/// Pure data type: 48 bytes (long + 5 doubles).
/// </summary>
[SkipLocalsInit]
public readonly struct TBar : IEquatable<TBar>
{
public readonly long Time;
public readonly double Open;
public readonly double High;
public readonly double Low;
public readonly double Close;
public readonly double Volume;
public DateTime AsDateTime => new(Time, DateTimeKind.Utc);
// TValue conversions (Zero-copy / lightweight creation)
public TValue O { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Time, Open); }
public TValue H { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Time, High); }
public TValue L { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Time, Low); }
public TValue C { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Time, Close); }
public TValue V { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Time, Volume); }
// Computed properties (calculated on demand, no storage overhead)
public double HL2 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low) * 0.5; }
public double OC2 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + Close) * 0.5; }
public double OHL3 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + High + Low) * 0.333333333333333333; }
public double HLC3 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low + Close) * 0.333333333333333333; }
public double OHLC4 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + High + Low + Close) * 0.25; }
public double HLCC4 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low + Close + Close) * 0.25; }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TBar(long time, double open, double high, double low, double close, double volume)
{
Time = time;
Open = open;
High = high;
Low = low;
Close = close;
Volume = volume;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TBar(DateTime time, double open, double high, double low, double close, double volume)
{
Time = time.Ticks;
Open = open;
High = high;
Low = low;
Close = close;
Volume = volume;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator double(TBar bar) => bar.Close;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator TValue(TBar bar) => new(bar.Time, bar.Close);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator DateTime(TBar bar) => new(bar.Time, DateTimeKind.Utc);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override string ToString() => $"[{AsDateTime:yyyy-MM-dd HH:mm:ss}: O={Open:F2}, H={High:F2}, L={Low:F2}, C={Close:F2}, V={Volume:F2}]";
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(TBar other) =>
Time == other.Time &&
Open == other.Open &&
High == other.High &&
Low == other.Low &&
Close == other.Close &&
Volume == other.Volume;
public override bool Equals(object? obj) => obj is TBar other && Equals(other);
public override int GetHashCode() => HashCode.Combine(Time, Open, High, Low, Close, Volume);
public static bool operator ==(TBar left, TBar right) => left.Equals(right);
public static bool operator !=(TBar left, TBar right) => !left.Equals(right);
}
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// A lightweight struct representing an OHLCV bar.
/// Pure data type: 48 bytes (long + 5 doubles).
/// </summary>
[SkipLocalsInit]
public readonly struct TBar : IEquatable<TBar>
{
public readonly long Time;
public readonly double Open;
public readonly double High;
public readonly double Low;
public readonly double Close;
public readonly double Volume;
public DateTime AsDateTime => new(Time, DateTimeKind.Utc);
// TValue conversions (Zero-copy / lightweight creation)
public TValue O { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Time, Open); }
public TValue H { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Time, High); }
public TValue L { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Time, Low); }
public TValue C { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Time, Close); }
public TValue V { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Time, Volume); }
// Computed properties (calculated on demand, no storage overhead)
public double HL2 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low) * 0.5; }
public double OC2 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + Close) * 0.5; }
public double OHL3 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + High + Low) * 0.333333333333333333; }
public double HLC3 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low + Close) * 0.333333333333333333; }
public double OHLC4 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + High + Low + Close) * 0.25; }
public double HLCC4 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low + Close + Close) * 0.25; }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TBar(long time, double open, double high, double low, double close, double volume)
{
Time = time;
Open = open;
High = high;
Low = low;
Close = close;
Volume = volume;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TBar(DateTime time, double open, double high, double low, double close, double volume)
{
Time = time.Ticks;
Open = open;
High = high;
Low = low;
Close = close;
Volume = volume;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator double(TBar bar) => bar.Close;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator TValue(TBar bar) => new(bar.Time, bar.Close);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator DateTime(TBar bar) => new(bar.Time, DateTimeKind.Utc);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override string ToString() => $"[{AsDateTime:yyyy-MM-dd HH:mm:ss}: O={Open:F2}, H={High:F2}, L={Low:F2}, C={Close:F2}, V={Volume:F2}]";
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(TBar other) =>
Time == other.Time &&
Open == other.Open &&
High == other.High &&
Low == other.Low &&
Close == other.Close &&
Volume == other.Volume;
public override bool Equals(object? obj) => obj is TBar other && Equals(other);
public override int GetHashCode() => HashCode.Combine(Time, Open, High, Low, Close, Volume);
public static bool operator ==(TBar left, TBar right) => left.Equals(right);
public static bool operator !=(TBar left, TBar right) => !left.Equals(right);
}
+147 -147
View File
@@ -1,147 +1,147 @@
using System.Collections;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// A high-performance OHLCV time series implementation using Structure of Arrays (SoA) layout.
/// Stores Time, Open, High, Low, Close, Volume in separate contiguous arrays for SIMD efficiency.
/// Exposes TSeries views for each component that share the underlying Time array.
/// </summary>
public class TBarSeries : IReadOnlyList<TBar>
{
protected readonly List<long> _t;
protected readonly List<double> _o;
protected readonly List<double> _h;
protected readonly List<double> _l;
protected readonly List<double> _c;
protected readonly List<double> _v;
public string Name { get; set; } = "Bar";
public event Action<TBar>? Pub;
// 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) { Name = "Open" };
High = new TSeries(_t, _h) { Name = "High" };
Low = new TSeries(_t, _l) { Name = "Low" };
Close = new TSeries(_t, _c) { Name = "Close" };
Volume = new TSeries(_t, _v) { Name = "Volume" };
}
public int Count
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _c.Count;
}
public TBar this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => new(_t[index], _o[index], _h[index], _l[index], _c[index], _v[index]);
}
public TBar Last
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _c.Count > 0 ? new(_t[^1], _o[^1], _h[^1], _l[^1], _c[^1], _v[^1]) : default;
}
public long LastTime { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _t.Count > 0 ? _t[^1] : 0; }
public double LastOpen { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _o.Count > 0 ? _o[^1] : double.NaN; }
public double LastHigh { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _h.Count > 0 ? _h[^1] : double.NaN; }
public double LastLow { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _l.Count > 0 ? _l[^1] : double.NaN; }
public double LastClose { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _c.Count > 0 ? _c[^1] : double.NaN; }
public double LastVolume { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _v.Count > 0 ? _v[^1] : double.NaN; }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(TBar bar, bool isNew = true)
{
if (isNew || _c.Count == 0)
{
_t.Add(bar.Time);
_o.Add(bar.Open);
_h.Add(bar.High);
_l.Add(bar.Low);
_c.Add(bar.Close);
_v.Add(bar.Volume);
}
else
{
int lastIdx = _c.Count - 1;
_t[lastIdx] = bar.Time;
_o[lastIdx] = bar.Open;
_h[lastIdx] = bar.High;
_l[lastIdx] = bar.Low;
_c[lastIdx] = bar.Close;
_v[lastIdx] = bar.Volume;
}
Pub?.Invoke(bar);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(long time, double open, double high, double low, double close, double volume, bool isNew = true) =>
Add(new TBar(time, open, high, low, close, volume), isNew);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(DateTime time, double open, double high, double low, double close, double volume, bool isNew = true) =>
Add(new TBar(time.Ticks, open, high, low, close, volume), isNew);
public void Add(IEnumerable<long> t, IEnumerable<double> o, IEnumerable<double> h, IEnumerable<double> l, IEnumerable<double> c, IEnumerable<double> v)
{
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");
}
for (int i = 0; i < tArr.Length; i++)
{
Add(tArr[i], oArr[i], hArr[i], lArr[i], cArr[i], vArr[i]);
}
}
public IEnumerator<TBar> GetEnumerator()
{
for (int i = 0; i < _c.Count; i++)
{
yield return new TBar(_t[i], _o[i], _h[i], _l[i], _c[i], _v[i]);
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
using System.Collections;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// A high-performance OHLCV time series implementation using Structure of Arrays (SoA) layout.
/// Stores Time, Open, High, Low, Close, Volume in separate contiguous arrays for SIMD efficiency.
/// Exposes TSeries views for each component that share the underlying Time array.
/// </summary>
public class TBarSeries : IReadOnlyList<TBar>
{
protected readonly List<long> _t;
protected readonly List<double> _o;
protected readonly List<double> _h;
protected readonly List<double> _l;
protected readonly List<double> _c;
protected readonly List<double> _v;
public string Name { get; set; } = "Bar";
public event Action<TBar>? Pub;
// 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) { Name = "Open" };
High = new TSeries(_t, _h) { Name = "High" };
Low = new TSeries(_t, _l) { Name = "Low" };
Close = new TSeries(_t, _c) { Name = "Close" };
Volume = new TSeries(_t, _v) { Name = "Volume" };
}
public int Count
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _c.Count;
}
public TBar this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => new(_t[index], _o[index], _h[index], _l[index], _c[index], _v[index]);
}
public TBar Last
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _c.Count > 0 ? new(_t[^1], _o[^1], _h[^1], _l[^1], _c[^1], _v[^1]) : default;
}
public long LastTime { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _t.Count > 0 ? _t[^1] : 0; }
public double LastOpen { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _o.Count > 0 ? _o[^1] : double.NaN; }
public double LastHigh { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _h.Count > 0 ? _h[^1] : double.NaN; }
public double LastLow { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _l.Count > 0 ? _l[^1] : double.NaN; }
public double LastClose { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _c.Count > 0 ? _c[^1] : double.NaN; }
public double LastVolume { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _v.Count > 0 ? _v[^1] : double.NaN; }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(TBar bar, bool isNew = true)
{
if (isNew || _c.Count == 0)
{
_t.Add(bar.Time);
_o.Add(bar.Open);
_h.Add(bar.High);
_l.Add(bar.Low);
_c.Add(bar.Close);
_v.Add(bar.Volume);
}
else
{
int lastIdx = _c.Count - 1;
_t[lastIdx] = bar.Time;
_o[lastIdx] = bar.Open;
_h[lastIdx] = bar.High;
_l[lastIdx] = bar.Low;
_c[lastIdx] = bar.Close;
_v[lastIdx] = bar.Volume;
}
Pub?.Invoke(bar);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(long time, double open, double high, double low, double close, double volume, bool isNew = true) =>
Add(new TBar(time, open, high, low, close, volume), isNew);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(DateTime time, double open, double high, double low, double close, double volume, bool isNew = true) =>
Add(new TBar(time.Ticks, open, high, low, close, volume), isNew);
public void Add(IEnumerable<long> t, IEnumerable<double> o, IEnumerable<double> h, IEnumerable<double> l, IEnumerable<double> c, IEnumerable<double> v)
{
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");
}
for (int i = 0; i < tArr.Length; i++)
{
Add(tArr[i], oArr[i], hArr[i], lArr[i], cArr[i], vArr[i]);
}
}
public IEnumerator<TBar> GetEnumerator()
{
for (int i = 0; i < _c.Count; i++)
{
yield return new TBar(_t[i], _o[i], _h[i], _l[i], _c[i], _v[i]);
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
+134 -134
View File
@@ -1,134 +1,134 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// A high-performance time series implementation using Structure of Arrays (SoA) layout.
/// Stores Time (long) and Value (double) in separate contiguous arrays for SIMD efficiency.
/// Supports "New Bar" vs "Update Last" streaming semantics.
/// </summary>
public class TSeries : IReadOnlyList<TValue>, ITValuePublisher
{
protected readonly List<long> _t;
protected readonly List<double> _v;
public string Name { get; set; } = "Data";
public event Action<TValue>? Pub;
public TSeries() : this(0)
{
}
public TSeries(int capacity)
{
_t = new List<long>(capacity);
_v = new List<double>(capacity);
}
public TSeries(List<long> time, List<double> values)
{
_t = time;
_v = values;
}
public int Count
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _v.Count;
}
public TValue this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => new(_t[index], _v[index]);
}
public TValue Last
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _v.Count > 0 ? new(_t[^1], _v[^1]) : default;
}
public double LastValue
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _v.Count > 0 ? _v[^1] : double.NaN;
}
public long LastTime
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _t.Count > 0 ? _t[^1] : 0;
}
/// <summary>
/// Direct access to the underlying Value array as a Span for SIMD operations.
/// </summary>
public ReadOnlySpan<double> Values
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => CollectionsMarshal.AsSpan(_v);
}
/// <summary>
/// Direct access to the underlying Time array as a Span.
/// </summary>
public ReadOnlySpan<long> Times
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => CollectionsMarshal.AsSpan(_t);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual void Add(TValue value, bool isNew)
{
if (isNew || _v.Count == 0)
{
_t.Add(value.Time);
_v.Add(value.Value);
}
else
{
int lastIdx = _v.Count - 1;
_t[lastIdx] = value.Time;
_v[lastIdx] = value.Value;
}
Pub?.Invoke(value);
}
// Overload for backward compatibility (assumes isNew=true)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual void Add(TValue value) => Add(value, true);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(long time, double value, bool isNew = true) => Add(new TValue(time, value), isNew);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(DateTime time, double value, bool isNew = true) => Add(new TValue(time, 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
public IEnumerator<TValue> GetEnumerator()
{
for (int i = 0; i < _v.Count; i++)
{
yield return new TValue(_t[i], _v[i]);
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// A high-performance time series implementation using Structure of Arrays (SoA) layout.
/// Stores Time (long) and Value (double) in separate contiguous arrays for SIMD efficiency.
/// Supports "New Bar" vs "Update Last" streaming semantics.
/// </summary>
public class TSeries : IReadOnlyList<TValue>, ITValuePublisher
{
protected readonly List<long> _t;
protected readonly List<double> _v;
public string Name { get; set; } = "Data";
public event Action<TValue>? Pub;
public TSeries() : this(0)
{
}
public TSeries(int capacity)
{
_t = new List<long>(capacity);
_v = new List<double>(capacity);
}
public TSeries(List<long> time, List<double> values)
{
_t = time;
_v = values;
}
public int Count
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _v.Count;
}
public TValue this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => new(_t[index], _v[index]);
}
public TValue Last
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _v.Count > 0 ? new(_t[^1], _v[^1]) : default;
}
public double LastValue
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _v.Count > 0 ? _v[^1] : double.NaN;
}
public long LastTime
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _t.Count > 0 ? _t[^1] : 0;
}
/// <summary>
/// Direct access to the underlying Value array as a Span for SIMD operations.
/// </summary>
public ReadOnlySpan<double> Values
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => CollectionsMarshal.AsSpan(_v);
}
/// <summary>
/// Direct access to the underlying Time array as a Span.
/// </summary>
public ReadOnlySpan<long> Times
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => CollectionsMarshal.AsSpan(_t);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual void Add(TValue value, bool isNew)
{
if (isNew || _v.Count == 0)
{
_t.Add(value.Time);
_v.Add(value.Value);
}
else
{
int lastIdx = _v.Count - 1;
_t[lastIdx] = value.Time;
_v[lastIdx] = value.Value;
}
Pub?.Invoke(value);
}
// Overload for backward compatibility (assumes isNew=true)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public virtual void Add(TValue value) => Add(value, true);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(long time, double value, bool isNew = true) => Add(new TValue(time, value), isNew);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(DateTime time, double value, bool isNew = true) => Add(new TValue(time, 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
public IEnumerator<TValue> GetEnumerator()
{
for (int i = 0; i < _v.Count; i++)
{
yield return new TValue(_t[i], _v[i]);
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
+47 -47
View File
@@ -1,47 +1,47 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// A lightweight struct representing a time-value pair.
/// Pure data type: 16 bytes (long + double).
/// </summary>
[SkipLocalsInit]
public readonly struct TValue : IEquatable<TValue>
{
public readonly long Time;
public readonly double Value;
public DateTime AsDateTime => new(Time, DateTimeKind.Utc);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue(long time, double value)
{
Time = time;
Value = value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue(DateTime time, double value)
{
Time = time.Kind == DateTimeKind.Utc ? time.Ticks : time.ToUniversalTime().Ticks;
Value = value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator double(TValue tv) => tv.Value;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator DateTime(TValue tv) => new(tv.Time, DateTimeKind.Utc);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override string ToString() => $"[{AsDateTime:yyyy-MM-dd HH:mm:ss}, {Value:F2}]";
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(TValue other) => Time == other.Time && Value == other.Value;
public override bool Equals(object? obj) => obj is TValue other && Equals(other);
public override int GetHashCode() => HashCode.Combine(Time, Value);
public static bool operator ==(TValue left, TValue right) => left.Equals(right);
public static bool operator !=(TValue left, TValue right) => !left.Equals(right);
}
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// A lightweight struct representing a time-value pair.
/// Pure data type: 16 bytes (long + double).
/// </summary>
[SkipLocalsInit]
public readonly struct TValue : IEquatable<TValue>
{
public readonly long Time;
public readonly double Value;
public DateTime AsDateTime => new(Time, DateTimeKind.Utc);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue(long time, double value)
{
Time = time;
Value = value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue(DateTime time, double value)
{
Time = time.Kind == DateTimeKind.Utc ? time.Ticks : time.ToUniversalTime().Ticks;
Value = value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator double(TValue tv) => tv.Value;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator DateTime(TValue tv) => new(tv.Time, DateTimeKind.Utc);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override string ToString() => $"[{AsDateTime:yyyy-MM-dd HH:mm:ss}, {Value:F2}]";
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(TValue other) => Time == other.Time && Value == other.Value;
public override bool Equals(object? obj) => obj is TValue other && Equals(other);
public override int GetHashCode() => HashCode.Combine(Time, Value);
public static bool operator ==(TValue left, TValue right) => left.Equals(right);
public static bool operator !=(TValue left, TValue right) => !left.Equals(right);
}
+3 -3
View File
@@ -56,7 +56,7 @@ public class AdxIndicatorTests
indicator.Initialize();
// After init, line series should exist (ADX, +DI, -DI)
Assert.Equal(3, indicator.LinesSeries.Length);
Assert.Equal(3, indicator.LinesSeries.Count);
}
[Fact]
@@ -71,7 +71,7 @@ public class AdxIndicatorTests
for (int i = 0; i < 20; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
// Process update for each bar to simulate history loading
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
@@ -79,7 +79,7 @@ public class AdxIndicatorTests
// Line series should have a value
double adx = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(adx));
}
}
+3 -3
View File
@@ -28,11 +28,11 @@ public class AdxIndicator : Indicator, IWatchlistIndicator
SeparateWindow = true;
Name = "ADX - Average Directional Index";
Description = "Measures the strength of a trend";
AdxSeries = new(name: "ADX", color: Color.Blue, width: 2, style: LineStyle.Solid);
DiPlusSeries = new(name: "+DI", color: Color.Green, width: 1, style: LineStyle.Solid);
DiMinusSeries = new(name: "-DI", color: Color.Red, width: 1, style: LineStyle.Solid);
AddLineSeries(AdxSeries);
AddLineSeries(DiPlusSeries);
AddLineSeries(DiMinusSeries);
@@ -51,7 +51,7 @@ public class AdxIndicator : Indicator, IWatchlistIndicator
TBar bar = this.GetInputBar(args);
TValue result = _adx!.Update(bar, isNew);
if (!_adx.IsHot && !ShowColdValues)
{
return;
+1 -1
View File
@@ -115,7 +115,7 @@ public class AdxTests
streamingResults.Add(adx.Update(bars[i]).Value);
}
var staticResults = Adx.Calculate(bars, 14);
var staticResults = Adx.Batch(bars, 14);
Assert.Equal(streamingResults.Count, staticResults.Count);
for (int i = 0; i < staticResults.Count; i++)
+19 -13
View File
@@ -38,7 +38,7 @@ public sealed class Adx : ITValuePublisher
private double _p_trSum, _p_dmPlusSum, _p_dmMinusSum;
private int _samples;
private int _p_samples;
private double _trSmooth, _dmPlusSmooth, _dmMinusSmooth;
private double _p_trSmooth, _p_dmPlusSmooth, _p_dmMinusSmooth;
@@ -47,7 +47,7 @@ public sealed class Adx : ITValuePublisher
private double _p_dxSum;
private int _dxSamples;
private int _p_dxSamples;
private double _adx;
private double _p_adx;
@@ -78,6 +78,11 @@ public sealed class Adx : ITValuePublisher
/// </summary>
public bool IsHot => _dxSamples >= _period;
/// <summary>
/// The number of bars required for the indicator to warm up.
/// </summary>
public int WarmupPeriod { get; }
/// <summary>
/// Creates ADX with specified period.
/// </summary>
@@ -89,6 +94,7 @@ public sealed class Adx : ITValuePublisher
_period = period;
Name = $"Adx({period})";
WarmupPeriod = period * 2; // Needs period for TR/DM smoothing, then period for ADX smoothing
_isInitialized = false;
}
@@ -101,19 +107,19 @@ public sealed class Adx : ITValuePublisher
_prevBar = default;
_p_prevBar = default;
_isInitialized = false;
_trSum = _dmPlusSum = _dmMinusSum = 0;
_p_trSum = _p_dmPlusSum = _p_dmMinusSum = 0;
_samples = _p_samples = 0;
_trSmooth = _dmPlusSmooth = _dmMinusSmooth = 0;
_p_trSmooth = _p_dmPlusSmooth = _p_dmMinusSmooth = 0;
_dxSum = _p_dxSum = 0;
_dxSamples = _p_dxSamples = 0;
_adx = _p_adx = 0;
Last = default;
DiPlus = default;
DiMinus = default;
@@ -175,7 +181,7 @@ public sealed class Adx : ITValuePublisher
if (upMove > downMove && upMove > 0)
dmPlus = upMove;
if (downMove > upMove && downMove > 0)
dmMinus = downMove;
@@ -211,7 +217,7 @@ public sealed class Adx : ITValuePublisher
// Wilder uses sums, but effectively it's RMA.
// Standard formula:
// Smooth = Smooth - (Smooth / Period) + Input
_trSmooth = _trSmooth - (_trSmooth / _period) + tr;
_dmPlusSmooth = _dmPlusSmooth - (_dmPlusSmooth / _period) + dmPlus;
_dmMinusSmooth = _dmMinusSmooth - (_dmMinusSmooth / _period) + dmMinus;
@@ -235,13 +241,13 @@ public sealed class Adx : ITValuePublisher
{
dx = (Math.Abs(diPlus - diMinus) / diSum) * 100.0;
}
// Smooth DX to get ADX
if (_dxSamples < _period)
{
_dxSum += dx;
_dxSamples++;
if (_dxSamples == _period)
{
_adx = _dxSum / _period; // First ADX is SMA of DX
@@ -257,7 +263,7 @@ public sealed class Adx : ITValuePublisher
DiPlus = new TValue(input.Time, diPlus);
DiMinus = new TValue(input.Time, diMinus);
Last = new TValue(input.Time, _adx);
Pub?.Invoke(Last);
return Last;
}
@@ -284,7 +290,7 @@ public sealed class Adx : ITValuePublisher
return new TSeries(t, v);
}
public static TSeries Calculate(TBarSeries source, int period)
public static TSeries Batch(TBarSeries source, int period)
{
var adx = new Adx(period);
return adx.Update(source);
+2 -2
View File
@@ -59,10 +59,10 @@ var series = new TBarSeries();
var results = adx.Update(series);
```
### Static Calculation
### Batch Calculation
```csharp
var results = Adx.Calculate(series, 14);
var results = Adx.Batch(series, 14);
```
## Interpretation
+4 -4
View File
@@ -58,7 +58,7 @@ public class AoIndicatorTests
indicator.Initialize();
// After init, line series should exist (Up and Down)
Assert.Equal(2, indicator.LinesSeries.Length);
Assert.Equal(2, indicator.LinesSeries.Count);
}
[Fact]
@@ -73,7 +73,7 @@ public class AoIndicatorTests
for (int i = 0; i < 20; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
// Process update for each bar to simulate history loading
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
@@ -83,7 +83,7 @@ public class AoIndicatorTests
// One should be NaN, other should be value, or both NaN if cold
double up = indicator.LinesSeries[0].GetValue(0);
double down = indicator.LinesSeries[1].GetValue(0);
Assert.True(double.IsFinite(up) || double.IsFinite(down));
}
@@ -100,7 +100,7 @@ public class AoIndicatorTests
}
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
// Add new bar
indicator.HistoricalData.AddBar(now.AddMinutes(20), 120, 130, 110, 125);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
+1 -1
View File
@@ -71,7 +71,7 @@ public class AoIndicator : Indicator, IWatchlistIndicator
// or just use _ao.Last (which is current) and we need the previous one.
// But _ao doesn't expose history directly unless we use TSeries.
// However, Quantower stores history in the Series.
// Get previous value from series
double prevAo = double.NaN;
if (Count > 1)
+2 -2
View File
@@ -101,7 +101,7 @@ public class AoTests
}
[Fact]
public void StaticCalculate_Matches_Streaming()
public void StaticBatch_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
@@ -113,7 +113,7 @@ public class AoTests
streamingResults.Add(ao.Update(bars[i]).Value);
}
var staticResults = Ao.Calculate(bars, 5, 34);
var staticResults = Ao.Batch(bars, 5, 34);
Assert.Equal(streamingResults.Count, staticResults.Count);
for (int i = 0; i < staticResults.Count; i++)
+7 -1
View File
@@ -41,6 +41,11 @@ public sealed class Ao : ITValuePublisher
/// </summary>
public bool IsHot => _smaSlow.IsHot;
/// <summary>
/// The number of bars required to warm up the indicator.
/// </summary>
public int WarmupPeriod { get; }
/// <summary>
/// Creates AO with specified periods.
/// </summary>
@@ -57,6 +62,7 @@ public sealed class Ao : ITValuePublisher
_smaFast = new Sma(fastPeriod);
_smaSlow = new Sma(slowPeriod);
WarmupPeriod = slowPeriod;
Name = $"Ao({fastPeriod},{slowPeriod})";
}
@@ -139,7 +145,7 @@ public sealed class Ao : ITValuePublisher
/// <param name="fastPeriod">Fast SMA period (default 5)</param>
/// <param name="slowPeriod">Slow SMA period (default 34)</param>
/// <returns>AO series</returns>
public static TSeries Calculate(TBarSeries source, int fastPeriod = 5, int slowPeriod = 34)
public static TSeries Batch(TBarSeries source, int fastPeriod = 5, int slowPeriod = 34)
{
var ao = new Ao(fastPeriod, slowPeriod);
return ao.Update(source);
+4
View File
@@ -30,6 +30,10 @@ var result = ao.Update(bar);
// Result contains the AO value
Console.WriteLine($"AO: {result.Value}");
// Batch calculation
var series = new TBarSeries();
var results = Ao.Batch(series, 5, 34);
```
### Parameters
+3 -3
View File
@@ -101,7 +101,7 @@ public class CfbIndicatorTests
indicator.HistoricalData.AddBar(now.AddMinutes(3), 103, 109, 101, 105);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
// Add new bar
indicator.HistoricalData.AddBar(now.AddMinutes(4), 105, 112, 103, 110);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
@@ -137,7 +137,7 @@ public class CfbIndicatorTests
{
var indicator = new CfbIndicator();
indicator.Initialize();
var method = indicator.GetType().GetMethod("OnPaintChart");
Assert.NotNull(method);
Assert.Equal(typeof(CfbIndicator), method.DeclaringType);
@@ -159,7 +159,7 @@ public class CfbIndicatorTests
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
}
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)),
+4 -4
View File
@@ -111,7 +111,7 @@ public class CfbTests
}
[Fact]
public void StaticCalculate_Matches_Streaming()
public void StaticBatch_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
@@ -124,7 +124,7 @@ public class CfbTests
streamingResults.Add(cfb.Update(new TValue(series.Times[i], series.Values[i])).Value);
}
var staticResults = Cfb.Calculate(series);
var staticResults = Cfb.Batch(series);
Assert.Equal(streamingResults.Count, staticResults.Count);
for (int i = 0; i < streamingResults.Count; i++)
@@ -134,7 +134,7 @@ public class CfbTests
}
[Fact]
public void SpanCalculate_Matches_Streaming()
public void SpanBatch_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
@@ -148,7 +148,7 @@ public class CfbTests
}
double[] spanResults = new double[bars.Count];
Cfb.Calculate(values, spanResults);
Cfb.Batch(values, spanResults);
for (int i = 0; i < streamingResults.Count; i++)
{
+8 -8
View File
@@ -16,9 +16,9 @@ public class CfbValidationTests
}
[Fact]
public void Validate_Consistency_UpdateVsCalculate()
public void Validate_Consistency_UpdateVsBatch()
{
// Verify that Update(TValue) and Calculate(TSeries) produce identical results
// Verify that Update(TValue) and Batch(TSeries) produce identical results
var cfb = new Cfb();
var streamResult = new TSeries();
foreach (var item in _testData.Data)
@@ -26,7 +26,7 @@ public class CfbValidationTests
streamResult.Add(cfb.Update(item));
}
var batchResult = Cfb.Calculate(_testData.Data);
var batchResult = Cfb.Batch(_testData.Data);
Assert.Equal(streamResult.Count, batchResult.Count);
Assert.NotEmpty(streamResult);
@@ -34,18 +34,18 @@ public class CfbValidationTests
{
Assert.Equal(streamResult[i].Value, batchResult[i].Value, 1e-9);
}
_output.WriteLine("CFB Update vs Calculate validated successfully");
_output.WriteLine("CFB Update vs Batch validated successfully");
}
[Fact]
public void Validate_Consistency_SeriesVsSpan()
{
// Verify that Calculate(TSeries) and Calculate(Span) produce identical results
var batchResult = Cfb.Calculate(_testData.Data);
// Verify that Batch(TSeries) and Batch(Span) produce identical results
var batchResult = Cfb.Batch(_testData.Data);
var spanInput = _testData.Data.Values.ToArray().AsSpan();
var spanOutput = new double[spanInput.Length];
Cfb.Calculate(spanInput, spanOutput);
Cfb.Batch(spanInput, spanOutput);
for (int i = 0; i < batchResult.Count; i++)
{
@@ -58,7 +58,7 @@ public class CfbValidationTests
public void Validate_Properties()
{
// CFB should be >= 1.0
var result = Cfb.Calculate(_testData.Data);
var result = Cfb.Batch(_testData.Data);
foreach (var val in result.Values)
{
Assert.True(val >= 1.0, $"CFB value {val} should be >= 1.0");
+20 -18
View File
@@ -45,6 +45,7 @@ public sealed class Cfb : ITValuePublisher
public event Action<TValue>? Pub;
public TValue Last { get; private set; }
public bool IsHot => _prices.IsFull;
public int WarmupPeriod { get; }
/// <summary>
/// Creates a CFB indicator with specified fractal lengths.
@@ -68,16 +69,17 @@ public sealed class Cfb : ITValuePublisher
}
_maxLen = _lengths[^1];
WarmupPeriod = _maxLen;
// We need maxLen + 1 capacity to handle the lookback correctly
// _prices stores raw prices
// _volatility stores bar-to-bar changes. _volatility[i] = Abs(Price[i] - Price[i-1])
_prices = new RingBuffer(_maxLen + 1);
_volatility = new RingBuffer(_maxLen + 1);
_runningSums = new double[_lengths.Length];
_p_runningSums = new double[_lengths.Length];
Name = "Cfb";
_state.PrevCfb = 1.0;
}
@@ -155,17 +157,17 @@ public sealed class Cfb : ITValuePublisher
for (int i = 0; i < _lengths.Length; i++)
{
int L = _lengths[i];
// Update running sum of volatility
// We always add the new volatility
// We only subtract if we have enough history
double volToRemove = 0.0;
if (count > L)
{
volToRemove = _volatility[count - 1 - L];
}
_runningSums[i] += vol - volToRemove;
if (count <= L) continue;
@@ -176,7 +178,7 @@ public sealed class Cfb : ITValuePublisher
// Net move over L bars
// Price at Count-1 is current. Price at Count-1-L is L bars ago.
double netMove = Math.Abs(price - _prices[count - 1 - L]);
double ratio = netMove / _runningSums[i];
@@ -199,7 +201,7 @@ public sealed class Cfb : ITValuePublisher
}
if (cfb < 1.0) cfb = 1.0;
// Round to nearest integer
cfb = Math.Round(cfb);
if (cfb < 1.0) cfb = 1.0;
@@ -224,14 +226,14 @@ public sealed class Cfb : ITValuePublisher
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
Calculate(source.Values, vSpan, _lengths);
Batch(source.Values, vSpan, _lengths);
source.Times.CopyTo(tSpan);
// Restore state logic would go here if needed for continuity,
// but for batch processing we usually just return the result.
// To properly support "Update(TValue)" after "Update(TSeries)", we would need to
// replay the last MaxLen bars to populate the buffers.
// Replay last MaxLen bars to restore state
int replayStart = Math.Max(0, len - _maxLen - 1);
_prices.Clear();
@@ -243,7 +245,7 @@ public sealed class Cfb : ITValuePublisher
// We need to re-run the update logic for the replay window to populate running sums correctly
// This is expensive but necessary for correct state restoration.
// For the purpose of this implementation, we will just ensure the buffers are populated.
for (int i = replayStart; i < len; i++)
{
Update(new TValue(source.Times[i], source.Values[i]), true);
@@ -252,14 +254,14 @@ public sealed class Cfb : ITValuePublisher
return new TSeries(t, v);
}
public static TSeries Calculate(TSeries source, int[]? lengths = null)
public static TSeries Batch(TSeries source, int[]? lengths = null)
{
var cfb = new Cfb(lengths);
return cfb.Update(source);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int[]? lengths = null)
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int[]? lengths = null)
{
if (source.Length == 0) return;
@@ -275,7 +277,7 @@ public sealed class Cfb : ITValuePublisher
lens = lengths;
}
int maxLen = 0;
for(int i=0; i<lens.Length; i++) if(lens[i] > maxLen) maxLen = lens[i];
for (int i = 0; i < lens.Length; i++) if (lens[i] > maxLen) maxLen = lens[i];
// Pre-calculate volatility for the whole series
// vol[i] = Abs(source[i] - source[i-1])
@@ -285,7 +287,7 @@ public sealed class Cfb : ITValuePublisher
volArray[0] = 0;
for (int i = 1; i < len; i++)
{
volArray[i] = Math.Abs(source[i] - source[i-1]);
volArray[i] = Math.Abs(source[i] - source[i - 1]);
}
// We need running sums for each length.
@@ -297,7 +299,7 @@ public sealed class Cfb : ITValuePublisher
{
double price = source[i];
double currentVol = volArray[i];
double sumWeightedLen = 0.0;
double sumWeights = 0.0;
@@ -317,14 +319,14 @@ public sealed class Cfb : ITValuePublisher
for (int k = 0; k < lens.Length; k++)
{
int L = lens[k];
// Update running sum
runningSums[k] += currentVol;
if (i > L)
{
runningSums[k] -= volArray[i - L];
}
if (i < L) continue;
double totalMove = runningSums[k];
+1 -1
View File
@@ -74,7 +74,7 @@ double[] prices = ...;
double[] output = new double[prices.Length];
// Calculate using default lengths
Cfb.Calculate(prices.AsSpan(), output.AsSpan());
Cfb.Batch(prices.AsSpan(), output.AsSpan());
```
### Bar Correction (isNew Parameter)
+2 -2
View File
@@ -96,7 +96,7 @@ public class DmxIndicatorTests
}
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
// Add new bar
indicator.HistoricalData.AddBar(now.AddMinutes(10), 110, 120, 100, 115);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
@@ -131,7 +131,7 @@ public class DmxIndicatorTests
{
var indicator = new DmxIndicator();
indicator.Initialize();
var method = indicator.GetType().GetMethod("OnPaintChart");
Assert.NotNull(method);
Assert.Equal(typeof(DmxIndicator), method.DeclaringType);
+2 -2
View File
@@ -112,7 +112,7 @@ public class DmxTests
}
[Fact]
public void StaticCalculate_Matches_Streaming()
public void StaticBatch_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
@@ -124,7 +124,7 @@ public class DmxTests
streamingResults.Add(dmx.Update(bars[i]).Value);
}
var staticResults = Dmx.Calculate(bars, 14);
var staticResults = Dmx.Batch(bars, 14);
Assert.Equal(streamingResults.Count, staticResults.Count);
for (int i = 0; i < streamingResults.Count; i++)
+6 -4
View File
@@ -23,10 +23,12 @@ public sealed class Dmx : ITValuePublisher
public string Name { get; }
public event Action<TValue>? Pub;
public TValue Last { get; private set; }
public int WarmupPeriod { get; }
public Dmx(int period)
{
Name = $"Dmx({period})";
WarmupPeriod = period;
_jmaDMp = new Jma(period);
_jmaDMm = new Jma(period);
_jmaTR = new Jma(period);
@@ -61,7 +63,7 @@ public sealed class Dmx : ITValuePublisher
// But we want to handle the first bar logic specifically
}
}
// We always update _lastInput to the current input
_lastInput = input;
@@ -80,14 +82,14 @@ public sealed class Dmx : ITValuePublisher
if (upMove > downMove && upMove > 0)
dmPlusRaw = upMove;
if (downMove > upMove && downMove > 0)
dmMinusRaw = downMove;
double tr1 = input.High - input.Low;
double tr2 = Math.Abs(input.High - _prevBar.Close);
double tr3 = Math.Abs(input.Low - _prevBar.Close);
trRaw = Math.Max(tr1, Math.Max(tr2, tr3));
}
@@ -130,7 +132,7 @@ public sealed class Dmx : ITValuePublisher
return new TSeries(t, v);
}
public static TSeries Calculate(TBarSeries source, int period = 14)
public static TSeries Batch(TBarSeries source, int period = 14)
{
var dmx = new Dmx(period);
return dmx.Update(source);
+1 -2
View File
@@ -85,8 +85,7 @@ foreach(var bar in bars) {
### Batch Processing
```csharp
var dmx = new Dmx(14);
var resultSeries = dmx.Update(bars);
var resultSeries = Dmx.Batch(bars, 14);
```
## Interpretation
+1 -1
View File
@@ -118,7 +118,7 @@ public class RsxIndicatorTests
{
var indicator = new RsxIndicator();
indicator.Initialize();
var method = indicator.GetType().GetMethod("OnPaintChart");
Assert.NotNull(method);
Assert.Equal(typeof(RsxIndicator), method.DeclaringType);
+4 -4
View File
@@ -58,7 +58,7 @@ public class RsxTests
}
[Fact]
public void StaticCalculate_Matches_Streaming()
public void StaticBatch_Matches_Streaming()
{
int period = 14;
int count = 100;
@@ -72,7 +72,7 @@ public class RsxTests
streamingResults.Add(rsx.Update(new TValue(series.Times[i], series.Values[i])).Value);
}
var staticResults = Rsx.Calculate(series, period);
var staticResults = Rsx.Batch(series, period);
Assert.Equal(streamingResults.Count, staticResults.Count);
for (int i = 0; i < count; i++)
@@ -82,7 +82,7 @@ public class RsxTests
}
[Fact]
public void SpanCalculate_Matches_Streaming()
public void SpanBatch_Matches_Streaming()
{
int period = 14;
int count = 100;
@@ -98,7 +98,7 @@ public class RsxTests
var spanInput = series.Values.ToArray();
var spanOutput = new double[count];
Rsx.Calculate(spanInput, spanOutput, period);
Rsx.Batch(spanInput, spanOutput, period);
for (int i = 0; i < count; i++)
{
+11 -5
View File
@@ -54,6 +54,11 @@ public sealed class Rsx : ITValuePublisher
public event Action<TValue>? Pub;
/// <summary>
/// The number of bars required to warm up the indicator.
/// </summary>
public int WarmupPeriod { get; }
/// <summary>
/// Creates RSX with specified period.
/// </summary>
@@ -64,6 +69,7 @@ public sealed class Rsx : ITValuePublisher
throw new ArgumentException("Period must be greater than 0", nameof(period));
_period = period;
WarmupPeriod = period;
_alpha = 3.0 / (period + 2.0);
Name = $"Rsx({period})";
}
@@ -113,10 +119,10 @@ public sealed class Rsx : ITValuePublisher
// Calculate momentum (change in price * 100)
double momentum = (price - _state.LastPrice) * 100.0;
if (isNew)
{
_state.LastPrice = price;
_state.LastPrice = price;
}
// --- Momentum Smoothing ---
@@ -184,7 +190,7 @@ public sealed class Rsx : ITValuePublisher
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
Calculate(source.Values, vSpan, _period);
Batch(source.Values, vSpan, _period);
source.Times.CopyTo(tSpan);
// Restore state by replaying the last few bars
@@ -199,14 +205,14 @@ public sealed class Rsx : ITValuePublisher
return new TSeries(t, v);
}
public static TSeries Calculate(TSeries source, int period)
public static TSeries Batch(TSeries source, int period)
{
var rsx = new Rsx(period);
return rsx.Update(source);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period)
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");
+1 -1
View File
@@ -45,7 +45,7 @@ Console.WriteLine($"RSX: {result.Value}");
double[] prices = { ... };
double[] results = new double[prices.Length];
Rsx.Calculate(prices, results, 14);
Rsx.Batch(prices, results, 14);
```
### Chaining
+1 -1
View File
@@ -118,7 +118,7 @@ public class VelIndicatorTests
{
var indicator = new VelIndicator();
indicator.Initialize();
var method = indicator.GetType().GetMethod("OnPaintChart");
Assert.NotNull(method);
Assert.Equal(typeof(VelIndicator), method.DeclaringType);
+5 -5
View File
@@ -106,14 +106,14 @@ public class VelTests
}
[Fact]
public void StaticCalculate_Matches_Streaming()
public void StaticBatch_Matches_Streaming()
{
var series = new TSeries();
series.Add(DateTime.UtcNow.Ticks, 10);
series.Add(DateTime.UtcNow.Ticks + 1, 20);
series.Add(DateTime.UtcNow.Ticks + 2, 30);
var results = Vel.Calculate(series, 3);
var results = Vel.Batch(series, 3);
Assert.Equal(3, results.Count);
@@ -125,7 +125,7 @@ public class VelTests
}
[Fact]
public void SpanCalculate_Matches_Streaming()
public void SpanBatch_Matches_Streaming()
{
var series = new TSeries();
double[] source = new double[100];
@@ -140,10 +140,10 @@ public class VelTests
}
// Calculate with TSeries API
var tseriesResult = Vel.Calculate(series, 10);
var tseriesResult = Vel.Batch(series, 10);
// Calculate with Span API
Vel.Calculate(source.AsSpan(), output.AsSpan(), 10);
Vel.Batch(source.AsSpan(), output.AsSpan(), 10);
// Compare results
for (int i = 0; i < 100; i++)
+7 -5
View File
@@ -25,14 +25,16 @@ public sealed class Vel : ITValuePublisher
public string Name { get; }
public TValue Last { get; private set; }
public bool IsHot => _pwma.IsHot && _wma.IsHot;
public int WarmupPeriod { get; }
public event Action<TValue>? Pub;
public Vel(int period)
{
if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period));
_pwma = new Pwma(period);
_wma = new Wma(period);
WarmupPeriod = period;
Name = $"Vel({period})";
}
@@ -68,7 +70,7 @@ public sealed class Vel : ITValuePublisher
CollectionsMarshal.SetCount(v, len);
var vSpan = CollectionsMarshal.AsSpan(v);
SimdExtensions.Subtract(pwmaSeries.Values, wmaSeries.Values, vSpan);
source.Times.CopyTo(CollectionsMarshal.AsSpan(t));
@@ -76,14 +78,14 @@ public sealed class Vel : ITValuePublisher
return new TSeries(t, v);
}
public static TSeries Calculate(TSeries source, int period)
public static TSeries Batch(TSeries source, int period)
{
var vel = new Vel(period);
return vel.Update(source);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period)
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");
@@ -92,7 +94,7 @@ public sealed class Vel : ITValuePublisher
Span<double> wma = source.Length <= 1024 ? stackalloc double[source.Length] : new double[source.Length];
Pwma.Calculate(source, pwma, period);
Wma.Calculate(source, wma, period);
Wma.Batch(source, wma, period);
SimdExtensions.Subtract(pwma, wma, output);
}
+2 -2
View File
@@ -47,12 +47,12 @@ var vel = new Vel(source, 14);
### Batch Calculation (Span)
For high-performance scenarios, use the static `Calculate` method with `Span<double>`.
For high-performance scenarios, use the static `Batch` method with `Span<double>`.
```csharp
double[] prices = { ... };
double[] results = new double[prices.Length];
Vel.Calculate(prices, results, 14);
Vel.Batch(prices, results, 14);
```
## Interpretation
+1 -1
View File
@@ -56,7 +56,7 @@ Trend indicators help identify the direction and strength of a market trend. Mov
| SINEMA | Sine-weighted MA | |
| [SMA](sma/Sma.md) | Simple MA | The unweighted mean of the previous n data. |
| SSF | Ehlers Super Smooth Filter | |
| SUPER | SuperTrend | |
| [SUPER](super/Super.md) | SuperTrend | Trend-following indicator using ATR to define upper and lower bands acting as a trailing stop. |
| [T3](t3/T3.md) | Tillson T3 MA | A smooth moving average that uses a smoothing factor to reduce lag. |
| [TEMA](tema/Tema.md) | Triple Exponential MA | Designed to smooth price fluctuations and filter out volatility. |
| [TRIMA](trima/Trima.md) | Triangular MA | A double-smoothed SMA that gives more weight to the middle of the data window. |
+3 -3
View File
@@ -87,7 +87,7 @@ public class AlmaTests
}
var instanceResults = new Alma(10).Update(series);
var staticResults = Alma.Calculate(series, 10);
var staticResults = Alma.Batch(series, 10);
for (int i = 0; i < instanceResults.Count; i++)
{
@@ -106,7 +106,7 @@ public class AlmaTests
series.Add(bar.Time, bar.Close);
}
var seriesResults = Alma.Calculate(series, 10);
var seriesResults = Alma.Batch(series, 10);
double[] input = series.Values.ToArray();
double[] output = new double[input.Length];
@@ -273,7 +273,7 @@ public class AlmaTests
var series = bars.Close;
// 1. Batch Mode
var batchSeries = Alma.Calculate(series, period);
var batchSeries = Alma.Batch(series, period);
double expected = batchSeries.Last.Value;
// 2. Span Mode
+33 -39
View File
@@ -23,7 +23,7 @@ namespace QuanTAlib;
/// The final ALMA is the weighted sum of the price window divided by the sum of weights.
/// </remarks>
[SkipLocalsInit]
public sealed class Alma : ITValuePublisher
public sealed class Alma : AbstractBase
{
private readonly int _period;
private readonly double _offset;
@@ -36,22 +36,7 @@ public sealed class Alma : ITValuePublisher
private State _state;
private State _p_state;
/// <summary>
/// Display name for the indicator.
/// </summary>
public string Name { get; }
public event Action<TValue>? Pub;
/// <summary>
/// Current ALMA value.
/// </summary>
public TValue Last { get; private set; }
/// <summary>
/// True if the ALMA has enough data to produce valid results (buffer is full).
/// </summary>
public bool IsHot => _buffer.IsFull;
public override bool IsHot => _buffer.IsFull;
/// <summary>
/// Creates ALMA with specified parameters.
@@ -74,6 +59,7 @@ public sealed class Alma : ITValuePublisher
_buffer = new RingBuffer(period);
_weights = new double[period];
Name = $"Alma({period}, {offset:F2}, {sigma:F2})";
WarmupPeriod = period;
// Precompute weights
double m = offset * (period - 1);
@@ -91,7 +77,7 @@ public sealed class Alma : ITValuePublisher
_weightSum = sum;
}
public Alma(ITValuePublisher source, int period, double offset = 0.85, double sigma = 6.0)
public Alma(ITValuePublisher source, int period, double offset = 0.85, double sigma = 6.0)
: this(period, offset, sigma)
{
source.Pub += (item) => Update(item);
@@ -109,7 +95,7 @@ public sealed class Alma : ITValuePublisher
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
public override TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
@@ -130,11 +116,11 @@ public sealed class Alma : ITValuePublisher
}
Last = new TValue(input.Time, result);
Pub?.Invoke(Last);
PubEvent(Last);
return Last;
}
public TSeries Update(TSeries source)
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return new TSeries([], []);
@@ -153,7 +139,7 @@ public sealed class Alma : ITValuePublisher
// Restore state
_buffer.Clear();
_state = default;
// Replay last part to restore buffer state
int startIndex = Math.Max(0, len - _period);
for (int i = startIndex; i < len; i++)
@@ -164,6 +150,14 @@ public sealed class Alma : ITValuePublisher
return new TSeries(t, v);
}
public override void Prime(ReadOnlySpan<double> source)
{
foreach (var value in source)
{
Update(new TValue(DateTime.MinValue, value));
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double CalculateWeightedSum()
{
@@ -176,17 +170,17 @@ public sealed class Alma : ITValuePublisher
// Buffer[0] (oldest) -> Weights[period - count]
ReadOnlySpan<double> bufferSpan = _buffer.GetSpan();
int weightOffset = _period - count;
// Use DotProduct for partial sum
double sum = bufferSpan.DotProduct(_weights.AsSpan(weightOffset, count));
// Calculate weightSum for this subset
double wSum = 0;
for (int i = 0; i < count; i++)
{
wSum += _weights[weightOffset + i];
}
return wSum > 0 ? sum / wSum : 0;
}
@@ -194,20 +188,20 @@ public sealed class Alma : ITValuePublisher
// We use InternalBuffer and StartIndex to avoid allocation and handle wrapping
ReadOnlySpan<double> internalBuf = _buffer.InternalBuffer;
int head = _buffer.StartIndex;
// Part 1: Oldest to End of Buffer -> InternalBuffer[Head ... Cap-1]
// Matches Weights[0 ... Cap-Head-1]
int part1Len = _period - head;
double sum1 = internalBuf.Slice(head, part1Len).DotProduct(_weights.AsSpan(0, part1Len));
// Part 2: Start of Buffer to Newest -> InternalBuffer[0 ... Head-1]
// Matches Weights[Cap-Head ... Cap-1]
double sum2 = internalBuf.Slice(0, head).DotProduct(_weights.AsSpan(part1Len));
double sum2 = internalBuf[..head].DotProduct(_weights.AsSpan(part1Len));
return (sum1 + sum2) / _weightSum;
}
public static TSeries Calculate(TSeries source, int period, double offset = 0.85, double sigma = 6.0)
public static TSeries Batch(TSeries source, int period, double offset = 0.85, double sigma = 6.0)
{
var alma = new Alma(period, offset, sigma);
return alma.Update(source);
@@ -260,39 +254,39 @@ public sealed class Alma : ITValuePublisher
// Oldest is at: (bufferIdx - count + period) % period
// But wait, the buffer wraps.
// Let's just iterate 0..count-1 and map to buffer index.
double sum = 0;
double currentWeightSum = 0;
int startIdx = (bufferIdx - count + period) % period;
int weightOffset = period - count; // Align weights to end
// Optimization: If full, we can use SIMD if we unwrap the buffer or handle wrapping.
// For simplicity in static method (and since we can't easily unwrap stackalloc),
// we'll use scalar loop with modulo.
// Or better: copy to a temporary linear buffer? No, that's too much copying.
// Actually, for full period, we can do two loops (part1, part2) to avoid modulo in loop.
if (count == period)
{
// Buffer is full. startIdx is bufferIdx (which is the oldest, since we just wrote to bufferIdx-1)
// Wait, bufferIdx points to the NEXT write position.
// So bufferIdx is the Oldest.
// Part 1: bufferIdx to End
int part1Len = period - bufferIdx;
for (int j = 0; j < part1Len; j++)
{
sum += buffer[bufferIdx + j] * weights[j];
}
// Part 2: 0 to bufferIdx
for (int j = 0; j < bufferIdx; j++)
{
sum += buffer[j] * weights[part1Len + j];
}
output[i] = sum / weightSum;
}
else
@@ -310,7 +304,7 @@ public sealed class Alma : ITValuePublisher
}
}
public void Reset()
public override void Reset()
{
_buffer.Clear();
_state = default;
+1 -1
View File
@@ -57,7 +57,7 @@ double[] prices = ...;
double[] output = new double[prices.Length];
// Calculate ALMA for the entire array
Alma.Calculate(prices.AsSpan(), output.AsSpan(), period: 9, offset: 0.85, sigma: 6.0);
Alma.Batch(prices.AsSpan(), output.AsSpan(), period: 9, offset: 0.85, sigma: 6.0);
```
### Bar Correction
+3 -3
View File
@@ -117,7 +117,7 @@ public class ConvIndicatorTests
{
var indicator = new ConvIndicator();
indicator.Initialize();
var method = indicator.GetType().GetMethod("OnPaintChart");
Assert.NotNull(method);
Assert.Equal(typeof(ConvIndicator), method.DeclaringType);
@@ -170,10 +170,10 @@ public class ConvIndicatorTests
public void ConvIndicator_InvalidWeights_FallsBackToDefault()
{
var indicator = new ConvIndicator { WeightsInput = "invalid" };
// Should not throw, but fallback
indicator.Initialize();
Assert.Single(indicator.LinesSeries);
}
+1 -1
View File
@@ -45,7 +45,7 @@ public class ConvIndicator : Indicator, IWatchlistIndicator
var weights = WeightsInput.Split(',')
.Select(s => double.Parse(s.Trim()))
.ToArray();
if (weights.Length == 0)
throw new ArgumentException("Weights cannot be empty");
+6 -6
View File
@@ -94,7 +94,7 @@ public class ConvTests
source.Add(new TValue(DateTime.UtcNow, 3));
source.Add(new TValue(DateTime.UtcNow, 4));
var result = Conv.Calculate(source, kernel);
var result = Conv.Batch(source, kernel);
Assert.Equal(1.0, result.Values[0]);
Assert.Equal(2.5, result.Values[1]);
@@ -173,14 +173,14 @@ public class ConvTests
var series = bars.Close;
// 1. Batch Mode
var batchSeries = Conv.Calculate(series, kernel);
var batchSeries = Conv.Batch(series, kernel);
double expected = batchSeries.Last.Value;
// 2. Span Mode
var tValues = series.Values.ToArray();
var spanInput = new ReadOnlySpan<double>(tValues);
var spanOutput = new double[tValues.Length];
Conv.Calculate(spanInput, spanOutput, kernel);
Conv.Batch(spanInput, spanOutput, kernel);
double spanResult = spanOutput[^1];
// 3. Streaming Mode
@@ -214,8 +214,8 @@ public class ConvTests
double[] wrongSizeOutput = new double[3];
double[] kernel = [0.5, 0.5];
Assert.Throws<ArgumentException>(() => Conv.Calculate(source.AsSpan(), output.AsSpan(), Array.Empty<double>()));
Assert.Throws<ArgumentException>(() => Conv.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), kernel));
Assert.Throws<ArgumentException>(() => Conv.Batch(source.AsSpan(), output.AsSpan(), Array.Empty<double>()));
Assert.Throws<ArgumentException>(() => Conv.Batch(source.AsSpan(), wrongSizeOutput.AsSpan(), kernel));
}
[Fact]
@@ -225,7 +225,7 @@ public class ConvTests
double[] output = new double[5];
double[] kernel = [0.5, 0.5];
Conv.Calculate(source.AsSpan(), output.AsSpan(), kernel);
Conv.Batch(source.AsSpan(), output.AsSpan(), kernel);
foreach (var val in output)
{
+25 -19
View File
@@ -19,7 +19,7 @@ namespace QuanTAlib;
/// Update: O(K) where K is kernel length.
/// </remarks>
[SkipLocalsInit]
public sealed class Conv : ITValuePublisher
public sealed class Conv : AbstractBase
{
private readonly int _period;
private readonly double[] _kernel;
@@ -29,10 +29,7 @@ public sealed class Conv : ITValuePublisher
private State _state;
private State _p_state;
public string Name { get; }
public TValue Last { get; private set; }
public bool IsHot => _buffer.IsFull;
public event Action<TValue>? Pub;
public override bool IsHot => _buffer.IsFull;
public Conv(double[] kernel)
{
@@ -44,6 +41,7 @@ public sealed class Conv : ITValuePublisher
Array.Copy(kernel, _kernel, _period);
_buffer = new RingBuffer(_period);
Name = $"Conv({_period})";
WarmupPeriod = _period;
_state.LastValidValue = double.NaN;
_p_state.LastValidValue = double.NaN;
}
@@ -65,7 +63,7 @@ public sealed class Conv : ITValuePublisher
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
public override TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
@@ -92,29 +90,29 @@ public sealed class Conv : ITValuePublisher
{
int count = _buffer.Count;
int kernelOffset = _period - count;
ReadOnlySpan<double> kernelSpan = _kernel.AsSpan().Slice(kernelOffset);
ReadOnlySpan<double> kernelSpan = _kernel.AsSpan()[kernelOffset..];
ReadOnlySpan<double> internalBuf = _buffer.InternalBuffer;
if (count < _period)
{
result = internalBuf.Slice(0, count).DotProduct(kernelSpan);
result = internalBuf[..count].DotProduct(kernelSpan);
}
else
{
// Full: data is split at StartIndex (which points to oldest)
int head = _buffer.StartIndex;
int part1Len = _period - head;
result = internalBuf.Slice(head, part1Len).DotProduct(kernelSpan.Slice(0, part1Len))
+ internalBuf.Slice(0, head).DotProduct(kernelSpan.Slice(part1Len));
result = internalBuf.Slice(head, part1Len).DotProduct(kernelSpan[..part1Len])
+ internalBuf[..head].DotProduct(kernelSpan[part1Len..]);
}
}
Last = new TValue(input.Time, result);
Pub?.Invoke(Last);
PubEvent(Last);
return Last;
}
public TSeries Update(TSeries source)
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
@@ -130,7 +128,7 @@ public sealed class Conv : ITValuePublisher
source.Times.CopyTo(tSpan);
var sourceValues = source.Values;
Calculate(sourceValues, vSpan, _kernel);
Batch(sourceValues, vSpan, _kernel);
// Restore state
// We need to replay the last few updates to restore _buffer and _lastValidValue
@@ -172,14 +170,22 @@ public sealed class Conv : ITValuePublisher
return new TSeries(t, v);
}
public static TSeries Calculate(TSeries source, double[] kernel)
public override void Prime(ReadOnlySpan<double> source)
{
foreach (var value in source)
{
Update(new TValue(DateTime.MinValue, value));
}
}
public static TSeries Batch(TSeries source, double[] kernel)
{
var conv = new Conv(kernel);
return conv.Update(source);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, double[] kernel)
public static void Batch(ReadOnlySpan<double> source, Span<double> output, double[] kernel)
{
if (source.Length != output.Length)
throw new ArgumentException("Source and output must have the same length");
@@ -223,21 +229,21 @@ public sealed class Conv : ITValuePublisher
{
int kernelOffset = period - count;
// Window is [0..count-1]
sum = window.Slice(0, count).DotProduct(kernelSpan.Slice(kernelOffset));
sum = window[..count].DotProduct(kernelSpan[kernelOffset..]);
}
else
{
// Full buffer - branchless version
int part1Len = period - windowIdx;
sum = window.Slice(windowIdx, part1Len).DotProduct(kernelSpan.Slice(0, part1Len))
+ window.Slice(0, windowIdx).DotProduct(kernelSpan.Slice(part1Len));
sum = window.Slice(windowIdx, part1Len).DotProduct(kernelSpan[..part1Len])
+ window[..windowIdx].DotProduct(kernelSpan[part1Len..]);
}
output[i] = sum;
}
}
public void Reset()
public override void Reset()
{
_buffer.Clear();
_state.LastValidValue = double.NaN;
+1 -1
View File
@@ -55,7 +55,7 @@ double[] weights = { 0.1, 0.2, 0.3, 0.4 };
ReadOnlySpan<double> input = ...;
Span<double> output = new double[input.Length];
Conv.Calculate(input, output, weights);
Conv.Batch(input, output, weights);
```
### Bar Correction
+1 -1
View File
@@ -116,7 +116,7 @@ public class DemaIndicatorTests
{
var indicator = new DemaIndicator();
indicator.Initialize();
// We can't easily mock PaintChartEventArgs fully, but we can verify the method exists and is callable
// if we could mock the args. Since we can't, we skip the actual call but verify the method is overridden.
var method = indicator.GetType().GetMethod("OnPaintChart");
+3 -3
View File
@@ -46,7 +46,7 @@ public class DemaTests
}
// Act
var demaSeries = Dema.Calculate(source, period);
var demaSeries = Dema.Batch(source, period);
var demaObj = new Dema(period);
// Assert
@@ -123,7 +123,7 @@ public class DemaTests
}
// Act
var demaSeries = Dema.Calculate(source, alpha);
var demaSeries = Dema.Batch(source, alpha);
var demaObj = new Dema(alpha);
// Assert
@@ -271,7 +271,7 @@ public class DemaTests
var series = bars.Close;
// 1. Batch Mode
var batchSeries = Dema.Calculate(series, period);
var batchSeries = Dema.Batch(series, period);
double expected = batchSeries.Last.Value;
// 2. Span Mode
+20 -14
View File
@@ -22,7 +22,7 @@ namespace QuanTAlib;
/// Becomes true when the second EMA converges (approx. 2x EMA convergence time).
/// </remarks>
[SkipLocalsInit]
public sealed class Dema : ITValuePublisher
public sealed class Dema : AbstractBase
{
private record struct EmaState(double Ema, double E, bool IsHot, bool IsCompensated)
{
@@ -31,19 +31,16 @@ public sealed class Dema : ITValuePublisher
private readonly double _alpha;
private readonly double _decay;
private EmaState _state1 = EmaState.New();
private EmaState _state2 = EmaState.New();
private EmaState _p_state1 = EmaState.New();
private EmaState _p_state2 = EmaState.New();
private double _lastValidValue;
private double _p_lastValidValue;
public string Name { get; }
public TValue Last { get; private set; }
public bool IsHot => _state2.IsHot;
public event Action<TValue>? Pub;
public override bool IsHot => _state2.IsHot;
public Dema(int period)
{
@@ -52,6 +49,7 @@ public sealed class Dema : ITValuePublisher
_alpha = 2.0 / (period + 1);
_decay = 1.0 - _alpha;
Name = $"Dema({period})";
WarmupPeriod = period;
}
public Dema(ITValuePublisher source, int period) : this(period)
@@ -69,7 +67,7 @@ public sealed class Dema : ITValuePublisher
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
public override TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
@@ -98,11 +96,11 @@ public sealed class Dema : ITValuePublisher
double result = 2 * e1 - e2;
Last = new TValue(input.Time, result);
Pub?.Invoke(Last);
PubEvent(Last);
return Last;
}
public TSeries Update(TSeries source)
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
@@ -117,7 +115,7 @@ public sealed class Dema : ITValuePublisher
source.Times.CopyTo(tSpan);
var sourceValues = source.Values;
// Use current state
EmaState s1 = _state1;
EmaState s2 = _state2;
@@ -151,6 +149,14 @@ public sealed class Dema : ITValuePublisher
return new TSeries(t, v);
}
public override void Prime(ReadOnlySpan<double> source)
{
foreach (var value in source)
{
Update(new TValue(DateTime.MinValue, value));
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double Compute(double input, double alpha, double decay, ref EmaState state)
{
@@ -182,13 +188,13 @@ public sealed class Dema : ITValuePublisher
return result;
}
public static TSeries Calculate(TSeries source, int period)
public static TSeries Batch(TSeries source, int period)
{
var dema = new Dema(period);
return dema.Update(source);
}
public static TSeries Calculate(TSeries source, double alpha)
public static TSeries Batch(TSeries source, double alpha)
{
var dema = new Dema(alpha);
return dema.Update(source);
@@ -280,7 +286,7 @@ public sealed class Dema : ITValuePublisher
}
}
public void Reset()
public override void Reset()
{
_state1 = EmaState.New();
_state2 = EmaState.New();
+3 -3
View File
@@ -62,12 +62,12 @@ Console.WriteLine($"Current DEMA: {result.Value}");
// Batch calculation (TSeries API)
TSeries source = ...;
TSeries results = Dema.Calculate(source, 14);
TSeries results = Dema.Batch(source, 14);
// High-performance Span API (zero allocation)
double[] prices = new double[10000];
double[] output = new double[10000];
Dema.Calculate(prices.AsSpan(), output.AsSpan(), period: 14);
Dema.Batch(prices.AsSpan(), output.AsSpan(), period: 14);
```
### Zero-Allocation Span API
@@ -80,7 +80,7 @@ double[] source = new double[200000];
double[] demaOutput = new double[200000];
// Zero heap allocation during calculation
Dema.Calculate(source.AsSpan(), demaOutput.AsSpan(), period: 50);
Dema.Batch(source.AsSpan(), demaOutput.AsSpan(), period: 50);
```
### Eventing and Reactive Support
+2 -2
View File
@@ -94,7 +94,7 @@ public class DwmaTests
dwma.Update(source.Last);
}
var staticResult = Dwma.Calculate(source, period);
var staticResult = Dwma.Batch(source, period);
Assert.Equal(source.Count, staticResult.Count);
Assert.Equal(dwma.Last.Value, staticResult.Last.Value, 8);
@@ -180,7 +180,7 @@ public class DwmaTests
var series = bars.Close;
// 1. Batch Mode
var batchSeries = Dwma.Calculate(series, period);
var batchSeries = Dwma.Batch(series, period);
double expected = batchSeries.Last.Value;
// 2. Span Mode
+25 -30
View File
@@ -15,28 +15,13 @@ namespace QuanTAlib;
/// DWMA = WMA(WMA(source, period), period)
/// </remarks>
[SkipLocalsInit]
public sealed class Dwma : ITValuePublisher
public sealed class Dwma : AbstractBase
{
private readonly int _period;
private readonly Wma _wma1;
private readonly Wma _wma2;
/// <summary>
/// Display name for the indicator.
/// </summary>
public string Name { get; }
/// <summary>
/// Current DWMA value.
/// </summary>
public TValue Last { get; private set; }
/// <summary>
/// True if the indicator has enough data to produce valid results.
/// </summary>
public bool IsHot => _wma1.IsHot && _wma2.IsHot;
public event Action<TValue>? Pub;
public override bool IsHot => _wma1.IsHot && _wma2.IsHot;
/// <summary>
/// Creates DWMA with specified period.
@@ -51,6 +36,7 @@ public sealed class Dwma : ITValuePublisher
_wma1 = new Wma(period);
_wma2 = new Wma(period);
Name = $"Dwma({period})";
WarmupPeriod = period * 2;
}
public Dwma(ITValuePublisher source, int period) : this(period)
@@ -59,15 +45,15 @@ public sealed class Dwma : ITValuePublisher
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
public override TValue Update(TValue input, bool isNew = true)
{
TValue wma1Result = _wma1.Update(input, isNew);
Last = _wma2.Update(wma1Result, isNew);
Pub?.Invoke(Last);
PubEvent(Last);
return Last;
}
public TSeries Update(TSeries source)
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
@@ -87,13 +73,13 @@ public sealed class Dwma : ITValuePublisher
// We need to replay the last part to restore the internal WMAs state
// Since DWMA is WMA(WMA), the effective lookback is roughly 2*Period
// But to be safe and simple, we can just reset and replay the last 2*Period bars.
_wma1.Reset();
_wma2.Reset();
int warmup = _period * 2; // Approximate warmup needed
int startIndex = Math.Max(0, len - warmup);
for (int i = startIndex; i < len; i++)
{
Update(new TValue(source.Times[i], source.Values[i]));
@@ -102,7 +88,16 @@ public sealed class Dwma : ITValuePublisher
return new TSeries(t, v);
}
public static TSeries Calculate(TSeries source, int period)
public override void Prime(ReadOnlySpan<double> source)
{
Reset();
foreach (var value in source)
{
Update(new TValue(DateTime.MinValue, value));
}
}
public static TSeries Batch(TSeries source, int period)
{
var dwma = new Dwma(period);
return dwma.Update(source);
@@ -119,18 +114,18 @@ public sealed class Dwma : ITValuePublisher
if (source.Length <= 1024)
{
Span<double> temp = stackalloc double[source.Length];
Wma.Calculate(source, temp, period);
Wma.Calculate(temp, output, period);
Wma.Batch(source, temp, period);
Wma.Batch(temp, output, period);
}
else
{
double[] temp = new double[source.Length];
Wma.Calculate(source, temp, period);
Wma.Calculate(temp, output, period);
Wma.Batch(source, temp, period);
Wma.Batch(temp, output, period);
}
}
public void Reset()
public override void Reset()
{
_wma1.Reset();
_wma2.Reset();
+1 -1
View File
@@ -91,7 +91,7 @@ Console.WriteLine($"DWMA: {result.Value}");
ReadOnlySpan<double> input = ...;
Span<double> output = new double[input.Length];
Dwma.Calculate(input, output, 14);
Dwma.Batch(input, output, 14);
```
### Bar Correction
+65 -65
View File
@@ -1,65 +1,65 @@
using System.Drawing;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
public class EmaIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Period", sortIndex: 1, 1, 1000, 1, 0)]
public int Period { get; set; } = 10;
[IndicatorExtensions.DataSourceInput]
public SourceType Source { get; set; } = SourceType.Close;
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private Ema? ma;
protected LineSeries? Series;
protected string? SourceName;
private int _warmupBarIndex = -1;
public int MinHistoryDepths => Period;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => $"EMA {Period}:{SourceName}";
public EmaIndicator()
{
OnBackGround = true;
SeparateWindow = false;
SourceName = Source.ToString();
Name = "EMA - Exponential Moving Average";
Description = "Exponential Moving Average";
Series = new(name: $"EMA {Period}", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
AddLineSeries(Series);
}
protected override void OnInit()
{
ma = new Ema(Period);
SourceName = Source.ToString();
_warmupBarIndex = -1; // Reset warmup tracking when period changes
base.OnInit();
}
protected override void OnUpdate(UpdateArgs args)
{
TValue input = this.GetInputValue(args, Source);
bool isNew = args.Reason == UpdateReason.NewBar || args.Reason == UpdateReason.HistoricalBar;
TValue result = ma!.Update(input, isNew);
Series!.SetValue(result.Value);
Series!.SetMarker(0, Color.Transparent); //OnPaintChart draws the line, hidden here
// Track when IsHot becomes true for the first time
if (_warmupBarIndex < 0 && ma!.IsHot)
_warmupBarIndex = Count;
}
public override void OnPaintChart(PaintChartEventArgs args)
{
base.OnPaintChart(args);
int warmupPeriod = _warmupBarIndex > 0 ? _warmupBarIndex : Count;
this.PaintSmoothCurve(args, Series!, warmupPeriod, showColdValues: ShowColdValues, tension: 0.2);
}
}
using System.Drawing;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
public class EmaIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Period", sortIndex: 1, 1, 1000, 1, 0)]
public int Period { get; set; } = 10;
[IndicatorExtensions.DataSourceInput]
public SourceType Source { get; set; } = SourceType.Close;
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private Ema? ma;
protected LineSeries? Series;
protected string? SourceName;
private int _warmupBarIndex = -1;
public int MinHistoryDepths => Period;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => $"EMA {Period}:{SourceName}";
public EmaIndicator()
{
OnBackGround = true;
SeparateWindow = false;
SourceName = Source.ToString();
Name = "EMA - Exponential Moving Average";
Description = "Exponential Moving Average";
Series = new(name: $"EMA {Period}", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
AddLineSeries(Series);
}
protected override void OnInit()
{
ma = new Ema(Period);
SourceName = Source.ToString();
_warmupBarIndex = -1; // Reset warmup tracking when period changes
base.OnInit();
}
protected override void OnUpdate(UpdateArgs args)
{
TValue input = this.GetInputValue(args, Source);
bool isNew = args.Reason == UpdateReason.NewBar || args.Reason == UpdateReason.HistoricalBar;
TValue result = ma!.Update(input, isNew);
Series!.SetValue(result.Value);
Series!.SetMarker(0, Color.Transparent); //OnPaintChart draws the line, hidden here
// Track when IsHot becomes true for the first time
if (_warmupBarIndex < 0 && ma!.IsHot)
_warmupBarIndex = Count;
}
public override void OnPaintChart(PaintChartEventArgs args)
{
base.OnPaintChart(args);
int warmupPeriod = _warmupBarIndex > 0 ? _warmupBarIndex : Count;
this.PaintSmoothCurve(args, Series!, warmupPeriod, showColdValues: ShowColdValues, tension: 0.2);
}
}
+103 -24
View File
@@ -363,34 +363,34 @@ public class EmaTests
// ============== Span API Tests ==============
[Fact]
public void Ema_SpanCalc_Period_ValidatesInput()
public void Ema_SpanBatch_Period_ValidatesInput()
{
double[] source = [1, 2, 3, 4, 5];
double[] output = new double[5];
double[] wrongSizeOutput = new double[3];
// Period must be > 0
Assert.Throws<ArgumentException>(() => Ema.Calculate(source.AsSpan(), output.AsSpan(), 0));
Assert.Throws<ArgumentException>(() => Ema.Calculate(source.AsSpan(), output.AsSpan(), -1));
Assert.Throws<ArgumentException>(() => Ema.Batch(source.AsSpan(), output.AsSpan(), 0));
Assert.Throws<ArgumentException>(() => Ema.Batch(source.AsSpan(), output.AsSpan(), -1));
// Output must be same length as source
Assert.Throws<ArgumentException>(() => Ema.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 3));
Assert.Throws<ArgumentException>(() => Ema.Batch(source.AsSpan(), wrongSizeOutput.AsSpan(), 3));
}
[Fact]
public void Ema_SpanCalc_Alpha_ValidatesInput()
public void Ema_SpanBatch_Alpha_ValidatesInput()
{
double[] source = [1, 2, 3, 4, 5];
double[] output = new double[5];
// Alpha must be > 0 and <= 1
Assert.Throws<ArgumentException>(() => Ema.Calculate(source.AsSpan(), output.AsSpan(), 0.0));
Assert.Throws<ArgumentException>(() => Ema.Calculate(source.AsSpan(), output.AsSpan(), -0.1));
Assert.Throws<ArgumentException>(() => Ema.Calculate(source.AsSpan(), output.AsSpan(), 1.1));
Assert.Throws<ArgumentException>(() => Ema.Batch(source.AsSpan(), output.AsSpan(), 0.0));
Assert.Throws<ArgumentException>(() => Ema.Batch(source.AsSpan(), output.AsSpan(), -0.1));
Assert.Throws<ArgumentException>(() => Ema.Batch(source.AsSpan(), output.AsSpan(), 1.1));
}
[Fact]
public void Ema_SpanCalc_MatchesTSeriesCalc()
public void Ema_SpanBatch_MatchesTSeriesBatch()
{
var series = new TSeries();
double[] source = new double[100];
@@ -405,10 +405,10 @@ public class EmaTests
}
// Calculate with TSeries API
var tseriesResult = Ema.Calculate(series, 10);
var tseriesResult = Ema.Batch(series, 10);
// Calculate with Span API
Ema.Calculate(source.AsSpan(), output.AsSpan(), 10);
Ema.Batch(source.AsSpan(), output.AsSpan(), 10);
// Compare results - allow small tolerance due to bias correction differences
for (int i = 0; i < 100; i++)
@@ -418,7 +418,7 @@ public class EmaTests
}
[Fact]
public void Ema_SpanCalc_PeriodAndAlphaEquivalent()
public void Ema_SpanBatch_PeriodAndAlphaEquivalent()
{
double[] source = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
double[] outputPeriod = new double[10];
@@ -427,8 +427,8 @@ public class EmaTests
int period = 5;
double alpha = 2.0 / (period + 1);
Ema.Calculate(source.AsSpan(), outputPeriod.AsSpan(), period);
Ema.Calculate(source.AsSpan(), outputAlpha.AsSpan(), alpha);
Ema.Batch(source.AsSpan(), outputPeriod.AsSpan(), period);
Ema.Batch(source.AsSpan(), outputAlpha.AsSpan(), alpha);
// Results should be identical
for (int i = 0; i < 10; i++)
@@ -438,7 +438,7 @@ public class EmaTests
}
[Fact]
public void Ema_SpanCalc_ZeroAllocation()
public void Ema_SpanBatch_ZeroAllocation()
{
double[] source = new double[10000];
double[] output = new double[10000];
@@ -448,19 +448,19 @@ public class EmaTests
source[i] = gbm.Next().Close;
// Warm up
Ema.Calculate(source.AsSpan(), output.AsSpan(), 100);
Ema.Batch(source.AsSpan(), output.AsSpan(), 100);
// This test verifies the method runs without throwing
Assert.True(double.IsFinite(output[^1]));
}
[Fact]
public void Ema_SpanCalc_HandlesNaN()
public void Ema_SpanBatch_HandlesNaN()
{
double[] source = [100, 110, double.NaN, 120, 130];
double[] output = new double[5];
Ema.Calculate(source.AsSpan(), output.AsSpan(), 3);
Ema.Batch(source.AsSpan(), output.AsSpan(), 3);
// All outputs should be finite
foreach (var val in output)
@@ -470,12 +470,12 @@ public class EmaTests
}
[Fact]
public void Ema_SpanCalc_BiasCorrection_Works()
public void Ema_SpanBatch_BiasCorrection_Works()
{
double[] source = [100, 100, 100, 100, 100];
double[] output = new double[5];
Ema.Calculate(source.AsSpan(), output.AsSpan(), 3);
Ema.Batch(source.AsSpan(), output.AsSpan(), 3);
// With bias correction, first value should equal input
Assert.Equal(100.0, output[0], 1e-10);
@@ -488,18 +488,97 @@ public class EmaTests
}
[Fact]
public void Ema_SpanCalc_Alpha_DirectUsage()
public void Ema_SpanBatch_Alpha_DirectUsage()
{
double[] source = [10, 20, 30, 40, 50];
double[] output = new double[5];
// Use alpha = 0.5 directly
Ema.Calculate(source.AsSpan(), output.AsSpan(), 0.5);
Ema.Batch(source.AsSpan(), output.AsSpan(), 0.5);
// Results should be finite and reasonable
Assert.True(double.IsFinite(output[^1]));
Assert.True(output[^1] > 10 && output[^1] <= 50);
}
[Fact]
public void Chainability_Works()
{
var source = new TSeries();
var ema = new Ema(source, 10);
source.Add(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100, ema.Last.Value, 1e-10);
}
[Fact]
public void Prime_SetsStateCorrectly()
{
var ema = new Ema(5);
double[] history = [10, 20, 30, 40, 50];
ema.Prime(history);
// EMA(5) of 10,20,30,40,50
// Alpha = 2/6 = 1/3
// 10 -> 10
// 20 -> 10 + 1/3(10) = 13.33...
// ...
// We can verify against a fresh EMA fed with same data
var verifyEma = new Ema(5);
foreach (var val in history) verifyEma.Update(new TValue(DateTime.UtcNow, val));
Assert.Equal(verifyEma.Last.Value, ema.Last.Value, 1e-10);
Assert.Equal(verifyEma.IsHot, ema.IsHot);
// Verify it continues correctly
ema.Update(new TValue(DateTime.UtcNow, 60));
verifyEma.Update(new TValue(DateTime.UtcNow, 60));
Assert.Equal(verifyEma.Last.Value, ema.Last.Value, 1e-10);
}
[Fact]
public void Prime_HandlesNaN_InHistory()
{
var ema = new Ema(5);
double[] history = [10, 20, double.NaN, 40, 50];
ema.Prime(history);
var verifyEma = new Ema(5);
foreach (var val in history) verifyEma.Update(new TValue(DateTime.UtcNow, val));
Assert.Equal(verifyEma.Last.Value, ema.Last.Value, 1e-10);
}
[Fact]
public void Calculate_ReturnsCorrectResultsAndHotIndicator()
{
var series = new TSeries();
for (int i = 1; i <= 20; i++) series.Add(DateTime.UtcNow, i * 10);
// EMA(5)
var (results, indicator) = Ema.Calculate(series, 5);
// Check results
Assert.Equal(20, results.Count);
// Verify against standard calculation
var verifyEma = new Ema(5);
var verifyResults = verifyEma.Update(series);
Assert.Equal(verifyResults.Last.Value, results.Last.Value, 1e-10);
Assert.Equal(verifyEma.Last.Value, indicator.Last.Value, 1e-10);
// Check indicator state
Assert.True(indicator.IsHot);
// Verify indicator continues correctly
indicator.Update(new TValue(DateTime.UtcNow, 210));
verifyEma.Update(new TValue(DateTime.UtcNow, 210));
Assert.Equal(verifyEma.Last.Value, indicator.Last.Value, 1e-10);
}
[Fact]
public void Ema_AllModes_ProduceSameResult()
{
@@ -510,14 +589,14 @@ public class EmaTests
var series = bars.Close;
// 1. Batch Mode
var batchSeries = Ema.Calculate(series, period);
var batchSeries = Ema.Batch(series, period);
double expected = batchSeries.Last.Value;
// 2. Span Mode
var tValues = series.Values.ToArray(); // Need array for Span modification safety if any
var spanInput = new ReadOnlySpan<double>(tValues);
var spanOutput = new double[tValues.Length];
Ema.Calculate(spanInput, spanOutput, period);
Ema.Batch(spanInput, spanOutput, period);
double spanResult = spanOutput[^1];
// 3. Streaming Mode
+3 -3
View File
@@ -91,7 +91,7 @@ public class EmaValidationTests : IDisposable
{
// Calculate QuanTAlib EMA (Span API)
double[] qOutput = new double[sourceData.Length];
global::QuanTAlib.Ema.Calculate(sourceData.AsSpan(), qOutput.AsSpan(), period);
global::QuanTAlib.Ema.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period);
// Calculate Skender EMA
var sResult = _testData.SkenderQuotes.GetEma(period).ToList();
@@ -173,7 +173,7 @@ public class EmaValidationTests : IDisposable
{
// Calculate QuanTAlib EMA (Span API)
double[] qOutput = new double[sourceData.Length];
global::QuanTAlib.Ema.Calculate(sourceData.AsSpan(), qOutput.AsSpan(), period);
global::QuanTAlib.Ema.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period);
// Calculate TA-Lib EMA
var retCode = TALib.Functions.Ema<double>(sourceData, 0..^0, talibOutput, out var outRange, period);
@@ -261,7 +261,7 @@ public class EmaValidationTests : IDisposable
{
// Calculate QuanTAlib EMA (Span API)
double[] qOutput = new double[sourceData.Length];
global::QuanTAlib.Ema.Calculate(sourceData.AsSpan(), qOutput.AsSpan(), period);
global::QuanTAlib.Ema.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period);
// Calculate Tulip EMA
var emaIndicator = Tulip.Indicators.ema;
+394 -296
View File
@@ -1,296 +1,394 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// EMA: Exponential Moving Average
/// </summary>
/// <remarks>
/// EMA applies exponential weighting to data points, giving more weight to recent values.
/// Uses a single state variable for O(1) complexity per update.
///
/// Calculation:
/// alpha = 2 / (period + 1)
/// EMA_new = EMA_old + alpha * (newest - EMA_old)
///
/// Initialization:
/// Uses a compensator factor to correct early-stage bias (when n < period).
/// Output = EMA_state / (1 - (1-alpha)^n)
///
/// O(1) update:
/// No buffer required, only previous EMA value and compensator state.
///
/// IsHot:
/// Becomes true when n = ln(0.05) / ln(1 - alpha)
/// </remarks>
[SkipLocalsInit]
public sealed class Ema : ITValuePublisher
{
private record struct State(double Ema, double E, bool IsHot, bool IsCompensated)
{
public static State New() => new() { Ema = 0, E = 1.0, IsHot = false, IsCompensated = false };
}
private readonly double _alpha;
private readonly double _decay;
private State _state = State.New();
private State _p_state = State.New();
private double _lastValidValue;
private double _p_lastValidValue;
/// <summary>
/// Display name for the indicator.
/// </summary>
public string Name { get; }
public event Action<TValue>? Pub;
/// <summary>
/// Creates EMA with specified period.
/// Alpha = 2 / (period + 1)
/// </summary>
/// <param name="period">Period for EMA calculation (must be > 0)</param>
public Ema(int period)
{
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
_alpha = 2.0 / (period + 1);
_decay = 1.0 - _alpha;
Name = $"Ema({period})";
}
/// <summary>
/// Creates EMA with specified source and period.
/// Subscribes to source.Pub event.
/// </summary>
/// <param name="source">Source to subscribe to</param>
/// <param name="period">Period for EMA calculation</param>
public Ema(ITValuePublisher source, int period) : this(period)
{
source.Pub += (item) => Update(item);
}
/// <summary>
/// Creates EMA with specified alpha smoothing factor.
/// </summary>
/// <param name="alpha">Smoothing factor (0 &lt; alpha &lt;= 1)</param>
public Ema(double alpha)
{
if (alpha <= 0 || alpha > 1)
throw new ArgumentException("Alpha must be between 0 and 1", nameof(alpha));
_alpha = alpha;
_decay = 1.0 - alpha;
Name = $"Ema(α={alpha:F4})";
}
/// <summary>
/// Current EMA value.
/// </summary>
public TValue Last { get; private set; }
/// <summary>
/// True if the EMA has warmed up and is providing valid results.
/// </summary>
public bool IsHot => _state.IsHot;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetValidValue(double input)
{
if (double.IsFinite(input))
{
_lastValidValue = input;
return input;
}
return _lastValidValue;
}
private const double COVERAGE_THRESHOLD = 0.05;
private const double COMPENSATOR_THRESHOLD = 1e-10;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
_p_state = _state;
_p_lastValidValue = _lastValidValue;
}
else
{
_state = _p_state;
_lastValidValue = _p_lastValidValue;
}
double val = GetValidValue(input.Value);
val = Compute(val, _alpha, _decay, ref _state);
Last = new TValue(input.Time, val);
Pub?.Invoke(Last);
return Last;
}
public 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);
var sourceValues = source.Values;
var sourceTimes = source.Times;
State state = _state;
double lastValidValue = _lastValidValue;
CalculateCore(sourceValues, vSpan, _alpha, ref state, ref lastValidValue);
_state = state;
_lastValidValue = lastValidValue;
sourceTimes.CopyTo(tSpan);
_p_state = _state;
_p_lastValidValue = _lastValidValue;
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double Compute(double input, double alpha, double decay, ref State state)
{
state.Ema += alpha * (input - state.Ema);
double result;
if (!state.IsCompensated)
{
state.E *= decay;
if (!state.IsHot && state.E <= COVERAGE_THRESHOLD)
state.IsHot = true;
if (state.E <= COMPENSATOR_THRESHOLD)
{
state.IsCompensated = true;
result = state.Ema;
}
else
{
result = state.Ema / (1.0 - state.E);
}
}
else
{
result = state.Ema;
}
return result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void CalculateCore(ReadOnlySpan<double> source, Span<double> output, double alpha, ref State state, ref double lastValidValue)
{
int len = source.Length;
double decay = 1.0 - alpha;
int i = 0;
if (!state.IsCompensated)
{
for (; i < len && state.E > COMPENSATOR_THRESHOLD; i++)
{
double val = source[i];
if (double.IsFinite(val))
lastValidValue = val;
else
val = lastValidValue;
state.Ema += alpha * (val - state.Ema);
state.E *= decay;
if (!state.IsHot && state.E <= COVERAGE_THRESHOLD)
state.IsHot = true;
output[i] = state.Ema / (1.0 - state.E);
}
if (state.E <= COMPENSATOR_THRESHOLD)
state.IsCompensated = true;
}
for (; i < len; i++)
{
double val = source[i];
if (double.IsFinite(val))
lastValidValue = val;
else
val = lastValidValue;
state.Ema += alpha * (val - state.Ema);
output[i] = state.Ema;
}
}
/// <summary>
/// Calculates EMA for the entire series using a new instance.
/// </summary>
/// <param name="source">Input series</param>
/// <param name="period">EMA period</param>
/// <returns>EMA series</returns>
public static TSeries Calculate(TSeries source, int period)
{
var ema = new Ema(period);
return ema.Update(source);
}
/// <summary>
/// Calculates EMA in-place using period, writing results to pre-allocated output span.
/// Zero-allocation method for maximum performance.
/// Alpha = 2 / (period + 1)
/// </summary>
/// <param name="source">Input values</param>
/// <param name="output">Output span (must be same length as source)</param>
/// <param name="period">EMA period (must be > 0)</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period)
{
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
double alpha = 2.0 / (period + 1);
Calculate(source, output, alpha);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, double alpha)
{
if (source.Length != output.Length)
throw new ArgumentException("Source and output must have the same length");
if (alpha <= 0 || alpha > 1)
throw new ArgumentException("Alpha must be between 0 and 1", nameof(alpha));
if (source.Length == 0) return;
var state = State.New();
double lastValid = 0;
CalculateCore(source, output, alpha, ref state, ref lastValid);
}
/// <summary>
/// Resets the EMA state.
/// </summary>
public void Reset()
{
_state = State.New();
_p_state = _state;
_lastValidValue = 0;
_p_lastValidValue = 0;
Last = default;
}
}
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// EMA: Exponential Moving Average
/// </summary>
/// <remarks>
/// EMA applies exponential weighting to data points, giving more weight to recent values.
/// Uses a single state variable for O(1) complexity per update.
///
/// Calculation:
/// alpha = 2 / (period + 1)
/// EMA_new = EMA_old + alpha * (newest - EMA_old)
///
/// Initialization:
/// Uses a compensator factor to correct early-stage bias (when n < period).
/// Output = EMA_state / (1 - (1-alpha)^n)
///
/// O(1) update:
/// No buffer required, only previous EMA value and compensator state.
///
/// IsHot:
/// Becomes true when n = ln(0.05) / ln(1 - alpha)
/// </remarks>
[SkipLocalsInit]
public sealed class Ema : AbstractBase
{
private record struct State(double Ema, double E, bool IsHot, bool IsCompensated)
{
public static State New() => new() { Ema = 0, E = 1.0, IsHot = false, IsCompensated = false };
}
private readonly double _alpha;
private readonly double _decay;
private State _state = State.New();
private State _p_state = State.New();
private double _lastValidValue;
private double _p_lastValidValue;
/// <summary>
/// Creates EMA with specified period.
/// Alpha = 2 / (period + 1)
/// </summary>
/// <param name="period">Period for EMA calculation (must be > 0)</param>
public Ema(int period)
{
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
_alpha = 2.0 / (period + 1);
_decay = 1.0 - _alpha;
Name = $"Ema({period})";
WarmupPeriod = period;
}
/// <summary>
/// Creates EMA with specified source and period.
/// Subscribes to source.Pub event.
/// </summary>
/// <param name="source">Source to subscribe to</param>
/// <param name="period">Period for EMA calculation</param>
public Ema(ITValuePublisher source, int period) : this(period)
{
source.Pub += (item) => Update(item);
}
public Ema(TSeries source, int period) : this(period)
{
Prime(source.Values);
if (source.Count > 0)
{
Last = new TValue(source.LastTime, Last.Value);
}
source.Pub += (item) => Update(item);
}
/// <summary>
/// Creates EMA with specified alpha smoothing factor.
/// </summary>
/// <param name="alpha">Smoothing factor (0 < alpha <= 1)</param>
public Ema(double alpha)
{
if (alpha <= 0 || alpha > 1)
throw new ArgumentException("Alpha must be between 0 and 1", nameof(alpha));
_alpha = alpha;
_decay = 1.0 - alpha;
Name = $"Ema(α={alpha:F4})";
// Approximate period from alpha: alpha = 2/(N+1) => N = 2/alpha - 1
WarmupPeriod = (int)(2.0 / alpha - 1.0);
}
/// <summary>
/// True if the EMA has warmed up and is providing valid results.
/// </summary>
public override bool IsHot => _state.IsHot;
/// <summary>
/// Initializes the indicator state using the provided history.
/// </summary>
/// <param name="source">Historical data</param>
public override void Prime(ReadOnlySpan<double> source)
{
if (source.Length == 0) return;
// Reset state
_state = State.New();
_p_state = State.New();
_lastValidValue = 0;
_p_lastValidValue = 0;
// Run the calculation on the history to update state
// We don't need the output, just the final state
int len = source.Length;
double decay = _decay;
int i = 0;
// Find first valid value to seed lastValid
for (int k = 0; k < len; k++)
{
if (double.IsFinite(source[k]))
{
_lastValidValue = source[k];
break;
}
}
if (!_state.IsCompensated)
{
for (; i < len && _state.E > COMPENSATOR_THRESHOLD; i++)
{
double val = source[i];
if (double.IsFinite(val))
_lastValidValue = val;
else
val = _lastValidValue;
_state.Ema += _alpha * (val - _state.Ema);
_state.E *= decay;
if (!_state.IsHot && _state.E <= COVERAGE_THRESHOLD)
_state.IsHot = true;
}
if (_state.E <= COMPENSATOR_THRESHOLD)
_state.IsCompensated = true;
}
for (; i < len; i++)
{
double val = source[i];
if (double.IsFinite(val))
_lastValidValue = val;
else
val = _lastValidValue;
_state.Ema += _alpha * (val - _state.Ema);
}
// Calculate the initial "Last" value
double result = _state.IsCompensated ? _state.Ema : _state.Ema / (1.0 - _state.E);
// 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;
_p_lastValidValue = _lastValidValue;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetValidValue(double input)
{
if (double.IsFinite(input))
{
_lastValidValue = input;
return input;
}
return _lastValidValue;
}
private const double COVERAGE_THRESHOLD = 0.05;
private const double COMPENSATOR_THRESHOLD = 1e-10;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
_p_state = _state;
_p_lastValidValue = _lastValidValue;
}
else
{
_state = _p_state;
_lastValidValue = _p_lastValidValue;
}
double val = GetValidValue(input.Value);
val = Compute(val, _alpha, _decay, ref _state);
Last = new TValue(input.Time, val);
PubEvent(Last);
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);
var sourceValues = source.Values;
var sourceTimes = source.Times;
State state = _state;
double lastValidValue = _lastValidValue;
CalculateCore(sourceValues, vSpan, _alpha, ref state, ref lastValidValue);
_state = state;
_lastValidValue = lastValidValue;
sourceTimes.CopyTo(tSpan);
_p_state = _state;
_p_lastValidValue = _lastValidValue;
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double Compute(double input, double alpha, double decay, ref State state)
{
state.Ema += alpha * (input - state.Ema);
double result;
if (!state.IsCompensated)
{
state.E *= decay;
if (!state.IsHot && state.E <= COVERAGE_THRESHOLD)
state.IsHot = true;
if (state.E <= COMPENSATOR_THRESHOLD)
{
state.IsCompensated = true;
result = state.Ema;
}
else
{
result = state.Ema / (1.0 - state.E);
}
}
else
{
result = state.Ema;
}
return result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void CalculateCore(ReadOnlySpan<double> source, Span<double> output, double alpha, ref State state, ref double lastValidValue)
{
int len = source.Length;
double decay = 1.0 - alpha;
int i = 0;
if (!state.IsCompensated)
{
for (; i < len && state.E > COMPENSATOR_THRESHOLD; i++)
{
double val = source[i];
if (double.IsFinite(val))
lastValidValue = val;
else
val = lastValidValue;
state.Ema += alpha * (val - state.Ema);
state.E *= decay;
if (!state.IsHot && state.E <= COVERAGE_THRESHOLD)
state.IsHot = true;
output[i] = state.Ema / (1.0 - state.E);
}
if (state.E <= COMPENSATOR_THRESHOLD)
state.IsCompensated = true;
}
for (; i < len; i++)
{
double val = source[i];
if (double.IsFinite(val))
lastValidValue = val;
else
val = lastValidValue;
state.Ema += alpha * (val - state.Ema);
output[i] = state.Ema;
}
}
/// <summary>
/// Runs a high-performance batch calculation on history and returns
/// a "Hot" Ema instance ready to process the next tick immediately.
/// </summary>
/// <param name="source">Historical time series</param>
/// <param name="period">EMA Period</param>
/// <returns>A tuple containing the full calculation results and the hot indicator instance</returns>
public static (TSeries Results, Ema Indicator) Calculate(TSeries source, int period)
{
var ema = new Ema(period);
TSeries results = ema.Update(source);
return (results, ema);
}
/// <summary>
/// Calculates EMA for the entire series using a new instance.
/// </summary>
/// <param name="source">Input series</param>
/// <param name="period">EMA period</param>
/// <returns>EMA series</returns>
public static TSeries Batch(TSeries source, int period)
{
var ema = new Ema(period);
return ema.Update(source);
}
/// <summary>
/// Calculates EMA in-place using period, writing results to pre-allocated output span.
/// Zero-allocation method for maximum performance.
/// Alpha = 2 / (period + 1)
/// </summary>
/// <param name="source">Input values</param>
/// <param name="output">Output span (must be same length as source)</param>
/// <param name="period">EMA period (must be > 0)</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period)
{
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
double alpha = 2.0 / (period + 1);
Batch(source, output, alpha);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Batch(ReadOnlySpan<double> source, Span<double> output, double alpha)
{
if (source.Length != output.Length)
throw new ArgumentException("Source and output must have the same length");
if (alpha <= 0 || alpha > 1)
throw new ArgumentException("Alpha must be between 0 and 1", nameof(alpha));
if (source.Length == 0) return;
var state = State.New();
double lastValid = 0;
// Find first valid value to seed lastValid
for (int k = 0; k < source.Length; k++)
{
if (double.IsFinite(source[k]))
{
lastValid = source[k];
break;
}
}
CalculateCore(source, output, alpha, ref state, ref lastValid);
}
/// <summary>
/// Resets the EMA state.
/// </summary>
public override void Reset()
{
_state = State.New();
_p_state = _state;
_lastValidValue = 0;
_p_lastValidValue = 0;
Last = default;
}
}
+5 -5
View File
@@ -79,14 +79,14 @@ Console.WriteLine($"Current Value: {ema.Value.Value}");
// Batch calculation (TSeries API)
TSeries source = ...;
TSeries results = Ema.Calculate(source, 10);
TSeries results = Ema.Batch(source, 10);
// High-performance Span API (zero allocation)
double[] prices = new double[10000];
double[] output = new double[10000];
Ema.Calculate(prices.AsSpan(), output.AsSpan(), period: 10);
Ema.Batch(prices.AsSpan(), output.AsSpan(), period: 10);
// Or with direct alpha:
Ema.Calculate(prices.AsSpan(), output.AsSpan(), alpha: 0.1818);
Ema.Batch(prices.AsSpan(), output.AsSpan(), alpha: 0.1818);
```
### Zero-Allocation Span API
@@ -99,10 +99,10 @@ double[] source = new double[200000];
double[] emaOutput = new double[200000];
// Zero heap allocation during calculation - by period
Ema.Calculate(source.AsSpan(), emaOutput.AsSpan(), period: 100);
Ema.Batch(source.AsSpan(), emaOutput.AsSpan(), period: 100);
// Or by alpha for direct control
Ema.Calculate(source.AsSpan(), emaOutput.AsSpan(), alpha: 0.02);
Ema.Batch(source.AsSpan(), emaOutput.AsSpan(), alpha: 0.02);
// Results are written directly to output buffer
Console.WriteLine($"Last EMA: {emaOutput[^1]}");
+1 -1
View File
@@ -117,7 +117,7 @@ public class HmaIndicatorTests
{
var indicator = new HmaIndicator();
indicator.Initialize();
var method = indicator.GetType().GetMethod("OnPaintChart");
Assert.NotNull(method);
Assert.Equal(typeof(HmaIndicator), method.DeclaringType);
+3 -3
View File
@@ -90,7 +90,7 @@ public class HmaTests
}
var instanceResults = new Hma(14).Update(series);
var staticResults = Hma.Calculate(series, 14);
var staticResults = Hma.Batch(series, 14);
for (int i = 0; i < instanceResults.Count; i++)
{
@@ -109,7 +109,7 @@ public class HmaTests
series.Add(bar.Time, bar.Close);
}
var seriesResults = Hma.Calculate(series, 14);
var seriesResults = Hma.Batch(series, 14);
double[] input = series.Values.ToArray();
double[] output = new double[input.Length];
@@ -247,7 +247,7 @@ public class HmaTests
var series = bars.Close;
// 1. Batch Mode
var batchSeries = Hma.Calculate(series, period);
var batchSeries = Hma.Batch(series, period);
double expected = batchSeries.Last.Value;
// 2. Span Mode
+42 -21
View File
@@ -20,7 +20,7 @@ namespace QuanTAlib;
/// https://alan.hull.com.au/hma.html
/// </remarks>
[SkipLocalsInit]
public sealed class Hma : ITValuePublisher
public sealed class Hma : AbstractBase
{
private readonly int _period;
private readonly int _sqrtPeriod;
@@ -29,10 +29,7 @@ public sealed class Hma : ITValuePublisher
private readonly Wma _wmaSqrt;
private int _sampleCount;
public string Name { get; }
public TValue Last { get; private set; }
public bool IsHot => _sampleCount >= _period + _sqrtPeriod - 1;
public event Action<TValue>? Pub;
public override bool IsHot => _sampleCount >= WarmupPeriod;
public Hma(int period)
{
@@ -47,6 +44,7 @@ public sealed class Hma : ITValuePublisher
_wmaSqrt = new Wma(_sqrtPeriod);
Name = $"Hma({period})";
WarmupPeriod = period + _sqrtPeriod - 1; // WMA needs period, then WMA(sqrt) needs sqrt_period. Total lag/warmup.
}
public Hma(ITValuePublisher source, int period) : this(period)
@@ -55,7 +53,7 @@ public sealed class Hma : ITValuePublisher
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
public override TValue Update(TValue input, bool isNew = true)
{
if (isNew) _sampleCount++;
@@ -71,13 +69,13 @@ public sealed class Hma : ITValuePublisher
// 4. Calculate HMA = WMA(sqrt(n), intermediate)
Last = _wmaSqrt.Update(new TValue(input.Time, intermediate), isNew);
Pub?.Invoke(Last);
PubEvent(Last);
return Last;
}
public TSeries Update(TSeries source)
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return new TSeries([], []);
if (source.Count == 0) return [];
int len = source.Count;
var t = new List<long>(len);
@@ -92,23 +90,37 @@ public sealed class Hma : ITValuePublisher
source.Times.CopyTo(tSpan);
// Restore state for streaming
_wmaFull.Reset();
_wmaHalf.Reset();
_wmaSqrt.Reset();
Reset();
int lookback = _period + (int)Math.Sqrt(_period) + 10; // Sufficient lookback
// We need to replay enough history to get the state right.
// HMA depends on 3 WMAs.
// WMA state depends on the last 'period' values.
// So we need to replay at least _period + _sqrtPeriod + buffer.
int lookback = _period + _sqrtPeriod + 10;
int startIndex = Math.Max(0, len - lookback);
_sampleCount = startIndex;
// We can't easily set _sampleCount without replaying, or we assume it's just count.
// But WMA internal state needs to be restored.
// Since WMA doesn't expose Prime/State easily (unless we cast and check), replaying is safer.
for (int i = startIndex; i < len; i++)
{
Update(source[i]);
Update(new TValue(source.Times[i], source.Values[i]));
}
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v);
}
public static TSeries Calculate(TSeries source, int period)
public override void Prime(ReadOnlySpan<double> source)
{
foreach (var value in source)
{
Update(new TValue(DateTime.MinValue, value));
}
}
public static TSeries Batch(TSeries source, int period)
{
int len = source.Count;
var t = new List<long>(len);
@@ -142,15 +154,24 @@ public sealed class Hma : ITValuePublisher
double[] rentedHalf = System.Buffers.ArrayPool<double>.Shared.Rent(len);
Span<double> halfWma = rentedHalf.AsSpan(0, len);
// Reuse halfWma buffer for intermediate results
// Reuse halfWma buffer for intermediate results to save memory/allocations
// But we need halfWma values for the calculation.
// Wait, CalculateIntermediate reads halfWma and fullWma and writes to output.
// So we can write to 'halfWma' IF we don't need 'halfWma' anymore.
// CalculateIntermediate iterates. If we write to halfWma in place, we overwrite values we might need if we were doing something else.
// But here: output[i] = 2*half[i] - full[i].
// This is element-wise. So we CAN overwrite half[i] with the result if we process carefully or if we don't need half[i] later.
// We don't need half[i] later.
// So we can use halfWma as the intermediate buffer.
Span<double> intermediate = halfWma;
try
{
Wma.Calculate(source, fullWma, period);
Wma.Calculate(source, halfWma, halfPeriod);
Wma.Batch(source, fullWma, period);
Wma.Batch(source, halfWma, halfPeriod);
CalculateIntermediate(halfWma, fullWma, intermediate);
Wma.Calculate(intermediate, output, sqrtPeriod);
Wma.Batch(intermediate, output, sqrtPeriod);
}
finally
{
@@ -209,7 +230,7 @@ public sealed class Hma : ITValuePublisher
}
}
public void Reset()
public override void Reset()
{
_wmaFull.Reset();
_wmaHalf.Reset();
+37
View File
@@ -62,3 +62,40 @@ HMA can be used in various trading strategies:
## References
* Hull, Alan. "Better Trading with the Hull Moving Average." MTA Symposium Proceedings, 2005
## C# Implementation
### Standard Usage
```csharp
using QuanTAlib;
// Initialize with period 9
var hma = new Hma(9);
// Update with new value
TValue result = hma.Update(new TValue(time, price));
Console.WriteLine($"HMA: {result.Value}");
```
### Zero-Allocation Span API
```csharp
double[] prices = ...;
double[] output = new double[prices.Length];
// Calculate HMA for the entire array
Hma.Batch(prices.AsSpan(), output.AsSpan(), period: 9);
```
### Bar Correction
```csharp
var hma = new Hma(9);
// Update with initial tick
hma.Update(new TValue(time, 100), isNew: true);
// Update with correction (same bar)
hma.Update(new TValue(time, 101), isNew: false);
```
+1 -1
View File
@@ -28,7 +28,7 @@ public class HtitIndicatorTests
{
var time = DateTime.UtcNow.AddMinutes(i);
indicator.HistoricalData.AddBar(time, 100 + i, 100 + i, 100 + i, 100 + i);
var args = new UpdateArgs(UpdateReason.NewBar);
indicator.ProcessUpdate(args);
}
+1 -1
View File
@@ -48,7 +48,7 @@ public class HtitIndicator : Indicator, IWatchlistIndicator
{
TValue input = this.GetInputValue(args, Source);
bool isNew = args.Reason == UpdateReason.NewBar || args.Reason == UpdateReason.HistoricalBar;
TValue result = _htit!.Update(input, isNew);
Series!.SetValue(result.Value);
Series!.SetMarker(0, Color.Transparent);
+1 -1
View File
@@ -165,7 +165,7 @@ public class HtitTests
var series = bars.Close;
// 1. Batch Mode
var batchSeries = Htit.Calculate(series);
var batchSeries = Htit.Batch(series);
double expected = batchSeries.Last.Value;
// 2. Span Mode
+24 -20
View File
@@ -17,13 +17,8 @@ namespace QuanTAlib;
/// https://dotnet.stockindicators.dev/indicators/HtTrendline/
/// </remarks>
[SkipLocalsInit]
public sealed class Htit : ITValuePublisher
public sealed class Htit : AbstractBase
{
public string Name { get; }
public bool IsHot { get; private set; }
public event Action<TValue>? Pub;
public TValue Last { get; private set; }
private readonly RingBuffer _priceBuffer;
private readonly RingBuffer _smoothBuffer;
private readonly RingBuffer _detrenderBuffer;
@@ -37,9 +32,12 @@ public sealed class Htit : ITValuePublisher
private State _state;
private State _p_state;
public override bool IsHot => _priceBuffer.Count >= WarmupPeriod;
public Htit()
{
Name = "Htit";
WarmupPeriod = 12; // Based on logic: _priceBuffer.Count >= 12
_priceBuffer = new RingBuffer(50);
_smoothBuffer = new RingBuffer(7);
_detrenderBuffer = new RingBuffer(7);
@@ -56,7 +54,7 @@ public sealed class Htit : ITValuePublisher
source.Pub += (item) => Update(item);
}
public void Init()
private void Init()
{
_priceBuffer.Clear();
_smoothBuffer.Clear();
@@ -68,12 +66,11 @@ public sealed class Htit : ITValuePublisher
_itBuffer.Clear();
_state = default;
_p_state = default;
IsHot = false;
Last = default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
public override TValue Update(TValue input, bool isNew = true)
{
ManageState(isNew);
double price = ValidateInput(input.Value);
@@ -123,13 +120,12 @@ public sealed class Htit : ITValuePublisher
? (4 * _itBuffer[^1] + 3 * _itBuffer[^2] + 2 * _itBuffer[^3] + _itBuffer[^4]) / 10.0
: price;
IsHot = _priceBuffer.Count >= 12;
Last = new TValue(input.Time, trendline);
Pub?.Invoke(Last);
PubEvent(Last);
return Last;
}
public TSeries Update(TSeries source)
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
@@ -157,6 +153,14 @@ public sealed class Htit : ITValuePublisher
return new TSeries(t, v);
}
public override void Prime(ReadOnlySpan<double> source)
{
foreach (var value in source)
{
Update(new TValue(DateTime.MinValue, value));
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ManageState(bool isNew)
{
@@ -173,7 +177,7 @@ public sealed class Htit : ITValuePublisher
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void UpdateBuffer(RingBuffer buffer, double val, bool isNew)
private static void UpdateBuffer(RingBuffer buffer, double val, bool isNew)
{
if (isNew) buffer.Add(val);
else buffer.UpdateNewest(val);
@@ -190,7 +194,7 @@ public sealed class Htit : ITValuePublisher
UpdateBuffer(_itBuffer, price, isNew);
Last = new TValue(input.Time, price);
Pub?.Invoke(Last);
PubEvent(Last);
return Last;
}
@@ -253,7 +257,7 @@ public sealed class Htit : ITValuePublisher
return count > 0 ? sumPr / count : price;
}
public static TSeries Calculate(TSeries source)
public static TSeries Batch(TSeries source)
{
var htit = new Htit();
return htit.Update(source);
@@ -318,12 +322,12 @@ public sealed class Htit : ITValuePublisher
// 2. Detrender
double prevPeriod = periodBuffer[(pdIdx - 1 + 2) % 2];
double adj = (0.075 * prevPeriod) + 0.54;
double s0 = smoothBuffer[sIdx];
double s2 = smoothBuffer[(sIdx - 2 + 7) % 7];
double s4 = smoothBuffer[(sIdx - 4 + 7) % 7];
double s6 = smoothBuffer[(sIdx - 6 + 7) % 7];
double detrender = (0.0962 * s0 + 0.5769 * s2 - 0.5769 * s4 - 0.0962 * s6) * adj;
detrenderBuffer[dIdx] = detrender;
@@ -332,10 +336,10 @@ public sealed class Htit : ITValuePublisher
double d2 = detrenderBuffer[(dIdx - 2 + 7) % 7];
double d4 = detrenderBuffer[(dIdx - 4 + 7) % 7];
double d6 = detrenderBuffer[(dIdx - 6 + 7) % 7];
double q1 = (0.0962 * d0 + 0.5769 * d2 - 0.5769 * d4 - 0.0962 * d6) * adj;
double i1 = detrenderBuffer[(dIdx - 3 + 7) % 7];
q1Buffer[q1Idx] = q1;
i1Buffer[i1Idx] = i1;
@@ -438,7 +442,7 @@ public sealed class Htit : ITValuePublisher
}
}
public void Reset()
public override void Reset()
{
Init();
}
+2 -2
View File
@@ -47,12 +47,12 @@ TValue result = htit.Update(new TValue(time, price));
// Batch
var series = new TSeries(times, prices);
var resultSeries = Htit.Calculate(series);
var resultSeries = Htit.Batch(series);
// Span (Zero-Allocation)
double[] input = ...;
double[] output = new double[input.Length];
Htit.Calculate(input, output);
Htit.Batch(input, output);
```
## Interpretation
+1 -1
View File
@@ -120,7 +120,7 @@ public class JmaIndicatorTests
{
var indicator = new JmaIndicator();
indicator.Initialize();
var method = indicator.GetType().GetMethod("OnPaintChart");
Assert.NotNull(method);
Assert.Equal(typeof(JmaIndicator), method.DeclaringType);
+5 -5
View File
@@ -163,7 +163,7 @@ public class JmaTests
}
// Calculate with TSeries API
var tseriesResult = new Jma(10).Update(series);
var tseriesResult = Jma.Batch(series, 10);
// Calculate with Span API
Jma.Calculate(source.AsSpan(), output.AsSpan(), 10);
@@ -185,7 +185,7 @@ public class JmaTests
var series = bars.Close;
// 1. Batch Mode
var batchSeries = new Jma(period).Update(series);
var batchSeries = Jma.Batch(series, period);
double expected = batchSeries.Last.Value;
// 2. Span Mode
@@ -229,9 +229,9 @@ public class JmaTests
series.Add(bar.Time, bar.Close);
}
var jmaPhase0 = new Jma(10, phase: 0).Update(series);
var jmaPhase100 = new Jma(10, phase: 100).Update(series);
var jmaPhaseMinus100 = new Jma(10, phase: -100).Update(series);
var jmaPhase0 = Jma.Batch(series, 10, phase: 0);
var jmaPhase100 = Jma.Batch(series, 10, phase: 100);
var jmaPhaseMinus100 = Jma.Batch(series, 10, phase: -100);
Assert.NotEqual(jmaPhase0.Last.Value, jmaPhase100.Last.Value);
Assert.NotEqual(jmaPhase0.Last.Value, jmaPhaseMinus100.Last.Value);
+51 -30
View File
@@ -13,7 +13,7 @@ namespace QuanTAlib;
/// - Jurik dynamic exponent and 2-pole IIR core
/// </summary>
[SkipLocalsInit]
public sealed class Jma : ITValuePublisher
public sealed class Jma : AbstractBase
{
private const int VolWindowSize = 128; // volatility history length
private const int DevWindowSize = 10; // short SMA length for deviation
@@ -24,7 +24,6 @@ public sealed class Jma : ITValuePublisher
private readonly double _lengthDivider; // L'/(L'+2), L' = 0.9*L
private readonly double _logSqrtDivider; // Precomputed log(_sqrtDivider) for Exp optimization
private readonly double _logLengthDivider; // Precomputed log(_lengthDivider) for Exp optimization
private readonly int _warmupBars; // for IsHot
// Constants for trimmed mean
private const int JurikTrimCount = 65; // canonical JMA: middle 65 of 128 samples
@@ -57,15 +56,7 @@ public sealed class Jma : ITValuePublisher
public int Bars;
}
public string Name { get; }
public event Action<TValue>? Pub;
public TValue Last { get; private set; }
/// <summary>
/// JMA is considered "hot" when enough bars have passed to stabilize
/// the internal volatility distribution.
/// </summary>
public bool IsHot => _state.Bars >= _warmupBars;
public override bool IsHot => _state.Bars >= WarmupPeriod;
public Jma(int period, int phase = 0, double power = 0.45)
{
@@ -100,7 +91,7 @@ public sealed class Jma : ITValuePublisher
_logSqrtDivider = Math.Log(sqrtDivider);
// same warmup heuristic used in the AFL port (SetBarsRequired)
_warmupBars = (int)Math.Ceiling(20.0 + 80.0 * Math.Pow(period, 0.36));
WarmupPeriod = (int)Math.Ceiling(20.0 + 80.0 * Math.Pow(period, 0.36));
Name = $"Jma({period},{phase},{power})"; // power kept for signature compatibility
@@ -118,7 +109,7 @@ public sealed class Jma : ITValuePublisher
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset()
public override void Reset()
{
_state = default;
_p_state = default;
@@ -232,45 +223,75 @@ public sealed class Jma : ITValuePublisher
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
public override TValue Update(TValue input, bool isNew = true)
{
double j = Step(input.Value, isNew);
Last = new TValue(input.Time, j);
Pub?.Invoke(Last);
PubEvent(Last);
return Last;
}
/// <summary>
/// Batch update: recomputes JMA for entire series using the same
/// streaming core, so results match Update(TValue) applied bar-by-bar.
/// </summary>
public TSeries Update(TSeries source)
public override TSeries Update(TSeries source)
{
int n = source.Count;
if (n == 0)
return [];
if (source.Count == 0) return [];
var t = new List<long>(n);
var v = new List<double>(n);
CollectionsMarshal.SetCount(t, n);
CollectionsMarshal.SetCount(v, n);
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);
source.Times.CopyTo(tSpan);
// Use static Calculate for performance
// But JMA has complex parameters, so we need to pass them.
// We can use the instance to calculate, but we need to be careful about state.
// Or we can just loop using Step, which is what the original code did.
// Since JMA is complex and not easily vectorizable, looping is fine.
// But we should restore state afterwards.
// RingBuffers are reference types, so we need to clone them or replay.
// Replaying is safer and cleaner for complex state.
Reset();
for (int i = 0; i < n; i++)
for (int i = 0; i < len; i++)
{
double j = Step(source.Values[i], true);
vSpan[i] = j;
}
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
// Restore state by replaying history
// JMA needs a lot of history (128 bars for volatility).
Reset();
int lookback = Math.Max(VolWindowSize + 10, WarmupPeriod + 10);
int startIndex = Math.Max(0, len - lookback);
for (int i = startIndex; i < len; i++)
{
Update(new TValue(source.Times[i], source.Values[i]));
}
return new TSeries(t, v);
}
public override void Prime(ReadOnlySpan<double> source)
{
foreach (var value in source)
{
Update(new TValue(DateTime.MinValue, value));
}
}
public static TSeries Batch(TSeries source, int period, int phase = 0, double power = 0.45)
{
var jma = new Jma(period, phase, power);
return jma.Update(source);
}
/// <summary>
/// Static helper compatible with your existing signature.
/// </summary>
@@ -325,6 +346,6 @@ public sealed class Jma : ITValuePublisher
if (end >= count) end = count - 1;
int len = end - start + 1;
return _sorted.AsSpan(start, len).SumSIMD() / len;
return ((ReadOnlySpan<double>)_sorted.AsSpan(start, len)).SumSIMD() / len;
}
}
+1 -1
View File
@@ -88,7 +88,7 @@ For high-performance batch processing:
double[] prices = { 100.0, 101.5, 99.8, ... };
double[] output = new double[prices.Length];
Jma.Calculate(prices, output, period: 10, phase: 0);
Jma.Batch(prices, output, period: 10, phase: 0);
```
### Batch with TSeries
+1 -1
View File
@@ -245,7 +245,7 @@ public class KamaTests
var series = bars.Close;
// 1. Batch Mode
var batchSeries = Kama.Calculate(series, period);
var batchSeries = Kama.Batch(series, period);
double expected = batchSeries.Last.Value;
// 2. Span Mode
+36 -39
View File
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
@@ -18,7 +20,7 @@ namespace QuanTAlib;
/// KAMA = KAMA[prev] + SC * (Price - KAMA[prev])
/// </remarks>
[SkipLocalsInit]
public sealed class Kama : ITValuePublisher
public sealed class Kama : AbstractBase
{
private readonly int _period;
private readonly double _fastAlpha;
@@ -29,22 +31,7 @@ public sealed class Kama : ITValuePublisher
private State _state;
private State _p_state;
/// <summary>
/// Display name for the indicator.
/// </summary>
public string Name { get; }
public event Action<TValue>? Pub;
/// <summary>
/// Current KAMA value.
/// </summary>
public TValue Last { get; private set; }
/// <summary>
/// True if the KAMA has enough data to produce valid results.
/// </summary>
public bool IsHot => _buffer.IsFull;
public override bool IsHot => _buffer.IsFull;
/// <summary>
/// Creates KAMA with specified parameters.
@@ -72,6 +59,8 @@ public sealed class Kama : ITValuePublisher
_slowAlpha = 2.0 / (slowPeriod + 1);
Name = $"Kama({period}, {fastPeriod}, {slowPeriod})";
WarmupPeriod = period + 1;
_state.Kama = double.NaN;
_state.LastValidValue = double.NaN;
_p_state.Kama = double.NaN;
@@ -96,7 +85,7 @@ public sealed class Kama : ITValuePublisher
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
public override TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
@@ -111,7 +100,7 @@ public sealed class Kama : ITValuePublisher
if (double.IsNaN(val))
{
Last = new TValue(input.Time, double.NaN);
Pub?.Invoke(Last);
PubEvent(Last);
return Last;
}
@@ -125,7 +114,7 @@ public sealed class Kama : ITValuePublisher
double diff_out = _p_state.NextDiffOut;
double diff_in = Math.Abs(_buffer[^1] - _buffer[^2]);
_state.VolatilitySum += diff_in - diff_out;
// Calculate NextDiffOut for the next step
// NextDiffOut = abs(buffer[0] - buffer[1])
_state.NextDiffOut = Math.Abs(_buffer[0] - _buffer[1]);
@@ -134,12 +123,12 @@ public sealed class Kama : ITValuePublisher
{
double diff_in = Math.Abs(_buffer[^1] - _buffer[^2]);
_state.VolatilitySum += diff_in;
if (_buffer.IsFull)
{
// Buffer just became full.
// NextDiffOut = abs(buffer[0] - buffer[1])
_state.NextDiffOut = Math.Abs(_buffer[0] - _buffer[1]);
// Buffer just became full.
// NextDiffOut = abs(buffer[0] - buffer[1])
_state.NextDiffOut = Math.Abs(_buffer[0] - _buffer[1]);
}
}
}
@@ -181,39 +170,38 @@ public sealed class Kama : ITValuePublisher
double prevKama = _p_state.Kama;
if (double.IsNaN(prevKama))
{
prevKama = _state.Kama;
prevKama = _state.Kama;
}
_state.Kama = prevKama + sc * (val - prevKama);
}
Last = new TValue(input.Time, _state.Kama);
Pub?.Invoke(Last);
PubEvent(Last);
return Last;
}
public TSeries Update(TSeries source)
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return new TSeries([], []);
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);
source.Times.CopyTo(tSpan);
// Use static Calculate for performance
var outputSpan = new double[len];
// fastPeriod = 2/fastAlpha - 1.
int fastPeriod = (int)Math.Round(2.0 / _fastAlpha - 1);
int slowPeriod = (int)Math.Round(2.0 / _slowAlpha - 1);
Calculate(source.Values, outputSpan, _period, fastPeriod, slowPeriod);
for (int i = 0; i < len; i++)
{
t.Add(source.Times[i]);
v.Add(outputSpan[i]);
}
Calculate(source.Values, vSpan, _period, fastPeriod, slowPeriod);
// Restore state by replaying the entire series
// This is expensive but necessary to sync the object state correctly
@@ -221,13 +209,22 @@ public sealed class Kama : ITValuePublisher
Reset();
for (int i = 0; i < len; i++)
{
Update(source[i]);
Update(new TValue(source.Times[i], source.Values[i]));
}
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v);
}
public static TSeries Calculate(TSeries source, int period, int fastPeriod = 2, int slowPeriod = 30)
public override void Prime(ReadOnlySpan<double> source)
{
foreach (var value in source)
{
Update(new TValue(DateTime.MinValue, value));
}
}
public static TSeries Batch(TSeries source, int period, int fastPeriod = 2, int slowPeriod = 30)
{
var kama = new Kama(period, fastPeriod, slowPeriod);
return kama.Update(source);
@@ -326,7 +323,7 @@ public sealed class Kama : ITValuePublisher
}
}
public void Reset()
public override void Reset()
{
_buffer.Clear();
_state = default;
+1 -1
View File
@@ -57,7 +57,7 @@ double[] prices = ...;
double[] output = new double[prices.Length];
// Calculate KAMA for the entire array
Kama.Calculate(prices.AsSpan(), output.AsSpan(), period: 10, fastPeriod: 2, slowPeriod: 30);
Kama.Batch(prices.AsSpan(), output.AsSpan(), period: 10, fastPeriod: 2, slowPeriod: 30);
```
### Bar Correction
+1 -1
View File
@@ -118,7 +118,7 @@ public class LsmaIndicatorTests
{
var indicator = new LsmaIndicator();
indicator.Initialize();
var method = indicator.GetType().GetMethod("OnPaintChart");
Assert.NotNull(method);
Assert.Equal(typeof(LsmaIndicator), method.DeclaringType);
+1 -1
View File
@@ -141,7 +141,7 @@ public class LsmaTests
var lsma = new Lsma(period);
var series1 = lsma.Update(source);
var series2 = Lsma.Calculate(source, period);
var series2 = Lsma.Batch(source, period);
Assert.Equal(series1.Count, series2.Count);
for (int i = 0; i < count; i++)
+33 -42
View File
@@ -25,7 +25,7 @@ namespace QuanTAlib;
/// Becomes true when the buffer is full (period samples processed).
/// </remarks>
[SkipLocalsInit]
public sealed class Lsma : ITValuePublisher
public sealed class Lsma : AbstractBase
{
private readonly int _period;
private readonly int _offset;
@@ -37,17 +37,12 @@ public sealed class Lsma : ITValuePublisher
private record struct State(double SumY, double SumXY, double LastVal, double LastValidValue);
private State _state;
private State _p_state;
private int _tickCount;
private const int ResyncInterval = 1000;
/// <summary>
/// Display name for the indicator.
/// </summary>
public string Name { get; }
public event Action<TValue>? Pub;
public override bool IsHot => _buffer.IsFull;
/// <summary>
/// Creates LSMA with specified period and offset.
@@ -63,14 +58,15 @@ public sealed class Lsma : ITValuePublisher
_offset = offset;
_buffer = new RingBuffer(period);
Name = $"Lsma({period})";
WarmupPeriod = period;
// Precalculate constants
// sum_x = 0 + 1 + ... + (n-1) = n(n-1)/2
_sum_x = 0.5 * period * (period - 1);
// sum_x2 = 0^2 + ... + (n-1)^2 = (n-1)n(2n-1)/6
double sum_x2 = (period - 1.0) * period * (2.0 * period - 1.0) / 6.0;
// denominator = n * sum_x2 - sum_x^2
_denominator = period * sum_x2 - _sum_x * _sum_x;
}
@@ -80,16 +76,6 @@ public sealed class Lsma : ITValuePublisher
source.Pub += (item) => Update(item);
}
/// <summary>
/// Current LSMA value.
/// </summary>
public TValue Last { get; private set; }
/// <summary>
/// True if the LSMA has enough data to produce valid results.
/// </summary>
public bool IsHot => _buffer.IsFull;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetValidValue(double input)
{
@@ -108,21 +94,21 @@ public sealed class Lsma : ITValuePublisher
{
double oldest = _buffer.Oldest;
double prev_sum_y = _state.SumY;
// O(1) update for sum_xy
// sum_xy_new = sum_xy_old + sum_y_prev - n * oldest
_state.SumXY = _state.SumXY + prev_sum_y - _period * oldest;
// O(1) update for sum_y
_state.SumY = _state.SumY - oldest + val;
_buffer.Add(val);
}
else
{
_buffer.Add(val);
_state.SumY += val;
// Recalculate sum_xy from scratch during warmup
_state.SumXY = 0;
var span = _buffer.GetSpan();
@@ -158,7 +144,7 @@ public sealed class Lsma : ITValuePublisher
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
public override TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
@@ -176,10 +162,10 @@ public sealed class Lsma : ITValuePublisher
// For isNew=false, we update the current bar.
// sum_xy remains constant because it depends on the previous window state which hasn't changed.
// sum_y updates to reflect the change in the newest value.
_state.SumY = _p_state.SumY - _p_state.LastVal + val;
_state.SumXY = _p_state.SumXY; // Restore sum_xy to the state after the shift
_buffer.UpdateNewest(val);
_state.LastVal = val;
}
@@ -196,7 +182,7 @@ public sealed class Lsma : ITValuePublisher
double n = _buffer.Count;
double sx = _sum_x;
double denom = _denominator;
if (!_buffer.IsFull)
{
// Recalculate constants for smaller n
@@ -213,20 +199,20 @@ public sealed class Lsma : ITValuePublisher
{
double m = (n * _state.SumXY - sx * _state.SumY) / denom;
double b = (_state.SumY - m * sx) / n;
// LSMA = b - m * offset
result = b - m * _offset;
}
}
Last = new TValue(input.Time, result);
Pub?.Invoke(Last);
PubEvent(Last);
return Last;
}
public TSeries Update(TSeries source)
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0) return new TSeries([], []);
int len = source.Count;
var t = new List<long>(len);
@@ -279,10 +265,15 @@ public sealed class Lsma : ITValuePublisher
return new TSeries(t, v);
}
/// <summary>
/// Calculates LSMA for the entire series using a new instance.
/// </summary>
public static TSeries Calculate(TSeries source, int period, int offset = 0)
public override void Prime(ReadOnlySpan<double> source)
{
foreach (var value in source)
{
Update(new TValue(DateTime.MinValue, value));
}
}
public static TSeries Batch(TSeries source, int period, int offset = 0)
{
var lsma = new Lsma(period, offset);
return lsma.Update(source);
@@ -333,7 +324,7 @@ public sealed class Lsma : ITValuePublisher
buffer[count] = val;
sum_y += val;
count++;
// Recalculate sum_xy for current count
sum_xy = 0;
for (int j = 0; j < count; j++)
@@ -365,7 +356,7 @@ public sealed class Lsma : ITValuePublisher
output[i] = b - m * offset;
}
}
if (count == period)
{
bufferIndex = 0; // Reset for circular buffer usage
@@ -376,13 +367,13 @@ public sealed class Lsma : ITValuePublisher
// Full buffer phase - O(1) update
double oldest = buffer[bufferIndex];
double prev_sum_y = sum_y;
// sum_xy_new = sum_xy_old + sum_y_prev - n * oldest
sum_xy = sum_xy + prev_sum_y - period * oldest;
sum_y = sum_y - oldest + val;
buffer[bufferIndex] = val;
bufferIndex++;
if (bufferIndex >= period)
bufferIndex = 0;
@@ -397,7 +388,7 @@ public sealed class Lsma : ITValuePublisher
/// <summary>
/// Resets the LSMA state.
/// </summary>
public void Reset()
public override void Reset()
{
_buffer.Clear();
_state = default;
+1 -1
View File
@@ -61,7 +61,7 @@ double[] input = { ... };
double[] output = new double[input.Length];
// Calculate LSMA in-place
Lsma.Calculate(input, output, period: 14);
Lsma.Batch(input, output, period: 14);
```
### Bar Correction
+1 -1
View File
@@ -47,7 +47,7 @@ public class MamaIndicatorTests
indicator.Initialize();
// After init, line series should exist (MAMA and FAMA)
Assert.Equal(2, indicator.LinesSeries.Length);
Assert.Equal(2, indicator.LinesSeries.Count);
}
[Fact]
+5 -5
View File
@@ -35,10 +35,10 @@ public class MamaIndicator : Indicator, IWatchlistIndicator
SourceName = Source.ToString();
Name = "MAMA - MESA Adaptive Moving Average";
Description = "MESA Adaptive Moving Average";
MamaSeries = new(name: "MAMA", color: Color.Red, width: 2, style: LineStyle.Solid);
FamaSeries = new(name: "FAMA", color: Color.Blue, width: 2, style: LineStyle.Solid);
AddLineSeries(MamaSeries);
AddLineSeries(FamaSeries);
}
@@ -55,12 +55,12 @@ public class MamaIndicator : Indicator, IWatchlistIndicator
{
TValue input = this.GetInputValue(args, Source);
bool isNew = args.Reason == UpdateReason.NewBar || args.Reason == UpdateReason.HistoricalBar;
TValue result = _ma!.Update(input, isNew);
MamaSeries!.SetValue(result.Value);
FamaSeries!.SetValue(_ma.Fama.Value);
MamaSeries!.SetMarker(0, Color.Transparent);
FamaSeries!.SetMarker(0, Color.Transparent);
+1 -1
View File
@@ -170,7 +170,7 @@ public class MamaTests
var mama = new Mama();
var series1 = mama.Update(source);
var series2 = Mama.Calculate(source);
var series2 = Mama.Batch(source);
Assert.Equal(series1.Count, series2.Count);
for (int i = 0; i < source.Count; i++)
+187 -20
View File
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
@@ -8,12 +10,10 @@ namespace QuanTAlib;
/// A trend-following indicator that adapts to the market's phase rate of change.
/// </summary>
[SkipLocalsInit]
public sealed class Mama : ITValuePublisher
public sealed class Mama : AbstractBase
{
public TValue Last { get; private set; }
public TValue Fama { get; private set; }
public bool IsHot => _state.Index > 6;
public event Action<TValue>? Pub;
public override bool IsHot => _state.Index > 6;
private readonly double _fastLimit;
private readonly double _slowLimit;
@@ -52,6 +52,7 @@ public sealed class Mama : ITValuePublisher
_Q1_buffer = new RingBuffer(7);
Name = $"Mama({fastLimit:F2},{slowLimit:F2})";
WarmupPeriod = 7;
Init();
}
@@ -65,7 +66,7 @@ public sealed class Mama : ITValuePublisher
Reset();
}
public void Reset()
public override void Reset()
{
_state = default;
_state.Mama = double.NaN;
@@ -83,7 +84,7 @@ public sealed class Mama : ITValuePublisher
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
private double Step(double price, bool isNew)
{
if (isNew)
{
@@ -95,7 +96,6 @@ public sealed class Mama : ITValuePublisher
_state = _p_state;
}
double price = input.Value;
if (!double.IsFinite(price))
{
price = _state.LastValidPrice;
@@ -186,7 +186,7 @@ public sealed class Mama : ITValuePublisher
double avg = _state.Index > 0 ? _state.SumPr / _state.Index : price;
_state.Mama = avg;
_state.Fama = avg;
// Initialize buffers with 0
_smoothBuffer.Add(0, isNew);
_detrender.Add(0, isNew);
@@ -194,15 +194,22 @@ public sealed class Mama : ITValuePublisher
_Q1_buffer.Add(0, isNew);
}
Last = new TValue(input.Time, _state.Mama);
return _state.Mama;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override TValue Update(TValue input, bool isNew = true)
{
double mama = Step(input.Value, isNew);
Last = new TValue(input.Time, mama);
Fama = new TValue(input.Time, _state.Fama);
Pub?.Invoke(Last);
PubEvent(Last);
return Last;
}
public TSeries Update(TSeries source)
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0) return new TSeries([], []);
int len = source.Count;
var v = new List<double>(len);
@@ -210,16 +217,23 @@ public sealed class Mama : ITValuePublisher
for (int i = 0; i < len; i++)
{
var item = source[i];
var result = Update(item);
var result = Update(new TValue(source.Times[i], source.Values[i]));
t.Add(result.Time);
v.Add(result.Value);
t.Add(item.Time);
}
return new TSeries(t, v);
}
public static TSeries Calculate(TSeries source, double fastLimit = 0.5, double slowLimit = 0.05)
public override void Prime(ReadOnlySpan<double> source)
{
foreach (var value in source)
{
Step(value, true);
}
}
public static TSeries Batch(TSeries source, double fastLimit = 0.5, double slowLimit = 0.05)
{
var mama = new Mama(fastLimit, slowLimit);
return mama.Update(source);
@@ -227,12 +241,165 @@ public sealed class Mama : ITValuePublisher
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, double fastLimit = 0.5, double slowLimit = 0.05)
{
var mama = new Mama(fastLimit, slowLimit);
if (source.Length == 0) return;
// Stack allocate buffers for high performance (size 8 for power of 2 masking)
// We need 7 elements, but 8 allows & 7 masking
Span<double> priceBuffer = stackalloc double[8];
Span<double> smoothBuffer = stackalloc double[8];
Span<double> detrender = stackalloc double[8];
Span<double> I1_buffer = stackalloc double[8];
Span<double> Q1_buffer = stackalloc double[8];
int bufferIdx = 0; // Current index for circular buffer
int count = 0;
// State variables
double period = 0, mama = 0, sumPr = 0;
double i2 = 0, q2 = 0, re = 0, im = 0, lastValidPrice = 0;
double p_period = 0, p_phase = 0, p_mama = 0;
double p_i2 = 0, p_q2 = 0, p_re = 0, p_im = 0;
// Constants
const int Mask = 7;
for (int i = 0; i < source.Length; i++)
{
output[i] = mama.Update(new TValue(DateTime.MinValue, source[i])).Value;
double price = source[i];
if (!double.IsFinite(price))
{
price = count > 0 ? lastValidPrice : 0.0;
}
else
{
lastValidPrice = price;
}
// Circular buffer update
bufferIdx = (bufferIdx + 1) & Mask;
priceBuffer[bufferIdx] = price;
count++;
if (count > 6)
{
double adj = (0.075 * period) + 0.54;
// Smooth
double smooth = (4.0 * priceBuffer[bufferIdx] +
3.0 * priceBuffer[(bufferIdx - 1) & Mask] +
2.0 * priceBuffer[(bufferIdx - 2) & Mask] +
priceBuffer[(bufferIdx - 3) & Mask]) * 0.1;
smoothBuffer[bufferIdx] = smooth;
// Detrender
double dt = (c1 * smoothBuffer[bufferIdx] +
c2 * smoothBuffer[(bufferIdx - 2) & Mask] -
c2 * smoothBuffer[(bufferIdx - 4) & Mask] -
c1 * smoothBuffer[(bufferIdx - 6) & Mask]) * adj;
detrender[bufferIdx] = dt;
// Q1
double q1 = (c1 * dt +
c2 * detrender[(bufferIdx - 2) & Mask] -
c2 * detrender[(bufferIdx - 4) & Mask] -
c1 * detrender[(bufferIdx - 6) & Mask]) * adj;
Q1_buffer[bufferIdx] = q1;
// I1 = dt[3]
double i1 = detrender[(bufferIdx - 3) & Mask];
I1_buffer[bufferIdx] = i1;
// Advance phases
double jI = (c1 * i1 +
c2 * I1_buffer[(bufferIdx - 2) & Mask] -
c2 * I1_buffer[(bufferIdx - 4) & Mask] -
c1 * I1_buffer[(bufferIdx - 6) & Mask]) * adj;
double jQ = (c1 * q1 +
c2 * Q1_buffer[(bufferIdx - 2) & Mask] -
c2 * Q1_buffer[(bufferIdx - 4) & Mask] -
c1 * Q1_buffer[(bufferIdx - 6) & Mask]) * adj;
// Phasor addition
double i2_val = i1 - jQ;
double q2_val = q1 + jI;
// Smooth i2, q2
i2 = 0.2 * i2_val + 0.8 * p_i2;
q2 = 0.2 * q2_val + 0.8 * p_q2;
// Homodyne discriminator
double re_val = (i2 * p_i2) + (q2 * p_q2);
double im_val = (i2 * p_q2) - (q2 * p_i2);
// Smooth re, im
re = 0.2 * re_val + 0.8 * p_re;
im = 0.2 * im_val + 0.8 * p_im;
// Calculate Period
double newPeriod = (Math.Abs(im) > double.Epsilon && Math.Abs(re) > double.Epsilon)
? TWOPI / Math.Atan(im / re)
: 0.0;
// Adjust Period
double periodCap = p_period * 1.5;
double periodFloor = p_period * 0.67;
if (newPeriod > periodCap) newPeriod = periodCap;
if (newPeriod < periodFloor) newPeriod = periodFloor;
if (newPeriod < 6.0) newPeriod = 6.0;
if (newPeriod > 50.0) newPeriod = 50.0;
// Smooth Period
period = 0.2 * newPeriod + 0.8 * p_period;
// Phase calculation
double phase = Math.Abs(i1) >= double.Epsilon ? Math.Atan(q1 / i1) * RadToDeg : 0.0;
// Adaptive alpha
double delta = Math.Max(p_phase - phase, 1.0);
double alpha = fastLimit / delta;
alpha = Math.Clamp(alpha, slowLimit, fastLimit);
// Final indicators
mama = alpha * priceBuffer[bufferIdx] + (1.0 - alpha) * p_mama;
// Update previous state
p_i2 = i2;
p_q2 = q2;
p_re = re;
p_im = im;
p_period = period;
p_phase = phase;
p_mama = mama;
}
else
{
// Initialization
sumPr += price;
double avg = count > 0 ? sumPr / count : price;
mama = avg;
// Init simple state
smoothBuffer[bufferIdx] = 0;
detrender[bufferIdx] = 0;
I1_buffer[bufferIdx] = 0;
Q1_buffer[bufferIdx] = 0;
// Set initial p_state
p_mama = avg;
p_period = 0; // Initial period state
p_phase = 0;
// Initialize other state variables if needed for next iteration logic?
// Actually they just stay 0/default until we hit count > 6
}
output[i] = mama;
}
}
public string Name { get; set; }
}
+38
View File
@@ -62,6 +62,44 @@ MAMA is particularly valuable for identifying trends in markets with varying cyc
* **Mathematical complexity:** Requires proper implementation of digital signal processing concepts for accurate results
* **Complementary tools:** Works best when combined with momentum indicators or volume analysis for confirmation
## C# Implementation
### Standard Usage
```csharp
using QuanTAlib;
// Create MAMA with default parameters
var mama = new Mama(fastLimit: 0.5, slowLimit: 0.05);
// Update with new price
var result = mama.Update(new TValue(DateTime.UtcNow, 100.0));
Console.WriteLine($"MAMA: {result.Value}");
Console.WriteLine($"FAMA: {mama.Fama.Value}");
```
### Static API (High Performance)
```csharp
// Calculate MAMA for an entire array
double[] prices = { ... };
double[] results = new double[prices.Length];
Mama.Batch(prices, results, fastLimit: 0.5, slowLimit: 0.05);
```
### Event-Driven
```csharp
var source = new TSeries();
var mama = new Mama(source);
mama.Pub += (item) => {
Console.WriteLine($"MAMA: {item.Value}");
Console.WriteLine($"FAMA: {mama.Fama.Value}");
};
```
## References
1. Ehlers, J. (2001). *MESA and Trading Market Cycles*. John Wiley & Sons.
+1 -1
View File
@@ -28,7 +28,7 @@ public class MgdiIndicatorTests
{
var time = DateTime.UtcNow.AddMinutes(i);
indicator.HistoricalData.AddBar(time, 100 + i, 100 + i, 100 + i, 100 + i);
var args = new UpdateArgs(UpdateReason.NewBar);
indicator.ProcessUpdate(args);
}
+1 -1
View File
@@ -51,7 +51,7 @@ public class MgdiIndicator : Indicator, IWatchlistIndicator
{
TValue input = this.GetInputValue(args, Source);
bool isNew = args.Reason == UpdateReason.NewBar || args.Reason == UpdateReason.HistoricalBar;
TValue result = _mgdi!.Update(input, isNew);
Series!.SetValue(result.Value);
Series!.SetMarker(0, Color.Transparent);
+1 -1
View File
@@ -34,7 +34,7 @@ public class MgdiTests
var data = _gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close;
var series = data;
var resultSeries = mgdi.Update(series);
var resultSeries = Mgdi.Batch(series);
// Reset and calculate streaming
mgdi.Reset();
+25 -22
View File
@@ -20,20 +20,17 @@ namespace QuanTAlib;
/// Default k = 0.6
/// </remarks>
[SkipLocalsInit]
public sealed class Mgdi : ITValuePublisher
public sealed class Mgdi : AbstractBase
{
public string Name { get; }
public bool IsHot { get; private set; }
public event Action<TValue>? Pub;
public TValue Last { get; private set; }
private readonly int _period;
private readonly double _k;
private record struct State(double LastMgdi, double LastValidValue, int Count);
private State _state;
private State _p_state;
public override bool IsHot => _state.Count >= _period;
public Mgdi(int period = 14, double k = 0.6)
{
if (period < 1) throw new ArgumentOutOfRangeException(nameof(period));
@@ -41,6 +38,7 @@ public sealed class Mgdi : ITValuePublisher
_period = period;
_k = k;
Name = $"Mgdi({period},{k})";
WarmupPeriod = period;
Init();
}
@@ -53,12 +51,11 @@ public sealed class Mgdi : ITValuePublisher
{
_state = default;
_p_state = default;
IsHot = false;
Last = default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
public override TValue Update(TValue input, bool isNew = true)
{
if (isNew) _p_state = _state;
else _state = _p_state;
@@ -87,25 +84,24 @@ public sealed class Mgdi : ITValuePublisher
double ratio = price / prev;
double ratio4 = ratio * ratio;
ratio4 *= ratio4;
double denominator = _k * _period * ratio4;
_state.LastMgdi = prev + (price - prev) / denominator;
}
else
{
_state.LastMgdi = price;
_state.LastMgdi = price;
}
}
IsHot = _state.Count >= _period;
Last = new TValue(input.Time, _state.LastMgdi);
Pub?.Invoke(Last);
PubEvent(Last);
return Last;
}
public TSeries Update(TSeries source)
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0) return new TSeries([], []);
int len = source.Count;
var t = new List<long>(len);
@@ -121,9 +117,8 @@ public sealed class Mgdi : ITValuePublisher
// Restore state
Init();
// Replay last portion to restore state
int startIndex = Math.Max(0, len - Math.Max(_period * 2, 100));
for (int i = startIndex; i < len; i++)
// Replay the whole series to restore state correctly as it is recursive
for (int i = 0; i < len; i++)
{
Update(new TValue(source.Times[i], source.Values[i]));
}
@@ -132,7 +127,15 @@ public sealed class Mgdi : ITValuePublisher
return new TSeries(t, v);
}
public static TSeries Calculate(TSeries source, int period = 14, double k = 0.6)
public override void Prime(ReadOnlySpan<double> source)
{
foreach (var value in source)
{
Update(new TValue(DateTime.MinValue, value));
}
}
public static TSeries Batch(TSeries source, int period = 14, double k = 0.6)
{
var mgdi = new Mgdi(period, k);
return mgdi.Update(source);
@@ -161,7 +164,7 @@ public sealed class Mgdi : ITValuePublisher
double ratio = price / lastMgdi;
double ratio4 = ratio * ratio;
ratio4 *= ratio4;
double denominator = k * period * ratio4;
lastMgdi += (price - lastMgdi) / denominator;
}
@@ -169,12 +172,12 @@ public sealed class Mgdi : ITValuePublisher
{
lastMgdi = price;
}
output[i] = lastMgdi;
}
}
public void Reset()
public override void Reset()
{
Init();
}
+1 -1
View File
@@ -58,7 +58,7 @@ double[] input = { ... }; // Your price data
double[] output = new double[input.Length];
// Calculate MGDI over the entire span
Mgdi.Calculate(input, output, period: 14, k: 0.6);
Mgdi.Batch(input, output, period: 14, k: 0.6);
```
### Event-Driven Usage
+1 -1
View File
@@ -118,7 +118,7 @@ public class PwmaIndicatorTests
{
var indicator = new PwmaIndicator();
indicator.Initialize();
var method = indicator.GetType().GetMethod("OnPaintChart");
Assert.NotNull(method);
Assert.Equal(typeof(PwmaIndicator), method.DeclaringType);
+4 -5
View File
@@ -187,7 +187,6 @@ public class PwmaTests
public void Pwma_BatchCalc_MatchesIterativeCalc()
{
var pwmaIterative = new Pwma(10);
var pwmaBatch = new Pwma(10);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
// Generate data
@@ -208,7 +207,7 @@ public class PwmaTests
}
// Calculate batch
var batchResults = pwmaBatch.Update(series);
var batchResults = Pwma.Batch(series, 10);
// Compare
Assert.Equal(iterativeResults.Count, batchResults.Count);
@@ -262,7 +261,7 @@ public class PwmaTests
series.Add(DateTime.UtcNow.Ticks + 1, 20);
series.Add(DateTime.UtcNow.Ticks + 2, 30);
var results = Pwma.Calculate(series, 3);
var results = Pwma.Batch(series, 3);
Assert.Equal(3, results.Count);
// PWMA(3) for last 3 values [10,20,30]: 360/14
@@ -324,7 +323,7 @@ public class PwmaTests
}
// Calculate with TSeries API
var tseriesResult = Pwma.Calculate(series, 10);
var tseriesResult = Pwma.Batch(series, 10);
// Calculate with Span API
Pwma.Calculate(source.AsSpan(), output.AsSpan(), 10);
@@ -363,7 +362,7 @@ public class PwmaTests
var series = bars.Close;
// 1. Batch Mode
var batchSeries = Pwma.Calculate(series, period);
var batchSeries = Pwma.Batch(series, period);
double expected = batchSeries.Last.Value;
// 2. Span Mode
+22 -16
View File
@@ -27,7 +27,7 @@ namespace QuanTAlib;
/// S3 is parabolic weighted sum
/// </remarks>
[SkipLocalsInit]
public sealed class Pwma : ITValuePublisher
public sealed class Pwma : AbstractBase
{
private readonly int _period;
private readonly double _divisor;
@@ -39,10 +39,7 @@ public sealed class Pwma : ITValuePublisher
private const int ResyncInterval = 1000;
public string Name { get; }
public TValue Last { get; private set; }
public bool IsHot => _buffer.IsFull;
public event Action<TValue>? Pub;
public override bool IsHot => _buffer.IsFull;
public Pwma(int period)
{
@@ -52,6 +49,7 @@ public sealed class Pwma : ITValuePublisher
_divisor = (double)period * (period + 1) * (2 * period + 1) / 6.0;
_buffer = new RingBuffer(period);
Name = $"Pwma({period})";
WarmupPeriod = period;
}
public Pwma(ITValuePublisher source, int period) : this(period)
@@ -115,7 +113,7 @@ public sealed class Pwma : ITValuePublisher
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
public override TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
@@ -136,10 +134,10 @@ public sealed class Pwma : ITValuePublisher
// S1' = S1 - last + new
// S2' = S2 - n*last + n*new
// S3' = S3 - n^2*last + n^2*new
int n = _buffer.IsFull ? _period : _buffer.Count;
double diff = val - _state.LastInput;
_state.Sum += diff;
_state.WSum += n * diff;
_state.PSum += (double)n * n * diff;
@@ -149,13 +147,13 @@ public sealed class Pwma : ITValuePublisher
double currentDivisor = _buffer.IsFull ? _divisor : (double)_buffer.Count * (_buffer.Count + 1) * (2 * _buffer.Count + 1) / 6.0;
Last = new TValue(input.Time, _state.PSum / currentDivisor);
Pub?.Invoke(Last);
PubEvent(Last);
return Last;
}
public TSeries Update(TSeries source)
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
if (source.Count == 0) return new TSeries([], []);
int len = source.Count;
List<long> t = new(len);
@@ -165,7 +163,7 @@ public sealed class Pwma : ITValuePublisher
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
Calculate(source.Values, vSpan, _period);
source.Times.CopyTo(tSpan);
@@ -209,7 +207,15 @@ public sealed class Pwma : ITValuePublisher
return new TSeries(t, v);
}
public static TSeries Calculate(TSeries source, int period)
public override void Prime(ReadOnlySpan<double> source)
{
foreach (var value in source)
{
Update(new TValue(DateTime.MinValue, value));
}
}
public static TSeries Batch(TSeries source, int period)
{
var pwma = new Pwma(period);
return pwma.Update(source);
@@ -290,12 +296,12 @@ public sealed class Pwma : ITValuePublisher
double recalcSum = 0;
double recalcWsum = 0;
double recalcPsum = 0;
for (int k = 0; k < period; k++)
{
int idx = bufferIdx + k;
if (idx >= period) idx -= period;
double v = buffer[idx];
recalcSum += v;
recalcWsum += (k + 1) * v;
@@ -310,7 +316,7 @@ public sealed class Pwma : ITValuePublisher
}
}
public void Reset()
public override void Reset()
{
_buffer.Clear();
_state = default;
+3 -3
View File
@@ -79,12 +79,12 @@ Console.WriteLine($"IsHot: {pwma.IsHot}"); // true when buffer is full
// Batch calculation (TSeries API)
TSeries source = ...;
TSeries results = Pwma.Calculate(source, 14);
TSeries results = Pwma.Batch(source, 14);
// High-performance Span API (zero allocation)
double[] prices = new double[10000];
double[] output = new double[10000];
Pwma.Calculate(prices.AsSpan(), output.AsSpan(), period: 14);
Pwma.Batch(prices.AsSpan(), output.AsSpan(), period: 14);
```
### Zero-Allocation Span API
@@ -97,7 +97,7 @@ double[] source = new double[200000];
double[] pwmaOutput = new double[200000];
// Zero heap allocation during calculation
Pwma.Calculate(source.AsSpan(), pwmaOutput.AsSpan(), period: 100);
Pwma.Batch(source.AsSpan(), pwmaOutput.AsSpan(), period: 100);
// Results are written directly to output buffer
Console.WriteLine($"Last PWMA: {pwmaOutput[^1]}");
+2 -2
View File
@@ -175,10 +175,10 @@ public class RmaTests
}
// Calculate with TSeries API
var tseriesResult = Rma.Calculate(series, 10);
var tseriesResult = Rma.Batch(series, 10);
// Calculate with Span API
Rma.Calculate(source.AsSpan(), output.AsSpan(), 10);
Rma.Batch(source.AsSpan(), output.AsSpan(), 10);
// Compare results
for (int i = 0; i < 100; i++)
+55 -23
View File
@@ -17,17 +17,9 @@ namespace QuanTAlib;
/// utilizing the same O(1) update complexity and zero-allocation architecture.
/// </remarks>
[SkipLocalsInit]
public sealed class Rma : ITValuePublisher
public sealed class Rma : AbstractBase
{
private readonly Ema _ema;
private readonly int _period;
/// <summary>
/// Display name for the indicator.
/// </summary>
public string Name => $"Rma({_period})";
public event Action<TValue>? Pub;
/// <summary>
/// Creates RMA with specified period.
@@ -39,9 +31,9 @@ public sealed class Rma : ITValuePublisher
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
_period = period;
_ema = new Ema(1.0 / period);
_ema.Pub += (item) => Pub?.Invoke(item);
Name = $"Rma({period})";
WarmupPeriod = _ema.WarmupPeriod;
}
/// <summary>
@@ -56,24 +48,49 @@ public sealed class Rma : ITValuePublisher
}
/// <summary>
/// Current RMA value.
/// Creates RMA with specified source and period.
/// </summary>
public TValue Last => _ema.Last;
/// <param name="source">Source series</param>
/// <param name="period">Period for RMA calculation</param>
public Rma(TSeries source, int period) : this(period)
{
Prime(source.Values);
if (source.Count > 0)
{
Last = new TValue(source.LastTime, Last.Value);
}
source.Pub += (item) => Update(item);
}
/// <summary>
/// True if the RMA has warmed up and is providing valid results.
/// </summary>
public bool IsHot => _ema.IsHot;
public override bool IsHot => _ema.IsHot;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
/// <summary>
/// Initializes the indicator state using the provided history.
/// </summary>
/// <param name="source">Historical data</param>
public override void Prime(ReadOnlySpan<double> source)
{
return _ema.Update(input, isNew);
_ema.Prime(source);
Last = _ema.Last;
}
public TSeries Update(TSeries source)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override TValue Update(TValue input, bool isNew = true)
{
return _ema.Update(source);
TValue result = _ema.Update(input, isNew);
Last = result;
PubEvent(Last);
return result;
}
public override TSeries Update(TSeries source)
{
TSeries result = _ema.Update(source);
Last = _ema.Last;
return result;
}
/// <summary>
@@ -82,7 +99,7 @@ public sealed class Rma : ITValuePublisher
/// <param name="source">Input series</param>
/// <param name="period">RMA period</param>
/// <returns>RMA series</returns>
public static TSeries Calculate(TSeries source, int period)
public static TSeries Batch(TSeries source, int period)
{
var rma = new Rma(period);
return rma.Update(source);
@@ -97,20 +114,35 @@ public sealed class Rma : ITValuePublisher
/// <param name="output">Output span (must be same length as source)</param>
/// <param name="period">RMA period (must be > 0)</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period)
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period)
{
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
double alpha = 1.0 / period;
Ema.Calculate(source, output, alpha);
Ema.Batch(source, output, alpha);
}
/// <summary>
/// Runs a high-performance batch calculation on history and returns
/// a "Hot" Rma instance ready to process the next tick immediately.
/// </summary>
/// <param name="source">Historical time series</param>
/// <param name="period">RMA Period</param>
/// <returns>A tuple containing the full calculation results and the hot indicator instance</returns>
public static (TSeries Results, Rma Indicator) Calculate(TSeries source, int period)
{
var rma = new Rma(period);
TSeries results = rma.Update(source);
return (results, rma);
}
/// <summary>
/// Resets the RMA state.
/// </summary>
public void Reset()
public override void Reset()
{
_ema.Reset();
Last = default;
}
}
+1 -1
View File
@@ -65,7 +65,7 @@ double[] source = ...;
double[] output = new double[source.Length];
// Zero-allocation calculation
Rma.Calculate(source, output, 14);
Rma.Batch(source, output, 14);
```
### Event-Driven
+1 -1
View File
@@ -116,7 +116,7 @@ public class SmaIndicatorTests
{
var indicator = new SmaIndicator();
indicator.Initialize();
var method = indicator.GetType().GetMethod("OnPaintChart");
Assert.NotNull(method);
Assert.Equal(typeof(SmaIndicator), method.DeclaringType);
+5 -1
View File
@@ -59,8 +59,12 @@ public class SmaIndicator : Indicator, IWatchlistIndicator
public override void OnPaintChart(PaintChartEventArgs args)
{
var savedColor = Series!.Color;
Series.Color = Color.Transparent;
base.OnPaintChart(args);
Series.Color = savedColor;
int warmupPeriod = _warmupBarIndex > 0 ? _warmupBarIndex : Count;
this.PaintSmoothCurve(args, Series!, warmupPeriod, showColdValues: ShowColdValues, tension: 0.2);
this.PaintLine(args, Series!, warmupPeriod, showColdValues: ShowColdValues);
}
}
+97 -19
View File
@@ -331,7 +331,7 @@ public class SmaTests
}
[Fact]
public void Sma_StaticCalculate_Works()
public void Sma_StaticBatch_Works()
{
var series = new TSeries();
series.Add(DateTime.UtcNow.Ticks, 10);
@@ -340,7 +340,7 @@ public class SmaTests
series.Add(DateTime.UtcNow.Ticks + 3, 40);
series.Add(DateTime.UtcNow.Ticks + 4, 50);
var results = Sma.Calculate(series, 3);
var results = Sma.Batch(series, 3);
Assert.Equal(5, results.Count);
// SMA(3) for last value: (30+40+50)/3 = 40
@@ -360,22 +360,22 @@ public class SmaTests
// ============== Span API Tests ==============
[Fact]
public void Sma_SpanCalc_ValidatesInput()
public void Sma_SpanBatch_ValidatesInput()
{
double[] source = [1, 2, 3, 4, 5];
double[] output = new double[5];
double[] wrongSizeOutput = new double[3];
// Period must be > 0
Assert.Throws<ArgumentException>(() => Sma.Calculate(source.AsSpan(), output.AsSpan(), 0));
Assert.Throws<ArgumentException>(() => Sma.Calculate(source.AsSpan(), output.AsSpan(), -1));
Assert.Throws<ArgumentException>(() => Sma.Batch(source.AsSpan(), output.AsSpan(), 0));
Assert.Throws<ArgumentException>(() => Sma.Batch(source.AsSpan(), output.AsSpan(), -1));
// Output must be same length as source
Assert.Throws<ArgumentException>(() => Sma.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 3));
Assert.Throws<ArgumentException>(() => Sma.Batch(source.AsSpan(), wrongSizeOutput.AsSpan(), 3));
}
[Fact]
public void Sma_SpanCalc_MatchesTSeriesCalc()
public void Sma_SpanBatch_MatchesTSeriesBatch()
{
var series = new TSeries();
double[] source = new double[100];
@@ -390,10 +390,10 @@ public class SmaTests
}
// Calculate with TSeries API
var tseriesResult = Sma.Calculate(series, 10);
var tseriesResult = Sma.Batch(series, 10);
// Calculate with Span API
Sma.Calculate(source.AsSpan(), output.AsSpan(), 10);
Sma.Batch(source.AsSpan(), output.AsSpan(), 10);
// Compare results
for (int i = 0; i < 100; i++)
@@ -403,12 +403,12 @@ public class SmaTests
}
[Fact]
public void Sma_SpanCalc_CalculatesCorrectly()
public void Sma_SpanBatch_CalculatesCorrectly()
{
double[] source = [10, 20, 30, 40, 50];
double[] output = new double[5];
Sma.Calculate(source.AsSpan(), output.AsSpan(), 3);
Sma.Batch(source.AsSpan(), output.AsSpan(), 3);
// SMA(3) warmup: 10, (10+20)/2=15, (10+20+30)/3=20, then sliding: (20+30+40)/3=30, (30+40+50)/3=40
Assert.Equal(10.0, output[0], 1e-10);
@@ -419,7 +419,7 @@ public class SmaTests
}
[Fact]
public void Sma_SpanCalc_ZeroAllocation()
public void Sma_SpanBatch_ZeroAllocation()
{
double[] source = new double[10000];
@@ -429,7 +429,7 @@ public class SmaTests
source[i] = gbm.Next().Close;
// Warm up
Sma.Calculate(source.AsSpan(), output.AsSpan(), 100);
Sma.Batch(source.AsSpan(), output.AsSpan(), 100);
// This test verifies the method runs without throwing
// (allocation is measured by BenchmarkDotNet, not unit tests)
@@ -437,12 +437,12 @@ public class SmaTests
}
[Fact]
public void Sma_SpanCalc_HandlesNaN()
public void Sma_SpanBatch_HandlesNaN()
{
double[] source = [100, 110, double.NaN, 120, 130];
double[] output = new double[5];
Sma.Calculate(source.AsSpan(), output.AsSpan(), 3);
Sma.Batch(source.AsSpan(), output.AsSpan(), 3);
// All outputs should be finite
foreach (var val in output)
@@ -452,12 +452,12 @@ public class SmaTests
}
[Fact]
public void Sma_SpanCalc_Period1_ReturnsInput()
public void Sma_SpanBatch_Period1_ReturnsInput()
{
double[] source = [10, 20, 30, 40, 50];
double[] output = new double[5];
Sma.Calculate(source.AsSpan(), output.AsSpan(), 1);
Sma.Batch(source.AsSpan(), output.AsSpan(), 1);
for (int i = 0; i < source.Length; i++)
{
@@ -474,14 +474,14 @@ public class SmaTests
var series = bars.Close;
// 1. Batch Mode
var batchSeries = Sma.Calculate(series, period);
var batchSeries = Sma.Batch(series, period);
double expected = batchSeries.Last.Value;
// 2. Span Mode
var tValues = series.Values.ToArray();
var spanInput = new ReadOnlySpan<double>(tValues);
var spanOutput = new double[tValues.Length];
Sma.Calculate(spanInput, spanOutput, period);
Sma.Batch(spanInput, spanOutput, period);
double spanResult = spanOutput[^1];
// 3. Streaming Mode
@@ -516,4 +516,82 @@ public class SmaTests
source.Add(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100, sma.Last.Value);
}
[Fact]
public void WarmupPeriod_IsSetCorrectly()
{
var sma = new Sma(10);
Assert.Equal(10, sma.WarmupPeriod);
}
[Fact]
public void Prime_SetsStateCorrectly()
{
var sma = new Sma(5);
double[] history = [10, 20, 30, 40, 50]; // SMA(5) = 30
sma.Prime(history);
Assert.True(sma.IsHot);
Assert.Equal(30.0, sma.Last.Value, 1e-10);
// Verify it continues correctly
sma.Update(new TValue(DateTime.UtcNow, 60)); // 20,30,40,50,60 -> 40
Assert.Equal(40.0, sma.Last.Value, 1e-10);
}
[Fact]
public void Prime_WithInsufficientHistory_IsNotHot()
{
var sma = new Sma(10);
double[] history = [10, 20, 30, 40, 50];
sma.Prime(history);
Assert.False(sma.IsHot);
Assert.Equal(30.0, sma.Last.Value, 1e-10); // It still calculates what it can
}
[Fact]
public void Prime_HandlesNaN_InHistory()
{
var sma = new Sma(3);
double[] history = [10, 20, double.NaN, 40];
// 10
// 10, 20
// 10, 20, 20 (NaN replaced by 20) -> Avg(10,20,20) = 16.666...
// 20, 20, 40 -> Avg(20,20,40) = 26.666...
sma.Prime(history);
Assert.True(sma.IsHot);
Assert.Equal(80.0 / 3.0, sma.Last.Value, 1e-9);
}
[Fact]
public void Calculate_ReturnsCorrectResultsAndHotIndicator()
{
var series = new TSeries();
for (int i = 1; i <= 10; i++) series.Add(DateTime.UtcNow, i * 10);
// 10, 20, 30, 40, 50, 60, 70, 80, 90, 100
// SMA(5)
var (results, indicator) = Sma.Calculate(series, 5);
// Check results
Assert.Equal(10, results.Count);
Assert.Equal(30.0, results[4].Value); // 5th element (index 4) is SMA(10..50) = 30
Assert.Equal(80.0, results.Last.Value); // Last element is SMA(60..100) = 80
// Check indicator state
Assert.True(indicator.IsHot);
Assert.Equal(80.0, indicator.Last.Value);
Assert.Equal(5, indicator.WarmupPeriod);
// Verify indicator continues correctly
indicator.Update(new TValue(DateTime.UtcNow, 110));
// Window was [60, 70, 80, 90, 100] -> Avg 80
// New Window [70, 80, 90, 100, 110] -> Avg 90
Assert.Equal(90.0, indicator.Last.Value);
}
}
+3 -3
View File
@@ -91,7 +91,7 @@ public class SmaValidationTests : IDisposable
{
// Calculate QuanTAlib SMA (Span API)
double[] qOutput = new double[sourceData.Length];
global::QuanTAlib.Sma.Calculate(sourceData.AsSpan(), qOutput.AsSpan(), period);
global::QuanTAlib.Sma.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period);
// Calculate Skender SMA
var sResult = _testData.SkenderQuotes.GetSma(period).ToList();
@@ -173,7 +173,7 @@ public class SmaValidationTests : IDisposable
{
// Calculate QuanTAlib SMA (Span API)
double[] qOutput = new double[sourceData.Length];
global::QuanTAlib.Sma.Calculate(sourceData.AsSpan(), qOutput.AsSpan(), period);
global::QuanTAlib.Sma.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period);
// Calculate TA-Lib SMA
var retCode = TALib.Functions.Sma<double>(sourceData, 0..^0, talibOutput, out var outRange, period);
@@ -263,7 +263,7 @@ public class SmaValidationTests : IDisposable
{
// Calculate QuanTAlib SMA (Span API)
double[] qOutput = new double[sourceData.Length];
global::QuanTAlib.Sma.Calculate(sourceData.AsSpan(), qOutput.AsSpan(), period);
global::QuanTAlib.Sma.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period);
// Calculate Tulip SMA
var smaIndicator = Tulip.Indicators.sma;
+111 -65
View File
@@ -26,7 +26,7 @@ namespace QuanTAlib;
/// Becomes true when the buffer is full (period samples processed).
/// </remarks>
[SkipLocalsInit]
public sealed class Sma : ITValuePublisher
public sealed class Sma : AbstractBase
{
private readonly int _period;
private readonly RingBuffer _buffer;
@@ -37,13 +37,6 @@ public sealed class Sma : ITValuePublisher
private const int ResyncInterval = 1000;
/// <summary>
/// Display name for the indicator.
/// </summary>
public string Name { get; }
public event Action<TValue>? Pub;
/// <summary>
/// Creates SMA with specified period.
/// </summary>
@@ -56,6 +49,7 @@ public sealed class Sma : ITValuePublisher
_period = period;
_buffer = new RingBuffer(period);
Name = $"Sma({period})";
WarmupPeriod = period;
}
public Sma(ITValuePublisher source, int period) : this(period)
@@ -63,16 +57,93 @@ public sealed class Sma : ITValuePublisher
source.Pub += (item) => Update(item);
}
/// <summary>
/// Current SMA value.
/// </summary>
public TValue Last { get; private set; }
public Sma(TSeries source, int period) : this(period)
{
Prime(source.Values);
if (source.Count > 0)
{
Last = new TValue(source.LastTime, Last.Value);
}
source.Pub += (item) => Update(item);
}
/////////////////////////////////////////////////////////////////////////////////////////////////
// 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 bool IsHot => _buffer.IsFull;
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)
{
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.
@@ -106,7 +177,7 @@ public sealed class Sma : ITValuePublisher
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
public override TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
@@ -127,11 +198,11 @@ public sealed class Sma : ITValuePublisher
double result = _state.Sum / _buffer.Count;
Last = new TValue(input.Time, result);
Pub?.Invoke(Last);
PubEvent(Last);
return Last;
}
public TSeries Update(TSeries source)
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return [];
@@ -144,65 +215,26 @@ public sealed class Sma : ITValuePublisher
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
Calculate(source.Values, vSpan, _period);
Batch(source.Values, vSpan, _period);
source.Times.CopyTo(tSpan);
// Restore state
int windowSize = Math.Min(len, _period);
int startIndex = len - windowSize;
_state.LastValidValue = double.NaN;
bool found = false;
if (startIndex > 0)
{
for (int i = startIndex - 1; i >= 0; i--)
{
if (double.IsFinite(source.Values[i]))
{
_state.LastValidValue = source.Values[i];
found = true;
break;
}
}
}
if (!found)
{
for (int i = 0; i < len; i++)
{
if (double.IsFinite(source.Values[i]))
{
_state.LastValidValue = source.Values[i];
break;
}
}
}
_buffer.Clear();
_state.Sum = 0;
_state.TickCount = 0;
for (int i = startIndex; i < len; i++)
{
double val = GetValidValue(source.Values[i]);
UpdateState(val);
_state.LastInput = val;
}
_p_state = _state;
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 Calculate(TSeries source, int period)
public static TSeries Batch(TSeries source, int period)
{
var sma = new Sma(period);
return sma.Update(source);
@@ -218,7 +250,7 @@ public sealed class Sma : ITValuePublisher
/// <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 Calculate(ReadOnlySpan<double> source, Span<double> output, int period)
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");
@@ -256,6 +288,20 @@ public sealed class Sma : ITValuePublisher
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)
{
@@ -268,7 +314,7 @@ public sealed class Sma : ITValuePublisher
double sum = 0;
double lastValid = double.NaN;
// Find first valid value to seed lastValid
for (int k = 0; k < len; k++)
{
@@ -541,7 +587,7 @@ public sealed class Sma : ITValuePublisher
/// <summary>
/// Resets the SMA state.
/// </summary>
public void Reset()
public override void Reset()
{
_buffer.Clear();
_state = default;
+3 -3
View File
@@ -68,12 +68,12 @@ Console.WriteLine($"IsHot: {sma.IsHot}"); // true when buffer is full
// Batch calculation (TSeries API)
TSeries source = ...;
TSeries results = Sma.Calculate(source, 10);
TSeries results = Sma.Batch(source, 10);
// High-performance Span API (zero allocation)
double[] prices = new double[10000];
double[] output = new double[10000];
Sma.Calculate(prices.AsSpan(), output.AsSpan(), period: 10);
Sma.Batch(prices.AsSpan(), output.AsSpan(), period: 10);
```
### Zero-Allocation Span API
@@ -86,7 +86,7 @@ double[] source = new double[200000];
double[] smaOutput = new double[200000];
// Zero heap allocation during calculation
Sma.Calculate(source.AsSpan(), smaOutput.AsSpan(), period: 100);
Sma.Batch(source.AsSpan(), smaOutput.AsSpan(), period: 100);
// Results are written directly to output buffer
Console.WriteLine($"Last SMA: {smaOutput[^1]}");
+4 -4
View File
@@ -58,7 +58,7 @@ public class SuperIndicatorTests
indicator.Initialize();
// After init, line series should exist (Up and Down)
Assert.Equal(2, indicator.LinesSeries.Length);
Assert.Equal(2, indicator.LinesSeries.Count);
}
[Fact]
@@ -73,7 +73,7 @@ public class SuperIndicatorTests
for (int i = 0; i < 20; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
// Process update for each bar to simulate history loading
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
@@ -83,7 +83,7 @@ public class SuperIndicatorTests
// One should be NaN, other should be value, or both NaN if cold
double up = indicator.LinesSeries[0].GetValue(0);
double down = indicator.LinesSeries[1].GetValue(0);
Assert.True(double.IsFinite(up) || double.IsFinite(down));
}
@@ -100,7 +100,7 @@ public class SuperIndicatorTests
}
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
// Add new bar
indicator.HistoricalData.AddBar(now.AddMinutes(20), 120, 130, 110, 125);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));

Some files were not shown because too many files have changed in this diff Show More