Files
2026-02-10 21:33:16 -08:00

400 lines
11 KiB
C#

using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// APCHANNEL: Adaptive Price Channel
/// An adaptive channel that uses exponential moving averages of highs and lows
/// with a configurable smoothing factor (alpha).
/// </summary>
/// <remarks>
/// 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
/// </remarks>
[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;
/// <summary>
/// True if the indicator has enough data to produce valid results.
/// </summary>
public override bool IsHot => _state.Count >= WarmupPeriod;
/// <summary>
/// Gets the current value of the upper band (exponential moving average of highs).
/// </summary>
public double UpperBand => _state.HighEma;
/// <summary>
/// Gets the current value of the lower band (exponential moving average of lows).
/// </summary>
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;
}
/// <summary>
/// Releases resources and unsubscribes from the source event.
/// </summary>
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<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);
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<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);
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<double> 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;
}
}
/// <summary>
/// Calculates the Adaptive Price Channel for the entire series and returns both
/// the result series and a primed indicator instance for continued streaming.
/// </summary>
[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);
}
/// <summary>
/// Calculates the Adaptive Price Channel using span-based batch processing.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Batch(
ReadOnlySpan<double> sourceHigh,
ReadOnlySpan<double> sourceLow,
Span<double> upperBand,
Span<double> 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<double> sourceHigh,
ReadOnlySpan<double> sourceLow,
Span<double> upperBand,
Span<double> 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;
}
}
}