using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// AOBV: Archer On-Balance Volume /// /// /// Applies dual EMA smoothing (4,14) to OBV for fast/slow signal lines. /// Fast crossing above slow indicates bullish momentum; below indicates bearish. /// /// Calculation: OBV = cumulative sum(±Volume) based on close direction, /// AOBV_Fast = EMA(OBV, 4), AOBV_Slow = EMA(OBV, 14). /// /// Detailed documentation /// Reference Pine Script implementation [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; } /// /// Updates AOBV with a TValue input. /// /// /// AOBV requires OHLCV bar data to calculate OBV from close and volume. /// Use Update(TBar) instead. /// #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(source.Count); var vFast = new List(source.Count); var tSlow = new List(source.Count); var vSlow = new List(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)); } /// /// 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 (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)); } /// /// Calculates AOBV (Archer On-Balance Volume) from close and volume spans. /// /// Input close prices. NaN/Infinity values are replaced with last valid value. /// Input volume values. NaN/Infinity values are replaced with last valid value. /// Output span for fast EMA line. /// Output span for slow EMA line. /// /// 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. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Batch(ReadOnlySpan close, ReadOnlySpan volume, Span outputFast, Span 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; } } }