feat: Add Cumulative Moving Average (CMA) implementation with detailed documentation

- Introduced Cma class for calculating the Cumulative Moving Average using Welford's algorithm with FMA for precision.
- Added methods for batch processing and streaming updates.
- Implemented a comprehensive markdown documentation for CMA, covering its mathematical foundation, performance profile, and use cases.
- Enhanced existing trend indicators (Bessel, Butter, Htit, Jma, Mama, Ssf, Vidya) with FMA for improved numerical stability and precision.
- Updated Adosc to utilize a single-pass algorithm for performance optimization.
- Fixed date initialization in benchmarks to ensure UTC consistency.
This commit is contained in:
Miha Kralj
2025-12-29 09:34:37 -08:00
parent 43ce6e63e4
commit 16a21a5b65
26 changed files with 1816 additions and 142 deletions
+1 -1
View File
@@ -74,4 +74,4 @@ Trend indicators are the bread and butter of technical analysis—and often just
| YZVAMA | Yang-Zhang Volatility Adjusted MA | |
| ZLDEMA | Zero-Lag Double Exponential MA | |
| ZLEMA | Zero-Lag Exponential MA | |
| ZLTEMA | Zero-Lag Triple Exponential MA | |
| ZLTEMA | Zero-Lag Triple Exponential MA | |
+6 -3
View File
@@ -135,7 +135,8 @@ public sealed class Bessel : AbstractBase, IDisposable
double filt = _state.Count < 3
? val
: (_c1 * val) + (_c2 * _state.F1) + (_c3 * _state.F2);
: Math.FusedMultiplyAdd(_c3, _state.F2,
Math.FusedMultiplyAdd(_c2, _state.F1, _c1 * val));
_state.F2 = _state.F1;
_state.F1 = filt;
@@ -184,7 +185,8 @@ public sealed class Bessel : AbstractBase, IDisposable
double filt = _state.Count < 3
? val
: (_c1 * val) + (_c2 * _state.F1) + (_c3 * _state.F2);
: Math.FusedMultiplyAdd(_c3, _state.F2,
Math.FusedMultiplyAdd(_c2, _state.F1, _c1 * val));
_state.F2 = _state.F1;
_state.F1 = filt;
@@ -276,7 +278,8 @@ public sealed class Bessel : AbstractBase, IDisposable
double filt = state.Count < 3
? val
: (c1 * val) + (c2 * state.F1) + (c3 * state.F2);
: Math.FusedMultiplyAdd(c3, state.F2,
Math.FusedMultiplyAdd(c2, state.F1, c1 * val));
state.F2 = state.F1;
state.F1 = filt;
+12 -2
View File
@@ -113,9 +113,14 @@ public sealed class Butter : AbstractBase
}
double x = input.Value;
// IIR: y = (b0*x + b1*x1 + b2*x2 - a1*y1 - a2*y2) * invA0
// Using chained FMA for precision
double y = _state.Count < 2
? x
: (_b0 * x + _b1 * _state.X1 + _b2 * _state.X2 - _a1 * _state.Y1 - _a2 * _state.Y2) * _invA0;
: Math.FusedMultiplyAdd(-_a2, _state.Y2,
Math.FusedMultiplyAdd(-_a1, _state.Y1,
Math.FusedMultiplyAdd(_b2, _state.X2,
Math.FusedMultiplyAdd(_b1, _state.X1, _b0 * x)))) * _invA0;
// Update state
_state.X2 = _state.X1;
@@ -185,9 +190,14 @@ public sealed class Butter : AbstractBase
destination[i] = i > 0 ? destination[i - 1] : initialLast;
continue;
}
// IIR: y = (b0*x + b1*x1 + b2*x2 - a1*y1 - a2*y2) * invA0
// Using chained FMA for precision
double y = i < 2
? x
: (b0 * x + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2) * invA0;
: Math.FusedMultiplyAdd(-a2, y2,
Math.FusedMultiplyAdd(-a1, y1,
Math.FusedMultiplyAdd(b2, x2,
Math.FusedMultiplyAdd(b1, x1, b0 * x)))) * invA0;
x2 = x1;
x1 = x;
+19 -19
View File
@@ -161,17 +161,17 @@ public sealed class Htit : AbstractBase
double i2_val = i1 - jQ;
double q2_val = q1 + jI;
// Smooth i2, q2
_state.I2 = 0.2 * i2_val + 0.8 * _p_state.I2;
_state.Q2 = 0.2 * q2_val + 0.8 * _p_state.Q2;
// Smooth i2, q2 (using FMA for precision)
_state.I2 = Math.FusedMultiplyAdd(0.2, i2_val, 0.8 * _p_state.I2);
_state.Q2 = Math.FusedMultiplyAdd(0.2, q2_val, 0.8 * _p_state.Q2);
// 6. Homodyne Discriminator
double re_val = (_state.I2 * _p_state.I2) + (_state.Q2 * _p_state.Q2);
double im_val = (_state.I2 * _p_state.Q2) - (_state.Q2 * _p_state.I2);
double re_val = Math.FusedMultiplyAdd(_state.I2, _p_state.I2, _state.Q2 * _p_state.Q2);
double im_val = Math.FusedMultiplyAdd(_state.I2, _p_state.Q2, -_state.Q2 * _p_state.I2);
// Smooth re, im
_state.Re = 0.2 * re_val + 0.8 * _p_state.Re;
_state.Im = 0.2 * im_val + 0.8 * _p_state.Im;
// Smooth re, im (using FMA)
_state.Re = Math.FusedMultiplyAdd(0.2, re_val, 0.8 * _p_state.Re);
_state.Im = Math.FusedMultiplyAdd(0.2, im_val, 0.8 * _p_state.Im);
// 7. Calculate Period
double angle = Math.Atan2(_state.Im, _state.Re);
@@ -190,9 +190,9 @@ public sealed class Htit : AbstractBase
if (period < 6) period = 6;
if (period > 50) period = 50;
// Smooth the period
_state.Period = 0.2 * period + 0.8 * prevPeriod;
_state.SmoothPeriod = 0.33 * _state.Period + 0.67 * _p_state.SmoothPeriod;
// Smooth the period (using FMA)
_state.Period = Math.FusedMultiplyAdd(0.2, period, 0.8 * prevPeriod);
_state.SmoothPeriod = Math.FusedMultiplyAdd(0.33, _state.Period, 0.67 * _p_state.SmoothPeriod);
// 8. Instantaneous Trend
int dcPeriods = (int)(double.IsNaN(_state.SmoothPeriod) ? 0 : _state.SmoothPeriod + 0.5);
@@ -376,15 +376,15 @@ public sealed class Htit : AbstractBase
double i2_val = i1 - jQ;
double q2_val = q1 + jI;
i2 = 0.2 * i2_val + 0.8 * p_i2;
q2 = 0.2 * q2_val + 0.8 * p_q2;
i2 = Math.FusedMultiplyAdd(0.2, i2_val, 0.8 * p_i2);
q2 = Math.FusedMultiplyAdd(0.2, q2_val, 0.8 * p_q2);
// 6. Homodyne Discriminator
double re_val = (i2 * p_i2) + (q2 * p_q2);
double im_val = (i2 * p_q2) - (q2 * p_i2);
double re_val = Math.FusedMultiplyAdd(i2, p_i2, q2 * p_q2);
double im_val = Math.FusedMultiplyAdd(i2, p_q2, -q2 * p_i2);
re = 0.2 * re_val + 0.8 * p_re;
im = 0.2 * im_val + 0.8 * p_im;
re = Math.FusedMultiplyAdd(0.2, re_val, 0.8 * p_re);
im = Math.FusedMultiplyAdd(0.2, im_val, 0.8 * p_im);
// 7. Calculate Period
double angle = Math.Atan2(im, re);
@@ -402,8 +402,8 @@ public sealed class Htit : AbstractBase
if (newPeriod < 6) newPeriod = 6;
if (newPeriod > 50) newPeriod = 50;
period = 0.2 * newPeriod + 0.8 * p_period;
smoothPeriod = 0.33 * period + 0.67 * p_smoothPeriod;
period = Math.FusedMultiplyAdd(0.2, newPeriod, 0.8 * p_period);
smoothPeriod = Math.FusedMultiplyAdd(0.33, period, 0.67 * p_smoothPeriod);
// 8. Instantaneous Trend
double safeSmooth = double.IsNaN(smoothPeriod) ? 0 : smoothPeriod;
+9 -5
View File
@@ -216,13 +216,17 @@ public sealed class Jma : AbstractBase
prevJma = value;
double alpha = Math.Exp(_logLengthDivider * d);
double decay = 1.0 - alpha;
double alpha2 = alpha * alpha;
double c0 = (1.0 - alpha) * value + alpha * _state.LastC0;
double c8 = (value - c0) * (1.0 - _lengthDivider) + _lengthDivider * _state.LastC8;
double a8 = (_phaseParam * c8 + c0 - prevJma) *
(alpha * (-2.0) + alpha2 + 1.0) +
alpha2 * _state.LastA8;
// EMA smoothing: c0 = decay * value + alpha * LastC0
double c0 = Math.FusedMultiplyAdd(_state.LastC0, alpha, decay * value);
// EMA smoothing: c8 = (value - c0) * (1 - lengthDivider) + lengthDivider * LastC8
double lengthDecay = 1.0 - _lengthDivider;
double c8 = Math.FusedMultiplyAdd(_state.LastC8, _lengthDivider, lengthDecay * (value - c0));
// IIR filter: a8 = (phase * c8 + c0 - prevJma) * coef + alpha2 * LastA8
double coef = Math.FusedMultiplyAdd(alpha, -2.0, alpha2 + 1.0);
double a8 = Math.FusedMultiplyAdd(_state.LastA8, alpha2, Math.FusedMultiplyAdd(_phaseParam, c8, c0 - prevJma) * coef);
double jma = prevJma + a8;
+20 -20
View File
@@ -170,17 +170,17 @@ public sealed class Mama : AbstractBase
double i2_val = i1 - jQ;
double q2_val = q1 + jI;
// Smooth i2, q2
_state.I2 = SmoothCoef * i2_val + SmoothPrev * _p_state.I2;
_state.Q2 = SmoothCoef * q2_val + SmoothPrev * _p_state.Q2;
// Smooth i2, q2 (using FMA for precision)
_state.I2 = Math.FusedMultiplyAdd(SmoothCoef, i2_val, SmoothPrev * _p_state.I2);
_state.Q2 = Math.FusedMultiplyAdd(SmoothCoef, q2_val, SmoothPrev * _p_state.Q2);
// Homodyne discriminator
double re_val = (_state.I2 * _p_state.I2) + (_state.Q2 * _p_state.Q2);
double im_val = (_state.I2 * _p_state.Q2) - (_state.Q2 * _p_state.I2);
double re_val = Math.FusedMultiplyAdd(_state.I2, _p_state.I2, _state.Q2 * _p_state.Q2);
double im_val = Math.FusedMultiplyAdd(_state.I2, _p_state.Q2, -_state.Q2 * _p_state.I2);
// Smooth re, im
_state.Re = SmoothCoef * re_val + SmoothPrev * _p_state.Re;
_state.Im = SmoothCoef * im_val + SmoothPrev * _p_state.Im;
// Smooth re, im (using FMA)
_state.Re = Math.FusedMultiplyAdd(SmoothCoef, re_val, SmoothPrev * _p_state.Re);
_state.Im = Math.FusedMultiplyAdd(SmoothCoef, im_val, SmoothPrev * _p_state.Im);
// Calculate Period
double angle = Math.Atan2(_state.Im, _state.Re);
@@ -198,8 +198,8 @@ public sealed class Mama : AbstractBase
if (period < MinPeriod) period = MinPeriod;
if (period > MaxPeriod) period = MaxPeriod;
// Smooth Period
_state.Period = SmoothCoef * period + SmoothPrev * _p_state.Period;
// Smooth Period (using FMA)
_state.Period = Math.FusedMultiplyAdd(SmoothCoef, period, SmoothPrev * _p_state.Period);
// Phase calculation
_state.Phase = Math.Atan2(q1, i1);
@@ -380,17 +380,17 @@ public sealed class Mama : AbstractBase
double i2_val = i1 - jQ;
double q2_val = q1 + jI;
// Smooth i2, q2
i2 = SmoothCoef * i2_val + SmoothPrev * p_i2;
q2 = SmoothCoef * q2_val + SmoothPrev * p_q2;
// Smooth i2, q2 (using FMA for precision)
i2 = Math.FusedMultiplyAdd(SmoothCoef, i2_val, SmoothPrev * p_i2);
q2 = Math.FusedMultiplyAdd(SmoothCoef, q2_val, SmoothPrev * p_q2);
// Homodyne discriminator
double re_val = (i2 * p_i2) + (q2 * p_q2);
double im_val = (i2 * p_q2) - (q2 * p_i2);
double re_val = Math.FusedMultiplyAdd(i2, p_i2, q2 * p_q2);
double im_val = Math.FusedMultiplyAdd(i2, p_q2, -q2 * p_i2);
// Smooth re, im
re = SmoothCoef * re_val + SmoothPrev * p_re;
im = SmoothCoef * im_val + SmoothPrev * p_im;
// Smooth re, im (using FMA)
re = Math.FusedMultiplyAdd(SmoothCoef, re_val, SmoothPrev * p_re);
im = Math.FusedMultiplyAdd(SmoothCoef, im_val, SmoothPrev * p_im);
// Calculate Period
double angle = Math.Atan2(im, re);
@@ -408,8 +408,8 @@ public sealed class Mama : AbstractBase
if (newPeriod < MinPeriod) newPeriod = MinPeriod;
if (newPeriod > MaxPeriod) newPeriod = MaxPeriod;
// Smooth Period
period = SmoothCoef * newPeriod + SmoothPrev * p_period;
// Smooth Period (using FMA)
period = Math.FusedMultiplyAdd(SmoothCoef, newPeriod, SmoothPrev * p_period);
// Phase calculation
double phase = Math.Atan2(q1, i1);
+6 -3
View File
@@ -127,7 +127,8 @@ public sealed class Ssf : AbstractBase
double ssf = (_state.Count < 4)
? val
: (_c1 * (val + _state.PrevInput) * 0.5) + (_c2 * _state.Ssf1) + (_c3 * _state.Ssf2);
: Math.FusedMultiplyAdd(_c3, _state.Ssf2,
Math.FusedMultiplyAdd(_c2, _state.Ssf1, _c1 * (val + _state.PrevInput) * 0.5));
_state.Ssf2 = _state.Ssf1;
_state.Ssf1 = ssf;
@@ -177,7 +178,8 @@ public sealed class Ssf : AbstractBase
double ssf = (_state.Count < 4)
? val
: (_c1 * (val + _state.PrevInput) * 0.5) + (_c2 * _state.Ssf1) + (_c3 * _state.Ssf2);
: Math.FusedMultiplyAdd(_c3, _state.Ssf2,
Math.FusedMultiplyAdd(_c2, _state.Ssf1, _c1 * (val + _state.PrevInput) * 0.5));
_state.Ssf2 = _state.Ssf1;
_state.Ssf1 = ssf;
@@ -268,7 +270,8 @@ public sealed class Ssf : AbstractBase
double ssf = (state.Count < 4)
? val
: (c1 * (val + state.PrevInput) * 0.5) + (c2 * state.Ssf1) + (c3 * state.Ssf2);
: Math.FusedMultiplyAdd(c3, state.Ssf2,
Math.FusedMultiplyAdd(c2, state.Ssf1, c1 * (val + state.PrevInput) * 0.5));
state.Ssf2 = state.Ssf1;
state.Ssf1 = ssf;
+6 -3
View File
@@ -131,7 +131,8 @@ public sealed class Vidya : AbstractBase, IDisposable
}
double dynamicAlpha = _alpha * vi;
_state.CurrentVidya = dynamicAlpha * price + (1.0 - dynamicAlpha) * _state.LastVidya;
double dynamicDecay = 1.0 - dynamicAlpha;
_state.CurrentVidya = Math.FusedMultiplyAdd(_state.LastVidya, dynamicDecay, dynamicAlpha * price);
_state.CurrentClose = price;
Last = new TValue(input.Time, _state.CurrentVidya);
@@ -239,7 +240,8 @@ public sealed class Vidya : AbstractBase, IDisposable
}
double dynamicAlpha = _alpha * vi;
double currentVidya = dynamicAlpha * price + (1.0 - dynamicAlpha) * lastVidya;
double dynamicDecay = 1.0 - dynamicAlpha;
double currentVidya = Math.FusedMultiplyAdd(lastVidya, dynamicDecay, dynamicAlpha * price);
_state.CurrentVidya = currentVidya;
_state.CurrentClose = price;
@@ -337,7 +339,8 @@ public sealed class Vidya : AbstractBase, IDisposable
}
double dynamicAlpha = alpha * vi;
double currentVidya = dynamicAlpha * price + (1.0 - dynamicAlpha) * lastVidya;
double dynamicDecay = 1.0 - dynamicAlpha;
double currentVidya = Math.FusedMultiplyAdd(lastVidya, dynamicDecay, dynamicAlpha * price);
output[i] = currentVidya;