Files
QuanTAlib/lib/trends_FIR/hwma/Hwma.cs
T
2026-02-10 21:33:16 -08:00

363 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// HWMA: Holt-Winters Moving Average
/// </summary>
/// <remarks>
/// Triple exponential smoothing tracking level (F), velocity (V), and acceleration (A).
/// O(1) adaptive trend follower responding quickly via higher-order derivatives.
///
/// Calculation: <c>Output = F + V + 0.5×A</c> with recursive updates.
/// </remarks>
/// <seealso href="Hwma.md">Detailed documentation</seealso>
[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;
/// <summary>
/// Creates HWMA with specified period. Calculates α, β, γ automatically.
/// </summary>
/// <param name="period">Period for smoothing factor calculation (must be > 0)</param>
[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);
}
/// <summary>
/// Creates HWMA with explicit smoothing factors.
/// </summary>
/// <param name="alpha">Level smoothing factor (0 to 1)</param>
/// <param name="beta">Velocity smoothing factor (0 to 1)</param>
/// <param name="gamma">Acceleration smoothing factor (0 to 1)</param>
[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);
}
/// <summary>
/// Creates HWMA with source for event-based chaining.
/// </summary>
/// <param name="source">Data source for event-based updates</param>
/// <param name="period">Period for smoothing factor calculation (default: 10)</param>
[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<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);
// 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<double> source, TimeSpan? step = null)
{
foreach (var value in source)
{
Update(new TValue(DateTime.MinValue, value));
}
}
/// <summary>
/// Calculates HWMA from a TSeries using streaming updates.
/// </summary>
public static TSeries Batch(TSeries source, int period = 10)
{
var hwma = new Hwma(period);
return hwma.Update(source);
}
/// <summary>
/// Calculates HWMA over a span of values.
/// </summary>
/// <param name="source">Input values</param>
/// <param name="output">Output buffer (must be same length as source)</param>
/// <param name="period">Period for smoothing factors (default: 10)</param>
/// <exception cref="ArgumentException">Thrown when output length doesn't match source length.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Batch(ReadOnlySpan<double> source, Span<double> 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;
}
}