Files
Miha Kralj 653aafacd8 feat: Add Prime method to various indicators for initializing state with historical data
- Implemented Prime method in Vel, Ao, Apo, Frama, Adl, Adosc, Aobv, Cmf, Efi, Eom, Iii, Kvo, Mfi, Nvi, Obv, Pvd, Pvi, Pvo, Pvr, Pvt, Tvi, Twap, Va, Vf, Vo, Vroc, Vwad, Vwap, and Vwma classes.
- The Prime method resets the indicator state and processes the provided historical bar data to initialize the indicator.
- Added warmup period property to Adl and Wad classes to define the minimum number of data points required for validity.
- Updated benchmark tests to use Batch methods for performance evaluation.
2026-02-11 20:38:38 -08:00

263 lines
7.1 KiB
C#

using System.Buffers;
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// ADXR: Average Directional Movement Rating
/// </summary>
/// <remarks>
/// 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: <c>ADXR = (ADX + ADX[Period]) / 2</c>.
/// </remarks>
/// <seealso href="Adxr.md">Detailed documentation</seealso>
[SkipLocalsInit]
public sealed class Adxr : ITValuePublisher
{
private readonly int _period;
private readonly Adx _adx;
private readonly RingBuffer _adxHistory;
private readonly RingBuffer _p_adxHistory;
/// <summary>
/// Display name for the indicator.
/// </summary>
public string Name { get; }
public event TValuePublishedHandler? Pub;
/// <summary>
/// Current ADXR value.
/// </summary>
public TValue Last { get; private set; }
/// <summary>
/// True if the ADXR has warmed up and is providing valid results.
/// </summary>
public bool IsHot => _adx.IsHot && _adxHistory.IsFull;
/// <summary>
/// The number of bars required for the indicator to warm up.
/// </summary>
public int WarmupPeriod { get; }
/// <summary>
/// Creates ADXR with specified period.
/// </summary>
/// <param name="period">Period for ADXR calculation (must be > 0)</param>
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;
}
/// <summary>
/// Resets the ADXR state.
/// </summary>
[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<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);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
/// <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);
}
}
public static void Batch(ReadOnlySpan<double> high, ReadOnlySpan<double> low, ReadOnlySpan<double> close, int period, Span<double> 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<double> adxSpan;
if (len <= StackallocThreshold)
{
adxSpan = stackalloc double[len];
}
else
{
rentedAdx = ArrayPool<double>.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<double> current = adxSpan[lag..];
ReadOnlySpan<double> previous = adxSpan[..(len - lag)];
Span<double> destTail = destination[lag..];
SimdExtensions.Add(current, previous, destTail);
SimdExtensions.Scale(destTail, 0.5, destTail);
}
finally
{
if (rentedAdx != null)
{
ArrayPool<double>.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<long>(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);
}
}