using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// HWMA: Holt-Winters Moving Average /// /// /// Triple exponential smoothing tracking level (F), velocity (V), and acceleration (A). /// O(1) adaptive trend follower responding quickly via higher-order derivatives. /// /// Calculation: Output = F + V + 0.5×A with recursive updates. /// /// Detailed documentation [SkipLocalsInit] public sealed class Hwma : AbstractBase { private readonly double _alpha; private readonly double _beta; private readonly double _gamma; private readonly double _decayAlpha; private readonly double _decayBeta; private readonly double _decayGamma; private readonly ITValuePublisher? _source; private readonly TValuePublishedHandler? _pubHandler; private bool _isNew = true; private bool _disposed; [StructLayout(LayoutKind.Auto)] private record struct State( double F, double V, double A, double LastValidValue, bool IsInitialized ); private State _state; private State _p_state; public bool IsNew => _isNew; public override bool IsHot => _state.IsInitialized; /// /// Creates HWMA with specified period. Calculates α, β, γ automatically. /// /// Period for smoothing factor calculation (must be > 0) [MethodImpl(MethodImplOptions.AggressiveInlining)] public Hwma(int period = 10) { if (period <= 0) { throw new ArgumentException("Period must be greater than 0", nameof(period)); } _alpha = 2.0 / (period + 1.0); _beta = 1.0 / period; _gamma = 1.0 / period; _decayAlpha = 1.0 - _alpha; _decayBeta = 1.0 - _beta; _decayGamma = 1.0 - _gamma; Name = $"Hwma({period})"; WarmupPeriod = period; _state = new State(double.NaN, 0, 0, double.NaN, IsInitialized: false); } /// /// Creates HWMA with explicit smoothing factors. /// /// Level smoothing factor (0 to 1) /// Velocity smoothing factor (0 to 1) /// Acceleration smoothing factor (0 to 1) [MethodImpl(MethodImplOptions.AggressiveInlining)] public Hwma(double alpha, double beta, double gamma) { if (alpha <= 0 || alpha > 1) { throw new ArgumentException("Alpha must be between 0 (exclusive) and 1 (inclusive)", nameof(alpha)); } if (beta < 0 || beta > 1) { throw new ArgumentException("Beta must be between 0 and 1", nameof(beta)); } if (gamma < 0 || gamma > 1) { throw new ArgumentException("Gamma must be between 0 and 1", nameof(gamma)); } int effectivePeriod = (int)(2.0 / alpha - 1.0); // Reverse calculate for display _alpha = alpha; _beta = beta; _gamma = gamma; _decayAlpha = 1.0 - alpha; _decayBeta = 1.0 - beta; _decayGamma = 1.0 - gamma; Name = $"Hwma({alpha:F3},{beta:F3},{gamma:F3})"; WarmupPeriod = effectivePeriod > 0 ? effectivePeriod : 10; _state = new State(double.NaN, 0, 0, double.NaN, IsInitialized: false); } /// /// Creates HWMA with source for event-based chaining. /// /// Data source for event-based updates /// Period for smoothing factor calculation (default: 10) [MethodImpl(MethodImplOptions.AggressiveInlining)] public Hwma(ITValuePublisher source, int period = 10) : this(period) { _source = source; _pubHandler = Handle; _source.Pub += _pubHandler; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew); protected override void Dispose(bool disposing) { if (!_disposed) { if (disposing && _source != null && _pubHandler != null) { _source.Pub -= _pubHandler; } _disposed = true; } base.Dispose(disposing); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private double GetValidValue(double input) { if (double.IsFinite(input)) { return input; } return _state.IsInitialized ? _state.LastValidValue : double.NaN; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public override TValue Update(TValue input, bool isNew = true) { _isNew = isNew; return Update(input, isNew, publish: true); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private TValue Update(TValue input, bool isNew, bool publish) { if (isNew) { _p_state = _state; } else { _state = _p_state; } double val = GetValidValue(input.Value); if (!double.IsFinite(val)) { // First value is NaN - return NaN Last = new TValue(input.Time, double.NaN); if (publish) { PubEvent(Last); } return Last; } _state = _state with { LastValidValue = val }; double result; if (!_state.IsInitialized) { // First valid value: initialize F to source, V and A to 0 _state = _state with { F = val, V = 0, A = 0, IsInitialized = true }; result = val; } else { double prevF = _state.F; double prevV = _state.V; double prevA = _state.A; // F = α × source + (1-α) × (prevF + prevV + 0.5 × prevA) double forecast = prevF + prevV + 0.5 * prevA; double newF = Math.FusedMultiplyAdd(forecast, _decayAlpha, _alpha * val); // V = β × (F - prevF) + (1-β) × (prevV + prevA) double newV = Math.FusedMultiplyAdd(prevV + prevA, _decayBeta, _beta * (newF - prevF)); // A = γ × (V - prevV) + (1-γ) × prevA double newA = Math.FusedMultiplyAdd(prevA, _decayGamma, _gamma * (newV - prevV)); _state = _state with { F = newF, V = newV, A = newA }; // output = F + V + 0.5 × A result = newF + newV + 0.5 * newA; } Last = new TValue(input.Time, result); if (publish) { PubEvent(Last); } return Last; } public override TSeries Update(TSeries source) { if (source.Count == 0) { return new TSeries([], []); } int len = source.Count; var t = new List(len); var v = new List(len); CollectionsMarshal.SetCount(t, len); CollectionsMarshal.SetCount(v, len); var tSpan = CollectionsMarshal.AsSpan(t); var vSpan = CollectionsMarshal.AsSpan(v); source.Times.CopyTo(tSpan); // HWMA has IIR filter state (F, V, A) that accumulates from the beginning. // Must process entire series through streaming to maintain correct state. Reset(); for (int i = 0; i < len; i++) { var result = Update(source[i], isNew: true, publish: false); vSpan[i] = result.Value; } return new TSeries(t, v); } public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { foreach (var value in source) { Update(new TValue(DateTime.MinValue, value)); } } /// /// Calculates HWMA from a TSeries using streaming updates. /// public static TSeries Batch(TSeries source, int period = 10) { var hwma = new Hwma(period); return hwma.Update(source); } /// /// Calculates HWMA over a span of values. /// /// Input values /// Output buffer (must be same length as source) /// Period for smoothing factors (default: 10) /// Thrown when output length doesn't match source length. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Batch(ReadOnlySpan source, Span output, int period = 10) { if (period <= 0) { throw new ArgumentException("Period must be greater than 0", nameof(period)); } if (source.Length != output.Length) { throw new ArgumentException("Source and output must have the same length", nameof(output)); } if (source.Length == 0) { return; } double alpha = 2.0 / (period + 1.0); double beta = 1.0 / period; double gamma = 1.0 / period; double decayAlpha = 1.0 - alpha; double decayBeta = 1.0 - beta; double decayGamma = 1.0 - gamma; double lastValid = double.NaN; double F = double.NaN; double V = 0; double A = 0; bool initialized = false; for (int i = 0; i < source.Length; i++) { double val = source[i]; // Handle NaN - use last valid if (!double.IsFinite(val)) { if (double.IsFinite(lastValid)) { val = lastValid; } else { output[i] = double.NaN; // No valid value yet continue; } } lastValid = val; if (!initialized) { F = val; V = 0; A = 0; initialized = true; output[i] = val; } else { double prevF = F; double prevV = V; double prevA = A; // F = α × source + (1-α) × (prevF + prevV + 0.5 × prevA) F = Math.FusedMultiplyAdd(prevF + prevV + 0.5 * prevA, decayAlpha, alpha * val); // V = β × (F - prevF) + (1-β) × (prevV + prevA) V = Math.FusedMultiplyAdd(prevV + prevA, decayBeta, beta * (F - prevF)); // A = γ × (V - prevV) + (1-γ) × prevA A = Math.FusedMultiplyAdd(prevA, decayGamma, gamma * (V - prevV)); // output = F + V + 0.5 × A output[i] = F + V + 0.5 * A; } } } public static (TSeries Results, Hwma Indicator) Calculate(TSeries source, int period = 10) { var indicator = new Hwma(period); TSeries results = indicator.Update(source); return (results, indicator); } public override void Reset() { _state = new State(double.NaN, 0, 0, double.NaN, IsInitialized: false); _p_state = _state; Last = default; } }