using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Columns; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Environments; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Running; using QuanTAlib; using QuanTAlib.Benchmarks; using Skender.Stock.Indicators; using TALib; using Tulip; using OoplesFinance.StockIndicators; using OoplesFinance.StockIndicators.Models; using OoplesFinance.StockIndicators.Enums; namespace QuanTAlib.Benchmarks; public static class Program { public static void Main(string[] args) { // Run: dotnet run -c Release -- --filter *Sma* *Ema* *Wma* var config = ManualConfig.Create(DefaultConfig.Instance) .AddJob(Job.ShortRun .WithRuntime(CoreRuntime.Core10_0) .WithId("NET10-JIT")) .AddColumn(StatisticColumn.Mean) .AddColumn(StatisticColumn.StdDev) .HideColumns(Column.Job, Column.Error, Column.RatioSD); if (args.Length == 0) { BenchmarkRunner.Run(config); } else { BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config); } } } [MemoryDiagnoser] [MarkdownExporter, HtmlExporter] [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] public class IndicatorBenchmarks { private const int BarCount = 500_000; private const int Period = 220; private double[] _closeValues = null!; private double[] _openValues = null!; private TSeries _closeTseries = null!; private List _quotes = null!; private List _ooplesData = null!; // Pre-allocated outputs for TA-Lib private double[] _talibOutput = null!; // Pre-allocated outputs for Tulip private double[][] _tulipSmaInputs = null!; private double[] _tulipSmaOptions = null!; private double[][] _tulipSmaOutputs = null!; private double[][] _tulipEmaInputs = null!; private double[] _tulipEmaOptions = null!; private double[][] _tulipEmaOutputs = null!; private double[][] _tulipWmaInputs = null!; private double[] _tulipWmaOptions = null!; private double[][] _tulipWmaOutputs = null!; private double[][] _tulipHmaInputs = null!; private double[] _tulipHmaOptions = null!; private double[][] _tulipHmaOutputs = null!; // Pre-allocated outputs for ADOSC private double[] _highValues = null!; private double[] _lowValues = null!; private double[] _volumeValues = null!; private TBarSeries _bars = null!; private double[][] _tulipAdoscInputs = null!; private double[] _tulipAdoscOptions = null!; private double[][] _tulipAdoscOutputs = null!; // Pre-allocated outputs for QuanTAlib Span API private double[] _quantalibOutput = null!; [GlobalSetup] public void Setup() { // Generate data using GBM var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42); var bars = gbm.Fetch(BarCount, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); _bars = bars; _closeValues = bars.Close.Values.ToArray(); _openValues = bars.Open.Values.ToArray(); _highValues = bars.High.Values.ToArray(); _lowValues = bars.Low.Values.ToArray(); _volumeValues = bars.Volume.Values.ToArray(); _closeTseries = bars.Close; // Create Skender Quote format _quotes = new List(BarCount); for (int i = 0; i < BarCount; i++) { _quotes.Add(new Quote { Date = new DateTime(_closeTseries.Times[i], DateTimeKind.Utc), Open = (decimal)bars.Open.Values[i], High = (decimal)bars.High.Values[i], Low = (decimal)bars.Low.Values[i], Close = (decimal)_closeValues[i], Volume = (decimal)bars.Volume.Values[i] }); } // Create Ooples TickerData format _ooplesData = new List(BarCount); for (int i = 0; i < BarCount; i++) { _ooplesData.Add(new TickerData { Date = new DateTime(_closeTseries.Times[i], DateTimeKind.Utc), Open = bars.Open.Values[i], High = bars.High.Values[i], Low = bars.Low.Values[i], Close = _closeValues[i], Volume = bars.Volume.Values[i] }); } // Pre-allocate TA-Lib output _talibOutput = new double[BarCount]; // Pre-allocate Tulip arrays int smaLookback = Period - 1; _tulipSmaInputs = new[] { _closeValues }; _tulipSmaOptions = new double[] { Period }; _tulipSmaOutputs = new[] { new double[BarCount - smaLookback] }; _tulipEmaInputs = new[] { _closeValues }; _tulipEmaOptions = new double[] { Period }; _tulipEmaOutputs = new[] { new double[BarCount] }; _tulipWmaInputs = new[] { _closeValues }; _tulipWmaOptions = new double[] { Period }; _tulipWmaOutputs = new[] { new double[BarCount - smaLookback] }; int hmaLookback = Period + (int)Math.Sqrt(Period) - 2; _tulipHmaInputs = new[] { _closeValues }; _tulipHmaOptions = new double[] { Period }; _tulipHmaOutputs = new[] { new double[BarCount - hmaLookback] }; // Pre-allocate Tulip ADOSC _tulipAdoscInputs = new[] { _highValues, _lowValues, _closeValues, _volumeValues }; _tulipAdoscOptions = new double[] { 3, 10 }; // Fast=3, Slow=10 _tulipAdoscOutputs = new[] { new double[BarCount - 1] }; // Tulip ADOSC starts at index 1? // Pre-allocate QuanTAlib output _quantalibOutput = new double[BarCount]; } // ==================== ADOSC ==================== [BenchmarkCategory("ADOSC")] [Benchmark(Description = "QuanTAlib ADOSC (Span)")] public void QuanTAlib_Adosc_Span() => Adosc.Batch(_highValues.AsSpan(), _lowValues.AsSpan(), _closeValues.AsSpan(), _volumeValues.AsSpan(), _quantalibOutput.AsSpan(), 3, 10); [BenchmarkCategory("ADOSC")] [Benchmark(Description = "QuanTAlib ADOSC (Batch)")] public TSeries QuanTAlib_Adosc_TSeries() => Adosc.Batch(_bars, 3, 10); [BenchmarkCategory("ADOSC")] [Benchmark(Description = "QuanTAlib ADOSC (Streaming)")] public void QuanTAlib_Adosc_Streaming() { var adosc = new Adosc(3, 10); for (int i = 0; i < _bars.Count; i++) { _quantalibOutput[i] = adosc.Update(_bars[i]).Value; } } [BenchmarkCategory("ADOSC")] [Benchmark(Description = "Tulip ADOSC")] public void Tulip_Adosc() => Tulip.Indicators.adosc.Run(_tulipAdoscInputs, _tulipAdoscOptions, _tulipAdoscOutputs); [BenchmarkCategory("ADOSC")] [Benchmark(Description = "TALib ADOSC")] public Core.RetCode TALib_Adosc() => TALib.Functions.AdOsc(_highValues, _lowValues, _closeValues, _volumeValues, 0..^0, _quantalibOutput, out _, 3, 10); [BenchmarkCategory("ADOSC")] [Benchmark(Description = "Skender ADOSC")] public object Skender_Adosc() => _quotes.GetChaikinOsc(3, 10); [BenchmarkCategory("ADOSC")] [Benchmark(Description = "Ooples ADOSC")] public object Ooples_Adosc() => new StockData(_ooplesData).CalculateChaikinOscillator(MovingAvgType.ExponentialMovingAverage, 3, 10); // ==================== SMA ==================== [BenchmarkCategory("SMA")] [Benchmark(Description = "QuanTAlib SMA (Span)")] public void QuanTAlib_Sma_Span() => Sma.Batch(_closeValues.AsSpan(), _quantalibOutput.AsSpan(), Period); [BenchmarkCategory("SMA")] [Benchmark(Description = "QuanTAlib SMA (Batch)")] public TSeries QuanTAlib_Sma_TSeries() => Sma.Calculate(_closeTseries, Period).Results; [BenchmarkCategory("SMA")] [Benchmark(Description = "QuanTAlib SMA (Streaming)")] public void QuanTAlib_Sma_Streaming() { var sma = new Sma(Period); for (int i = 0; i < _closeValues.Length; i++) { _quantalibOutput[i] = sma.Update(new TValue(_closeTseries.Times[i], _closeValues[i])).Value; } } [BenchmarkCategory("SMA")] [Benchmark(Description = "QuanTAlib SMA (Eventing)")] public void QuanTAlib_Sma_Eventing() { var source = new TSeries(); var sma = new Sma(source, Period); for (int i = 0; i < _closeValues.Length; i++) { source.Add(new TValue(_closeTseries.Times[i], _closeValues[i])); _quantalibOutput[i] = sma.Last.Value; } } [BenchmarkCategory("SMA")] [Benchmark(Description = "Tulip SMA")] public void Tulip_Sma() => Tulip.Indicators.sma.Run(_tulipSmaInputs, _tulipSmaOptions, _tulipSmaOutputs); [BenchmarkCategory("SMA")] [Benchmark(Description = "TALib SMA")] public Core.RetCode TALib_Sma() => TALib.Functions.Sma(_closeValues, 0..^0, _talibOutput, out _, Period); [BenchmarkCategory("SMA")] [Benchmark(Description = "Skender SMA")] public object Skender_Sma() => _quotes.GetSma(Period); [BenchmarkCategory("SMA")] [Benchmark(Description = "Ooples SMA")] public object Ooples_Sma() => new StockData(_ooplesData).CalculateSimpleMovingAverage(Period); // ==================== EMA ==================== [BenchmarkCategory("EMA")] [Benchmark(Description = "QuanTAlib EMA (Span)")] public void QuanTAlib_Ema_Span() => Ema.Batch(_closeValues.AsSpan(), _quantalibOutput.AsSpan(), Period); [BenchmarkCategory("EMA")] [Benchmark(Description = "QuanTAlib EMA (Batch)")] public TSeries QuanTAlib_Ema_TSeries() => Ema.Calculate(_closeTseries, Period).Results; [BenchmarkCategory("EMA")] [Benchmark(Description = "QuanTAlib EMA (Streaming)")] public void QuanTAlib_Ema_Streaming() { var ema = new Ema(Period); for (int i = 0; i < _closeValues.Length; i++) { _quantalibOutput[i] = ema.Update(new TValue(_closeTseries.Times[i], _closeValues[i])).Value; } } [BenchmarkCategory("EMA")] [Benchmark(Description = "QuanTAlib EMA (Eventing)")] public void QuanTAlib_Ema_Eventing() { var source = new TSeries(); var ema = new Ema(source, Period); for (int i = 0; i < _closeValues.Length; i++) { source.Add(new TValue(_closeTseries.Times[i], _closeValues[i])); _quantalibOutput[i] = ema.Last.Value; } } [BenchmarkCategory("EMA")] [Benchmark(Description = "Tulip EMA")] public void Tulip_Ema() => Tulip.Indicators.ema.Run(_tulipEmaInputs, _tulipEmaOptions, _tulipEmaOutputs); [BenchmarkCategory("EMA")] [Benchmark(Description = "TALib EMA")] public Core.RetCode TALib_Ema() => TALib.Functions.Ema(_closeValues, 0..^0, _talibOutput, out _, Period); [BenchmarkCategory("EMA")] [Benchmark(Description = "Skender EMA")] public object Skender_Ema() => _quotes.GetEma(Period); [BenchmarkCategory("EMA")] [Benchmark(Description = "Ooples EMA")] public object Ooples_Ema() => new StockData(_ooplesData).CalculateExponentialMovingAverage(Period); // ==================== WMA ==================== [BenchmarkCategory("WMA")] [Benchmark(Description = "QuanTAlib WMA (Span)")] public void QuanTAlib_Wma_Span() => Wma.Batch(_closeValues.AsSpan(), _quantalibOutput.AsSpan(), Period); [BenchmarkCategory("WMA")] [Benchmark(Description = "QuanTAlib WMA (Batch)")] public TSeries QuanTAlib_Wma_TSeries() => Wma.Batch(_closeTseries, Period); [BenchmarkCategory("WMA")] [Benchmark(Description = "QuanTAlib WMA (Streaming)")] public void QuanTAlib_Wma_Streaming() { var wma = new Wma(Period); for (int i = 0; i < _closeValues.Length; i++) { _quantalibOutput[i] = wma.Update(new TValue(_closeTseries.Times[i], _closeValues[i])).Value; } } [BenchmarkCategory("WMA")] [Benchmark(Description = "QuanTAlib WMA (Eventing)")] public void QuanTAlib_Wma_Eventing() { var source = new TSeries(); var wma = new Wma(source, Period); for (int i = 0; i < _closeValues.Length; i++) { source.Add(new TValue(_closeTseries.Times[i], _closeValues[i])); _quantalibOutput[i] = wma.Last.Value; } } [BenchmarkCategory("WMA")] [Benchmark(Description = "Tulip WMA")] public void Tulip_Wma() => Tulip.Indicators.wma.Run(_tulipWmaInputs, _tulipWmaOptions, _tulipWmaOutputs); [BenchmarkCategory("WMA")] [Benchmark(Description = "TALib WMA")] public Core.RetCode TALib_Wma() => TALib.Functions.Wma(_closeValues, 0..^0, _talibOutput, out _, Period); [BenchmarkCategory("WMA")] [Benchmark(Description = "Skender WMA")] public object Skender_Wma() => _quotes.GetWma(Period); [BenchmarkCategory("WMA")] [Benchmark(Description = "Ooples WMA")] public object Ooples_Wma() => new StockData(_ooplesData).CalculateWeightedMovingAverage(Period); // ==================== HMA ==================== [BenchmarkCategory("HMA")] [Benchmark(Description = "QuanTAlib HMA (Span)")] public void QuanTAlib_Hma_Span() => Hma.Batch(_closeValues.AsSpan(), _quantalibOutput.AsSpan(), Period); [BenchmarkCategory("HMA")] [Benchmark(Description = "QuanTAlib HMA (Batch)")] public TSeries QuanTAlib_Hma_TSeries() => Hma.Batch(_closeTseries, Period); [BenchmarkCategory("HMA")] [Benchmark(Description = "QuanTAlib HMA (Streaming)")] public void QuanTAlib_Hma_Streaming() { var hma = new Hma(Period); for (int i = 0; i < _closeValues.Length; i++) { _quantalibOutput[i] = hma.Update(new TValue(_closeTseries.Times[i], _closeValues[i])).Value; } } [BenchmarkCategory("HMA")] [Benchmark(Description = "QuanTAlib HMA (Eventing)")] public void QuanTAlib_Hma_Eventing() { var source = new TSeries(); var hma = new Hma(source, Period); for (int i = 0; i < _closeValues.Length; i++) { source.Add(new TValue(_closeTseries.Times[i], _closeValues[i])); _quantalibOutput[i] = hma.Last.Value; } } [BenchmarkCategory("HMA")] [Benchmark(Description = "Tulip HMA")] public void Tulip_Hma() => Tulip.Indicators.hma.Run(_tulipHmaInputs, _tulipHmaOptions, _tulipHmaOutputs); [BenchmarkCategory("HMA")] [Benchmark(Description = "Skender HMA")] public object Skender_Hma() => _quotes.GetHma(Period); [BenchmarkCategory("HMA")] [Benchmark(Description = "Ooples HMA")] public object Ooples_Hma() => new StockData(_ooplesData).CalculateHullMovingAverage(MovingAvgType.WeightedMovingAverage, Period); // ==================== CORRELATION ==================== [BenchmarkCategory("CORRELATION")] [Benchmark(Description = "QuanTAlib Correlation (Span)")] public void QuanTAlib_Correlation_Span() => Correlation.Batch(_closeValues.AsSpan(), _openValues.AsSpan(), _quantalibOutput.AsSpan(), Period); [BenchmarkCategory("CORRELATION")] [Benchmark(Description = "QuanTAlib Correlation (Streaming)")] public void QuanTAlib_Correlation_Streaming() { var corr = new Correlation(Period); for (int i = 0; i < _closeValues.Length; i++) { _quantalibOutput[i] = corr.Update(_closeValues[i], _openValues[i]).Value; } } [BenchmarkCategory("CORRELATION")] [Benchmark(Description = "TALib Correlation")] public Core.RetCode TALib_Correlation() => TALib.Functions.Correl(_closeValues, _openValues, 0..^0, _talibOutput, out _, Period); [BenchmarkCategory("CORRELATION")] [Benchmark(Description = "Skender Correlation")] public object Skender_Correlation() => _quotes.GetCorrelation(_quotes, Period); // ==================== SKEW ==================== [BenchmarkCategory("SKEW")] [Benchmark(Description = "QuanTAlib Skew (Span)")] public void QuanTAlib_Skew_Span() => Skew.Batch(_closeValues.AsSpan(), _quantalibOutput.AsSpan(), Period); [BenchmarkCategory("SKEW")] [Benchmark(Description = "QuanTAlib Skew (Batch)")] public TSeries QuanTAlib_Skew_TSeries() => Skew.Batch(_closeTseries, Period); [BenchmarkCategory("SKEW")] [Benchmark(Description = "QuanTAlib Skew (Streaming)")] public void QuanTAlib_Skew_Streaming() { var skew = new Skew(Period); for (int i = 0; i < _closeValues.Length; i++) { _quantalibOutput[i] = skew.Update(new TValue(_closeTseries.Times[i], _closeValues[i])).Value; } } }