using System.Buffers; using System.Runtime.CompilerServices; namespace QuanTAlib; /// /// ADXR: Average Directional Movement Rating /// /// /// ADX momentum measure averaging current ADX with ADX from N periods ago (Wilder). /// Smooths ADX to reduce noise and confirm sustained trend strength changes. /// /// Calculation: ADXR = (ADX + ADX[Period]) / 2. /// /// Detailed documentation [SkipLocalsInit] public sealed class Adxr : ITValuePublisher { private readonly int _period; private readonly Adx _adx; private readonly RingBuffer _adxHistory; private readonly RingBuffer _p_adxHistory; /// /// Display name for the indicator. /// public string Name { get; } public event TValuePublishedHandler? Pub; /// /// Current ADXR value. /// public TValue Last { get; private set; } /// /// True if the ADXR has warmed up and is providing valid results. /// public bool IsHot => _adx.IsHot && _adxHistory.IsFull; /// /// The number of bars required for the indicator to warm up. /// public int WarmupPeriod { get; } /// /// Creates ADXR with specified period. /// /// Period for ADXR calculation (must be > 0) public Adxr(int period) { if (period <= 0) { throw new ArgumentException("Period must be greater than 0", nameof(period)); } _period = period; Name = $"Adxr({period})"; _adx = new Adx(period); // We need the ADX value from 'period' bars ago. // TA-Lib uses (Period-1) lag for ADXR. _adxHistory = new RingBuffer(period - 1); _p_adxHistory = new RingBuffer(period - 1); // ADXR needs valid ADX from 'period' bars ago. // ADX takes 2*period to warm up. // So ADXR takes 2*period + period - 1 to warm up. WarmupPeriod = _adx.WarmupPeriod + period - 1; } /// /// Resets the ADXR state. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() { _adx.Reset(); _adxHistory.Clear(); _p_adxHistory.Clear(); Last = default; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar input, bool isNew = true) { // Update ADX first TValue adxResult = _adx.Update(input, isNew); double currentAdx = adxResult.Value; if (isNew) { _p_adxHistory.CopyFrom(_adxHistory); } else { _adxHistory.CopyFrom(_p_adxHistory); } double prevAdx = double.NaN; if (_adxHistory.IsFull) { prevAdx = _adxHistory.Oldest; } _adxHistory.Add(currentAdx); // Calculate ADXR: average of current ADX and ADX from 'period' bars ago // When prevAdx is NaN (insufficient history), use currentAdx as fallback double adxr = double.IsNaN(prevAdx) ? currentAdx : (currentAdx + prevAdx) * 0.5; Last = new TValue(input.Time, adxr); Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); return Last; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TValue input, bool isNew = true) { return Update(new TBar(input.Time, input.Value, input.Value, input.Value, input.Value, 0), isNew); } public TSeries Update(TBarSeries source) { if (source.Count == 0) { return new TSeries([], []); } int len = source.Count; var v = new double[len]; Batch(source.High.Values, source.Low.Values, source.Close.Values, _period, v); var tList = new List(len); var vList = new List(v); var times = source.Open.Times; for (int i = 0; i < len; i++) { tList.Add(times[i]); } Reset(); for (int i = 0; i < len; i++) { Update(source[i], isNew: true); } return new TSeries(tList, vList); } [MethodImpl(MethodImplOptions.AggressiveInlining)] /// /// Initializes the indicator state using the provided bar series history. /// /// Historical bar data. public void Prime(TBarSeries source) { Reset(); if (source.Count == 0) { return; } for (int i = 0; i < source.Count; i++) { Update(source[i], isNew: true); } } public static void Batch(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, int period, Span destination) { int len = high.Length; if (len == 0 || len != low.Length || len != close.Length || len != destination.Length) { if (destination.Length > 0) { destination.Clear(); } return; } const int StackallocThreshold = 256; double[]? rentedAdx = null; scoped Span adxSpan; if (len <= StackallocThreshold) { adxSpan = stackalloc double[len]; } else { rentedAdx = ArrayPool.Shared.Rent(len); adxSpan = rentedAdx.AsSpan(0, len); } try { Adx.Batch(high, low, close, period, adxSpan); destination.Clear(); int lag = period - 1; if (lag <= 0) { adxSpan.CopyTo(destination); return; } if (lag >= len) { return; } ReadOnlySpan current = adxSpan[lag..]; ReadOnlySpan previous = adxSpan[..(len - lag)]; Span destTail = destination[lag..]; SimdExtensions.Add(current, previous, destTail); SimdExtensions.Scale(destTail, 0.5, destTail); } finally { if (rentedAdx != null) { ArrayPool.Shared.Return(rentedAdx); } } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TSeries Batch(TBarSeries source, int period) { if (source.Count == 0) { return new TSeries([], []); } int len = source.Count; var v = new double[len]; Batch(source.High.Values, source.Low.Values, source.Close.Values, period, v); var tList = new List(len); var times = source.Open.Times; for (int i = 0; i < len; i++) { tList.Add(times[i]); } return new TSeries(tList, [.. v]); } public static (TSeries Results, Adxr Indicator) Calculate(TBarSeries source, int period) { var indicator = new Adxr(period); TSeries results = indicator.Update(source); return (results, indicator); } }