using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// APCHANNEL: Adaptive Price Channel /// An adaptive channel that uses exponential moving averages of highs and lows /// with a configurable smoothing factor (alpha). /// /// /// The APCHANNEL creates dynamic support and resistance levels by applying /// exponential smoothing to price highs and lows. The alpha parameter controls /// the sensitivity: higher alpha (closer to 1) makes the channel more responsive, /// while lower alpha creates smoother, slower-moving bands. /// /// Key characteristics: /// - Exponential weighting for recent price action /// - Adaptive to volatility through alpha parameter /// - Zero-allocation O(1) updates via FMA optimization /// - Provides dynamic support/resistance zones /// [SkipLocalsInit] public sealed class Apchannel : AbstractBase { private readonly double _alpha; private readonly double _decay; private TBarSeries? _source; private bool _disposed; [StructLayout(LayoutKind.Auto)] private record struct State( double HighEma, double LowEma, double LastValidHigh, double LastValidLow, int Count ); private State _state; private State _p_state; /// /// True if the indicator has enough data to produce valid results. /// public override bool IsHot => _state.Count >= WarmupPeriod; /// /// Gets the current value of the upper band (exponential moving average of highs). /// public double UpperBand => _state.HighEma; /// /// Gets the current value of the lower band (exponential moving average of lows). /// public double LowerBand => _state.LowEma; [MethodImpl(MethodImplOptions.AggressiveInlining)] public Apchannel(double alpha = 0.2) { if (alpha <= 0 || alpha > 1) { throw new ArgumentOutOfRangeException(nameof(alpha), "Alpha must be greater than 0 and less than or equal to 1."); } _alpha = alpha; _decay = 1.0 - alpha; WarmupPeriod = (int)Math.Ceiling(3.0 / alpha); // ~95% convergence Name = $"Apchannel({alpha:F2})"; Init(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public Apchannel(TBarSeries source, double alpha = 0.2) : this(alpha) { _source = source; source.Pub += Handle; } /// /// Releases resources and unsubscribes from the source event. /// protected override void Dispose(bool disposing) { if (_disposed) { return; } _disposed = true; if (disposing && _source != null) { _source.Pub -= Handle; _source = null; } base.Dispose(disposing); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Init() { _state = new State(0, 0, 0, 0, 0); _p_state = _state; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Handle(object? source, in TBarEventArgs args) => _ = Add(args.Value, args.IsNew); [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ManageState(bool isNew) { if (isNew) { _p_state = _state; _state = _state with { Count = _state.Count + 1 }; } else { _state = _p_state; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void UpdateCore(double high, double low, long time, bool isNew) { ManageState(isNew); double validHigh = double.IsFinite(high) ? high : _state.LastValidHigh; double validLow = double.IsFinite(low) ? low : _state.LastValidLow; double highEma, lowEma; if (_state.Count == 1) { highEma = validHigh; lowEma = validLow; } else { highEma = Math.FusedMultiplyAdd(_decay, _state.HighEma, _alpha * validHigh); lowEma = Math.FusedMultiplyAdd(_decay, _state.LowEma, _alpha * validLow); } _state = _state with { HighEma = highEma, LowEma = lowEma, LastValidHigh = validHigh, LastValidLow = validLow, }; double mid = (highEma + lowEma) * 0.5; Last = new TValue(time, mid); PubEvent(Last, isNew); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Reset() { Init(); Last = new TValue(0, 0); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Add(TBar bar, bool isNew = true) => Update(bar, isNew); [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar bar, bool isNew = true) { UpdateCore(bar.High, bar.Low, bar.Time, isNew); return Last; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public TSeries Update(TBarSeries source) { if (source.Count == 0) { return []; } 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); Reset(); for (int i = 0; i < len; i++) { var val = Update(source[i], isNew: true); tSpan[i] = val.Time; vSpan[i] = val.Value; } return new TSeries(t, v); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public override TValue Update(TValue input, bool isNew = true) { UpdateCore(input.Value, input.Value, input.Time, isNew); return Last; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public override TSeries Update(TSeries source) { if (source.Count == 0) { return []; } 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); Reset(); for (int i = 0; i < len; i++) { var val = Update(source[i], isNew: true); tSpan[i] = val.Time; vSpan[i] = val.Value; } return new TSeries(t, v); } public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { Init(); if (source.Length == 0) { return; } long time = DateTime.UtcNow.Ticks; long dt = step?.Ticks ?? TimeSpan.TicksPerMinute; for (int i = 0; i < source.Length; i++) { Update(new TValue(time, source[i]), isNew: true); time += dt; } } /// /// Calculates the Adaptive Price Channel for the entire series and returns both /// the result series and a primed indicator instance for continued streaming. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static (TBarSeries Results, Apchannel Indicator) Calculate( TBarSeries source, double alpha = 0.2) { var indicator = new Apchannel(alpha); var results = new TBarSeries(); foreach (var bar in source) { _ = indicator.Add(bar); results.Add(bar.Time, indicator.UpperBand, indicator.UpperBand, indicator.LowerBand, indicator.LowerBand, 0); } return (results, indicator); } /// /// Calculates the Adaptive Price Channel using span-based batch processing. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Batch( ReadOnlySpan sourceHigh, ReadOnlySpan sourceLow, Span upperBand, Span lowerBand, double alpha = 0.2) { int length = sourceHigh.Length; if (sourceLow.Length != length) { throw new ArgumentException("Source arrays must have the same length.", nameof(sourceLow)); } if (upperBand.Length != length) { throw new ArgumentException("Upper band array must match source length.", nameof(upperBand)); } if (lowerBand.Length != length) { throw new ArgumentException("Lower band array must match source length.", nameof(lowerBand)); } if (alpha <= 0 || alpha > 1) { throw new ArgumentOutOfRangeException(nameof(alpha), "Alpha must be greater than 0 and less than or equal to 1."); } if (length == 0) { return; } double decay = 1.0 - alpha; CalculateScalar(sourceHigh, sourceLow, upperBand, lowerBand, alpha, decay); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void CalculateScalar( ReadOnlySpan sourceHigh, ReadOnlySpan sourceLow, Span upperBand, Span lowerBand, double alpha, double decay) { int length = sourceHigh.Length; // Scan for first finite values in both high and low arrays double lastValidHigh = 0; double lastValidLow = 0; int firstValidIdx = 0; for (int i = 0; i < length; i++) { if (double.IsFinite(sourceHigh[i]) && double.IsFinite(sourceLow[i])) { lastValidHigh = sourceHigh[i]; lastValidLow = sourceLow[i]; firstValidIdx = i; break; } } // Fill NaN for indices before first valid for (int i = 0; i < firstValidIdx; i++) { upperBand[i] = double.NaN; lowerBand[i] = double.NaN; } // Initialize with first valid values double highEma = lastValidHigh; double lowEma = lastValidLow; upperBand[firstValidIdx] = highEma; lowerBand[firstValidIdx] = lowEma; // Early return if no more elements after first valid if (firstValidIdx >= length - 1) { return; } for (int i = firstValidIdx + 1; i < length; i++) { double high = sourceHigh[i]; double low = sourceLow[i]; // Handle NaN/Infinity if (!double.IsFinite(high)) { high = lastValidHigh; } if (!double.IsFinite(low)) { low = lastValidLow; } // Use FMA for optimal performance and precision highEma = Math.FusedMultiplyAdd(decay, highEma, alpha * high); lowEma = Math.FusedMultiplyAdd(decay, lowEma, alpha * low); upperBand[i] = highEma; lowerBand[i] = lowEma; lastValidHigh = high; lastValidLow = low; } } }