mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-22 20:48:04 +00:00
Add Stochastic Oscillator implementation and validation tests
- Implemented Stochastic Oscillator (%K and %D) in Stoch.cs with streaming and batch processing capabilities. - Added validation tests for the Stochastic Oscillator in Stoch.Validation.Tests.cs, ensuring consistency with Skender.Stock.Indicators. - Created documentation for the Stochastic Oscillator in Stoch.md, detailing its mathematical formula, architecture, parameters, and common pitfalls. - Updated project file to include necessary numeric libraries for highest and lowest calculations.
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class AcIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void AcIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new AcIndicator();
|
||||
|
||||
Assert.Equal(5, indicator.FastPeriod);
|
||||
Assert.Equal(34, indicator.SlowPeriod);
|
||||
Assert.Equal(5, indicator.AcPeriod);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("AC - Acceleration Oscillator", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new AcIndicator { SlowPeriod = 20 };
|
||||
|
||||
Assert.Equal(0, AcIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new AcIndicator { FastPeriod = 10, SlowPeriod = 40, AcPeriod = 7 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Contains("AC", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("10", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("40", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("7", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new AcIndicator();
|
||||
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Ac.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcIndicator_Initialize_CreatesInternalAc()
|
||||
{
|
||||
var indicator = new AcIndicator { FastPeriod = 5, SlowPeriod = 34, AcPeriod = 5 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist (Up and Down)
|
||||
Assert.Equal(2, indicator.LinesSeries.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new AcIndicator { FastPeriod = 2, SlowPeriod = 5, AcPeriod = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
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);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value (either Up or Down)
|
||||
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 AcIndicator_ProcessUpdate_NewBar_UpdatesValue()
|
||||
{
|
||||
var indicator = new AcIndicator { FastPeriod = 2, SlowPeriod = 5, AcPeriod = 3 };
|
||||
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);
|
||||
|
||||
var reason = i < 19 ? UpdateReason.HistoricalBar : UpdateReason.NewBar;
|
||||
var args = new UpdateArgs(reason);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Verify line series has values
|
||||
double up = indicator.LinesSeries[0].GetValue(0);
|
||||
double down = indicator.LinesSeries[1].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(up) || double.IsFinite(down));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class AcIndicator : 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("AC Period", sortIndex: 3, 1, 1000, 1, 0)]
|
||||
public int AcPeriod { get; set; } = 5;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Ac _ac = null!;
|
||||
private readonly LineSeries _upSeries;
|
||||
private readonly LineSeries _downSeries;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"AC {FastPeriod}:{SlowPeriod}:{AcPeriod}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/oscillators/ac/Ac.Quantower.cs";
|
||||
|
||||
public AcIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "AC - Acceleration Oscillator";
|
||||
Description = "Measures acceleration/deceleration of market driving force";
|
||||
|
||||
_upSeries = new LineSeries(name: "AC Up", color: Color.Green, width: 2, style: LineStyle.Solid);
|
||||
_downSeries = new LineSeries(name: "AC Down", color: Color.Red, width: 2, style: LineStyle.Solid);
|
||||
|
||||
AddLineSeries(_upSeries);
|
||||
AddLineSeries(_downSeries);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_ac = new Ac(FastPeriod, SlowPeriod, AcPeriod);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
TValue result = _ac.Update(this.GetInputBar(args), args.IsNewBar());
|
||||
|
||||
if (!_ac.IsHot && !ShowColdValues)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
double prevAc = double.NaN;
|
||||
if (Count > 1)
|
||||
{
|
||||
prevAc = _upSeries.GetValue(1);
|
||||
if (double.IsNaN(prevAc))
|
||||
{
|
||||
prevAc = _downSeries.GetValue(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (double.IsNaN(prevAc) || result.Value > prevAc)
|
||||
{
|
||||
_upSeries.SetValue(result.Value);
|
||||
_downSeries.SetValue(double.NaN);
|
||||
}
|
||||
else
|
||||
{
|
||||
_downSeries.SetValue(result.Value);
|
||||
_upSeries.SetValue(double.NaN);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class AcTests
|
||||
{
|
||||
private readonly GBM _gbm = new(1000.0, 0.05, 0.3, seed: 42);
|
||||
|
||||
// ── A) Constructor validation ──
|
||||
|
||||
[Fact]
|
||||
public void Constructor_FastPeriodZero_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Ac(fastPeriod: 0));
|
||||
Assert.Equal("fastPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SlowPeriodZero_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Ac(slowPeriod: 0));
|
||||
Assert.Equal("slowPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_FastGeSlow_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Ac(fastPeriod: 34, slowPeriod: 5));
|
||||
Assert.Equal("fastPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_AcPeriodZero_Throws()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Ac(acPeriod: 0));
|
||||
Assert.Equal("acPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Defaults_NameCorrect()
|
||||
{
|
||||
var ac = new Ac();
|
||||
Assert.Equal("Ac(5,34,5)", ac.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Custom_WarmupPeriod()
|
||||
{
|
||||
var ac = new Ac(5, 34, 5);
|
||||
Assert.Equal(38, ac.WarmupPeriod); // 34 + 5 - 1
|
||||
}
|
||||
|
||||
// ── B) Basic calculation ──
|
||||
|
||||
[Fact]
|
||||
public void Update_SingleBar_ReturnsValue()
|
||||
{
|
||||
var ac = new Ac();
|
||||
var bar = _gbm.Next(isNew: true);
|
||||
var result = ac.Update(bar);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Last_IsAccessible()
|
||||
{
|
||||
var ac = new Ac();
|
||||
var bar = _gbm.Next(isNew: true);
|
||||
_ = ac.Update(bar);
|
||||
Assert.True(double.IsFinite(ac.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ConstantPrice_ConvergesToZero()
|
||||
{
|
||||
var ac = new Ac();
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), 100.0, 100.0, 100.0, 100.0, 1000.0);
|
||||
_ = ac.Update(bar, isNew: true);
|
||||
}
|
||||
|
||||
Assert.True(ac.IsHot);
|
||||
Assert.Equal(0.0, ac.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
// ── C) State + bar correction ──
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNew_True_AdvancesState()
|
||||
{
|
||||
var ac = new Ac();
|
||||
// Feed enough bars so the values diverge from zero
|
||||
for (int i = 0; i < 40; i++)
|
||||
{
|
||||
_ = ac.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
}
|
||||
|
||||
var bar1 = _gbm.Next(isNew: true);
|
||||
var result1 = ac.Update(bar1, isNew: true);
|
||||
|
||||
var bar2 = _gbm.Next(isNew: true);
|
||||
var result2 = ac.Update(bar2, isNew: true);
|
||||
|
||||
Assert.NotEqual(result1.Value, result2.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNew_False_Rewrites()
|
||||
{
|
||||
var ac = new Ac();
|
||||
for (int i = 0; i < 40; i++)
|
||||
{
|
||||
_ = ac.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
}
|
||||
|
||||
var bar = _gbm.Next(isNew: true);
|
||||
var first = ac.Update(bar, isNew: true);
|
||||
|
||||
var correctionBar = new TBar(bar.Time, bar.Open * 1.01, bar.High * 1.01, bar.Low * 1.01, bar.Close * 1.01, bar.Volume);
|
||||
var corrected = ac.Update(correctionBar, isNew: false);
|
||||
|
||||
Assert.NotEqual(first.Value, corrected.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IterativeCorrections_Restore()
|
||||
{
|
||||
var ac = new Ac();
|
||||
for (int i = 0; i < 40; i++)
|
||||
{
|
||||
_ = ac.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
}
|
||||
|
||||
var bar = _gbm.Next(isNew: true);
|
||||
var first = ac.Update(bar, isNew: true);
|
||||
|
||||
// Apply corrections multiple times
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
_ = ac.Update(bar, isNew: false);
|
||||
}
|
||||
|
||||
var final = ac.Update(bar, isNew: false);
|
||||
Assert.Equal(first.Value, final.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var ac = new Ac();
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
_ = ac.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
}
|
||||
|
||||
Assert.True(ac.IsHot);
|
||||
|
||||
ac.Reset();
|
||||
|
||||
Assert.False(ac.IsHot);
|
||||
Assert.Equal(0.0, ac.Last.Value);
|
||||
}
|
||||
|
||||
// ── D) Warmup / convergence ──
|
||||
|
||||
[Fact]
|
||||
public void IsHot_FlipsAfterSufficientData()
|
||||
{
|
||||
var ac = new Ac(5, 34, 5);
|
||||
|
||||
// Feed just 1 bar — should not be hot yet
|
||||
_ = ac.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
// May already become hot if inner SMA sees enough values
|
||||
|
||||
// After feeding enough bars, must be hot
|
||||
for (int i = 1; i < 50; i++)
|
||||
{
|
||||
_ = ac.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
}
|
||||
|
||||
Assert.True(ac.IsHot);
|
||||
}
|
||||
|
||||
// ── E) Robustness ──
|
||||
|
||||
[Fact]
|
||||
public void Update_NaN_KeepsLastValid()
|
||||
{
|
||||
var ac = new Ac();
|
||||
for (int i = 0; i < 40; i++)
|
||||
{
|
||||
_ = ac.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
}
|
||||
|
||||
var lastBefore = ac.Last;
|
||||
var nanInput = new TValue(DateTime.UtcNow, double.NaN);
|
||||
var result = ac.Update(nanInput, isNew: true);
|
||||
|
||||
Assert.Equal(lastBefore.Value, result.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Infinity_KeepsLastValid()
|
||||
{
|
||||
var ac = new Ac();
|
||||
for (int i = 0; i < 40; i++)
|
||||
{
|
||||
_ = ac.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
}
|
||||
|
||||
var lastBefore = ac.Last;
|
||||
var infInput = new TValue(DateTime.UtcNow, double.PositiveInfinity);
|
||||
var result = ac.Update(infInput, isNew: true);
|
||||
|
||||
Assert.Equal(lastBefore.Value, result.Value, 1e-10);
|
||||
}
|
||||
|
||||
// ── F) Consistency (batch == streaming == span == eventing) ──
|
||||
|
||||
[Fact]
|
||||
public void BatchCalc_Matches_Streaming()
|
||||
{
|
||||
var gbm = new GBM(500.0, 0.05, 0.3, seed: 99);
|
||||
var series = new TBarSeries();
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
series.Add(gbm.Next(isNew: true));
|
||||
}
|
||||
|
||||
// Streaming
|
||||
var streaming = new Ac();
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
_ = streaming.Update(series[i], isNew: true);
|
||||
}
|
||||
|
||||
// Batch via Update(TBarSeries)
|
||||
var batchAc = new Ac();
|
||||
var batchResult = batchAc.Update(series);
|
||||
|
||||
// Compare last values
|
||||
Assert.Equal(streaming.Last.Value, batchResult[^1].Value, 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_Matches_Streaming()
|
||||
{
|
||||
var gbm = new GBM(500.0, 0.05, 0.3, seed: 99);
|
||||
var series = new TBarSeries();
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
series.Add(gbm.Next(isNew: true));
|
||||
}
|
||||
|
||||
// Streaming
|
||||
var streaming = new Ac();
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
_ = streaming.Update(series[i], isNew: true);
|
||||
}
|
||||
|
||||
// Span batch
|
||||
var output = new double[series.Count];
|
||||
Ac.Batch(series.High.Values, series.Low.Values, output);
|
||||
|
||||
Assert.Equal(streaming.Last.Value, output[^1], 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventPub_FiresOnUpdate()
|
||||
{
|
||||
var ac = new Ac();
|
||||
int pubCount = 0;
|
||||
ac.Pub += (object? sender, in TValueEventArgs e) => pubCount++;
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
_ = ac.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
}
|
||||
|
||||
Assert.Equal(5, pubCount);
|
||||
}
|
||||
|
||||
// ── G) Span API tests ──
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_MismatchedLengths_Throws()
|
||||
{
|
||||
var high = new double[10];
|
||||
var low = new double[10];
|
||||
var dest = new double[5]; // wrong length
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Ac.Batch(high, low, dest));
|
||||
Assert.Equal("destination", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_Empty_NoException()
|
||||
{
|
||||
var output = Array.Empty<double>();
|
||||
Ac.Batch(ReadOnlySpan<double>.Empty, ReadOnlySpan<double>.Empty, output);
|
||||
Assert.Empty(output);
|
||||
}
|
||||
|
||||
// ── H) Chainability ──
|
||||
|
||||
[Fact]
|
||||
public void EventChaining_Works()
|
||||
{
|
||||
var ac = new Ac();
|
||||
var values = new List<double>();
|
||||
|
||||
ac.Pub += (object? sender, in TValueEventArgs e) => values.Add(e.Value.Value);
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
_ = ac.Update(_gbm.Next(isNew: true), isNew: true);
|
||||
}
|
||||
|
||||
Assert.Equal(50, values.Count);
|
||||
}
|
||||
|
||||
// ── Additional: TValue Update path ──
|
||||
|
||||
[Fact]
|
||||
public void TValueUpdate_Works()
|
||||
{
|
||||
var ac = new Ac();
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
var val = new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + i * 0.1);
|
||||
_ = ac.Update(val, isNew: true);
|
||||
}
|
||||
|
||||
Assert.True(ac.IsHot);
|
||||
Assert.True(double.IsFinite(ac.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prime_SetsState()
|
||||
{
|
||||
var gbm = new GBM(500.0, 0.05, 0.3, seed: 77);
|
||||
var series = new TBarSeries();
|
||||
for (int i = 0; i < 60; i++)
|
||||
{
|
||||
series.Add(gbm.Next(isNew: true));
|
||||
}
|
||||
|
||||
var ac = new Ac();
|
||||
ac.Prime(series);
|
||||
|
||||
Assert.True(ac.IsHot);
|
||||
Assert.True(double.IsFinite(ac.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsResultAndIndicator()
|
||||
{
|
||||
var gbm = new GBM(500.0, 0.05, 0.3, seed: 88);
|
||||
var series = new TBarSeries();
|
||||
for (int i = 0; i < 60; i++)
|
||||
{
|
||||
series.Add(gbm.Next(isNew: true));
|
||||
}
|
||||
|
||||
var (results, indicator) = Ac.Calculate(series);
|
||||
|
||||
Assert.Equal(60, results.Count);
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_TBarSeries_Empty()
|
||||
{
|
||||
var result = Ac.Batch(new TBarSeries());
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TBarSeries_Empty()
|
||||
{
|
||||
var ac = new Ac();
|
||||
var result = ac.Update(new TBarSeries());
|
||||
Assert.Empty(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Self-consistency validation for AC. No external library implements AC with
|
||||
/// identical SMA-based methodology, so we validate AC = AO - SMA(AO, acPeriod)
|
||||
/// identity, determinism, and cross-mode consistency.
|
||||
/// </summary>
|
||||
public sealed class AcValidationTests
|
||||
{
|
||||
private static TBarSeries GenerateSeries(int count, int seed = 42)
|
||||
{
|
||||
var gbm = new GBM(500.0, 0.05, 0.3, seed: seed);
|
||||
var series = new TBarSeries();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
series.Add(gbm.Next(isNew: true));
|
||||
}
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AC_Equals_AO_Minus_SMA_AO()
|
||||
{
|
||||
var series = GenerateSeries(200);
|
||||
|
||||
// Compute AO
|
||||
var ao = new Ao();
|
||||
var aoValues = new List<double>();
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
var r = ao.Update(series[i], isNew: true);
|
||||
aoValues.Add(r.Value);
|
||||
}
|
||||
|
||||
// Compute SMA(AO, 5)
|
||||
var smaAo = new Sma(5);
|
||||
var smaAoValues = new List<double>();
|
||||
for (int i = 0; i < aoValues.Count; i++)
|
||||
{
|
||||
var r = smaAo.Update(new TValue(DateTime.UtcNow.AddMinutes(i), aoValues[i]), isNew: true);
|
||||
smaAoValues.Add(r.Value);
|
||||
}
|
||||
|
||||
// Compute AC via streaming
|
||||
var ac = new Ac();
|
||||
var acValues = new List<double>();
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
var r = ac.Update(series[i], isNew: true);
|
||||
acValues.Add(r.Value);
|
||||
}
|
||||
|
||||
// Verify AC = AO - SMA(AO, 5) once all are hot
|
||||
int start = 38; // slowPeriod(34) + acPeriod(5) - 1
|
||||
for (int i = start; i < series.Count; i++)
|
||||
{
|
||||
double expected = aoValues[i] - smaAoValues[i];
|
||||
Assert.Equal(expected, acValues[i], 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchAndStreaming_Match()
|
||||
{
|
||||
var series = GenerateSeries(200);
|
||||
|
||||
// Streaming
|
||||
var streaming = new Ac();
|
||||
var streamValues = new List<double>();
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
var r = streaming.Update(series[i], isNew: true);
|
||||
streamValues.Add(r.Value);
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchResult = Ac.Batch(series);
|
||||
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamValues[i], batchResult[i].Value, 4);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Determinism_SameSeedProducesSameResults()
|
||||
{
|
||||
var series1 = GenerateSeries(100, seed: 123);
|
||||
var series2 = GenerateSeries(100, seed: 123);
|
||||
|
||||
var ac1 = new Ac();
|
||||
var ac2 = new Ac();
|
||||
|
||||
for (int i = 0; i < series1.Count; i++)
|
||||
{
|
||||
var r1 = ac1.Update(series1[i], isNew: true);
|
||||
var r2 = ac2.Update(series2[i], isNew: true);
|
||||
Assert.Equal(r1.Value, r2.Value, 1e-12);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_Matches_TBarSeriesBatch()
|
||||
{
|
||||
var series = GenerateSeries(150);
|
||||
|
||||
var batchResult = Ac.Batch(series);
|
||||
|
||||
var output = new double[series.Count];
|
||||
Ac.Batch(series.High.Values, series.Low.Values, output);
|
||||
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult[i].Value, output[i], 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParameterSensitivity_DifferentPeriods_DifferentResults()
|
||||
{
|
||||
var series = GenerateSeries(100);
|
||||
|
||||
var ac1 = new Ac(5, 34, 5);
|
||||
var ac2 = new Ac(3, 20, 5);
|
||||
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
_ = ac1.Update(series[i], isNew: true);
|
||||
_ = ac2.Update(series[i], isNew: true);
|
||||
}
|
||||
|
||||
Assert.NotEqual(ac1.Last.Value, ac2.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LargeDataset_Stability()
|
||||
{
|
||||
var series = GenerateSeries(5000, seed: 55);
|
||||
|
||||
var ac = new Ac();
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
var result = ac.Update(series[i], isNew: true);
|
||||
Assert.True(double.IsFinite(result.Value), $"Non-finite at bar {i}");
|
||||
}
|
||||
|
||||
Assert.True(ac.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MonotonicConvergence_ConstantInput()
|
||||
{
|
||||
var ac = new Ac();
|
||||
double prevAbsValue = double.MaxValue;
|
||||
bool convergenceStarted = false;
|
||||
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), 50.0, 50.0, 50.0, 50.0, 1000.0);
|
||||
var result = ac.Update(bar, isNew: true);
|
||||
|
||||
if (ac.IsHot && i > 50)
|
||||
{
|
||||
double absVal = Math.Abs(result.Value);
|
||||
if (convergenceStarted)
|
||||
{
|
||||
Assert.True(absVal <= prevAbsValue + 1e-10, $"Not converging at bar {i}: {absVal} > {prevAbsValue}");
|
||||
}
|
||||
|
||||
convergenceStarted = true;
|
||||
prevAbsValue = absVal;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(convergenceStarted);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// AC: Acceleration Oscillator
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Bill Williams' Acceleration Oscillator measures the acceleration or deceleration
|
||||
/// of the current market driving force. AC is the second derivative of price momentum:
|
||||
///
|
||||
/// Median Price = (High + Low) / 2
|
||||
/// AO = SMA(Median Price, fastPeriod) - SMA(Median Price, slowPeriod)
|
||||
/// AC = AO - SMA(AO, acPeriod)
|
||||
///
|
||||
/// Sources:
|
||||
/// https://www.investopedia.com/terms/a/accelerationdeceleration-indicator.asp
|
||||
/// https://www.tradingview.com/support/solutions/43000501837-accelerator-oscillator-ac/
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Ac : ITValuePublisher
|
||||
{
|
||||
private readonly int _fastPeriod;
|
||||
private readonly int _slowPeriod;
|
||||
private readonly int _acPeriod;
|
||||
private readonly Sma _smaFast;
|
||||
private readonly Sma _smaSlow;
|
||||
private readonly Sma _smaAc;
|
||||
|
||||
private TValue _p_Last;
|
||||
|
||||
/// <summary>Display name for the indicator.</summary>
|
||||
public string Name { get; }
|
||||
|
||||
public event TValuePublishedHandler? Pub;
|
||||
|
||||
/// <summary>Current AC value.</summary>
|
||||
public TValue Last { get; private set; }
|
||||
|
||||
/// <summary>True if the AC has enough data to produce valid results.</summary>
|
||||
public bool IsHot => _smaAc.IsHot;
|
||||
|
||||
/// <summary>The number of bars required to warm up the indicator.</summary>
|
||||
public int WarmupPeriod { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates AC with specified periods.
|
||||
/// </summary>
|
||||
/// <param name="fastPeriod">Fast SMA period for AO calculation (default 5)</param>
|
||||
/// <param name="slowPeriod">Slow SMA period for AO calculation (default 34)</param>
|
||||
/// <param name="acPeriod">SMA period applied to AO for AC calculation (default 5)</param>
|
||||
public Ac(int fastPeriod = 5, int slowPeriod = 34, int acPeriod = 5)
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
if (acPeriod <= 0)
|
||||
{
|
||||
throw new ArgumentException("AC period must be greater than 0", nameof(acPeriod));
|
||||
}
|
||||
|
||||
_fastPeriod = fastPeriod;
|
||||
_slowPeriod = slowPeriod;
|
||||
_acPeriod = acPeriod;
|
||||
|
||||
_smaFast = new Sma(fastPeriod);
|
||||
_smaSlow = new Sma(slowPeriod);
|
||||
_smaAc = new Sma(acPeriod);
|
||||
WarmupPeriod = slowPeriod + acPeriod - 1;
|
||||
Name = $"Ac({fastPeriod},{slowPeriod},{acPeriod})";
|
||||
}
|
||||
|
||||
/// <summary>Resets the AC state.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Reset()
|
||||
{
|
||||
_smaFast.Reset();
|
||||
_smaSlow.Reset();
|
||||
_smaAc.Reset();
|
||||
Last = default;
|
||||
_p_Last = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the AC 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 AC value</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TBar input, bool isNew = true)
|
||||
{
|
||||
if (!double.IsFinite(input.High) || !double.IsFinite(input.Low))
|
||||
{
|
||||
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = false });
|
||||
return Last;
|
||||
}
|
||||
|
||||
double medianPrice = (input.High + input.Low) * 0.5;
|
||||
var val = new TValue(input.Time, medianPrice);
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
_p_Last = Last;
|
||||
}
|
||||
else
|
||||
{
|
||||
Last = _p_Last;
|
||||
}
|
||||
|
||||
var sFast = _smaFast.Update(val, isNew);
|
||||
var sSlow = _smaSlow.Update(val, isNew);
|
||||
|
||||
double ao = sFast.Value - sSlow.Value;
|
||||
var aoVal = new TValue(input.Time, ao);
|
||||
|
||||
var sAc = _smaAc.Update(aoVal, isNew);
|
||||
double ac = ao - sAc.Value;
|
||||
|
||||
Last = new TValue(input.Time, ac);
|
||||
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
|
||||
return Last;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the AC 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 AC value</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
if (!double.IsFinite(input.Value))
|
||||
{
|
||||
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = false });
|
||||
return Last;
|
||||
}
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
_p_Last = Last;
|
||||
}
|
||||
else
|
||||
{
|
||||
Last = _p_Last;
|
||||
}
|
||||
|
||||
var sFast = _smaFast.Update(input, isNew);
|
||||
var sSlow = _smaSlow.Update(input, isNew);
|
||||
|
||||
double ao = sFast.Value - sSlow.Value;
|
||||
var aoVal = new TValue(input.Time, ao);
|
||||
|
||||
var sAc = _smaAc.Update(aoVal, isNew);
|
||||
double ac = ao - sAc.Value;
|
||||
|
||||
Last = new TValue(input.Time, ac);
|
||||
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
|
||||
return Last;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the AC with a series of bars.
|
||||
/// </summary>
|
||||
/// <param name="source">The source series of bars</param>
|
||||
/// <returns>The AC series</returns>
|
||||
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, _acPeriod);
|
||||
|
||||
var tList = new List<long>(len);
|
||||
CollectionsMarshal.SetCount(tList, len);
|
||||
var tSpan = CollectionsMarshal.AsSpan(tList);
|
||||
source.Open.Times.CopyTo(tSpan);
|
||||
|
||||
var vList = new List<double>(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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the indicator state using the provided bar series history.
|
||||
/// </summary>
|
||||
/// <param name="source">Historical bar data.</param>
|
||||
public void Prime(TBarSeries source)
|
||||
{
|
||||
Reset();
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Update(source[i], isNew: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates AC over OHLC spans into a preallocated output span.
|
||||
/// Median price is computed as (High + Low) / 2.
|
||||
/// </summary>
|
||||
/// <param name="high">High prices</param>
|
||||
/// <param name="low">Low prices</param>
|
||||
/// <param name="destination">Output AC values</param>
|
||||
/// <param name="fastPeriod">Fast SMA period (default 5)</param>
|
||||
/// <param name="slowPeriod">Slow SMA period (default 34)</param>
|
||||
/// <param name="acPeriod">AC SMA period (default 5)</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Batch(ReadOnlySpan<double> high, ReadOnlySpan<double> low, Span<double> destination, int fastPeriod = 5, int slowPeriod = 34, int acPeriod = 5)
|
||||
{
|
||||
if (fastPeriod <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(fastPeriod), "Fast period must be greater than 0.");
|
||||
}
|
||||
|
||||
if (slowPeriod <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(slowPeriod), "Slow period must be greater than 0.");
|
||||
}
|
||||
|
||||
if (fastPeriod >= slowPeriod)
|
||||
{
|
||||
throw new ArgumentException("Fast period must be less than slow period.", nameof(fastPeriod));
|
||||
}
|
||||
|
||||
if (acPeriod <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(acPeriod), "AC period must be greater than 0.");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Rent buffers: median + fast + slow + ao = 4 * len
|
||||
double[] rentedBuffer = ArrayPool<double>.Shared.Rent(len * 4);
|
||||
try
|
||||
{
|
||||
Span<double> median = rentedBuffer.AsSpan(0, len);
|
||||
Span<double> fast = rentedBuffer.AsSpan(len, len);
|
||||
Span<double> slow = rentedBuffer.AsSpan(len * 2, len);
|
||||
Span<double> ao = rentedBuffer.AsSpan(len * 3, 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);
|
||||
|
||||
// AO = fast - slow
|
||||
SimdExtensions.Subtract(fast, slow, ao);
|
||||
|
||||
// AC = AO - SMA(AO, acPeriod)
|
||||
Sma.Batch(ao, destination, acPeriod);
|
||||
SimdExtensions.Subtract(ao, destination, destination);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<double>.Shared.Return(rentedBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates AC for the entire series using a stateless batch path.
|
||||
/// </summary>
|
||||
/// <param name="source">Input bar series</param>
|
||||
/// <param name="fastPeriod">Fast SMA period (default 5)</param>
|
||||
/// <param name="slowPeriod">Slow SMA period (default 34)</param>
|
||||
/// <param name="acPeriod">AC SMA period (default 5)</param>
|
||||
/// <returns>AC series</returns>
|
||||
public static TSeries Batch(TBarSeries source, int fastPeriod = 5, int slowPeriod = 34, int acPeriod = 5)
|
||||
{
|
||||
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, acPeriod);
|
||||
|
||||
var tList = new List<long>(len);
|
||||
CollectionsMarshal.SetCount(tList, len);
|
||||
var tSpan = CollectionsMarshal.AsSpan(tList);
|
||||
source.Open.Times.CopyTo(tSpan);
|
||||
|
||||
var vList = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(vList, len);
|
||||
var vSpan = CollectionsMarshal.AsSpan(vList);
|
||||
v.AsSpan().CopyTo(vSpan);
|
||||
|
||||
return new TSeries(tList, vList);
|
||||
}
|
||||
|
||||
public static (TSeries Results, Ac Indicator) Calculate(TBarSeries source, int fastPeriod = 5, int slowPeriod = 34, int acPeriod = 5)
|
||||
{
|
||||
var indicator = new Ac(fastPeriod, slowPeriod, acPeriod);
|
||||
TSeries results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
# AC: Acceleration Oscillator
|
||||
|
||||
> "Knowing speed is useful. Knowing whether you're speeding up or slowing down is what keeps you alive."
|
||||
|
||||
## Introduction
|
||||
|
||||
The Acceleration Oscillator (AC) is Bill Williams' second-derivative momentum indicator. Where the Awesome Oscillator (AO) measures the speed of market momentum, AC measures whether that momentum is accelerating or decelerating. AC is computed as AO minus a 5-period SMA of AO. Zero crossings and color changes signal shifts in market driving force before price reverses.
|
||||
|
||||
## Historical Context
|
||||
|
||||
Bill Williams introduced AC alongside AO in his "Trading Chaos" methodology. While AO already strips trend by subtracting a slow SMA from a fast SMA (both applied to the bar midpoint), traders found they needed earlier warning of momentum shifts. AC provides exactly that: the rate of change of AO itself. When AC crosses zero from below, the market's driving force is accelerating upward, often preceding AO's own zero crossing by several bars.
|
||||
|
||||
## Calculation
|
||||
|
||||
The AC indicator is calculated in two stages:
|
||||
|
||||
### Stage 1: Awesome Oscillator
|
||||
|
||||
$$\text{Median Price} = \frac{\text{High} + \text{Low}}{2}$$
|
||||
|
||||
$$\text{AO} = \text{SMA}(\text{Median Price}, \text{fast}) - \text{SMA}(\text{Median Price}, \text{slow})$$
|
||||
|
||||
### Stage 2: Acceleration
|
||||
|
||||
$$\text{AC} = \text{AO} - \text{SMA}(\text{AO}, \text{acPeriod})$$
|
||||
|
||||
Default parameters: fast = 5, slow = 34, acPeriod = 5.
|
||||
|
||||
## Interpretation
|
||||
|
||||
- **AC > 0 and rising (green):** Bullish acceleration. Momentum is strengthening.
|
||||
- **AC > 0 and falling (red):** Bullish deceleration. Momentum still positive but weakening.
|
||||
- **AC < 0 and falling (green to red):** Bearish acceleration. Momentum is weakening further.
|
||||
- **AC < 0 and rising (red to green):** Bearish deceleration. Downward momentum is weakening.
|
||||
- **Zero crossings:** Often precede AO zero crossings, providing earlier entry/exit signals.
|
||||
|
||||
### Bill Williams' Trading Rules
|
||||
|
||||
1. **Buy signal:** AC is green (rising) for two consecutive bars above zero, or three consecutive green bars below zero.
|
||||
2. **Sell signal:** AC is red (falling) for two consecutive bars below zero, or three consecutive red bars above zero.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Default | Range | Description |
|
||||
| :-------- | :------ | :---- | :---------- |
|
||||
| fastPeriod | 5 | > 0 | Fast SMA period for AO calculation |
|
||||
| slowPeriod | 34 | > fast | Slow SMA period for AO calculation |
|
||||
| acPeriod | 5 | > 0 | SMA period applied to AO values |
|
||||
|
||||
## API
|
||||
|
||||
### Streaming
|
||||
|
||||
```csharp
|
||||
var ac = new Ac(fastPeriod: 5, slowPeriod: 34, acPeriod: 5);
|
||||
TValue result = ac.Update(bar, isNew: true);
|
||||
```
|
||||
|
||||
### Batch (TBarSeries)
|
||||
|
||||
```csharp
|
||||
TSeries results = Ac.Batch(barSeries);
|
||||
```
|
||||
|
||||
### Batch (Span)
|
||||
|
||||
```csharp
|
||||
Ac.Batch(highSpan, lowSpan, outputSpan, fastPeriod: 5, slowPeriod: 34, acPeriod: 5);
|
||||
```
|
||||
|
||||
### Calculate
|
||||
|
||||
```csharp
|
||||
var (results, indicator) = Ac.Calculate(barSeries, fastPeriod: 5, slowPeriod: 34, acPeriod: 5);
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```csharp
|
||||
// Streaming
|
||||
var ac = new Ac();
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
var result = ac.Update(bar);
|
||||
if (ac.IsHot && result.Value > 0)
|
||||
{
|
||||
// Bullish momentum accelerating
|
||||
}
|
||||
}
|
||||
|
||||
// Event-driven chaining
|
||||
ac.Pub += (sender, e) => Console.WriteLine($"AC: {e.Value.Value:F4}");
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
| Operation | Complexity | Allocations |
|
||||
| :-------- | :--------- | :---------- |
|
||||
| Update (streaming) | O(1) | Zero |
|
||||
| Batch (Span) | O(n) | ArrayPool |
|
||||
| Warmup period | slow + ac - 1 | — |
|
||||
|
||||
AC uses three internal SMA instances. Each SMA uses a RingBuffer for O(1) sliding window computation. The Batch path uses SIMD-accelerated subtraction via `SimdExtensions.Subtract`.
|
||||
|
||||
## Validation
|
||||
|
||||
AC is validated via self-consistency (AC = AO - SMA(AO, acPeriod)) and batch/streaming equivalence. No external library implements AC with identical SMA methodology for cross-library validation.
|
||||
|
||||
| Test | Status |
|
||||
| :--- | :----- |
|
||||
| AC = AO - SMA(AO) identity | Pass |
|
||||
| Batch/streaming match | Pass |
|
||||
| Span/TBarSeries match | Pass |
|
||||
| Determinism | Pass |
|
||||
| Constant input convergence | Pass (→ 0) |
|
||||
| Large dataset stability | Pass (5000 bars) |
|
||||
|
||||
## Sources
|
||||
|
||||
- Williams, Bill. "Trading Chaos." Wiley, 1995.
|
||||
- Williams, Bill. "New Trading Dimensions." Wiley, 1998.
|
||||
- [Investopedia: Accelerator Oscillator](https://www.investopedia.com/terms/a/accelerationdeceleration-indicator.asp)
|
||||
- [TradingView: AC](https://www.tradingview.com/support/solutions/43000501837-accelerator-oscillator-ac/)
|
||||
Reference in New Issue
Block a user