using System.Buffers; using System.Runtime.CompilerServices; namespace QuanTAlib; /// /// AROON: Aroon Indicator /// /// /// Trend timing indicator measuring bars since period high/low (Chande). /// Outputs Up [0-100], Down [0-100], and Oscillator (Up - Down). /// /// Calculation: Up = (Period - DaysSinceHigh) / Period × 100; Down = (Period - DaysSinceLow) / Period × 100. /// /// Detailed documentation [SkipLocalsInit] public sealed class Aroon : ITValuePublisher { private readonly int _period; private readonly RingBuffer _highs; private readonly RingBuffer _lows; /// /// Display name for the indicator. /// public string Name { get; } public event TValuePublishedHandler? Pub; /// /// Current Aroon Oscillator value (Up - Down). /// public TValue Last { get; private set; } /// /// Current Aroon Up value. /// public TValue Up { get; private set; } /// /// Current Aroon Down value. /// public TValue Down { get; private set; } /// /// True if the indicator has enough data for a full period calculation. /// public bool IsHot => _highs.IsFull; /// /// The number of bars required for the indicator to warm up. /// public int WarmupPeriod { get; } /// /// Creates Aroon indicator with specified period. /// /// Lookback period (must be > 0) public Aroon(int period) { if (period <= 0) { throw new ArgumentException("Period must be greater than 0", nameof(period)); } _period = period; Name = $"Aroon({period})"; WarmupPeriod = period; // We need Period + 1 samples to cover the range [0, Period] days ago. _highs = new RingBuffer(period + 1); _lows = new RingBuffer(period + 1); } /// /// Resets the indicator state. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() { _highs.Clear(); _lows.Clear(); Last = default; Up = default; Down = default; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar input, bool isNew = true) { _highs.Add(input.High, isNew); _lows.Add(input.Low, isNew); if (_highs.Count == 0) { return default; } // Find max index in highs (Zero allocation) var highsBuffer = _highs.InternalBuffer; int count = _highs.Count; int capacity = _highs.Capacity; int start = _highs.StartIndex; double maxVal = double.MinValue; int maxIdxRelative = 0; for (int i = 0; i < count; i++) { int idx = (start + i) % capacity; double val = highsBuffer[idx]; // Use >= to find the most recent high if values are equal if (val >= maxVal) { maxVal = val; maxIdxRelative = i; } } // Find min index in lows (Zero allocation) var lowsBuffer = _lows.InternalBuffer; double minVal = double.MaxValue; int minIdxRelative = 0; for (int i = 0; i < count; i++) { int idx = (start + i) % capacity; double val = lowsBuffer[idx]; // Use <= to find the most recent low if values are equal if (val <= minVal) { minVal = val; minIdxRelative = i; } } // Calculate days since (0 means current bar is the high/low) int daysSinceHigh = count - 1 - maxIdxRelative; int daysSinceLow = count - 1 - minIdxRelative; double up = ((double)(_period - daysSinceHigh) / _period) * 100.0; double down = ((double)(_period - daysSinceLow) / _period) * 100.0; double osc = up - down; Up = new TValue(input.Time, up); Down = new TValue(input.Time, down); Last = new TValue(input.Time, osc); 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, _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); } /// /// 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); } } /// /// Calculates Aroon oscillator values using O(n) monotonic deque algorithm. /// /// High prices /// Low prices /// Lookback period /// Output oscillator values (Up - Down) [MethodImpl(MethodImplOptions.AggressiveOptimization)] public static void Batch(ReadOnlySpan high, ReadOnlySpan low, int period, Span destination) { int len = high.Length; if (len == 0 || len != low.Length || len != destination.Length || period <= 0) { if (destination.Length > 0) { destination.Clear(); } return; } // Use monotonic deques for O(n) complexity // Deque stores indices; front has the max/min index within the window // Max deque size is bounded by window size (period + 1), but we use circular indexing int windowSize = period + 1; int[]? rented = ArrayPool.Shared.Rent(windowSize * 2); try { Span buffer = rented.AsSpan(0, windowSize * 2); Span maxDeque = buffer.Slice(0, windowSize); // circular buffer for max indices Span minDeque = buffer.Slice(windowSize, windowSize); // circular buffer for min indices int maxHead = 0, maxTail = 0, maxCount = 0; // circular deque for highs int minHead = 0, minTail = 0, minCount = 0; // circular deque for lows double invPeriod = 100.0 / period; for (int i = 0; i < len; i++) { // Remove elements outside the window [i - period, i] int windowStart = i - period; // Remove old indices from front of max deque while (maxCount > 0 && maxDeque[maxHead] < windowStart) { maxHead = (maxHead + 1) % windowSize; maxCount--; } // Remove old indices from front of min deque while (minCount > 0 && minDeque[minHead] < windowStart) { minHead = (minHead + 1) % windowSize; minCount--; } // Add current index to max deque (maintain decreasing order) // Use <= to keep most recent max when values equal double h = high[i]; while (maxCount > 0 && high[maxDeque[(maxTail - 1 + windowSize) % windowSize]] <= h) { maxTail = (maxTail - 1 + windowSize) % windowSize; maxCount--; } maxDeque[maxTail] = i; maxTail = (maxTail + 1) % windowSize; maxCount++; // Add current index to min deque (maintain increasing order) // Use >= to keep most recent min when values equal double l = low[i]; while (minCount > 0 && low[minDeque[(minTail - 1 + windowSize) % windowSize]] >= l) { minTail = (minTail - 1 + windowSize) % windowSize; minCount--; } minDeque[minTail] = i; minTail = (minTail + 1) % windowSize; minCount++; // Calculate Aroon values int maxIdx = maxDeque[maxHead]; int minIdx = minDeque[minHead]; int daysSinceHigh = i - maxIdx; int daysSinceLow = i - minIdx; double up = (period - daysSinceHigh) * invPeriod; double down = (period - daysSinceLow) * invPeriod; destination[i] = up - down; } } finally { ArrayPool.Shared.Return(rented); } } [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, 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, Aroon Indicator) Calculate(TBarSeries source, int period) { var indicator = new Aroon(period); TSeries results = indicator.Update(source); return (results, indicator); } }