2026-01-18 19:02:03 -08:00
|
|
|
using System.Runtime.CompilerServices;
|
|
|
|
|
|
|
|
|
|
namespace QuanTAlib;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-01-31 14:05:53 -08:00
|
|
|
/// AROONOSC: Aroon Oscillator
|
2026-01-18 19:02:03 -08:00
|
|
|
/// </summary>
|
|
|
|
|
/// <remarks>
|
2026-01-31 14:05:53 -08:00
|
|
|
/// Single-line trend indicator derived from Aroon Up minus Aroon Down (Chande).
|
|
|
|
|
/// Range [-100, +100]: positive = uptrend, negative = downtrend.
|
2026-01-18 19:02:03 -08:00
|
|
|
///
|
2026-01-31 14:05:53 -08:00
|
|
|
/// Calculation: <c>AroonOsc = AroonUp - AroonDown</c>.
|
2026-01-18 19:02:03 -08:00
|
|
|
/// </remarks>
|
2026-01-31 14:05:53 -08:00
|
|
|
/// <seealso href="AroonOsc.md">Detailed documentation</seealso>
|
2026-01-18 19:02:03 -08:00
|
|
|
[SkipLocalsInit]
|
|
|
|
|
public sealed class AroonOsc : ITValuePublisher
|
|
|
|
|
{
|
|
|
|
|
private readonly int _period;
|
|
|
|
|
private readonly RingBuffer _highs;
|
|
|
|
|
private readonly RingBuffer _lows;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Display name for the indicator.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public string Name { get; }
|
|
|
|
|
|
|
|
|
|
public event TValuePublishedHandler? Pub;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Current Aroon Oscillator value.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public TValue Last { get; private set; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// True if the indicator has enough data for a full period calculation.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public bool IsHot => _highs.IsFull;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// The number of bars required for the indicator to warm up.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public int WarmupPeriod { get; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Creates Aroon Oscillator with specified period.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="period">Lookback period (must be > 0)</param>
|
|
|
|
|
public AroonOsc(int period)
|
|
|
|
|
{
|
|
|
|
|
if (period <= 0)
|
2026-01-25 16:01:45 -08:00
|
|
|
{
|
2026-01-18 19:02:03 -08:00
|
|
|
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
2026-01-25 16:01:45 -08:00
|
|
|
}
|
2026-01-18 19:02:03 -08:00
|
|
|
|
|
|
|
|
_period = period;
|
|
|
|
|
Name = $"AroonOsc({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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Resets the indicator state.
|
|
|
|
|
/// </summary>
|
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
|
|
|
public void Reset()
|
|
|
|
|
{
|
|
|
|
|
_highs.Clear();
|
|
|
|
|
_lows.Clear();
|
|
|
|
|
Last = 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;
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
{
|
2026-01-25 16:01:45 -08:00
|
|
|
if (source.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
return new TSeries([], []);
|
|
|
|
|
}
|
2026-01-18 19:02:03 -08:00
|
|
|
|
|
|
|
|
int len = source.Count;
|
|
|
|
|
var v = new double[len];
|
|
|
|
|
|
2026-02-10 21:33:16 -08:00
|
|
|
Batch(source.High.Values, source.Low.Values, period: _period, destination: v);
|
2026-01-18 19:02:03 -08:00
|
|
|
|
|
|
|
|
var tList = new List<long>(len);
|
|
|
|
|
var vList = new List<double>(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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-11 20:38:38 -08:00
|
|
|
/// <summary>
|
|
|
|
|
/// Initializes the indicator state using the provided bar series history.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="source">Historical bar data.</param>
|
|
|
|
|
public void Prime(TBarSeries source)
|
|
|
|
|
{
|
|
|
|
|
Reset();
|
|
|
|
|
if (source.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < source.Count; i++)
|
|
|
|
|
{
|
|
|
|
|
Update(source[i], isNew: true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 19:02:03 -08:00
|
|
|
/// <summary>
|
|
|
|
|
/// Calculates Aroon oscillator values using the shared O(n) algorithm from Aroon.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="high">High prices</param>
|
|
|
|
|
/// <param name="low">Low prices</param>
|
|
|
|
|
/// <param name="period">Lookback period</param>
|
|
|
|
|
/// <param name="destination">Output oscillator values (Up - Down)</param>
|
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
2026-02-10 21:33:16 -08:00
|
|
|
public static void Batch(ReadOnlySpan<double> high, ReadOnlySpan<double> low, int period, Span<double> destination)
|
2026-01-18 19:02:03 -08:00
|
|
|
{
|
|
|
|
|
// Delegate to Aroon's O(n) monotonic deque implementation
|
2026-02-10 21:33:16 -08:00
|
|
|
Aroon.Batch(high, low, period, destination);
|
2026-01-18 19:02:03 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
|
|
|
public static TSeries Batch(TBarSeries source, int period)
|
|
|
|
|
{
|
2026-01-25 16:01:45 -08:00
|
|
|
if (source.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
return new TSeries([], []);
|
|
|
|
|
}
|
2026-01-18 19:02:03 -08:00
|
|
|
|
|
|
|
|
int len = source.Count;
|
|
|
|
|
var v = new double[len];
|
|
|
|
|
|
2026-02-10 21:33:16 -08:00
|
|
|
Batch(source.High.Values, source.Low.Values, period, v);
|
2026-01-18 19:02:03 -08:00
|
|
|
|
|
|
|
|
var tList = new List<long>(len);
|
|
|
|
|
var times = source.Open.Times;
|
|
|
|
|
for (int i = 0; i < len; i++)
|
|
|
|
|
{
|
|
|
|
|
tList.Add(times[i]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new TSeries(tList, [.. v]);
|
|
|
|
|
}
|
2026-02-10 21:33:16 -08:00
|
|
|
|
|
|
|
|
public static (TSeries Results, AroonOsc Indicator) Calculate(TBarSeries source, int period)
|
|
|
|
|
{
|
|
|
|
|
var indicator = new AroonOsc(period);
|
|
|
|
|
TSeries results = indicator.Update(source);
|
|
|
|
|
return (results, indicator);
|
|
|
|
|
}
|
2026-01-25 16:01:45 -08:00
|
|
|
}
|