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

436 lines
13 KiB
C#

using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// AOBV: Archer On-Balance Volume
/// </summary>
/// <remarks>
/// Applies dual EMA smoothing (4,14) to OBV for fast/slow signal lines.
/// Fast crossing above slow indicates bullish momentum; below indicates bearish.
///
/// Calculation: <c>OBV = cumulative sum(±Volume)</c> based on close direction,
/// <c>AOBV_Fast = EMA(OBV, 4)</c>, <c>AOBV_Slow = EMA(OBV, 14)</c>.
/// </remarks>
/// <seealso href="Aobv.md">Detailed documentation</seealso>
/// <seealso href="aobv.pine">Reference Pine Script implementation</seealso>
[SkipLocalsInit]
public sealed class Aobv : ITValuePublisher
{
private const int FastPeriod = 4;
private const int SlowPeriod = 14;
[StructLayout(LayoutKind.Auto)]
private record struct State
{
public double Obv;
public double EmaFast;
public double EmaSlow;
public double EFast;
public double ESlow;
public double PrevClose;
public double LastValidClose; // NaN sentinel - no valid value yet
public double LastValidVolume; // NaN sentinel - no valid value yet
public bool WarmupFast;
public bool WarmupSlow;
public int Index;
}
private State _s;
private State _ps;
private readonly double _alphaFast;
private readonly double _betaFast;
private readonly double _alphaSlow;
private readonly double _betaSlow;
#pragma warning disable S2325 // Interface contract cannot be static
public string Name => "AOBV(4,14)";
#pragma warning restore S2325
public event TValuePublishedHandler? Pub;
public TValue Last { get; private set; }
public TValue LastFast { get; private set; }
public TValue LastSlow { get; private set; }
public bool IsHot => _s.Index >= SlowPeriod;
#pragma warning disable S2325 // Interface contract cannot be static
public int WarmupPeriod => SlowPeriod;
#pragma warning restore S2325
public Aobv()
{
_alphaFast = 2.0 / (FastPeriod + 1);
_betaFast = 1.0 - _alphaFast;
_alphaSlow = 2.0 / (SlowPeriod + 1);
_betaSlow = 1.0 - _alphaSlow;
_s = new State
{
EFast = 1.0,
ESlow = 1.0,
WarmupFast = true,
WarmupSlow = true,
LastValidClose = double.NaN, // NaN sentinel until first valid value
LastValidVolume = double.NaN // NaN sentinel until first valid value
};
_ps = _s;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset()
{
_s = new State
{
EFast = 1.0,
ESlow = 1.0,
WarmupFast = true,
WarmupSlow = true,
LastValidClose = double.NaN, // NaN sentinel until first valid value
LastValidVolume = double.NaN // NaN sentinel until first valid value
};
_ps = _s;
Last = default;
LastFast = default;
LastSlow = default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TBar input, bool isNew = true)
{
if (isNew)
{
_ps = _s;
}
else
{
_s = _ps;
}
var s = _s;
// Handle NaN/Infinity for close - use input if finite, else last valid, else skip this bar's OBV contribution
double close;
if (double.IsFinite(input.Close))
{
close = input.Close;
s.LastValidClose = input.Close;
}
else if (double.IsFinite(s.LastValidClose))
{
close = s.LastValidClose;
}
else
{
// No valid close seen yet - use 0 as neutral (won't affect OBV comparison meaningfully on first bar)
close = 0;
}
// Handle NaN/Infinity for volume - use input if finite, else last valid, else 0 (neutral)
double volume;
if (double.IsFinite(input.Volume))
{
volume = input.Volume;
s.LastValidVolume = input.Volume;
}
else if (double.IsFinite(s.LastValidVolume))
{
volume = s.LastValidVolume;
}
else
{
// No valid volume seen yet - use 0 as neutral (won't change OBV)
volume = 0;
}
// Calculate OBV
if (s.Index == 0)
{
// First bar initialization - all values start at 0
s.Obv = 0;
s.EmaFast = 0;
s.EmaSlow = 0;
}
else
{
double prevClose = s.PrevClose;
if (close > prevClose)
{
s.Obv += volume;
}
else if (close < prevClose)
{
s.Obv -= volume;
}
}
// Calculate EMA Fast with warmup compensation
s.EmaFast = Math.FusedMultiplyAdd(_alphaFast, s.Obv - s.EmaFast, s.EmaFast);
double resultFast;
if (s.WarmupFast)
{
s.EFast *= _betaFast;
double c = 1.0 / (1.0 - s.EFast);
resultFast = c * s.EmaFast;
if (s.EFast <= 1e-10)
{
s.WarmupFast = false;
}
}
else
{
resultFast = s.EmaFast;
}
// Calculate EMA Slow with warmup compensation
s.EmaSlow = Math.FusedMultiplyAdd(_alphaSlow, s.Obv - s.EmaSlow, s.EmaSlow);
double resultSlow;
if (s.WarmupSlow)
{
s.ESlow *= _betaSlow;
double c = 1.0 / (1.0 - s.ESlow);
resultSlow = c * s.EmaSlow;
if (s.ESlow <= 1e-10)
{
s.WarmupSlow = false;
}
}
else
{
resultSlow = s.EmaSlow;
}
// Store previous close for next iteration
s.PrevClose = close;
if (isNew)
{
s.Index++;
}
_s = s;
LastFast = new TValue(input.Time, resultFast);
LastSlow = new TValue(input.Time, resultSlow);
Last = LastFast; // Primary output is fast line
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
return Last;
}
/// <summary>
/// Updates AOBV with a TValue input.
/// </summary>
/// <exception cref="NotSupportedException">
/// AOBV requires OHLCV bar data to calculate OBV from close and volume.
/// Use Update(TBar) instead.
/// </exception>
#pragma warning disable S2325 // Method signature must match ITValuePublisher contract
public TValue Update(TValue input, bool isNew = true)
#pragma warning restore S2325
{
throw new NotSupportedException(
"AOBV requires OHLCV bar data to calculate OBV from close and volume. " +
"Use Update(TBar) instead.");
}
public (TSeries Fast, TSeries Slow) Update(TBarSeries source)
{
var tFast = new List<long>(source.Count);
var vFast = new List<double>(source.Count);
var tSlow = new List<long>(source.Count);
var vSlow = new List<double>(source.Count);
Reset();
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
tFast.Add(LastFast.Time);
vFast.Add(LastFast.Value);
tSlow.Add(LastSlow.Time);
vSlow.Add(LastSlow.Value);
}
return (new TSeries(tFast, vFast), new TSeries(tSlow, vSlow));
}
/// <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 (TSeries Fast, TSeries Slow) Calculate(TBarSeries source)
{
if (source.Count == 0)
{
return ([], []);
}
var t = source.Open.Times.ToArray();
var vFast = new double[source.Count];
var vSlow = new double[source.Count];
Batch(source.Close.Values, source.Volume.Values, vFast, vSlow);
return (new TSeries(t, vFast), new TSeries(t, vSlow));
}
/// <summary>
/// Calculates AOBV (Archer On-Balance Volume) from close and volume spans.
/// </summary>
/// <param name="close">Input close prices. NaN/Infinity values are replaced with last valid value.</param>
/// <param name="volume">Input volume values. NaN/Infinity values are replaced with last valid value.</param>
/// <param name="outputFast">Output span for fast EMA line.</param>
/// <param name="outputSlow">Output span for slow EMA line.</param>
/// <remarks>
/// Input sanitization: NaN/Infinity values in close or volume are replaced with the last valid
/// value seen. If no valid value has been seen yet, 0 is used as a neutral fallback.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Batch(ReadOnlySpan<double> close, ReadOnlySpan<double> volume,
Span<double> outputFast, Span<double> outputSlow)
{
if (close.Length != volume.Length)
{
throw new ArgumentException("Close and Volume spans must be of the same length", nameof(volume));
}
if (close.Length != outputFast.Length)
{
throw new ArgumentException("Output Fast span must be of the same length as input", nameof(outputFast));
}
if (close.Length != outputSlow.Length)
{
throw new ArgumentException("Output Slow span must be of the same length as input", nameof(outputSlow));
}
int len = close.Length;
if (len == 0)
{
return;
}
double alphaFast = 2.0 / (FastPeriod + 1);
double betaFast = 1.0 - alphaFast;
double alphaSlow = 2.0 / (SlowPeriod + 1);
double betaSlow = 1.0 - alphaSlow;
double obv = 0;
double emaFast = 0;
double emaSlow = 0;
double eFast = 1.0;
double eSlow = 1.0;
bool warmupFast = true;
bool warmupSlow = true;
// NaN sentinel for last valid values
double lastValidClose = double.NaN;
double lastValidVolume = double.NaN;
double prevClose = 0;
for (int i = 0; i < len; i++)
{
// Handle NaN/Infinity for close - use input if finite, else last valid, else 0 (neutral)
double c;
if (double.IsFinite(close[i]))
{
c = close[i];
lastValidClose = close[i];
}
else if (double.IsFinite(lastValidClose))
{
c = lastValidClose;
}
else
{
c = 0;
}
// Handle NaN/Infinity for volume - use input if finite, else last valid, else 0 (neutral)
double v;
if (double.IsFinite(volume[i]))
{
v = volume[i];
lastValidVolume = volume[i];
}
else if (double.IsFinite(lastValidVolume))
{
v = lastValidVolume;
}
else
{
v = 0;
}
// Calculate OBV
if (i == 0)
{
obv = 0; // First bar, no comparison
}
else
{
if (c > prevClose)
{
obv += v;
}
else if (c < prevClose)
{
obv -= v;
}
}
// EMA Fast
emaFast = Math.FusedMultiplyAdd(alphaFast, obv - emaFast, emaFast);
if (warmupFast)
{
eFast *= betaFast;
double comp = 1.0 / (1.0 - eFast);
outputFast[i] = comp * emaFast;
if (eFast <= 1e-10)
{
warmupFast = false;
}
}
else
{
outputFast[i] = emaFast;
}
// EMA Slow
emaSlow = Math.FusedMultiplyAdd(alphaSlow, obv - emaSlow, emaSlow);
if (warmupSlow)
{
eSlow *= betaSlow;
double comp = 1.0 / (1.0 - eSlow);
outputSlow[i] = comp * emaSlow;
if (eSlow <= 1e-10)
{
warmupSlow = false;
}
}
else
{
outputSlow[i] = emaSlow;
}
prevClose = c;
}
}
}