using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// HWC: Holt-Winter Channel /// /// /// Volatility channel built around the Holt-Winters Moving Average (HWMA). /// Bands are placed at ±multiplier × √(filt) where filt is an EMA-smoothed /// squared forecast error, giving adaptive-width bands that widen with /// prediction error and contract when the HWMA tracks price well. /// /// Calculation: /// Middle = HWMA(source), /// filt = α×(source − forecast)² + (1−α)×prev_filt, /// Upper = Middle + mult×√filt, /// Lower = Middle − mult×√filt. /// [SkipLocalsInit] public sealed class Hwc : 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 double _multiplier; [StructLayout(LayoutKind.Auto)] private record struct State( double F, double V, double A, double Filt, double LastValidValue, bool IsInitialized ); private State _state; private State _p_state; public override bool IsHot => _state.IsInitialized; /// Upper band = HWMA + mult × √filt public TValue Upper { get; private set; } /// Middle band = HWMA output public TValue Middle { get; private set; } /// Lower band = HWMA − mult × √filt public TValue Lower { get; private set; } // ────────────────────────── constructors ────────────────────────── /// /// Creates HWC with auto-derived α/β/γ from period. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public Hwc(int period = 20, double multiplier = 1.0) { if (period <= 0) { throw new ArgumentException("Period must be > 0", nameof(period)); } if (multiplier <= 0) { throw new ArgumentException("Multiplier must be > 0", nameof(multiplier)); } _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; _multiplier = multiplier; WarmupPeriod = period; Name = $"Hwc({period},{multiplier:F1})"; _state = new State(double.NaN, 0, 0, 0, double.NaN, false); } /// /// Creates HWC with explicit smoothing factors. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public Hwc(double alpha, double beta, double gamma, double multiplier = 1.0) { if (alpha is <= 0 or > 1) { throw new ArgumentException("Alpha must be (0,1]", nameof(alpha)); } if (beta is < 0 or > 1) { throw new ArgumentException("Beta must be [0,1]", nameof(beta)); } if (gamma is < 0 or > 1) { throw new ArgumentException("Gamma must be [0,1]", nameof(gamma)); } if (multiplier <= 0) { throw new ArgumentException("Multiplier must be > 0", nameof(multiplier)); } _alpha = alpha; _beta = beta; _gamma = gamma; _decayAlpha = 1.0 - alpha; _decayBeta = 1.0 - beta; _decayGamma = 1.0 - gamma; _multiplier = multiplier; int effectivePeriod = Math.Max((int)(2.0 / alpha - 1.0), 1); WarmupPeriod = effectivePeriod; Name = $"Hwc({alpha:F3},{beta:F3},{gamma:F3},{multiplier:F1})"; _state = new State(double.NaN, 0, 0, 0, double.NaN, false); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public Hwc(ITValuePublisher source, int period = 20, double multiplier = 1.0) : this(period, multiplier) { source.Pub += Handle; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew); [MethodImpl(MethodImplOptions.AggressiveInlining)] private double GetValidValue(double input) { if (double.IsFinite(input)) { return input; } return _state.IsInitialized ? _state.LastValidValue : double.NaN; } // ────────────────────────── core Update ────────────────────────── [MethodImpl(MethodImplOptions.AggressiveInlining)] public override TValue Update(TValue input, bool isNew = true) { if (isNew) { _p_state = _state; } else { _state = _p_state; } double val = GetValidValue(input.Value); if (!double.IsFinite(val)) { Last = new TValue(input.Time, double.NaN); Upper = Middle = Lower = Last; PubEvent(Last); return Last; } _state = _state with { LastValidValue = val }; double result; double filtVal; if (!_state.IsInitialized) { _state = _state with { F = val, V = 0, A = 0, Filt = 0, IsInitialized = true }; result = val; filtVal = 0; } else { double prevF = _state.F; double prevV = _state.V; double prevA = _state.A; // HWMA: F = α×src + (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)); result = newF + newV + 0.5 * newA; // Adaptive volatility filter: filt = α×(src − forecast)² + (1−α)×prevFilt double err = val - forecast; filtVal = Math.FusedMultiplyAdd(err * err, _alpha, _state.Filt * _decayAlpha); _state = _state with { F = newF, V = newV, A = newA, Filt = filtVal }; } double band = _multiplier * Math.Sqrt(filtVal); Last = new TValue(input.Time, result); Middle = Last; Upper = new TValue(input.Time, result + band); Lower = new TValue(input.Time, result - band); PubEvent(Last); return Last; } // ────────────────────────── Update(TSeries) ────────────────────────── public override TSeries Update(TSeries source) { if (source == null) { throw new ArgumentNullException(nameof(source)); } int len = source.Count; TSeries middleSeries = new(capacity: len); Reset(); for (int i = 0; i < len; i++) { TValue input = source[i]; Update(input, isNew: true); middleSeries.Add(input.Time, Middle.Value, isNew: true); } return middleSeries; } // ────────────────────────── Prime ────────────────────────── public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { step ??= TimeSpan.FromSeconds(1); DateTime startTime = DateTime.UtcNow; for (int i = 0; i < source.Length; i++) { Update(new TValue(startTime + i * step.Value, source[i]), isNew: true); } } // ────────────────────────── Reset ────────────────────────── [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Reset() { _state = new State(double.NaN, 0, 0, 0, double.NaN, false); _p_state = _state; Upper = Middle = Lower = default; } // ────────────────────────── static Batch (Span) ────────────────────────── public static void Batch( ReadOnlySpan source, Span upper, Span middle, Span lower, int period = 20, double multiplier = 1.0) { int n = source.Length; if (n != upper.Length || n != middle.Length || n != lower.Length) { throw new ArgumentException("All spans must have the same length", nameof(source)); } double alpha = 2.0 / (period + 1.0); double beta = 1.0 / period; double gamma = 1.0 / period; double dA = 1.0 - alpha; double dB = 1.0 - beta; double dG = 1.0 - gamma; double f = double.NaN, v = 0, a = 0, filt = 0; bool init = false; double lastValid = double.NaN; for (int i = 0; i < n; i++) { double val = source[i]; if (!double.IsFinite(val)) { val = double.IsFinite(lastValid) ? lastValid : 0; } else { lastValid = val; } double result; if (!init) { f = val; v = 0; a = 0; filt = 0; init = true; result = val; } else { double forecast = f + v + 0.5 * a; double newF = Math.FusedMultiplyAdd(forecast, dA, alpha * val); double newV = Math.FusedMultiplyAdd(v + a, dB, beta * (newF - f)); double newA = Math.FusedMultiplyAdd(a, dG, gamma * (newV - v)); result = newF + newV + 0.5 * newA; double err = val - forecast; filt = Math.FusedMultiplyAdd(err * err, alpha, filt * dA); f = newF; v = newV; a = newA; } double band = multiplier * Math.Sqrt(filt); middle[i] = result; upper[i] = result + band; lower[i] = result - band; } } // ────────────────────────── static Batch (TSeries) ────────────────────────── public static (TSeries Upper, TSeries Middle, TSeries Lower) Batch( TSeries source, int period = 20, double multiplier = 1.0) { var ind = new Hwc(period, multiplier); int len = source.Count; if (len == 0) { return ([], [], []); } var tU = new List(len); var vU = new List(len); var tM = new List(len); var vM = new List(len); var tL = new List(len); var vL = new List(len); CollectionsMarshal.SetCount(tU, len); CollectionsMarshal.SetCount(vU, len); CollectionsMarshal.SetCount(tM, len); CollectionsMarshal.SetCount(vM, len); CollectionsMarshal.SetCount(tL, len); CollectionsMarshal.SetCount(vL, len); var tuSpan = CollectionsMarshal.AsSpan(tU); var vuSpan = CollectionsMarshal.AsSpan(vU); var tmSpan = CollectionsMarshal.AsSpan(tM); var vmSpan = CollectionsMarshal.AsSpan(vM); var tlSpan = CollectionsMarshal.AsSpan(tL); var vlSpan = CollectionsMarshal.AsSpan(vL); for (int i = 0; i < len; i++) { ind.Update(source[i], isNew: true); long time = source[i].Time; tuSpan[i] = time; vuSpan[i] = ind.Upper.Value; tmSpan[i] = time; vmSpan[i] = ind.Middle.Value; tlSpan[i] = time; vlSpan[i] = ind.Lower.Value; } return (new TSeries(tU, vU), new TSeries(tM, vM), new TSeries(tL, vL)); } public static ((TSeries Upper, TSeries Middle, TSeries Lower) Results, Hwc Indicator) Calculate( TSeries source, int period = 20, double multiplier = 1.0) { var ind = new Hwc(period, multiplier); int len = source.Count; if (len == 0) { return (([], [], []), ind); } var tU = new List(len); var vU = new List(len); var tM = new List(len); var vM = new List(len); var tL = new List(len); var vL = new List(len); CollectionsMarshal.SetCount(tU, len); CollectionsMarshal.SetCount(vU, len); CollectionsMarshal.SetCount(tM, len); CollectionsMarshal.SetCount(vM, len); CollectionsMarshal.SetCount(tL, len); CollectionsMarshal.SetCount(vL, len); var tuSpan = CollectionsMarshal.AsSpan(tU); var vuSpan = CollectionsMarshal.AsSpan(vU); var tmSpan = CollectionsMarshal.AsSpan(tM); var vmSpan = CollectionsMarshal.AsSpan(vM); var tlSpan = CollectionsMarshal.AsSpan(tL); var vlSpan = CollectionsMarshal.AsSpan(vL); for (int i = 0; i < len; i++) { ind.Update(source[i], isNew: true); long time = source[i].Time; tuSpan[i] = time; vuSpan[i] = ind.Upper.Value; tmSpan[i] = time; vmSpan[i] = ind.Middle.Value; tlSpan[i] = time; vlSpan[i] = ind.Lower.Value; } return ((new TSeries(tU, vU), new TSeries(tM, vM), new TSeries(tL, vL)), ind); } }