using System.Runtime.CompilerServices; namespace QuanTAlib; /// /// AO: Awesome Oscillator /// /// /// The Awesome Oscillator (AO) is a momentum indicator used to measure market momentum. /// It calculates the difference between a 5-period and 34-period Simple Moving Average (SMA) /// of the median prices (High + Low) / 2. /// /// Calculation: /// Median Price = (High + Low) / 2 /// AO = SMA(Median Price, 5) - SMA(Median Price, 34) /// /// Sources: /// https://www.investopedia.com/terms/a/awesomeoscillator.asp /// https://www.tradingview.com/support/solutions/43000501826-awesome-oscillator-ao/ /// [SkipLocalsInit] public sealed class Ao : ITValuePublisher { private readonly int _fastPeriod; private readonly int _slowPeriod; private readonly Sma _smaFast; private readonly Sma _smaSlow; /// /// Display name for the indicator. /// public string Name { get; } public event TValuePublishedHandler? Pub; /// /// Current AO value. /// public TValue Last { get; private set; } /// /// True if the AO has enough data to produce valid results. /// public bool IsHot => _smaSlow.IsHot; /// /// The number of bars required to warm up the indicator. /// public int WarmupPeriod { get; } /// /// Creates AO with specified periods. /// /// Fast SMA period (default 5) /// Slow SMA period (default 34) public Ao(int fastPeriod = 5, int slowPeriod = 34) { 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)); _fastPeriod = fastPeriod; _slowPeriod = slowPeriod; _smaFast = new Sma(fastPeriod); _smaSlow = new Sma(slowPeriod); WarmupPeriod = slowPeriod; Name = $"Ao({fastPeriod},{slowPeriod})"; } /// /// Resets the AO state. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() { _smaFast.Reset(); _smaSlow.Reset(); Last = default; } /// /// Updates the AO with a new bar. /// /// The new bar data /// Whether this is a new bar or an update to the last bar /// The updated AO value [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar input, bool isNew = true) { double medianPrice = (input.High + input.Low) * 0.5; var val = new TValue(input.Time, medianPrice); var sFast = _smaFast.Update(val, isNew); var sSlow = _smaSlow.Update(val, isNew); double ao = sFast.Value - sSlow.Value; Last = new TValue(input.Time, ao); Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = true }); return Last; } /// /// Updates the AO 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 AO value [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TValue input, bool isNew = true) { var sFast = _smaFast.Update(input, isNew); var sSlow = _smaSlow.Update(input, isNew); double ao = sFast.Value - sSlow.Value; Last = new TValue(input.Time, ao); Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = true }); return Last; } /// /// Updates the AO with a series of bars. /// /// The source series of bars /// The AO series public TSeries Update(TBarSeries source) { if (source.Count == 0) return new TSeries([], []); int len = source.Count; var v = new double[len]; Calculate(source.High.Values, source.Low.Values, v, _fastPeriod, _slowPeriod); var tList = new List(len); var vList = new List(v); var times = source.Open.Times; for (int i = 0; i < len; i++) { tList.Add(times[i]); } // Restore streaming state so the instance is hot after batch update Reset(); for (int i = 0; i < len; i++) { Update(source[i], true); } return new TSeries(tList, vList); } /// /// Calculates AO over OHLC spans into a preallocated output span. /// Median price is computed as (High + Low) / 2. /// /// High prices /// Low prices /// Fast SMA period (default 5) /// Slow SMA period (default 34) /// Output AO values [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Calculate(ReadOnlySpan high, ReadOnlySpan low, Span destination, int fastPeriod = 5, int slowPeriod = 34) { 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; const int StackallocThreshold = 256; Span median = len <= StackallocThreshold ? stackalloc double[len] : new double[len]; for (int i = 0; i < len; i++) { median[i] = (high[i] + low[i]) * 0.5; } Span fast = len <= StackallocThreshold ? stackalloc double[len] : new double[len]; Span slow = len <= StackallocThreshold ? stackalloc double[len] : new double[len]; Sma.Batch(median, fast, fastPeriod); Sma.Batch(median, slow, slowPeriod); SimdExtensions.Subtract(fast, slow, destination); } /// /// Calculates AO for the entire series using a stateless batch path. /// /// Input series /// Fast SMA period (default 5) /// Slow SMA period (default 34) /// AO series [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TSeries Batch(TBarSeries source, int fastPeriod = 5, int slowPeriod = 34) { if (source.Count == 0) return new TSeries([], []); int len = source.Count; var v = new double[len]; Calculate(source.High.Values, source.Low.Values, v, fastPeriod, slowPeriod); var tList = new List(len); var times = source.Open.Times; for (int i = 0; i < len; i++) { tList.Add(times[i]); } return new TSeries(tList, [.. v]); } }