mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-09 06:27:45 +00:00
feat: Add Awesome Oscillator (AO) implementation with tests and documentation
This commit is contained in:
@@ -7,7 +7,7 @@ Momentum indicators measure the speed or strength of price movements. This inclu
|
||||
| AC | Acceleration Oscillator | |
|
||||
| [ADX](adx/Adx.md) | Average Directional Index | Measures the strength of a trend, regardless of its direction. |
|
||||
| ADXR | Average Directional Movement Rating | |
|
||||
| AO | Awesome Oscillator | |
|
||||
| [AO](ao/Ao.md) | Awesome Oscillator | Measures market momentum using the difference between 34-period and 5-period SMAs of median price. |
|
||||
| APO | Absolute Price Oscillator | |
|
||||
| AROON | Aroon | |
|
||||
| AROONOSC | Aroon Oscillator | |
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
using Xunit;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class AoIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void AoIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new AoIndicator();
|
||||
|
||||
Assert.Equal(5, indicator.FastPeriod);
|
||||
Assert.Equal(34, indicator.SlowPeriod);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("AO - Awesome Oscillator", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AoIndicator_MinHistoryDepths_EqualsSlowPeriod()
|
||||
{
|
||||
var indicator = new AoIndicator { SlowPeriod = 20 };
|
||||
|
||||
Assert.Equal(20, indicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(20, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AoIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new AoIndicator { FastPeriod = 10, SlowPeriod = 40 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Contains("AO", indicator.ShortName);
|
||||
Assert.Contains("10", indicator.ShortName);
|
||||
Assert.Contains("40", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AoIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new AoIndicator();
|
||||
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink);
|
||||
Assert.Contains("Ao.Quantower.cs", indicator.SourceCodeLink);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AoIndicator_Initialize_CreatesInternalAo()
|
||||
{
|
||||
var indicator = new AoIndicator { FastPeriod = 5, SlowPeriod = 34 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist (Up and Down)
|
||||
Assert.Equal(2, indicator.LinesSeries.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AoIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new AoIndicator { FastPeriod = 2, SlowPeriod = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
// Need enough bars for Period
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
// Process update for each bar to simulate history loading
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value (either Up or Down)
|
||||
// One should be NaN, other should be value, or both NaN if cold
|
||||
double up = indicator.LinesSeries[0].GetValue(0);
|
||||
double down = indicator.LinesSeries[1].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(up) || double.IsFinite(down));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AoIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new AoIndicator { FastPeriod = 2, SlowPeriod = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
}
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Add new bar
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(20), 120, 130, 110, 125);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AoIndicator_Parameters_CanBeChanged()
|
||||
{
|
||||
var indicator = new AoIndicator { FastPeriod = 5, SlowPeriod = 34 };
|
||||
Assert.Equal(5, indicator.FastPeriod);
|
||||
Assert.Equal(34, indicator.SlowPeriod);
|
||||
|
||||
indicator.FastPeriod = 10;
|
||||
indicator.SlowPeriod = 40;
|
||||
|
||||
Assert.Equal(10, indicator.FastPeriod);
|
||||
Assert.Equal(40, indicator.SlowPeriod);
|
||||
Assert.Equal(40, indicator.MinHistoryDepths);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
using System.Drawing;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class AoIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Fast Period", sortIndex: 1, 1, 1000, 1, 0)]
|
||||
public int FastPeriod { get; set; } = 5;
|
||||
|
||||
[InputParameter("Slow Period", sortIndex: 2, 1, 1000, 1, 0)]
|
||||
public int SlowPeriod { get; set; } = 34;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Ao? _ao;
|
||||
protected LineSeries? UpSeries;
|
||||
protected LineSeries? DownSeries;
|
||||
|
||||
public int MinHistoryDepths => SlowPeriod;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"AO {FastPeriod}:{SlowPeriod}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/momentum/ao/Ao.Quantower.cs";
|
||||
|
||||
public AoIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "AO - Awesome Oscillator";
|
||||
Description = "Momentum indicator measuring market momentum";
|
||||
|
||||
UpSeries = new(name: "AO Up", color: Color.Green, width: 2, style: LineStyle.Solid);
|
||||
DownSeries = new(name: "AO Down", color: Color.Red, width: 2, style: LineStyle.Solid);
|
||||
|
||||
AddLineSeries(UpSeries);
|
||||
AddLineSeries(DownSeries);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_ao = new Ao(FastPeriod, SlowPeriod);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
bool isNew = args.Reason == UpdateReason.NewBar || args.Reason == UpdateReason.HistoricalBar;
|
||||
|
||||
TBar bar = this.GetInputBar(args);
|
||||
TValue result = _ao!.Update(bar, isNew);
|
||||
|
||||
if (!_ao.IsHot && !ShowColdValues)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine color based on momentum
|
||||
// Green if rising, Red if falling
|
||||
// We need previous value to compare.
|
||||
// Since OnUpdate is called multiple times for the same bar (ticks),
|
||||
// we need to be careful about "previous value".
|
||||
// Ideally, we compare with the value of the *previous bar*.
|
||||
// But AO coloring is usually: Current > Previous Bar's AO => Green.
|
||||
// Or Current > Previous Value (intra-bar)?
|
||||
// Standard is: "Green bar if the bar is higher than the previous bar. Red bar if the bar is lower than the previous bar."
|
||||
// "Previous bar" usually means the AO value of the previous period.
|
||||
|
||||
// We can get the previous value from the indicator history if we stored it,
|
||||
// or just use _ao.Last (which is current) and we need the previous one.
|
||||
// But _ao doesn't expose history directly unless we use TSeries.
|
||||
// However, Quantower stores history in the Series.
|
||||
|
||||
// Get previous value from series
|
||||
double prevAo = double.NaN;
|
||||
if (Count > 1)
|
||||
{
|
||||
// Try to get from UpSeries
|
||||
prevAo = UpSeries!.GetValue(1);
|
||||
if (double.IsNaN(prevAo))
|
||||
{
|
||||
prevAo = DownSeries!.GetValue(1);
|
||||
}
|
||||
}
|
||||
|
||||
// If first bar, just pick a color (e.g. Green) or NaN
|
||||
if (double.IsNaN(prevAo) || result.Value > prevAo)
|
||||
{
|
||||
UpSeries!.SetValue(result.Value);
|
||||
DownSeries!.SetValue(double.NaN);
|
||||
}
|
||||
else
|
||||
{
|
||||
UpSeries!.SetValue(double.NaN);
|
||||
DownSeries!.SetValue(result.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class AoTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_ValidatesParameters()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Ao(0, 34));
|
||||
Assert.Throws<ArgumentException>(() => new Ao(5, 0));
|
||||
Assert.Throws<ArgumentException>(() => new Ao(34, 5)); // Fast >= Slow
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BecomesTrueAfterSlowPeriod()
|
||||
{
|
||||
var ao = new Ao(2, 5);
|
||||
|
||||
// Add 4 values
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
ao.Update(new TBar(DateTime.UtcNow, 10, 10, 10, 10, 100));
|
||||
Assert.False(ao.IsHot);
|
||||
}
|
||||
|
||||
// Add 5th value
|
||||
ao.Update(new TBar(DateTime.UtcNow, 10, 10, 10, 10, 100));
|
||||
Assert.True(ao.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculation_Correctness()
|
||||
{
|
||||
// AO = SMA(Median, 5) - SMA(Median, 34)
|
||||
// Let's use smaller periods for testing: 2 and 4
|
||||
var ao = new Ao(2, 4);
|
||||
|
||||
// Median prices: 10, 20, 30, 40, 50
|
||||
// SMA2: -, 15, 25, 35, 45
|
||||
// SMA4: -, -, -, 25, 35
|
||||
// AO: -, -, -, 10, 10
|
||||
|
||||
var data = new[] { 10.0, 20.0, 30.0, 40.0, 50.0 };
|
||||
// Sma returns average of available data.
|
||||
// SMA2(10) = 10
|
||||
// SMA2(10, 20) = 15
|
||||
// SMA2(20, 30) = 25
|
||||
// SMA2(30, 40) = 35
|
||||
// SMA2(40, 50) = 45
|
||||
|
||||
// SMA4(10) = 10
|
||||
// SMA4(10, 20) = 15
|
||||
// SMA4(10, 20, 30) = 20
|
||||
// SMA4(10, 20, 30, 40) = 25
|
||||
// SMA4(20, 30, 40, 50) = 35
|
||||
|
||||
// AO:
|
||||
// 1: 10 - 10 = 0
|
||||
// 2: 15 - 15 = 0
|
||||
// 3: 25 - 20 = 5
|
||||
// 4: 35 - 25 = 10
|
||||
// 5: 45 - 35 = 10
|
||||
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
var bar = new TBar(DateTime.UtcNow, data[i], data[i], data[i], data[i], 100);
|
||||
var result = ao.Update(bar);
|
||||
|
||||
if (i == 2) Assert.Equal(5.0, result.Value);
|
||||
if (i >= 3) Assert.Equal(10.0, result.Value);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithIsNewFalse_UpdatesLastValue()
|
||||
{
|
||||
var ao = new Ao(2, 4);
|
||||
|
||||
// 1. Add 10
|
||||
ao.Update(new TBar(DateTime.UtcNow, 10, 10, 10, 10, 100));
|
||||
// SMA2=10, SMA4=10, AO=0
|
||||
|
||||
// 2. Add 20
|
||||
ao.Update(new TBar(DateTime.UtcNow, 20, 20, 20, 20, 100));
|
||||
// SMA2=15, SMA4=15, AO=0
|
||||
|
||||
// 3. Update last with 30 (instead of 20)
|
||||
var result = ao.Update(new TBar(DateTime.UtcNow, 30, 30, 30, 30, 100), isNew: false);
|
||||
|
||||
// SMA2(10, 30) = 20
|
||||
// SMA4(10, 30) = 20
|
||||
// AO = 0
|
||||
Assert.Equal(0.0, result.Value);
|
||||
|
||||
// 4. Add 40
|
||||
result = ao.Update(new TBar(DateTime.UtcNow, 40, 40, 40, 40, 100));
|
||||
// SMA2(30, 40) = 35
|
||||
// SMA4(10, 30, 40) = 26.666...
|
||||
// AO = 35 - 26.666... = 8.333...
|
||||
|
||||
Assert.True(result.Value > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var ao = new Ao(2, 4);
|
||||
ao.Update(new TBar(DateTime.UtcNow, 10, 10, 10, 10, 100));
|
||||
ao.Update(new TBar(DateTime.UtcNow, 20, 20, 20, 20, 100));
|
||||
|
||||
ao.Reset();
|
||||
|
||||
Assert.False(ao.IsHot);
|
||||
Assert.Equal(0, ao.Last.Value);
|
||||
|
||||
// Should behave like new
|
||||
ao.Update(new TBar(DateTime.UtcNow, 10, 10, 10, 10, 100));
|
||||
Assert.Equal(0, ao.Last.Value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Skender.Stock.Indicators;
|
||||
using Tulip;
|
||||
using OoplesFinance.StockIndicators;
|
||||
using OoplesFinance.StockIndicators.Models;
|
||||
using Xunit;
|
||||
using QuanTAlib.Tests;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class AoValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _data;
|
||||
|
||||
public AoValidationTests()
|
||||
{
|
||||
_data = new ValidationTestData();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_data.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MatchesSkender()
|
||||
{
|
||||
var ao = new Ao(5, 34);
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
var res = ao.Update(_data.Bars[i]);
|
||||
results.Add(res.Value);
|
||||
}
|
||||
|
||||
var skenderResults = _data.SkenderQuotes.GetAwesome(5, 34).ToList();
|
||||
|
||||
Assert.Equal(_data.Bars.Count, skenderResults.Count);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
// Skender returns null for warmup
|
||||
if (skenderResults[i].Oscillator == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Assert.Equal((double)skenderResults[i].Oscillator!, results[i], 1e-6);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MatchesTulip()
|
||||
{
|
||||
var ao = new Ao(5, 34);
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
var res = ao.Update(_data.Bars[i]);
|
||||
results.Add(res.Value);
|
||||
}
|
||||
|
||||
var high = _data.Bars.High.Select(x => x.Value).ToArray();
|
||||
var low = _data.Bars.Low.Select(x => x.Value).ToArray();
|
||||
|
||||
var tulipIndicator = Tulip.Indicators.ao;
|
||||
double[][] inputs = { high, low };
|
||||
double[] options = { };
|
||||
|
||||
int lookback = 33;
|
||||
double[][] outputs = { new double[_data.Bars.Count - lookback] };
|
||||
|
||||
tulipIndicator.Run(inputs, options, outputs);
|
||||
var tulipResults = outputs[0];
|
||||
|
||||
for (int i = 0; i < tulipResults.Length; i++)
|
||||
{
|
||||
Assert.Equal(tulipResults[i], results[i + lookback], 1e-6);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MatchesOoples()
|
||||
{
|
||||
var ao = new Ao(5, 34);
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
var res = ao.Update(_data.Bars[i]);
|
||||
results.Add(res.Value);
|
||||
}
|
||||
|
||||
var ooplesData = _data.SkenderQuotes.Select(q => new TickerData
|
||||
{
|
||||
Date = q.Date,
|
||||
Open = (double)q.Open,
|
||||
High = (double)q.High,
|
||||
Low = (double)q.Low,
|
||||
Close = (double)q.Close,
|
||||
Volume = (double)q.Volume
|
||||
}).ToList();
|
||||
|
||||
var stockData = new StockData(ooplesData);
|
||||
var oResult = stockData.CalculateAwesomeOscillator(fastLength: 5, slowLength: 34);
|
||||
var oValues = oResult.OutputValues["Ao"];
|
||||
|
||||
Assert.Equal(_data.Bars.Count, oValues.Count);
|
||||
|
||||
for (int i = 0; i < _data.Bars.Count; i++)
|
||||
{
|
||||
// Ooples might return 0 for warmup
|
||||
if (i < 33) continue; // Skip warmup
|
||||
|
||||
Assert.Equal(oValues[i], results[i], 1e-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// AO: Awesome Oscillator
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The Awesome Oscillator (AO) is a momentum indicator used to measure market momentum.
|
||||
/// It calculates the difference between a 34-period and 5-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/
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Ao : ITValuePublisher
|
||||
{
|
||||
private readonly Sma _smaFast;
|
||||
private readonly Sma _smaSlow;
|
||||
|
||||
/// <summary>
|
||||
/// Display name for the indicator.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
public event Action<TValue>? Pub;
|
||||
|
||||
/// <summary>
|
||||
/// Current AO value.
|
||||
/// </summary>
|
||||
public TValue Last { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the AO has enough data to produce valid results.
|
||||
/// </summary>
|
||||
public bool IsHot => _smaSlow.IsHot;
|
||||
|
||||
/// <summary>
|
||||
/// Creates AO with specified periods.
|
||||
/// </summary>
|
||||
/// <param name="fastPeriod">Fast SMA period (default 5)</param>
|
||||
/// <param name="slowPeriod">Slow SMA period (default 34)</param>
|
||||
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));
|
||||
|
||||
_smaFast = new Sma(fastPeriod);
|
||||
_smaSlow = new Sma(slowPeriod);
|
||||
Name = $"Ao({fastPeriod},{slowPeriod})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the AO state.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Reset()
|
||||
{
|
||||
_smaFast.Reset();
|
||||
_smaSlow.Reset();
|
||||
Last = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the AO with a new bar.
|
||||
/// </summary>
|
||||
/// <param name="input">The new bar data</param>
|
||||
/// <param name="isNew">Whether this is a new bar or an update to the last bar</param>
|
||||
/// <returns>The updated AO value</returns>
|
||||
[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(Last);
|
||||
return Last;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the AO with a new value (assumes value is Median Price).
|
||||
/// </summary>
|
||||
/// <param name="input">The new value</param>
|
||||
/// <param name="isNew">Whether this is a new value or an update to the last value</param>
|
||||
/// <returns>The updated AO value</returns>
|
||||
[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(Last);
|
||||
return Last;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the AO with a series of bars.
|
||||
/// </summary>
|
||||
/// <param name="source">The source series of bars</param>
|
||||
/// <returns>The AO series</returns>
|
||||
public TSeries Update(TBarSeries source)
|
||||
{
|
||||
var t = new List<long>(source.Count);
|
||||
var v = new List<double>(source.Count);
|
||||
|
||||
Reset();
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
var val = Update(source[i], true);
|
||||
t.Add(val.Time);
|
||||
v.Add(val.Value);
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates AO for the entire series using a new instance.
|
||||
/// </summary>
|
||||
/// <param name="source">Input series</param>
|
||||
/// <param name="fastPeriod">Fast SMA period (default 5)</param>
|
||||
/// <param name="slowPeriod">Slow SMA period (default 34)</param>
|
||||
/// <returns>AO series</returns>
|
||||
public static TSeries Calculate(TBarSeries source, int fastPeriod = 5, int slowPeriod = 34)
|
||||
{
|
||||
var ao = new Ao(fastPeriod, slowPeriod);
|
||||
return ao.Update(source);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
# AO - Awesome Oscillator
|
||||
|
||||
The Awesome Oscillator (AO) is a momentum indicator used to measure market momentum. It calculates the difference between a 34-period and 5-period Simple Moving Average (SMA) of the median prices (High + Low) / 2.
|
||||
|
||||
## Formula
|
||||
|
||||
$$Median Price = \frac{High + Low}{2}$$
|
||||
|
||||
$$AO = SMA(Median Price, 5) - SMA(Median Price, 34)$$
|
||||
|
||||
Where:
|
||||
|
||||
- $SMA$ is the Simple Moving Average.
|
||||
|
||||
## Usage
|
||||
|
||||
### C# Code
|
||||
|
||||
```csharp
|
||||
using QuanTAlib;
|
||||
|
||||
// Create AO with default periods (5, 34)
|
||||
var ao = new Ao();
|
||||
|
||||
// Or specify custom periods
|
||||
var aoCustom = new Ao(5, 34);
|
||||
|
||||
// Update with a bar
|
||||
var result = ao.Update(bar);
|
||||
|
||||
// Result contains the AO value
|
||||
Console.WriteLine($"AO: {result.Value}");
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| fastPeriod | int | 5 | The period for the fast SMA. |
|
||||
| slowPeriod | int | 34 | The period for the slow SMA. |
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| Last | TValue | The latest calculated AO value. |
|
||||
| IsHot | bool | Indicates if the indicator has enough data to be valid (slow period reached). |
|
||||
| Name | string | The name of the indicator, e.g., "Ao(5,34)". |
|
||||
|
||||
## Methods
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| Update(TBar bar) | Updates the indicator with a new bar. |
|
||||
| Update(TValue val) | Updates the indicator with a new value (assumed to be Median Price). |
|
||||
| Reset() | Resets the indicator state. |
|
||||
@@ -21,6 +21,7 @@
|
||||
<Compile Include="..\lib\trends\wma\Wma.cs" />
|
||||
<Compile Include="..\lib\trends\pwma\Pwma.cs" />
|
||||
<Compile Include="..\lib\trends\jma\Jma.cs" />
|
||||
<Compile Include="..\lib\trends\sma\Sma.cs" />
|
||||
<Reference Include="TradingPlatform.BusinessLayer">
|
||||
<HintPath>..\.github\TradingPlatform.BusinessLayer.dll</HintPath>
|
||||
</Reference>
|
||||
|
||||
Reference in New Issue
Block a user