using System.Runtime.CompilerServices; using System.Numerics; namespace QuanTAlib; /// /// AD: Accumulation/Distribution Line /// /// /// Cumulative indicator using volume and price to assess accumulation or distribution. /// Rising AD confirms accumulation; falling confirms distribution. /// /// Calculation: MFM = [(Close - Low) - (High - Close)] / (High - Low), /// MFV = MFM × Volume, AD = prev_AD + MFV. If High equals Low, MFM is 0. /// /// Detailed documentation /// Reference Pine Script implementation [SkipLocalsInit] public sealed class Ad : ITValuePublisher { private double _ad; private double _p_ad; private bool _isInitialized; /// /// Display name for the indicator. /// public static string Name => "AD"; public event TValuePublishedHandler? Pub; /// /// Current AD value. /// public TValue Last { get; private set; } /// /// Minimum number of data points required before the indicator becomes valid. /// public int WarmupPeriod { get; } = 1; /// /// True if the indicator has processed at least one bar. /// public bool IsHot => _isInitialized; /// /// Creates a new AD indicator. /// public Ad() { _isInitialized = false; } /// /// Resets the indicator state. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() { _ad = 0; _p_ad = 0; _isInitialized = false; Last = default; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar input, bool isNew = true) { if (isNew) { _p_ad = _ad; } else { _ad = _p_ad; } double highLowRange = input.High - input.Low; double mfm = 0; if (highLowRange > double.Epsilon) { mfm = (input.Close - input.Low - (input.High - input.Close)) / highLowRange; } double mfv = mfm * input.Volume; _ad += mfv; _isInitialized = true; Last = new TValue(input.Time, _ad); Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); return Last; } /// /// Updates AD with a TValue input. /// /// /// AD requires OHLCV bar data to calculate the Money Flow Multiplier 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( "AD requires OHLCV bar data to calculate the Money Flow Multiplier and Volume. " + "Use Update(TBar) instead."); } public TSeries Update(TBarSeries source) { var t = new List(source.Count); var v = new List(source.Count); Reset(); for (int i = 0; i < source.Count; i++) { var val = Update(source[i], isNew: true); t.Add(val.Time); v.Add(val.Value); } return new TSeries(t, v); } /// /// 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 Batch(TBarSeries source) { if (source.Count == 0) { return []; } var t = source.Open.Times.ToArray(); // Times are same for all series var v = new double[source.Count]; Batch(source.High.Values, source.Low.Values, source.Close.Values, source.Volume.Values, v); return new TSeries(t, v); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Batch(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, ReadOnlySpan volume, Span output) { if (high.Length != low.Length || high.Length != close.Length || high.Length != volume.Length || high.Length != output.Length) { throw new ArgumentException("All spans must be of the same length", nameof(output)); } int len = high.Length; int i = 0; if (Vector.IsHardwareAccelerated && len >= Vector.Count) { int vectorSize = Vector.Count; var epsilon = new Vector(double.Epsilon); for (; i <= len - vectorSize; i += vectorSize) { var h = new Vector(high.Slice(i, vectorSize)); var l = new Vector(low.Slice(i, vectorSize)); var c = new Vector(close.Slice(i, vectorSize)); var vol = new Vector(volume.Slice(i, vectorSize)); var hl = h - l; var num = c - l - (h - c); var mask = Vector.GreaterThan(hl, epsilon); var safeHl = Vector.ConditionalSelect(mask, hl, Vector.One); var mfm = num / safeHl; mfm = Vector.ConditionalSelect(mask, mfm, Vector.Zero); var mfv = mfm * vol; mfv.CopyTo(output.Slice(i, vectorSize)); } } for (; i < len; i++) { double h = high[i]; double l = low[i]; double c = close[i]; double vol = volume[i]; double hl = h - l; double mfm = 0; if (hl > double.Epsilon) { mfm = (c - l - (h - c)) / hl; } output[i] = mfm * vol; } double sum = 0; for (i = 0; i < len; i++) { sum += output[i]; output[i] = sum; } } public static (TSeries Results, Ad Indicator) Calculate(TBarSeries source) { var indicator = new Ad(); TSeries results = indicator.Update(source); return (results, indicator); } }