mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-17 18:18:04 +00:00
refactoring
This commit is contained in:
+2
-2
@@ -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 |
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
+713
-711
File diff suppressed because it is too large
Load Diff
+83
-83
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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++)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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++)
|
||||
{
|
||||
|
||||
@@ -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
@@ -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];
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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++)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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++)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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 < 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})";
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]}");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
```
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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]}");
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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]}");
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user