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;
}
}