using System.Runtime.CompilerServices; namespace QuanTAlib; /// /// APO: Absolute Price Oscillator /// /// /// The Absolute Price Oscillator (APO) is a momentum indicator that shows the difference /// between two Exponential Moving Averages (EMAs) of a security's price. /// /// Calculation: /// APO = FastEMA(Price) - SlowEMA(Price) /// /// Standard Parameters: /// Fast Period: 12 /// Slow Period: 26 /// Source: Close price /// /// Sources: /// https://www.investopedia.com/terms/a/apo.asp /// https://school.stockcharts.com/doku.php?id=technical_indicators:price_oscillators_ppo /// [SkipLocalsInit] public sealed class Apo : ITValuePublisher, IDisposable { private readonly Ema _emaFast; private readonly Ema _emaSlow; private readonly TValuePublishedHandler _handler; private ITValuePublisher? _source; private bool _disposed; /// /// Display name for the indicator. /// public string Name { get; } public event TValuePublishedHandler? Pub; /// /// Current APO value. /// public TValue Last { get; private set; } /// /// True if the APO 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 APO with specified periods. /// /// Fast EMA period (default 12) /// Slow EMA period (default 26) public Apo(int fastPeriod = 12, int slowPeriod = 26) { 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)); } _emaFast = new Ema(fastPeriod); _emaSlow = new Ema(slowPeriod); _handler = Handle; WarmupPeriod = slowPeriod; Name = $"Apo({fastPeriod},{slowPeriod})"; } /// /// Creates APO with specified source and periods. /// /// Source to subscribe to /// Fast EMA period (default 12) /// Slow EMA period (default 26) public Apo(ITValuePublisher source, int fastPeriod = 12, int slowPeriod = 26) : this(fastPeriod, slowPeriod) { _source = source; _source.Pub += _handler; } /// /// Resets the APO state. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() { _emaFast.Reset(); _emaSlow.Reset(); Last = default; } /// /// Updates the APO with a new value. /// /// The new value /// Whether this is a new value or an update to the last value /// The updated APO 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 apo = eFast.Value - eSlow.Value; Last = new TValue(input.Time, apo); Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); return Last; } /// /// Updates the APO with a new bar (uses Close price). /// /// The new bar data /// Whether this is a new bar or an update to the last bar /// The updated APO value [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar input, bool isNew = true) { return Update(new TValue(input.Time, input.Close), isNew); } /// /// Updates the APO with a series of values. /// /// The source series of values /// The APO series public TSeries Update(TSeries 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); } private void Handle(object? sender, in TValueEventArgs args) { Update(args.Value, args.IsNew); } /// /// Initializes the indicator state using the provided series history. /// /// Historical data. public void Prime(TSeries source) { Reset(); if (source.Count == 0) { return; } for (int i = 0; i < source.Count; i++) { Update(new TValue(new DateTime(source.Times[i], DateTimeKind.Utc), source.Values[i]), isNew: true); } } /// /// Calculates APO for the entire series using a new instance. /// /// Input series /// Fast EMA period (default 12) /// Slow EMA period (default 26) /// APO series public static TSeries Batch(TSeries source, int fastPeriod = 12, int slowPeriod = 26) { var apo = new Apo(fastPeriod, slowPeriod); return apo.Update(source); } /// /// Calculates APO for the entire span. /// /// Input span /// Output span /// Fast EMA period (default 12) /// Slow EMA period (default 26) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Batch(ReadOnlySpan source, Span output, int fastPeriod = 12, int slowPeriod = 26) { if (source.Length != output.Length) { throw new ArgumentException("Source and output spans must be of the same length.", nameof(output)); } Span fastEma = source.Length <= 1024 ? stackalloc double[source.Length] : new double[source.Length]; Span slowEma = source.Length <= 1024 ? stackalloc double[source.Length] : new double[source.Length]; Ema.Batch(source, fastEma, fastPeriod); Ema.Batch(source, slowEma, slowPeriod); SimdExtensions.Subtract(fastEma, slowEma, output); } public static (TSeries Results, Apo Indicator) Calculate(TSeries source, int fastPeriod = 12, int slowPeriod = 26) { var indicator = new Apo(fastPeriod, slowPeriod); TSeries results = indicator.Update(source); return (results, indicator); } /// /// Disposes resources and unsubscribes from the source publisher. /// public void Dispose() { if (_disposed) { return; } _disposed = true; if (_source != null) { _source.Pub -= _handler; _source = null; } } }