Add unit tests for various moving average indicators

- Implement tests for HMA (Hull Moving Average) indicator to verify default settings, history depth calculations, and value computations during updates.
- Create tests for KAMA (Kaufman Adaptive Moving Average) indicator, ensuring correct defaults, history depth, and value calculations.
- Add tests for SMA (Simple Moving Average) indicator, checking default values, history depth, and value computations.
- Develop tests for T3 (Tillson T3 Moving Average) indicator, validating defaults, history depth, and value calculations.
- Implement tests for TEMA (Triple Exponential Moving Average) indicator, ensuring correct defaults and value computations.
- Create tests for TRIMA (Triangular Moving Average) indicator, verifying defaults, history depth, and value calculations.
- Add tests for WMA (Weighted Moving Average) indicator, checking default values, history depth, and value computations.
This commit is contained in:
Miha Kralj
2025-12-08 11:00:58 -08:00
parent 488de7ea1e
commit ed5e5c8209
72 changed files with 2834 additions and 92 deletions
+66
View File
@@ -0,0 +1,66 @@
using System.Drawing;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
public class SmaIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Period", sortIndex: 1, 1, 1000, 1, 0)]
public int Period { get; set; } = 10;
[IndicatorExtensions.DataSourceInput]
public SourceType Source { get; set; } = SourceType.Close;
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private Sma? ma;
protected LineSeries? Series;
protected string? SourceName;
private int _warmupBarIndex = -1;
public int MinHistoryDepths => Period;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => $"SMA {Period}:{SourceName}";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/trends/sma/Sma.Quantower.cs";
public SmaIndicator()
{
OnBackGround = true;
SeparateWindow = false;
SourceName = Source.ToString();
Name = "SMA - Simple Moving Average";
Description = "Simple Moving Average";
Series = new(name: $"SMA {Period}", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
AddLineSeries(Series);
}
protected override void OnInit()
{
ma = new Sma(Period);
SourceName = Source.ToString();
_warmupBarIndex = -1; // Reset warmup tracking when period changes
base.OnInit();
}
protected override void OnUpdate(UpdateArgs args)
{
TValue input = this.GetInputValue(args, Source);
bool isNew = args.Reason == UpdateReason.NewBar || args.Reason == UpdateReason.HistoricalBar;
TValue result = ma!.Update(input, isNew);
Series!.SetValue(result.Value);
Series!.SetMarker(0, Color.Transparent); //OnPaintChart draws the line, hidden here
// Track when IsHot becomes true for the first time
if (_warmupBarIndex < 0 && ma!.IsHot)
_warmupBarIndex = Count;
}
public override void OnPaintChart(PaintChartEventArgs args)
{
base.OnPaintChart(args);
int warmupPeriod = _warmupBarIndex > 0 ? _warmupBarIndex : Count;
this.PaintSmoothCurve(args, Series!, warmupPeriod, showColdValues: ShowColdValues, tension: 0.2);
}
}
+509
View File
@@ -0,0 +1,509 @@
namespace QuanTAlib.Tests;
#pragma warning disable S2245 // Random is acceptable for simulation/testing purposes
public class SmaTests
{
[Fact]
public void Sma_Constructor_ValidatesInput()
{
Assert.Throws<ArgumentException>(() => new Sma(0));
Assert.Throws<ArgumentException>(() => new Sma(-1));
var sma = new Sma(10);
Assert.NotNull(sma);
}
[Fact]
public void Sma_Calc_ReturnsValue()
{
var sma = new Sma(10);
Assert.Equal(0, sma.Last.Value);
TValue result = sma.Update(new TValue(DateTime.UtcNow, 100));
Assert.True(result.Value > 0);
Assert.Equal(result.Value, sma.Last.Value);
}
[Fact]
public void Sma_FirstValue_ReturnsItself()
{
var sma = new Sma(10);
TValue result = sma.Update(new TValue(DateTime.UtcNow, 100));
Assert.Equal(100.0, result.Value, 1e-10);
}
[Fact]
public void Sma_Calc_IsNew_AcceptsParameter()
{
var sma = new Sma(10);
sma.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
double value1 = sma.Last.Value;
sma.Update(new TValue(DateTime.UtcNow, 200), isNew: true);
double value2 = sma.Last.Value;
// Values should change with new bars
Assert.NotEqual(value1, value2);
}
[Fact]
public void Sma_Calc_IsNew_False_UpdatesValue()
{
var sma = new Sma(10);
sma.Update(new TValue(DateTime.UtcNow, 100));
sma.Update(new TValue(DateTime.UtcNow, 110), isNew: true);
double beforeUpdate = sma.Last.Value;
sma.Update(new TValue(DateTime.UtcNow, 120), isNew: false);
double afterUpdate = sma.Last.Value;
// Update should change the value
Assert.NotEqual(beforeUpdate, afterUpdate);
}
[Fact]
public void Sma_Reset_ClearsState()
{
var sma = new Sma(10);
sma.Update(new TValue(DateTime.UtcNow, 100));
sma.Update(new TValue(DateTime.UtcNow, 105));
double valueBefore = sma.Last.Value;
sma.Reset();
Assert.Equal(0, sma.Last.Value);
// After reset, should accept new values
sma.Update(new TValue(DateTime.UtcNow, 50));
Assert.NotEqual(0, sma.Last.Value);
Assert.NotEqual(valueBefore, sma.Last.Value);
}
[Fact]
public void Sma_Properties_Accessible()
{
var sma = new Sma(10);
Assert.Equal(0, sma.Last.Value);
Assert.False(sma.IsHot);
sma.Update(new TValue(DateTime.UtcNow, 100));
Assert.NotEqual(0, sma.Last.Value);
}
[Fact]
public void Sma_IsHot_BecomesTrueWhenBufferFull()
{
var sma = new Sma(5);
Assert.False(sma.IsHot);
for (int i = 1; i <= 4; i++)
{
sma.Update(new TValue(DateTime.UtcNow, i * 10));
Assert.False(sma.IsHot);
}
sma.Update(new TValue(DateTime.UtcNow, 50));
Assert.True(sma.IsHot);
}
[Fact]
public void Sma_CalculatesCorrectAverage()
{
var sma = new Sma(5);
sma.Update(new TValue(DateTime.UtcNow, 10));
sma.Update(new TValue(DateTime.UtcNow, 20));
sma.Update(new TValue(DateTime.UtcNow, 30));
sma.Update(new TValue(DateTime.UtcNow, 40));
sma.Update(new TValue(DateTime.UtcNow, 50));
// SMA(5) of 10,20,30,40,50 = 150/5 = 30
Assert.Equal(30.0, sma.Last.Value, 1e-10);
}
[Fact]
public void Sma_SlidingWindow_Works()
{
var sma = new Sma(3);
sma.Update(new TValue(DateTime.UtcNow, 10));
sma.Update(new TValue(DateTime.UtcNow, 20));
sma.Update(new TValue(DateTime.UtcNow, 30));
// SMA(3) of 10,20,30 = 60/3 = 20
Assert.Equal(20.0, sma.Last.Value, 1e-10);
sma.Update(new TValue(DateTime.UtcNow, 40));
// SMA(3) of 20,30,40 = 90/3 = 30
Assert.Equal(30.0, sma.Last.Value, 1e-10);
sma.Update(new TValue(DateTime.UtcNow, 50));
// SMA(3) of 30,40,50 = 120/3 = 40
Assert.Equal(40.0, sma.Last.Value, 1e-10);
}
[Fact]
public void Sma_IterativeCorrections_RestoreToOriginalState()
{
var sma = new Sma(5);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
// Feed 10 new values
TValue tenthInput = default;
for (int i = 0; i < 10; i++)
{
var bar = gbm.Next(isNew: true);
tenthInput = new TValue(bar.Time, bar.Close);
sma.Update(tenthInput, isNew: true);
}
// Remember SMA state after 10 values
double smaAfterTen = sma.Last.Value;
// Generate 9 corrections with isNew=false (different values)
for (int i = 0; i < 9; i++)
{
var bar = gbm.Next(isNew: false);
sma.Update(new TValue(bar.Time, bar.Close), isNew: false);
}
// Feed the remembered 10th input again with isNew=false
TValue finalSma = sma.Update(tenthInput, isNew: false);
// SMA should match the original state after 10 values
Assert.Equal(smaAfterTen, finalSma.Value, 1e-10);
}
[Fact]
public void Sma_BatchCalc_MatchesIterativeCalc()
{
var smaIterative = new Sma(10);
var smaBatch = new Sma(10);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
// Generate data
var series = new TSeries();
for (int i = 0; i < 100; i++)
{
var bar = gbm.Next(isNew: true);
series.Add(bar.Time, bar.Close);
}
Assert.True(series.Count > 0);
// Calculate iteratively
var iterativeResults = new TSeries();
foreach (var item in series)
{
iterativeResults.Add(smaIterative.Update(item));
}
// Calculate batch
var batchResults = smaBatch.Update(series);
// Compare
Assert.Equal(iterativeResults.Count, batchResults.Count);
for (int i = 0; i < iterativeResults.Count; i++)
{
Assert.Equal(iterativeResults[i].Value, batchResults[i].Value, 1e-10);
Assert.Equal(iterativeResults[i].Time, batchResults[i].Time);
}
}
[Fact]
public void Sma_Result_ImplicitConversionToDouble()
{
var sma = new Sma(10);
sma.Update(new TValue(DateTime.UtcNow, 100));
// This should compile and work because TValue has implicit conversion to double
double result = sma.Last.Value;
Assert.Equal(100.0, result, 1e-10);
}
[Fact]
public void Sma_NaN_Input_UsesLastValidValue()
{
var sma = new Sma(5);
// Feed some valid values
sma.Update(new TValue(DateTime.UtcNow, 100));
sma.Update(new TValue(DateTime.UtcNow, 110));
// Feed NaN - should use last valid value (110)
var resultAfterNaN = sma.Update(new TValue(DateTime.UtcNow, double.NaN));
// Result should be finite (not NaN)
Assert.True(double.IsFinite(resultAfterNaN.Value));
Assert.NotEqual(0, resultAfterNaN.Value);
}
[Fact]
public void Sma_Infinity_Input_UsesLastValidValue()
{
var sma = new Sma(5);
// Feed some valid values
sma.Update(new TValue(DateTime.UtcNow, 100));
sma.Update(new TValue(DateTime.UtcNow, 110));
// Feed positive infinity - should use last valid value
var resultAfterPosInf = sma.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
Assert.True(double.IsFinite(resultAfterPosInf.Value));
// Feed negative infinity - should use last valid value
var resultAfterNegInf = sma.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
Assert.True(double.IsFinite(resultAfterNegInf.Value));
}
[Fact]
public void Sma_MultipleNaN_ContinuesWithLastValid()
{
var sma = new Sma(5);
// Feed valid values
sma.Update(new TValue(DateTime.UtcNow, 100));
sma.Update(new TValue(DateTime.UtcNow, 110));
sma.Update(new TValue(DateTime.UtcNow, 120));
// Feed multiple NaN values
var r1 = sma.Update(new TValue(DateTime.UtcNow, double.NaN));
var r2 = sma.Update(new TValue(DateTime.UtcNow, double.NaN));
var r3 = sma.Update(new TValue(DateTime.UtcNow, double.NaN));
// All results should be finite
Assert.True(double.IsFinite(r1.Value));
Assert.True(double.IsFinite(r2.Value));
Assert.True(double.IsFinite(r3.Value));
}
[Fact]
public void Sma_BatchCalc_HandlesNaN()
{
var sma = new Sma(5);
// Create series with NaN values interspersed
var series = new TSeries();
series.Add(DateTime.UtcNow.Ticks, 100);
series.Add(DateTime.UtcNow.Ticks + 1, 110);
series.Add(DateTime.UtcNow.Ticks + 2, double.NaN);
series.Add(DateTime.UtcNow.Ticks + 3, 120);
series.Add(DateTime.UtcNow.Ticks + 4, double.PositiveInfinity);
series.Add(DateTime.UtcNow.Ticks + 5, 130);
var results = sma.Update(series);
// All results should be finite
foreach (var result in results)
{
Assert.True(double.IsFinite(result.Value), $"Expected finite value but got {result.Value}");
}
}
[Fact]
public void Sma_Reset_ClearsLastValidValue()
{
var sma = new Sma(5);
// Feed values including NaN
sma.Update(new TValue(DateTime.UtcNow, 100));
sma.Update(new TValue(DateTime.UtcNow, double.NaN));
// Reset
sma.Reset();
// After reset, first valid value should establish new baseline
var result = sma.Update(new TValue(DateTime.UtcNow, 50));
Assert.Equal(50.0, result.Value, 1e-10);
}
[Fact]
public void Sma_StaticCalculate_Works()
{
var series = new TSeries();
series.Add(DateTime.UtcNow.Ticks, 10);
series.Add(DateTime.UtcNow.Ticks + 1, 20);
series.Add(DateTime.UtcNow.Ticks + 2, 30);
series.Add(DateTime.UtcNow.Ticks + 3, 40);
series.Add(DateTime.UtcNow.Ticks + 4, 50);
var results = Sma.Calculate(series, 3);
Assert.Equal(5, results.Count);
// SMA(3) for last value: (30+40+50)/3 = 40
Assert.Equal(40.0, results.Last.Value, 1e-10);
}
[Fact]
public void Sma_Period1_ReturnsInputValues()
{
var sma = new Sma(1);
Assert.Equal(100.0, sma.Update(new TValue(DateTime.UtcNow, 100)).Value, 1e-10);
Assert.Equal(200.0, sma.Update(new TValue(DateTime.UtcNow, 200)).Value, 1e-10);
Assert.Equal(150.0, sma.Update(new TValue(DateTime.UtcNow, 150)).Value, 1e-10);
}
// ============== Span API Tests ==============
[Fact]
public void Sma_SpanCalc_ValidatesInput()
{
double[] source = [1, 2, 3, 4, 5];
double[] output = new double[5];
double[] wrongSizeOutput = new double[3];
// Period must be > 0
Assert.Throws<ArgumentException>(() => Sma.Calculate(source.AsSpan(), output.AsSpan(), 0));
Assert.Throws<ArgumentException>(() => Sma.Calculate(source.AsSpan(), output.AsSpan(), -1));
// Output must be same length as source
Assert.Throws<ArgumentException>(() => Sma.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 3));
}
[Fact]
public void Sma_SpanCalc_MatchesTSeriesCalc()
{
var series = new TSeries();
double[] source = new double[100];
double[] output = new double[100];
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
for (int i = 0; i < 100; i++)
{
var bar = gbm.Next(isNew: true);
source[i] = bar.Close;
series.Add(bar.Time, bar.Close);
}
// Calculate with TSeries API
var tseriesResult = Sma.Calculate(series, 10);
// Calculate with Span API
Sma.Calculate(source.AsSpan(), output.AsSpan(), 10);
// Compare results
for (int i = 0; i < 100; i++)
{
Assert.Equal(tseriesResult[i].Value, output[i], 1e-10);
}
}
[Fact]
public void Sma_SpanCalc_CalculatesCorrectly()
{
double[] source = [10, 20, 30, 40, 50];
double[] output = new double[5];
Sma.Calculate(source.AsSpan(), output.AsSpan(), 3);
// SMA(3) warmup: 10, (10+20)/2=15, (10+20+30)/3=20, then sliding: (20+30+40)/3=30, (30+40+50)/3=40
Assert.Equal(10.0, output[0], 1e-10);
Assert.Equal(15.0, output[1], 1e-10);
Assert.Equal(20.0, output[2], 1e-10);
Assert.Equal(30.0, output[3], 1e-10);
Assert.Equal(40.0, output[4], 1e-10);
}
[Fact]
public void Sma_SpanCalc_ZeroAllocation()
{
double[] source = new double[10000];
double[] output = new double[10000];
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
for (int i = 0; i < source.Length; i++)
source[i] = gbm.Next().Close;
// Warm up
Sma.Calculate(source.AsSpan(), output.AsSpan(), 100);
// This test verifies the method runs without throwing
// (allocation is measured by BenchmarkDotNet, not unit tests)
Assert.True(double.IsFinite(output[^1]));
}
[Fact]
public void Sma_SpanCalc_HandlesNaN()
{
double[] source = [100, 110, double.NaN, 120, 130];
double[] output = new double[5];
Sma.Calculate(source.AsSpan(), output.AsSpan(), 3);
// All outputs should be finite
foreach (var val in output)
{
Assert.True(double.IsFinite(val), $"Expected finite value but got {val}");
}
}
[Fact]
public void Sma_SpanCalc_Period1_ReturnsInput()
{
double[] source = [10, 20, 30, 40, 50];
double[] output = new double[5];
Sma.Calculate(source.AsSpan(), output.AsSpan(), 1);
for (int i = 0; i < source.Length; i++)
{
Assert.Equal(source[i], output[i], 1e-10);
}
}
[Fact]
public void Sma_AllModes_ProduceSameResult()
{
// Arrange
int period = 10;
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
// 1. Batch Mode
var batchSeries = Sma.Calculate(series, period);
double expected = batchSeries.Last.Value;
// 2. Span Mode
var tValues = series.Values.ToArray();
var spanInput = new ReadOnlySpan<double>(tValues);
var spanOutput = new double[tValues.Length];
Sma.Calculate(spanInput, spanOutput, period);
double spanResult = spanOutput[^1];
// 3. Streaming Mode
var streamingInd = new Sma(period);
for (int i = 0; i < series.Count; i++)
{
streamingInd.Update(series[i]);
}
double streamingResult = streamingInd.Last.Value;
// 4. Eventing Mode
var pubSource = new TSeries();
var eventingInd = new Sma(pubSource, period);
for (int i = 0; i < series.Count; i++)
{
pubSource.Add(series[i]);
}
double eventingResult = eventingInd.Last.Value;
// Assert
Assert.Equal(expected, spanResult, precision: 9);
Assert.Equal(expected, streamingResult, precision: 9);
Assert.Equal(expected, eventingResult, precision: 9);
}
}
+469
View File
@@ -0,0 +1,469 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Skender.Stock.Indicators;
using TALib;
using Tulip;
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
public class SmaValidationTests
{
private readonly TBarSeries _bars;
private readonly TSeries _data;
private readonly List<Quote> _skenderQuotes;
private readonly ITestOutputHelper _output;
public SmaValidationTests(ITestOutputHelper output)
{
_output = output;
// 1. Generate 5000 records using GBM feed
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2);
_bars = gbm.Fetch(5000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// 2. Extract Close TSeries
_data = _bars.Close;
// 3. Prepare data for Skender (List<Quote>)
_skenderQuotes = new List<Quote>();
for (int i = 0; i < _bars.Count; i++)
{
_skenderQuotes.Add(new Quote
{
Date = new DateTime(_bars.Open.Times[i], DateTimeKind.Utc),
Open = (decimal)_bars.Open[i].Value,
High = (decimal)_bars.High[i].Value,
Low = (decimal)_bars.Low[i].Value,
Close = (decimal)_bars.Close[i].Value,
Volume = (decimal)_bars.Volume[i].Value
});
}
}
[Fact]
public void Validate_Skender_Batch()
{
int[] periods = { 5, 10, 20, 50, 100 };
foreach (var period in periods)
{
// Calculate QuanTAlib SMA (batch TSeries)
var sma = new global::QuanTAlib.Sma(period);
var qResult = sma.Update(_data);
// Calculate Skender SMA
var sResult = _skenderQuotes.GetSma(period).ToList();
// Compare last 100 records
VerifyData_Skender(qResult, sResult);
}
_output.WriteLine("SMA Batch(TSeries) validated successfully against Skender");
}
[Fact]
public void Validate_Skender_Streaming()
{
int[] periods = { 5, 10, 20, 50, 100 };
foreach (var period in periods)
{
// Calculate QuanTAlib SMA (streaming)
var sma = new global::QuanTAlib.Sma(period);
var qResults = new List<double>();
foreach (var item in _data)
{
qResults.Add(sma.Update(item).Value);
}
// Calculate Skender SMA
var sResult = _skenderQuotes.GetSma(period).ToList();
// Compare last 100 records
VerifyData_Skender_Streaming(qResults, sResult);
}
_output.WriteLine("SMA Streaming validated successfully against Skender");
}
[Fact]
public void Validate_Skender_Span()
{
int[] periods = { 5, 10, 20, 50, 100 };
// Prepare data for Span API
double[] sourceData = _data.Select(x => x.Value).ToArray();
foreach (var period in periods)
{
// Calculate QuanTAlib SMA (Span API)
double[] qOutput = new double[sourceData.Length];
global::QuanTAlib.Sma.Calculate(sourceData.AsSpan(), qOutput.AsSpan(), period);
// Calculate Skender SMA
var sResult = _skenderQuotes.GetSma(period).ToList();
// Compare last 100 records
VerifyData_Skender_Span(qOutput, sResult);
}
_output.WriteLine("SMA Span validated successfully against Skender");
}
[Fact]
public void Validate_Talib_Batch()
{
int[] periods = { 5, 10, 20, 50, 100 };
// Prepare data for TA-Lib (double[])
double[] tData = _data.Select(x => x.Value).ToArray();
double[] output = new double[tData.Length];
foreach (var period in periods)
{
// Calculate QuanTAlib SMA (batch TSeries)
var sma = new global::QuanTAlib.Sma(period);
var qResult = sma.Update(_data);
// Calculate TA-Lib SMA
var retCode = TALib.Functions.Sma<double>(tData, 0..^0, output, out var outRange, period);
Assert.Equal(Core.RetCode.Success, retCode);
int lookback = TALib.Functions.SmaLookback(period);
// Compare last 100 records
VerifyData_Talib(qResult, output, outRange, lookback);
}
_output.WriteLine("SMA Batch(TSeries) validated successfully against TA-Lib");
}
[Fact]
public void Validate_Talib_Streaming()
{
int[] periods = { 5, 10, 20, 50, 100 };
// Prepare data for TA-Lib (double[])
double[] tData = _data.Select(x => x.Value).ToArray();
double[] output = new double[tData.Length];
foreach (var period in periods)
{
// Calculate QuanTAlib SMA (streaming)
var sma = new global::QuanTAlib.Sma(period);
var qResults = new List<double>();
foreach (var item in _data)
{
qResults.Add(sma.Update(item).Value);
}
// Calculate TA-Lib SMA
var retCode = TALib.Functions.Sma<double>(tData, 0..^0, output, out var outRange, period);
Assert.Equal(Core.RetCode.Success, retCode);
int lookback = TALib.Functions.SmaLookback(period);
// Compare last 100 records
VerifyData_Talib_Streaming(qResults, output, outRange, lookback);
}
_output.WriteLine("SMA Streaming validated successfully against TA-Lib");
}
[Fact]
public void Validate_Talib_Span()
{
int[] periods = { 5, 10, 20, 50, 100 };
// Prepare data
double[] sourceData = _data.Select(x => x.Value).ToArray();
double[] talibOutput = new double[sourceData.Length];
foreach (var period in periods)
{
// Calculate QuanTAlib SMA (Span API)
double[] qOutput = new double[sourceData.Length];
global::QuanTAlib.Sma.Calculate(sourceData.AsSpan(), qOutput.AsSpan(), period);
// Calculate TA-Lib SMA
var retCode = TALib.Functions.Sma<double>(sourceData, 0..^0, talibOutput, out var outRange, period);
Assert.Equal(Core.RetCode.Success, retCode);
int lookback = TALib.Functions.SmaLookback(period);
// Compare last 100 records
VerifyData_Talib_Span(qOutput, talibOutput, outRange, lookback);
}
_output.WriteLine("SMA Span validated successfully against TA-Lib");
}
[Fact]
public void Validate_Tulip_Batch()
{
int[] periods = { 5, 10, 20, 50, 100 };
// Prepare data for Tulip (double[])
double[] tData = _data.Select(x => x.Value).ToArray();
foreach (var period in periods)
{
// Calculate QuanTAlib SMA (batch TSeries)
var sma = new global::QuanTAlib.Sma(period);
var qResult = sma.Update(_data);
// Calculate Tulip SMA
var smaIndicator = Tulip.Indicators.sma;
double[][] inputs = { tData };
double[] options = { period };
int lookback = period - 1;
double[][] outputs = { new double[tData.Length - lookback] };
smaIndicator.Run(inputs, options, outputs);
var tResult = outputs[0];
// Compare last 100 records
VerifyData_Tulip(qResult, tResult, lookback);
}
_output.WriteLine("SMA Batch(TSeries) validated successfully against Tulip");
}
[Fact]
public void Validate_Tulip_Streaming()
{
int[] periods = { 5, 10, 20, 50, 100 };
// Prepare data for Tulip (double[])
double[] tData = _data.Select(x => x.Value).ToArray();
foreach (var period in periods)
{
// Calculate QuanTAlib SMA (streaming)
var sma = new global::QuanTAlib.Sma(period);
var qResults = new List<double>();
foreach (var item in _data)
{
qResults.Add(sma.Update(item).Value);
}
// Calculate Tulip SMA
var smaIndicator = Tulip.Indicators.sma;
double[][] inputs = { tData };
double[] options = { period };
int lookback = period - 1;
double[][] outputs = { new double[tData.Length - lookback] };
smaIndicator.Run(inputs, options, outputs);
var tResult = outputs[0];
// Compare last 100 records
VerifyData_Tulip_Streaming(qResults, tResult, lookback);
}
_output.WriteLine("SMA Streaming validated successfully against Tulip");
}
[Fact]
public void Validate_Tulip_Span()
{
int[] periods = { 5, 10, 20, 50, 100 };
// Prepare data
double[] sourceData = _data.Select(x => x.Value).ToArray();
foreach (var period in periods)
{
// Calculate QuanTAlib SMA (Span API)
double[] qOutput = new double[sourceData.Length];
global::QuanTAlib.Sma.Calculate(sourceData.AsSpan(), qOutput.AsSpan(), period);
// Calculate Tulip SMA
var smaIndicator = Tulip.Indicators.sma;
double[][] inputs = { sourceData };
double[] options = { period };
int lookback = period - 1;
double[][] outputs = { new double[sourceData.Length - lookback] };
smaIndicator.Run(inputs, options, outputs);
var tResult = outputs[0];
// Compare last 100 records
VerifyData_Tulip_Span(qOutput, tResult, lookback);
}
_output.WriteLine("SMA Span validated successfully against Tulip");
}
// ==================== Verification Helpers ====================
private static void VerifyData_Skender(TSeries qSeries, List<SmaResult> sSeries)
{
Assert.Equal(qSeries.Count, sSeries.Count);
int count = qSeries.Count;
int skip = count - 100;
for (int i = skip; i < count; i++)
{
double qValue = qSeries[i].Value;
double? sValue = sSeries[i].Sma;
if (!sValue.HasValue) continue;
Assert.Equal(sValue.Value, qValue, 1e-6);
}
}
private static void VerifyData_Skender_Streaming(List<double> qResults, List<SmaResult> sSeries)
{
Assert.Equal(qResults.Count, sSeries.Count);
int count = qResults.Count;
int skip = count - 100;
for (int i = skip; i < count; i++)
{
double qValue = qResults[i];
double? sValue = sSeries[i].Sma;
if (!sValue.HasValue) continue;
Assert.Equal(sValue.Value, qValue, 1e-6);
}
}
private static void VerifyData_Skender_Span(double[] qOutput, List<SmaResult> sSeries)
{
Assert.Equal(qOutput.Length, sSeries.Count);
int count = qOutput.Length;
int skip = count - 100;
for (int i = skip; i < count; i++)
{
double qValue = qOutput[i];
double? sValue = sSeries[i].Sma;
if (!sValue.HasValue) continue;
Assert.Equal(sValue.Value, qValue, 1e-6);
}
}
private static void VerifyData_Talib(TSeries qSeries, double[] tOutput, Range outRange, int lookback)
{
int count = qSeries.Count;
int skip = count - 100;
int validCount = outRange.End.Value - outRange.Start.Value;
for (int i = skip; i < count; i++)
{
double qValue = qSeries[i].Value;
if (i < lookback) continue;
int tIndex = i - lookback;
if (tIndex >= validCount) continue;
double tValue = tOutput[tIndex];
Assert.Equal(tValue, qValue, 1e-6);
}
}
private static void VerifyData_Talib_Streaming(List<double> qResults, double[] tOutput, Range outRange, int lookback)
{
int count = qResults.Count;
int skip = count - 100;
int validCount = outRange.End.Value - outRange.Start.Value;
for (int i = skip; i < count; i++)
{
double qValue = qResults[i];
if (i < lookback) continue;
int tIndex = i - lookback;
if (tIndex >= validCount) continue;
double tValue = tOutput[tIndex];
Assert.Equal(tValue, qValue, 1e-6);
}
}
private static void VerifyData_Talib_Span(double[] qOutput, double[] tOutput, Range outRange, int lookback)
{
int count = qOutput.Length;
int skip = count - 100;
int validCount = outRange.End.Value - outRange.Start.Value;
for (int i = skip; i < count; i++)
{
double qValue = qOutput[i];
if (i < lookback) continue;
int tIndex = i - lookback;
if (tIndex >= validCount) continue;
double tValue = tOutput[tIndex];
Assert.Equal(tValue, qValue, 1e-6);
}
}
private static void VerifyData_Tulip(TSeries qSeries, double[] tOutput, int lookback)
{
int count = qSeries.Count;
int skip = count - 100;
for (int i = skip; i < count; i++)
{
double qValue = qSeries[i].Value;
if (i < lookback) continue;
int tIndex = i - lookback;
if (tIndex >= tOutput.Length) continue;
double tValue = tOutput[tIndex];
Assert.Equal(tValue, qValue, 1e-6);
}
}
private static void VerifyData_Tulip_Streaming(List<double> qResults, double[] tOutput, int lookback)
{
int count = qResults.Count;
int skip = count - 100;
for (int i = skip; i < count; i++)
{
double qValue = qResults[i];
if (i < lookback) continue;
int tIndex = i - lookback;
if (tIndex >= tOutput.Length) continue;
double tValue = tOutput[tIndex];
Assert.Equal(tValue, qValue, 1e-6);
}
}
private static void VerifyData_Tulip_Span(double[] qOutput, double[] tOutput, int lookback)
{
int count = qOutput.Length;
int skip = count - 100;
for (int i = skip; i < count; i++)
{
double qValue = qOutput[i];
if (i < lookback) continue;
int tIndex = i - lookback;
if (tIndex >= tOutput.Length) continue;
double tValue = tOutput[tIndex];
Assert.Equal(tValue, qValue, 1e-6);
}
}
}
+394
View File
@@ -0,0 +1,394 @@
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
namespace QuanTAlib;
/// <summary>
/// SMA: Simple Moving Average
/// </summary>
/// <remarks>
/// SMA calculates the arithmetic mean of the last n values.
/// Uses a RingBuffer for storage and manual running sum for O(1) complexity per update.
///
/// Calculation:
/// SMA = (P_n + P_(n-1) + ... + P_1) / n
///
/// O(1) update:
/// S_new = S_old - oldest + newest
/// SMA = S_new / n
///
/// IsHot:
/// Becomes true when the buffer is full (period samples processed).
/// </remarks>
[SkipLocalsInit]
public sealed class Sma : ITValuePublisher
{
private readonly int _period;
private readonly RingBuffer _buffer;
private double _sum;
private double _p_sum;
private double _p_lastInput;
private double _lastValidValue;
private double _p_lastValidValue;
private int _tickCount;
private const int ResyncInterval = 1000;
/// <summary>
/// Display name for the indicator.
/// </summary>
public string Name { get; }
public event Action<TValue>? Pub;
/// <summary>
/// Creates SMA with specified period.
/// </summary>
/// <param name="period">Number of values to average (must be > 0)</param>
public Sma(int period)
{
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
_period = period;
_buffer = new RingBuffer(period);
Name = $"Sma({period})";
}
public Sma(ITValuePublisher source, int period) : this(period)
{
source.Pub += (item) => Update(item);
}
/// <summary>
/// Current SMA value.
/// </summary>
public TValue Last { get; private set; }
/// <summary>
/// True if the SMA has enough data to produce valid results.
/// SMA is "hot" when the buffer is full (has received at least 'period' values).
/// </summary>
public bool IsHot => _buffer.IsFull;
/// <summary>
/// Gets a valid input value, using last-value substitution for non-finite inputs.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetValidValue(double input)
{
if (double.IsFinite(input))
{
_lastValidValue = input;
return input;
}
return _lastValidValue;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void UpdateState(double val)
{
double removedValue = _buffer.Count == _buffer.Capacity ? _buffer.Oldest : 0.0;
_sum = _sum - removedValue + val;
_buffer.Add(val);
_tickCount++;
if (_buffer.IsFull && _tickCount >= ResyncInterval)
{
_tickCount = 0;
_sum = _buffer.Sum();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
double val = GetValidValue(input.Value);
UpdateState(val);
_p_sum = _sum;
_p_lastInput = val;
_p_lastValidValue = _lastValidValue;
}
else
{
_lastValidValue = _p_lastValidValue;
double val = GetValidValue(input.Value);
_sum = _p_sum - _p_lastInput + val;
_buffer.UpdateNewest(val);
}
double result = _sum / _buffer.Count;
Last = new TValue(input.Time, result);
Pub?.Invoke(Last);
return Last;
}
public TSeries Update(TSeries source)
{
if (source.Count == 0) return new TSeries(new List<long>(), new List<double>());
int len = source.Count;
var t = new List<long>(len);
var v = new List<double>(len);
CollectionsMarshal.SetCount(t, len);
CollectionsMarshal.SetCount(v, len);
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
Calculate(source.Values, vSpan, _period);
source.Times.CopyTo(tSpan);
// Restore state
int windowSize = Math.Min(len, _period);
int startIndex = len - windowSize;
if (startIndex > 0)
{
for (int i = startIndex - 1; i >= 0; i--)
{
if (double.IsFinite(source.Values[i]))
{
_lastValidValue = source.Values[i];
break;
}
}
}
else
{
_lastValidValue = 0;
}
_buffer.Clear();
_sum = 0;
_tickCount = 0;
for (int i = startIndex; i < len; i++)
{
double val = GetValidValue(source.Values[i]);
UpdateState(val);
}
_p_sum = _sum;
_p_lastInput = source.Values[len - 1];
_p_lastValidValue = _lastValidValue;
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v);
}
/// <summary>
/// Calculates SMA for the entire series using a new instance.
/// </summary>
/// <param name="source">Input series</param>
/// <param name="period">SMA period</param>
/// <returns>SMA series</returns>
public static TSeries Calculate(TSeries source, int period)
{
var sma = new Sma(period);
return sma.Update(source);
}
/// <summary>
/// Calculates SMA in-place, writing results to pre-allocated output span.
/// Zero-allocation method for maximum performance.
/// Uses stackalloc circular buffer for NaN-safe sliding window calculation.
/// Automatically uses SIMD acceleration for large, clean datasets.
/// </summary>
/// <param name="source">Input values</param>
/// <param name="output">Output span (must be same length as source)</param>
/// <param name="period">SMA period (must be > 0)</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period)
{
if (source.Length != output.Length)
throw new ArgumentException("Source and output must have the same length");
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
int len = source.Length;
if (len == 0) return;
// Try SIMD path for large, clean datasets
// Requirements: AVX2 support, large enough dataset, no NaN values
const int SimdThreshold = 256;
if (Avx2.IsSupported && len >= SimdThreshold && !HasNonFiniteValues(source))
{
CalculateSimdCore(source, output, period);
return;
}
// Scalar path with NaN handling
CalculateScalarCore(source, output, period);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void CalculateScalarCore(ReadOnlySpan<double> source, Span<double> output, int period)
{
int len = source.Length;
const int StackAllocThreshold = 256;
Span<double> buffer = period <= StackAllocThreshold
? stackalloc double[period]
: new double[period];
double sum = 0;
double lastValid = 0;
int bufferIndex = 0;
int i = 0;
int warmupEnd = Math.Min(period, len);
for (; i < warmupEnd; i++)
{
double val = source[i];
if (double.IsFinite(val))
lastValid = val;
else
val = lastValid;
sum += val;
buffer[i] = val;
output[i] = sum / (i + 1);
}
int tickCount = 0;
for (; i < len; i++)
{
double val = source[i];
if (double.IsFinite(val))
lastValid = val;
else
val = lastValid;
sum = sum - buffer[bufferIndex] + val;
buffer[bufferIndex] = val;
bufferIndex++;
if (bufferIndex >= period)
bufferIndex = 0;
output[i] = sum / period;
tickCount++;
if (tickCount >= ResyncInterval)
{
tickCount = 0;
double recalcSum = 0;
for (int k = 0; k < period; k++)
{
recalcSum += buffer[k];
}
sum = recalcSum;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private static void CalculateSimdCore(ReadOnlySpan<double> source, Span<double> output, int period)
{
int len = source.Length;
const int VectorWidth = 4;
ref double srcRef = ref MemoryMarshal.GetReference(source);
ref double outRef = ref MemoryMarshal.GetReference(output);
double invPeriod = 1.0 / period;
int warmupEnd = Math.Min(period, len);
double sum = 0;
for (int i = 0; i < warmupEnd; i++)
{
sum += Unsafe.Add(ref srcRef, i);
Unsafe.Add(ref outRef, i) = sum / (i + 1);
}
if (len <= period)
return;
var vInvPeriod = Vector256.Create(invPeriod);
var vZero = Vector256<double>.Zero;
int simdEnd = period + ((len - period) / VectorWidth) * VectorWidth;
int tickCount = 0;
for (int i = period; i < simdEnd; i += VectorWidth)
{
var vNew = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, i));
var vOld = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, i - period));
var vDelta = Avx.Subtract(vNew, vOld);
var vShift1 = Avx2.Permute4x64(vDelta.AsUInt64(), 0b_10_01_00_00).AsDouble();
vShift1 = Avx.Blend(vZero, vShift1, 0b_1110);
var vP1 = Avx.Add(vDelta, vShift1);
var vShift2 = Avx2.Permute4x64(vP1.AsUInt64(), 0b_01_00_00_00).AsDouble();
vShift2 = Avx.Blend(vZero, vShift2, 0b_1100);
var vP2 = Avx.Add(vP1, vShift2);
var vSumPrev = Vector256.Create(sum);
var vSums = Avx.Add(vSumPrev, vP2);
var vResult = Avx.Multiply(vSums, vInvPeriod);
Vector256.StoreUnsafe(vResult, ref Unsafe.Add(ref outRef, i));
sum = vSums.GetElement(3);
tickCount += VectorWidth;
if (tickCount >= ResyncInterval)
{
tickCount = 0;
int lastIdx = i + VectorWidth - 1;
double recalcSum = 0;
for (int k = 0; k < period; k++)
{
recalcSum += Unsafe.Add(ref srcRef, lastIdx - k);
}
sum = recalcSum;
}
}
for (int i = simdEnd; i < len; i++)
{
sum = sum - Unsafe.Add(ref srcRef, i - period) + Unsafe.Add(ref srcRef, i);
Unsafe.Add(ref outRef, i) = sum * invPeriod;
}
}
/// <summary>
/// Checks if span contains any non-finite values (NaN or Infinity).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool HasNonFiniteValues(ReadOnlySpan<double> span)
{
for (int idx = 0; idx < span.Length; idx++)
{
if (!double.IsFinite(span[idx]))
return true;
}
return false;
}
/// <summary>
/// Resets the SMA state.
/// </summary>
public void Reset()
{
_buffer.Clear();
var resetSum = 0;
_sum = resetSum;
Last = default;
_tickCount = 0;
}
}
+235
View File
@@ -0,0 +1,235 @@
# SMA: Simple Moving Average
## Overview and Purpose
The Simple Moving Average (SMA) is one of the most fundamental and widely used technical indicators in financial analysis. It calculates the arithmetic mean of a selected range of prices over a specified number of periods. Developed in the early days of technical analysis, the SMA provides traders with a straightforward method to identify trends by smoothing price data and filtering out short-term fluctuations.
Unlike the Exponential Moving Average (EMA) which gives more weight to recent data, the SMA treats all data points in the window equally. This equal weighting makes the SMA particularly intuitive to understand, as it simply represents the average price over the specified time period. Due to its simplicity and effectiveness, it remains a cornerstone indicator that forms the basis for numerous other technical analysis tools.
## Core Concepts
* **Equal weighting:** SMA gives equal importance to each price point in the calculation period, unlike weighted averages that emphasize certain data points
* **Noise reduction:** Smooths price fluctuations to help identify the underlying trend direction
* **Timeframe flexibility:** Effective across all timeframes, with shorter periods for short-term analysis and longer periods for identifying major trends
* **Foundation indicator:** Serves as the mathematical basis for Bollinger Bands, moving average envelopes, and other derived indicators
The core principle of SMA is its unbiased approach to price data. By treating all prices within the lookback period with equal importance, SMA creates a balanced view of recent market activity. This equal weighting makes the SMA particularly intuitive to understand, as it simply represents the average price over the specified time period.
## Common Settings and Parameters
| Parameter | Default | Function | When to Adjust |
|-----------|---------|----------|---------------|
| Period | 20 | Controls the lookback period | Increase for smoother signals in volatile markets, decrease for responsiveness |
| Source | Close | Price data used for calculation | Consider using HLC3 for a more balanced price representation |
**Pro Tip:** For trend following strategies, consider using two SMAs with different periods (e.g., 50 and 200) crossovers between these can identify significant trend changes while filtering out minor fluctuations. This "golden cross" (50 crossing above 200) and "death cross" (50 crossing below 200) are among the most watched signals in technical analysis.
## Calculation and Mathematical Foundation
**Simplified explanation:**
SMA adds up the prices for a specific number of periods and divides by that number. For example, a 10-period SMA adds the last 10 closing prices and divides by 10 to find the average.
**Technical formula:**
The standard calculation:
$$SMA = \frac{P_1 + P_2 + ... + P_n}{n} = \frac{1}{n}\sum_{i=1}^{n}P_i$$
An optimized recursive calculation used in the implementation:
$$SMA_t = SMA_{t-1} + \frac{P_t - P_{t-n}}{n}$$
Where:
* $P_1, P_2, ..., P_n$ are price values in the lookback window
* $n$ is the period length
* $P_{t-n}$ is the oldest price leaving the window
> 🔍 **Technical Note:** The SMA has a precisely defined lag of $(n-1)/2$ periods, meaning a 21-period SMA lags behind price by 10 bars. This consistent, deterministic lag makes its behavior predictable across all market conditions. The implementation uses a running sum approach for O(1) update complexity regardless of period length.
## C# Implementation
The library provides two implementations: a standard scalar version and a multi-period vector version for calculating multiple SMAs simultaneously.
### Single SMA (`Sma`)
The `Sma` class calculates a single simple moving average with O(1) update complexity.
```csharp
using QuanTAlib;
// Initialize with period 10
var sma = new Sma(10);
// Streaming update
TValue result = sma.Update(new TValue(time, price));
Console.WriteLine($"Current SMA: {result.Value}");
// Access properties
Console.WriteLine($"Name: {sma.Name}"); // "Sma(10)"
Console.WriteLine($"IsHot: {sma.IsHot}"); // true when buffer is full
// Batch calculation (TSeries API)
TSeries source = ...;
TSeries results = Sma.Calculate(source, 10);
// High-performance Span API (zero allocation)
double[] prices = new double[10000];
double[] output = new double[10000];
Sma.Calculate(prices.AsSpan(), output.AsSpan(), period: 10);
```
### Zero-Allocation Span API
For performance-critical scenarios (backtesting, HFT), use the Span-based overload:
```csharp
// Allocate buffers once, reuse across calculations
double[] source = new double[200000];
double[] smaOutput = new double[200000];
// Zero heap allocation during calculation
Sma.Calculate(source.AsSpan(), smaOutput.AsSpan(), period: 100);
// Results are written directly to output buffer
Console.WriteLine($"Last SMA: {smaOutput[^1]}");
```
**Benefits:**
* **Zero allocation**: No GC pressure during calculation
* **Cache-friendly**: Sequential memory access patterns
* **2-3x faster** than TSeries API for large datasets
* **Compatible** with `ArrayPool<T>` for buffer management
### Bar Correction (isNew Parameter)
`Sma` supports intra-bar updates for real-time trading systems:
```csharp
var sma = new Sma(10);
// Process historical bars
for (int i = 0; i < historicalBars.Count; i++)
{
sma.Update(historicalBars[i], isNew: true);
}
// Real-time: receive initial tick for new bar
sma.Update(new TValue(time, 100.5), isNew: true);
// Real-time: price updates within same bar
sma.Update(new TValue(time, 101.0), isNew: false); // O(1) correction
sma.Update(new TValue(time, 100.8), isNew: false); // O(1) correction
// Bar closes, next bar starts
sma.Update(new TValue(time + 1, 101.2), isNew: true);
```
**Implementation detail:** Bar correction is O(1) using scalar state save/restore, not buffer copying.
### Eventing and Reactive Support
This indicator implements the `ITValuePublisher` interface, enabling event-driven and reactive workflows.
* **Subscription:** Can be constructed with an `ITValuePublisher` (e.g., `TSeries`) to automatically update when the source emits a new value.
* **Publication:** Emits a `Pub` event with the new `TValue` whenever it is updated.
```csharp
using QuanTAlib;
// 1. Setup a source (publisher)
var source = new TSeries();
// 2. Create indicator subscribed to source
// It waits for events from 'source'
var sma = new Sma(source, period: 10);
// 3. Optional: Subscribe to indicator's output
sma.Pub += (item) => Console.WriteLine($"SMA Updated: {item.Value}");
// 4. Ingest data into source
// This triggers the chain: source -> sma -> Console.WriteLine
source.Add(new TValue(DateTime.Now, 100));
source.Add(new TValue(DateTime.Now, 105));
```
This pattern allows building complex, reactive processing pipelines without manual update loops.
### Handling Invalid Values (NaN/Infinity)
`Sma` uses **last-value substitution** for handling invalid inputs:
```csharp
var sma = new Sma(10);
// Valid values establish baseline
sma.Update(new TValue(time, 100));
sma.Update(new TValue(time, 110));
// NaN or Infinity inputs are replaced with last valid value (110)
var result = sma.Update(new TValue(time, double.NaN));
Console.WriteLine(double.IsFinite(result.Value)); // true
// Works identically for batch operations
var series = new TSeries();
series.Add(time, 100);
series.Add(time + 1, double.NaN); // Will use 100
series.Add(time + 2, 120);
var results = sma.Update(series); // All values are finite
```
**Behavior:**
* When `NaN`, `PositiveInfinity`, or `NegativeInfinity` is encountered, the last valid value is substituted
* This provides output continuity instead of propagating invalid values
* `Reset()` clears the last valid value, so the next valid input establishes a new baseline
### Performance Characteristics
| Operation | Complexity | Notes |
|-----------|------------|-------|
| Update (isNew=true) | O(1) | Running sum: `sum = sum - oldest + newest` |
| Update (isNew=false) | O(1) | Scalar state restore + recalculate |
| Batch processing | O(n) | Where n is series length |
| Memory (single) | O(period) | One RingBuffer for values |
| Memory (state) | O(1) | 6 doubles for bar correction |
The implementation uses:
* **Running sum** for O(1) average calculation
* **Scalar state save/restore** for O(1) bar correction
* **Pinned memory** in RingBuffer for cache-friendly access
* **CollectionsMarshal.SetCount** for zero-allocation batch processing
## Interpretation Details
SMA can be used in various trading strategies:
* **Trend identification:** The direction of SMA indicates the prevailing trend
* **Signal generation:** Crossovers between price and SMA generate basic trade signals
* **Support/resistance levels:** SMA can act as dynamic support during uptrends and resistance during downtrends
* **Multiple timeframe analysis:** Using SMAs with different periods can confirm trends across different timeframes
* **Moving average crossovers:** When a shorter-period SMA crosses above a longer-period SMA, it signals a potential uptrend (and vice versa)
### SMA vs EMA Comparison
| Aspect | SMA | EMA |
|--------|-----|-----|
| Weighting | Equal for all values | Recent values weighted more |
| Lag | Higher: $(n-1)/2$ bars | Lower due to recent weighting |
| Sensitivity | Slower to react | Faster reaction to changes |
| Noise | Better noise filtering | More responsive but noisier |
| Sudden changes | Abrupt when oldest value exits | Smooth exponential decay |
| Best use | Long-term trends, support/resistance | Short-term signals, momentum |
## Limitations and Considerations
* **Market conditions:** Less effective in choppy, sideways markets where price oscillates around the average
* **Lag factor:** Significant lag in responding to rapid price changes means SMA will always be late to signal reversals
* **Equal weighting:** Treats recent and older prices equally, which may not reflect current market dynamics
* **Sudden changes:** When a price point leaves the calculation window, it can cause abrupt changes in the SMA
* **Complementary tools:** Best used with momentum oscillators, volume indicators, or other trend confirmation tools
## References
1. Edwards, R.D. and Magee, J. (2007). *Technical Analysis of Stock Trends*. CRC Press.
2. Murphy, J.J. (1999). *Technical Analysis of the Financial Markets*. New York Institute of Finance.
3. Kaufman, P. (2013). *Trading Systems and Methods*, 5th Edition. Wiley Trading.