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;
}
}
}