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);
}
}