using System.Runtime.CompilerServices; namespace QuanTAlib; /// /// WAD: Williams Accumulation/Distribution /// /// /// Uses True Range concepts and volume to measure buying and selling pressure based on /// close position relative to previous close. Rising WAD confirms accumulation; falling confirms distribution. /// /// Calculation: TRH = max(High, prev_Close), TRL = min(Low, prev_Close), /// PM = Close - TRL (if up), Close - TRH (if down), 0 (unchanged), /// WAD = cumulative sum(PM × Volume). /// /// Detailed documentation /// Reference Pine Script implementation [SkipLocalsInit] public sealed class Wad : ITValuePublisher { private double _wad; private double _p_wad; private double _prevClose; private double _p_prevClose; private bool _isInitialized; /// /// Display name for the indicator. /// public static string Name => "WAD"; public event TValuePublishedHandler? Pub; /// /// Current WAD 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 WAD indicator. /// public Wad() { _isInitialized = false; } /// /// Resets the indicator state. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() { _wad = 0; _p_wad = 0; _prevClose = 0; _p_prevClose = 0; _isInitialized = false; Last = default; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar input, bool isNew = true) { if (isNew) { _p_wad = _wad; _p_prevClose = _prevClose; } else { _wad = _p_wad; _prevClose = _p_prevClose; } double close = input.Close; double high = input.High; double low = input.Low; double volume = input.Volume; if (!_isInitialized) { // First bar: no previous close, WAD starts at 0 _prevClose = close; _isInitialized = true; Last = new TValue(input.Time, _wad); Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); return Last; } // True Range High and Low double trueHigh = Math.Max(high, _prevClose); double trueLow = Math.Min(low, _prevClose); // Price Movement calculation double pm; if (close > _prevClose) { pm = close - trueLow; } else if (close < _prevClose) { pm = close - trueHigh; } else { pm = 0; } // A/D value and cumulative WAD double ad = pm * volume; _wad += ad; // Update previous close for next bar if (isNew) { _prevClose = close; } Last = new TValue(input.Time, _wad); Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); return Last; } /// /// Updates WAD with a TValue input. /// /// /// WAD requires OHLCV bar data to calculate True Range 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( "WAD requires OHLCV bar data to calculate True Range 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(); 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; if (len == 0) { return; } // First bar: WAD = 0 output[0] = 0; double prevClose = close[0]; double wad = 0; for (int i = 1; i < len; i++) { double h = high[i]; double l = low[i]; double c = close[i]; double vol = volume[i]; // True Range High and Low double trueHigh = Math.Max(h, prevClose); double trueLow = Math.Min(l, prevClose); // Price Movement double pm; if (c > prevClose) { pm = c - trueLow; } else if (c < prevClose) { pm = c - trueHigh; } else { pm = 0; } // Accumulate wad += pm * vol; output[i] = wad; prevClose = c; } } public static (TSeries Results, Wad Indicator) Calculate(TBarSeries source) { var indicator = new Wad(); TSeries results = indicator.Update(source); return (results, indicator); } }