using System.Runtime.CompilerServices; namespace QuanTAlib; /// /// ADOSC: Accumulation/Distribution Oscillator (Chaikin Oscillator) /// /// /// Measures momentum of the ADL using dual EMAs. Positive values indicate accumulation momentum; /// negative indicates distribution. Standard parameters: fast=3, slow=10. /// /// Calculation: ADOSC = EMA(ADL, fast) - EMA(ADL, slow). /// /// Detailed documentation /// Reference Pine Script implementation [SkipLocalsInit] public sealed class Adosc : ITValuePublisher { private readonly Ad _ad; private readonly Ema _emaFast; private readonly Ema _emaSlow; /// /// Display name for the indicator. /// public string Name { get; } public event TValuePublishedHandler? Pub; /// /// Current ADOSC value. /// public TValue Last { get; private set; } /// /// True if the indicator has enough data to produce valid results. /// public bool IsHot => _emaSlow.IsHot; /// /// The number of bars required to warm up the indicator. /// public int WarmupPeriod { get; } /// /// Creates ADOSC with specified periods. /// /// Fast EMA period (default 3) /// Slow EMA period (default 10) public Adosc(int fastPeriod = 3, int slowPeriod = 10) { if (fastPeriod <= 0) { throw new ArgumentException("Fast period must be greater than 0", nameof(fastPeriod)); } if (slowPeriod <= 0) { throw new ArgumentException("Slow period must be greater than 0", nameof(slowPeriod)); } if (fastPeriod >= slowPeriod) { throw new ArgumentException("Fast period must be less than slow period", nameof(fastPeriod)); } _ad = new Ad(); _emaFast = new Ema(fastPeriod); _emaSlow = new Ema(slowPeriod); WarmupPeriod = slowPeriod; Name = $"Adosc({fastPeriod},{slowPeriod})"; } /// /// Resets the indicator state. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() { _ad.Reset(); _emaFast.Reset(); _emaSlow.Reset(); Last = default; } /// /// Updates the indicator with a new ADL value. /// /// The new ADL value /// Whether this is a new value or an update to the last value /// The updated ADOSC value [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TValue input, bool isNew = true) { var eFast = _emaFast.Update(input, isNew); var eSlow = _emaSlow.Update(input, isNew); double adosc = eFast.Value - eSlow.Value; Last = new TValue(input.Time, adosc); Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); return Last; } /// /// Updates the indicator with a new bar. /// /// The new bar data /// Whether this is a new bar or an update to the last bar /// The updated ADOSC value [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar input, bool isNew = true) { var adl = _ad.Update(input, isNew); return Update(adl, isNew); } /// /// Updates the indicator with a series of bars. /// /// The source series of bars /// The ADOSC series 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); } // EMA compensator threshold (same as in Ema.cs) private const double COMPENSATOR_THRESHOLD = 1e-10; /// /// 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); } } /// /// Calculates ADOSC for the entire series using a new instance. /// /// Input series /// Fast EMA period (default 3) /// Slow EMA period (default 10) /// ADOSC series public static TSeries Batch(TBarSeries source, int fastPeriod = 3, int slowPeriod = 10) { var adosc = new Adosc(fastPeriod, slowPeriod); return adosc.Update(source); } /// /// Calculates ADOSC for the entire span using a single-pass algorithm. /// Zero allocation for maximum performance. /// Uses compensator pattern from EMA for proper early-stage bias correction. /// /// High prices /// Low prices /// Close prices /// Volume /// Output span /// Fast EMA period (default 3) /// Slow EMA period (default 10) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Batch(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, ReadOnlySpan volume, Span output, int fastPeriod = 3, int slowPeriod = 10) { 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)); } if (fastPeriod <= 0) { throw new ArgumentException("Fast period must be greater than 0", nameof(fastPeriod)); } if (slowPeriod <= 0) { throw new ArgumentException("Slow period must be greater than 0", nameof(slowPeriod)); } if (fastPeriod >= slowPeriod) { throw new ArgumentException("Fast period must be less than slow period", nameof(fastPeriod)); } int len = high.Length; if (len == 0) { return; } // EMA parameters (same formula as Ema.cs: alpha = 2 / (period + 1)) double alphaFast = 2.0 / (fastPeriod + 1); double alphaSlow = 2.0 / (slowPeriod + 1); double decayFast = 1.0 - alphaFast; double decaySlow = 1.0 - alphaSlow; // State variables (no heap allocations) double adl = 0; double emaFast = 0; double emaSlow = 0; double eFast = 1.0; // Compensation factor for fast EMA (starts at 1, decays toward 0) double eSlow = 1.0; // Compensation factor for slow EMA bool fastCompensated = false; bool slowCompensated = false; // Single pass: compute ADL, both EMAs, and output in one loop for (int i = 0; i < len; i++) { double h = high[i]; double l = low[i]; double c = close[i]; double vol = volume[i]; // 1. Compute Money Flow Multiplier and Volume double hl = h - l; double mfm = 0; if (hl > double.Epsilon) { mfm = (c - l - (h - c)) / hl; } double mfv = mfm * vol; // 2. Update ADL (cumulative) adl += mfv; // 3. Update Fast EMA with FMA (same pattern as Ema.cs Compute method) // state.Ema = Math.FusedMultiplyAdd(state.Ema, decay, alpha * input) emaFast = Math.FusedMultiplyAdd(emaFast, decayFast, alphaFast * adl); // 4. Update Slow EMA with FMA emaSlow = Math.FusedMultiplyAdd(emaSlow, decaySlow, alphaSlow * adl); // 5. Compute compensated EMA values (same logic as Ema.cs Compute method) // Compensator decays: e *= decay, then result = ema / (1 - e) until e <= threshold double fastValue = 0, slowValue = 0; if (!fastCompensated) { eFast *= decayFast; if (eFast <= COMPENSATOR_THRESHOLD) { fastCompensated = true; fastValue = emaFast; } else { fastValue = emaFast / (1.0 - eFast); } } else { fastValue = emaFast; } if (!slowCompensated) { eSlow *= decaySlow; if (eSlow <= COMPENSATOR_THRESHOLD) { slowCompensated = true; slowValue = emaSlow; } else { slowValue = emaSlow / (1.0 - eSlow); } } else { slowValue = emaSlow; } output[i] = fastValue - slowValue; } } public static (TSeries Results, Adosc Indicator) Calculate(TBarSeries source, int fastPeriod = 3, int slowPeriod = 10) { var indicator = new Adosc(fastPeriod, slowPeriod); TSeries results = indicator.Update(source); return (results, indicator); } }