using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// AC: Accelerator Oscillator /// /// /// Bill Williams' Acceleration Oscillator measures the acceleration or deceleration /// of the current market driving force. AC is the second derivative of price momentum: /// /// Median Price = (High + Low) / 2 /// AO = SMA(Median Price, fastPeriod) - SMA(Median Price, slowPeriod) /// AC = AO - SMA(AO, acPeriod) /// /// Design note: Ac implements directly rather than inheriting /// from AbstractBase. This is intentional: Ac is an OHLC-based indicator whose primary input /// is a (requiring High and Low), not a single . /// AbstractBase's contract (Update(TValue), Prime(ReadOnlySpan<double>)) does not fit /// OHLC indicators. The practical entry points are Update(TBar) and Prime(TBarSeries). /// If a future TBarIndicatorBase is introduced, Ac would be a candidate to migrate. /// /// Sources: /// https://www.investopedia.com/terms/a/accelerationdeceleration-indicator.asp /// https://www.tradingview.com/support/solutions/43000501837-accelerator-oscillator-ac/ /// [SkipLocalsInit] public sealed class Ac : ITValuePublisher { private readonly int _fastPeriod; private readonly int _slowPeriod; private readonly int _acPeriod; private readonly Sma _smaFast; private readonly Sma _smaSlow; private readonly Sma _smaAc; private TValue _p_Last; /// Display name for the indicator. public string Name { get; } public event TValuePublishedHandler? Pub; /// Current AC value. public TValue Last { get; private set; } /// True if the AC has enough data to produce valid results. public bool IsHot => _smaAc.IsHot; /// The number of bars required to warm up the indicator. public int WarmupPeriod { get; } /// /// Creates AC with specified periods. /// /// Fast SMA period for AO calculation (default 5) /// Slow SMA period for AO calculation (default 34) /// SMA period applied to AO for AC calculation (default 5) public Ac(int fastPeriod = 5, int slowPeriod = 34, int acPeriod = 5) { 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)); } if (acPeriod <= 0) { throw new ArgumentException("AC period must be greater than 0", nameof(acPeriod)); } _fastPeriod = fastPeriod; _slowPeriod = slowPeriod; _acPeriod = acPeriod; _smaFast = new Sma(fastPeriod); _smaSlow = new Sma(slowPeriod); _smaAc = new Sma(acPeriod); WarmupPeriod = slowPeriod + acPeriod - 1; Name = $"Ac({fastPeriod},{slowPeriod},{acPeriod})"; } /// Resets the AC state. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() { _smaFast.Reset(); _smaSlow.Reset(); _smaAc.Reset(); Last = default; _p_Last = default; } /// /// Updates the AC with a new bar. /// /// The new bar data /// Whether this is a new bar or an update to the last bar /// The updated AC value [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar input, bool isNew = true) { if (!double.IsFinite(input.High) || !double.IsFinite(input.Low)) { Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = false }); return Last; } double medianPrice = (input.High + input.Low) * 0.5; var val = new TValue(input.Time, medianPrice); if (isNew) { _p_Last = Last; } else { Last = _p_Last; } var sFast = _smaFast.Update(val, isNew); var sSlow = _smaSlow.Update(val, isNew); double ao = sFast.Value - sSlow.Value; var aoVal = new TValue(input.Time, ao); var sAc = _smaAc.Update(aoVal, isNew); double ac = ao - sAc.Value; Last = new TValue(input.Time, ac); Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); return Last; } /// /// Updates the AC with a new value (assumes value is Median Price). /// /// The new value /// Whether this is a new value or an update to the last value /// The updated AC value [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TValue input, bool isNew = true) { if (!double.IsFinite(input.Value)) { Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = false }); return Last; } if (isNew) { _p_Last = Last; } else { Last = _p_Last; } var sFast = _smaFast.Update(input, isNew); var sSlow = _smaSlow.Update(input, isNew); double ao = sFast.Value - sSlow.Value; var aoVal = new TValue(input.Time, ao); var sAc = _smaAc.Update(aoVal, isNew); double ac = ao - sAc.Value; Last = new TValue(input.Time, ac); Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); return Last; } /// /// Updates the AC with a series of bars. /// /// The source series of bars /// The AC series public TSeries Update(TBarSeries source) { if (source.Count == 0) { return new TSeries([], []); } int len = source.Count; var v = new double[len]; Batch(source.High.Values, source.Low.Values, v, _fastPeriod, _slowPeriod, _acPeriod); var tList = new List(len); CollectionsMarshal.SetCount(tList, len); var tSpan = CollectionsMarshal.AsSpan(tList); source.Open.Times.CopyTo(tSpan); var vList = new List(len); CollectionsMarshal.SetCount(vList, len); var vSpan = CollectionsMarshal.AsSpan(vList); v.AsSpan().CopyTo(vSpan); // Restore streaming state so the instance is hot after batch update Reset(); for (int i = 0; i < len; i++) { Update(source[i], isNew: true); } return new TSeries(tList, vList); } /// /// 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 AC over OHLC spans into a preallocated output span. /// Median price is computed as (High + Low) / 2. /// /// High prices /// Low prices /// Output AC values /// Fast SMA period (default 5) /// Slow SMA period (default 34) /// AC SMA period (default 5) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Batch(ReadOnlySpan high, ReadOnlySpan low, Span destination, int fastPeriod = 5, int slowPeriod = 34, int acPeriod = 5) { if (fastPeriod <= 0) { throw new ArgumentOutOfRangeException(nameof(fastPeriod), "Fast period must be greater than 0."); } if (slowPeriod <= 0) { throw new ArgumentOutOfRangeException(nameof(slowPeriod), "Slow period must be greater than 0."); } if (fastPeriod >= slowPeriod) { throw new ArgumentException("Fast period must be less than slow period.", nameof(fastPeriod)); } if (acPeriod <= 0) { throw new ArgumentOutOfRangeException(nameof(acPeriod), "AC period must be greater than 0."); } if (high.Length != low.Length || high.Length != destination.Length) { throw new ArgumentException("High, low, and destination spans must have the same length.", nameof(destination)); } int len = high.Length; if (len == 0) { return; } // Rent buffers: median + fast + slow + ao = 4 * len double[] rentedBuffer = ArrayPool.Shared.Rent(len * 4); try { Span median = rentedBuffer.AsSpan(0, len); Span fast = rentedBuffer.AsSpan(len, len); Span slow = rentedBuffer.AsSpan(len * 2, len); Span ao = rentedBuffer.AsSpan(len * 3, len); for (int i = 0; i < len; i++) { median[i] = (high[i] + low[i]) * 0.5; } Sma.Batch(median, fast, fastPeriod); Sma.Batch(median, slow, slowPeriod); // AO = fast - slow SimdExtensions.Subtract(fast, slow, ao); // AC = AO - SMA(AO, acPeriod) Sma.Batch(ao, destination, acPeriod); SimdExtensions.Subtract(ao, destination, destination); } finally { ArrayPool.Shared.Return(rentedBuffer); } } /// /// Calculates AC for the entire series using a stateless batch path. /// /// Input bar series /// Fast SMA period (default 5) /// Slow SMA period (default 34) /// AC SMA period (default 5) /// AC series public static TSeries Batch(TBarSeries source, int fastPeriod = 5, int slowPeriod = 34, int acPeriod = 5) { if (source.Count == 0) { return new TSeries([], []); } int len = source.Count; var v = new double[len]; Batch(source.High.Values, source.Low.Values, v, fastPeriod, slowPeriod, acPeriod); var tList = new List(len); CollectionsMarshal.SetCount(tList, len); var tSpan = CollectionsMarshal.AsSpan(tList); source.Open.Times.CopyTo(tSpan); var vList = new List(len); CollectionsMarshal.SetCount(vList, len); var vSpan = CollectionsMarshal.AsSpan(vList); v.AsSpan().CopyTo(vSpan); return new TSeries(tList, vList); } public static (TSeries Results, Ac Indicator) Calculate(TBarSeries source, int fastPeriod = 5, int slowPeriod = 34, int acPeriod = 5) { var indicator = new Ac(fastPeriod, slowPeriod, acPeriod); TSeries results = indicator.Update(source); return (results, indicator); } }