using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
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;
private TValue _p_Last;
///
/// 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;
_p_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);
// Save state for potential rollback
if (isNew)
{
_p_Last = Last;
}
else
{
// Rollback to previous state - SMAs handle their own rollback
Last = _p_Last;
}
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 = isNew });
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)
{
// Guard against non-finite input
if (!double.IsFinite(input.Value))
{
// Keep Last unchanged, publish with IsNew=false to indicate no state change
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = false });
return Last;
}
// Save state for potential rollback
if (isNew)
{
_p_Last = Last;
}
else
{
// Rollback to previous state - SMAs handle their own rollback
Last = _p_Last;
}
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 = isNew });
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];
Batch(source.High.Values, source.Low.Values, v, _fastPeriod, _slowPeriod);
// Bulk copy timestamps using CollectionsMarshal
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 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 Batch(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;
}
// Always use pooled buffer to avoid CS8353 stackalloc escape issues
// For small sizes, ArrayPool overhead is minimal
double[] rentedBuffer = ArrayPool.Shared.Rent(len * 3);
try
{
Span median = rentedBuffer.AsSpan(0, len);
Span fast = rentedBuffer.AsSpan(len, len);
Span slow = rentedBuffer.AsSpan(len * 2, 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);
SimdExtensions.Subtract(fast, slow, destination);
}
finally
{
ArrayPool.Shared.Return(rentedBuffer);
}
}
///
/// 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
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];
Batch(source.High.Values, source.Low.Values, v, fastPeriod, slowPeriod);
// Bulk copy timestamps using CollectionsMarshal
var tList = new List(len);
CollectionsMarshal.SetCount(tList, len);
var tSpan = CollectionsMarshal.AsSpan(tList);
source.Open.Times.CopyTo(tSpan);
// Pass values list directly, avoiding spread operator allocation
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, Ao Indicator) Calculate(TBarSeries source, int fastPeriod = 5, int slowPeriod = 34)
{
var indicator = new Ao(fastPeriod, slowPeriod);
TSeries results = indicator.Update(source);
return (results, indicator);
}
}