mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-23 21:18:04 +00:00
SIMD Refactor: Merge simd-dev into dev (#55)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) <aider@aider.chat> Co-authored-by: Warp <agent@warp.dev>
This commit is contained in:
co-authored by
Claude Opus 4.5
aider
Warp
parent
5bcdf8d614
commit
86fe32a682
@@ -0,0 +1,169 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class CmaIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void CmaIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new CmaIndicator();
|
||||
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("CMA - Cumulative Moving Average", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CmaIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new CmaIndicator();
|
||||
|
||||
Assert.Equal(0, CmaIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CmaIndicator_ShortName_IncludesSource()
|
||||
{
|
||||
var indicator = new CmaIndicator();
|
||||
|
||||
Assert.Contains("CMA", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CmaIndicator_Initialize_CreatesInternalCma()
|
||||
{
|
||||
var indicator = new CmaIndicator();
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CmaIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new CmaIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process update
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// Line series should have a value
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CmaIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new CmaIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CmaIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new CmaIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
double firstValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
double secondValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(firstValue));
|
||||
Assert.True(double.IsFinite(secondValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CmaIndicator_MultipleUpdates_ProducesCorrectCmaSequence()
|
||||
{
|
||||
var indicator = new CmaIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = { 100, 102, 104, 103, 105 };
|
||||
|
||||
foreach (var close in closes)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now, close, close + 2, close - 2, close);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
now = now.AddMinutes(1);
|
||||
}
|
||||
|
||||
// All values should be finite
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(closes.Length - 1 - i)));
|
||||
}
|
||||
|
||||
// Last CMA should be average of all values: (100 + 102 + 104 + 103 + 105) / 5 = 102.8
|
||||
double lastCma = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(102.8, lastCma, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CmaIndicator_DifferentSourceTypes_Work()
|
||||
{
|
||||
var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 };
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var indicator = new CmaIndicator { Source = source };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)),
|
||||
$"Source {source} should produce finite value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CmaIndicator_CalculatesRunningAverage()
|
||||
{
|
||||
var indicator = new CmaIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Add bars with known close prices: 10, 20, 30
|
||||
indicator.HistoricalData.AddBar(now, 10, 10, 10, 10);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
Assert.Equal(10.0, indicator.LinesSeries[0].GetValue(0), 1e-10); // CMA = 10
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 20, 20, 20, 20);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
Assert.Equal(15.0, indicator.LinesSeries[0].GetValue(0), 1e-10); // CMA = (10+20)/2 = 15
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(2), 30, 30, 30, 30);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
Assert.Equal(20.0, indicator.LinesSeries[0].GetValue(0), 1e-10); // CMA = (10+20+30)/3 = 20
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class CmaIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Cma _cma = null!;
|
||||
private readonly LineSeries _series;
|
||||
private string _sourceName = null!;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"CMA:{_sourceName}";
|
||||
|
||||
public CmaIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
Name = "CMA - Cumulative Moving Average";
|
||||
Description = "Cumulative Moving Average (Running Average)";
|
||||
_series = new LineSeries(name: "CMA", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
_sourceName = Source.ToString();
|
||||
_cma = new Cma();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
bool isNew = args.IsNewBar();
|
||||
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _cma.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), isNew).Value;
|
||||
_series.SetValue(value, _cma.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,576 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class CmaTests
|
||||
{
|
||||
[Fact]
|
||||
public void Cma_Calc_ReturnsValue()
|
||||
{
|
||||
var cma = new Cma();
|
||||
|
||||
Assert.Equal(0, cma.Last.Value);
|
||||
|
||||
TValue result = cma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
Assert.True(result.Value > 0);
|
||||
Assert.Equal(result.Value, cma.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cma_FirstValue_ReturnsItself()
|
||||
{
|
||||
var cma = new Cma();
|
||||
|
||||
TValue result = cma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
Assert.Equal(100.0, result.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cma_Calc_IsNew_AcceptsParameter()
|
||||
{
|
||||
var cma = new Cma();
|
||||
|
||||
cma.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
|
||||
double value1 = cma.Last.Value;
|
||||
|
||||
cma.Update(new TValue(DateTime.UtcNow, 200), isNew: true);
|
||||
double value2 = cma.Last.Value;
|
||||
|
||||
// Values should change with new bars
|
||||
Assert.NotEqual(value1, value2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cma_Calc_IsNew_False_UpdatesValue()
|
||||
{
|
||||
var cma = new Cma();
|
||||
|
||||
cma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
cma.Update(new TValue(DateTime.UtcNow, 110), isNew: true);
|
||||
double beforeUpdate = cma.Last.Value;
|
||||
|
||||
cma.Update(new TValue(DateTime.UtcNow, 120), isNew: false);
|
||||
double afterUpdate = cma.Last.Value;
|
||||
|
||||
// Update should change the value
|
||||
Assert.NotEqual(beforeUpdate, afterUpdate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cma_Reset_ClearsState()
|
||||
{
|
||||
var cma = new Cma();
|
||||
|
||||
cma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
cma.Update(new TValue(DateTime.UtcNow, 105));
|
||||
double valueBefore = cma.Last.Value;
|
||||
|
||||
cma.Reset();
|
||||
|
||||
Assert.Equal(0, cma.Last.Value);
|
||||
Assert.False(cma.IsHot);
|
||||
|
||||
// After reset, should accept new values
|
||||
cma.Update(new TValue(DateTime.UtcNow, 50));
|
||||
Assert.NotEqual(0, cma.Last.Value);
|
||||
Assert.NotEqual(valueBefore, cma.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cma_Properties_Accessible()
|
||||
{
|
||||
var cma = new Cma();
|
||||
|
||||
Assert.Equal(0, cma.Last.Value);
|
||||
Assert.False(cma.IsHot);
|
||||
|
||||
cma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
Assert.NotEqual(0, cma.Last.Value);
|
||||
Assert.True(cma.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cma_IsHot_BecomesTrueAfterFirstValue()
|
||||
{
|
||||
var cma = new Cma();
|
||||
|
||||
Assert.False(cma.IsHot);
|
||||
|
||||
cma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
Assert.True(cma.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cma_CalculatesCorrectAverage()
|
||||
{
|
||||
var cma = new Cma();
|
||||
|
||||
cma.Update(new TValue(DateTime.UtcNow, 10));
|
||||
Assert.Equal(10.0, cma.Last.Value, 1e-10); // (10)/1 = 10
|
||||
|
||||
cma.Update(new TValue(DateTime.UtcNow, 20));
|
||||
Assert.Equal(15.0, cma.Last.Value, 1e-10); // (10+20)/2 = 15
|
||||
|
||||
cma.Update(new TValue(DateTime.UtcNow, 30));
|
||||
Assert.Equal(20.0, cma.Last.Value, 1e-10); // (10+20+30)/3 = 20
|
||||
|
||||
cma.Update(new TValue(DateTime.UtcNow, 40));
|
||||
Assert.Equal(25.0, cma.Last.Value, 1e-10); // (10+20+30+40)/4 = 25
|
||||
|
||||
cma.Update(new TValue(DateTime.UtcNow, 50));
|
||||
Assert.Equal(30.0, cma.Last.Value, 1e-10); // (10+20+30+40+50)/5 = 30
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cma_IncludesAllValues_NoSlidingWindow()
|
||||
{
|
||||
var cma = new Cma();
|
||||
|
||||
// Add 10 values: 10, 20, 30, ..., 100
|
||||
for (int i = 1; i <= 10; i++)
|
||||
{
|
||||
cma.Update(new TValue(DateTime.UtcNow, i * 10));
|
||||
}
|
||||
|
||||
// CMA of 10,20,30,40,50,60,70,80,90,100 = 550/10 = 55
|
||||
Assert.Equal(55.0, cma.Last.Value, 1e-10);
|
||||
|
||||
// Add one more value
|
||||
cma.Update(new TValue(DateTime.UtcNow, 110));
|
||||
|
||||
// CMA now includes ALL 11 values: (550 + 110)/11 = 660/11 = 60
|
||||
Assert.Equal(60.0, cma.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cma_IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var cma = new Cma();
|
||||
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);
|
||||
cma.Update(tenthInput, isNew: true);
|
||||
}
|
||||
|
||||
// Remember CMA state after 10 values
|
||||
double cmaAfterTen = cma.Last.Value;
|
||||
|
||||
// Generate 9 corrections with isNew=false (different values)
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: false);
|
||||
cma.Update(new TValue(bar.Time, bar.Close), isNew: false);
|
||||
}
|
||||
|
||||
// Feed the remembered 10th input again with isNew=false
|
||||
TValue finalCma = cma.Update(tenthInput, isNew: false);
|
||||
|
||||
// CMA should match the original state after 10 values
|
||||
Assert.Equal(cmaAfterTen, finalCma.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cma_BatchCalc_MatchesIterativeCalc()
|
||||
{
|
||||
var cmaIterative = new Cma();
|
||||
var cmaBatch = new Cma();
|
||||
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(cmaIterative.Update(item));
|
||||
}
|
||||
|
||||
// Calculate batch
|
||||
var batchResults = cmaBatch.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 Cma_NaN_Input_UsesLastValidValue()
|
||||
{
|
||||
var cma = new Cma();
|
||||
|
||||
// Feed some valid values
|
||||
cma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
cma.Update(new TValue(DateTime.UtcNow, 110));
|
||||
|
||||
// Feed NaN - should use last valid value (110)
|
||||
var resultAfterNaN = cma.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 Cma_Infinity_Input_UsesLastValidValue()
|
||||
{
|
||||
var cma = new Cma();
|
||||
|
||||
// Feed some valid values
|
||||
cma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
cma.Update(new TValue(DateTime.UtcNow, 110));
|
||||
|
||||
// Feed positive infinity - should use last valid value
|
||||
var resultAfterPosInf = cma.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(resultAfterPosInf.Value));
|
||||
|
||||
// Feed negative infinity - should use last valid value
|
||||
var resultAfterNegInf = cma.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
|
||||
Assert.True(double.IsFinite(resultAfterNegInf.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cma_MultipleNaN_ContinuesWithLastValid()
|
||||
{
|
||||
var cma = new Cma();
|
||||
|
||||
// Feed valid values
|
||||
cma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
cma.Update(new TValue(DateTime.UtcNow, 110));
|
||||
cma.Update(new TValue(DateTime.UtcNow, 120));
|
||||
|
||||
// Feed multiple NaN values
|
||||
var r1 = cma.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
var r2 = cma.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
var r3 = cma.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 Cma_BatchCalc_HandlesNaN()
|
||||
{
|
||||
var cma = new Cma();
|
||||
|
||||
// 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 = cma.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 Cma_Reset_ClearsLastValidValue()
|
||||
{
|
||||
var cma = new Cma();
|
||||
|
||||
// Feed values including NaN
|
||||
cma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
cma.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
|
||||
// Reset
|
||||
cma.Reset();
|
||||
|
||||
// After reset, first valid value should establish new baseline
|
||||
var result = cma.Update(new TValue(DateTime.UtcNow, 50));
|
||||
Assert.Equal(50.0, result.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cma_StaticBatch_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 = Cma.Batch(series);
|
||||
|
||||
Assert.Equal(5, results.Count);
|
||||
// CMA for last value: (10+20+30+40+50)/5 = 30
|
||||
Assert.Equal(30.0, results.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cma_FlatLine_ReturnsSameValue()
|
||||
{
|
||||
var cma = new Cma();
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
cma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
}
|
||||
|
||||
Assert.Equal(100.0, cma.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
// ============== Span API Tests ==============
|
||||
|
||||
[Fact]
|
||||
public void Cma_SpanBatch_ValidatesInput()
|
||||
{
|
||||
double[] source = [1, 2, 3, 4, 5];
|
||||
double[] wrongSizeOutput = new double[3];
|
||||
|
||||
// Output must be same length as source
|
||||
Assert.Throws<ArgumentException>(() => Cma.Batch(source.AsSpan(), wrongSizeOutput.AsSpan()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cma_SpanBatch_MatchesTSeriesBatch()
|
||||
{
|
||||
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 = Cma.Batch(series);
|
||||
|
||||
// Calculate with Span API
|
||||
Cma.Batch(source.AsSpan(), output.AsSpan());
|
||||
|
||||
// Compare results
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
Assert.Equal(tseriesResult[i].Value, output[i], 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cma_SpanBatch_CalculatesCorrectly()
|
||||
{
|
||||
double[] source = [10, 20, 30, 40, 50];
|
||||
double[] output = new double[5];
|
||||
|
||||
Cma.Batch(source.AsSpan(), output.AsSpan());
|
||||
|
||||
Assert.Equal(10.0, output[0], 1e-10); // 10/1 = 10
|
||||
Assert.Equal(15.0, output[1], 1e-10); // (10+20)/2 = 15
|
||||
Assert.Equal(20.0, output[2], 1e-10); // (10+20+30)/3 = 20
|
||||
Assert.Equal(25.0, output[3], 1e-10); // (10+20+30+40)/4 = 25
|
||||
Assert.Equal(30.0, output[4], 1e-10); // (10+20+30+40+50)/5 = 30
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cma_SpanBatch_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
|
||||
Cma.Batch(source.AsSpan(), output.AsSpan());
|
||||
|
||||
// This test verifies the method runs without throwing
|
||||
Assert.True(double.IsFinite(output[^1]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cma_SpanBatch_HandlesNaN()
|
||||
{
|
||||
double[] source = [100, 110, double.NaN, 120, 130];
|
||||
double[] output = new double[5];
|
||||
|
||||
Cma.Batch(source.AsSpan(), output.AsSpan());
|
||||
|
||||
// All outputs should be finite
|
||||
foreach (var val in output)
|
||||
{
|
||||
Assert.True(double.IsFinite(val), $"Expected finite value but got {val}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cma_AllModes_ProduceSameResult()
|
||||
{
|
||||
// Arrange
|
||||
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 = Cma.Batch(series);
|
||||
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];
|
||||
Cma.Batch(spanInput, spanOutput);
|
||||
double spanResult = spanOutput[^1];
|
||||
|
||||
// 3. Streaming Mode
|
||||
var streamingInd = new Cma();
|
||||
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 Cma(pubSource);
|
||||
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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Chainability_Works()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var cma = new Cma(source);
|
||||
|
||||
source.Add(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.Equal(100, cma.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_IsSetCorrectly()
|
||||
{
|
||||
var cma = new Cma();
|
||||
Assert.Equal(1, cma.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prime_SetsStateCorrectly()
|
||||
{
|
||||
var cma = new Cma();
|
||||
double[] history = [10, 20, 30, 40, 50]; // CMA = 30
|
||||
|
||||
cma.Prime(history);
|
||||
|
||||
Assert.True(cma.IsHot);
|
||||
Assert.Equal(30.0, cma.Last.Value, 1e-10);
|
||||
|
||||
// Verify it continues correctly
|
||||
cma.Update(new TValue(DateTime.UtcNow, 60)); // (10+20+30+40+50+60)/6 = 35
|
||||
Assert.Equal(35.0, cma.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prime_HandlesNaN_InHistory()
|
||||
{
|
||||
var cma = new Cma();
|
||||
double[] history = [10, 20, double.NaN, 40];
|
||||
// 10 -> 10
|
||||
// 10, 20 -> 15
|
||||
// 10, 20, 20 (NaN replaced by 20) -> 16.666...
|
||||
// 10, 20, 20, 40 -> 22.5
|
||||
|
||||
cma.Prime(history);
|
||||
|
||||
Assert.True(cma.IsHot);
|
||||
Assert.Equal(22.5, cma.Last.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsCorrectResultsAndHotIndicator()
|
||||
{
|
||||
var series = new TSeries();
|
||||
for (int i = 1; i <= 10; i++) series.Add(DateTime.UtcNow, i * 10);
|
||||
// 10, 20, 30, 40, 50, 60, 70, 80, 90, 100
|
||||
|
||||
var (results, indicator) = Cma.Calculate(series);
|
||||
|
||||
// Check results
|
||||
Assert.Equal(10, results.Count);
|
||||
Assert.Equal(30.0, results[4].Value, 1e-10); // CMA after 5 values = 30
|
||||
Assert.Equal(55.0, results.Last.Value, 1e-10); // CMA of all 10 = 55
|
||||
|
||||
// Check indicator state
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.Equal(55.0, indicator.Last.Value, 1e-10);
|
||||
Assert.Equal(1, indicator.WarmupPeriod);
|
||||
|
||||
// Verify indicator continues correctly
|
||||
indicator.Update(new TValue(DateTime.UtcNow, 110));
|
||||
// CMA now = (550 + 110)/11 = 60
|
||||
Assert.Equal(60.0, indicator.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cma_NumericalStability_LargeDataset()
|
||||
{
|
||||
// Test that CMA remains stable over a large number of values
|
||||
var cma = new Cma();
|
||||
double expectedSum = 0;
|
||||
|
||||
for (int i = 1; i <= 100000; i++)
|
||||
{
|
||||
cma.Update(new TValue(DateTime.UtcNow, 100.0)); // All same value
|
||||
expectedSum += 100.0;
|
||||
}
|
||||
|
||||
// CMA of 100000 values all equal to 100 should be exactly 100
|
||||
Assert.Equal(100.0, cma.Last.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cma_NumericalStability_VaryingValues()
|
||||
{
|
||||
// Test with alternating values
|
||||
var cma = new Cma();
|
||||
|
||||
for (int i = 0; i < 10000; i++)
|
||||
{
|
||||
double value = (i % 2 == 0) ? 100.0 : 200.0;
|
||||
cma.Update(new TValue(DateTime.UtcNow, value));
|
||||
}
|
||||
|
||||
// CMA of alternating 100, 200 should converge to 150
|
||||
Assert.Equal(150.0, cma.Last.Value, 1e-9);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for CMA (Cumulative Moving Average).
|
||||
/// CMA is not commonly found in standard TA libraries (like TA-Lib, Skender, etc.)
|
||||
/// as it's a fundamental statistical concept rather than a trading indicator.
|
||||
/// These tests validate against known mathematical results.
|
||||
/// </summary>
|
||||
public sealed class CmaValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _testData;
|
||||
private readonly ITestOutputHelper _output;
|
||||
private bool _disposed;
|
||||
|
||||
public CmaValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
_testData = new ValidationTestData();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_testData?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_MathematicalCorrectness_Batch()
|
||||
{
|
||||
// Calculate QuanTAlib CMA (batch TSeries)
|
||||
var cma = new Cma();
|
||||
var qResult = cma.Update(_testData.Data);
|
||||
|
||||
// Calculate expected CMA manually using running sum
|
||||
double runningSum = 0;
|
||||
int count = 0;
|
||||
|
||||
foreach (var item in _testData.Data)
|
||||
{
|
||||
count++;
|
||||
runningSum += item.Value;
|
||||
double expectedCma = runningSum / count;
|
||||
|
||||
// Get corresponding QuanTAlib result
|
||||
double qValue = qResult[count - 1].Value;
|
||||
|
||||
Assert.True(
|
||||
Math.Abs(qValue - expectedCma) <= ValidationHelper.DefaultTolerance,
|
||||
$"Mismatch at index {count - 1}: QuanTAlib={qValue:G17}, Expected={expectedCma:G17}");
|
||||
}
|
||||
|
||||
_output.WriteLine("CMA Batch(TSeries) validated successfully against manual calculation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_MathematicalCorrectness_Streaming()
|
||||
{
|
||||
// Calculate QuanTAlib CMA (streaming)
|
||||
var cma = new Cma();
|
||||
var qResults = new List<double>();
|
||||
foreach (var item in _testData.Data)
|
||||
{
|
||||
qResults.Add(cma.Update(item).Value);
|
||||
}
|
||||
|
||||
// Calculate expected CMA manually
|
||||
double runningSum = 0;
|
||||
|
||||
for (int i = 0; i < _testData.Data.Count; i++)
|
||||
{
|
||||
runningSum += _testData.Data[i].Value;
|
||||
double expectedCma = runningSum / (i + 1);
|
||||
|
||||
Assert.True(
|
||||
Math.Abs(qResults[i] - expectedCma) <= ValidationHelper.DefaultTolerance,
|
||||
$"Mismatch at index {i}: QuanTAlib={qResults[i]:G17}, Expected={expectedCma:G17}");
|
||||
}
|
||||
|
||||
_output.WriteLine("CMA Streaming validated successfully against manual calculation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_MathematicalCorrectness_Span()
|
||||
{
|
||||
// Prepare data for Span API
|
||||
double[] sourceData = _testData.RawData.ToArray();
|
||||
double[] qOutput = new double[sourceData.Length];
|
||||
|
||||
// Calculate QuanTAlib CMA (Span API)
|
||||
Cma.Batch(sourceData.AsSpan(), qOutput.AsSpan());
|
||||
|
||||
// Calculate expected CMA manually
|
||||
double runningSum = 0;
|
||||
|
||||
for (int i = 0; i < sourceData.Length; i++)
|
||||
{
|
||||
runningSum += sourceData[i];
|
||||
double expectedCma = runningSum / (i + 1);
|
||||
|
||||
Assert.True(
|
||||
Math.Abs(qOutput[i] - expectedCma) <= ValidationHelper.DefaultTolerance,
|
||||
$"Mismatch at index {i}: QuanTAlib={qOutput[i]:G17}, Expected={expectedCma:G17}");
|
||||
}
|
||||
|
||||
_output.WriteLine("CMA Span validated successfully against manual calculation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WelfordAlgorithm_Stability()
|
||||
{
|
||||
// Test numerical stability with large values
|
||||
// Welford's algorithm should handle this without overflow
|
||||
var cma = new Cma();
|
||||
double[] largeValues = new double[1000];
|
||||
const double baseValue = 1e10;
|
||||
|
||||
for (int i = 0; i < largeValues.Length; i++)
|
||||
{
|
||||
largeValues[i] = baseValue + i;
|
||||
}
|
||||
|
||||
// Calculate CMA
|
||||
foreach (var val in largeValues)
|
||||
{
|
||||
cma.Update(new TValue(DateTime.UtcNow, val));
|
||||
}
|
||||
|
||||
// Expected: average of 1e10, 1e10+1, ..., 1e10+999
|
||||
// = 1e10 + average of 0,1,2,...,999
|
||||
// = 1e10 + 499.5
|
||||
double expectedMean = baseValue + 499.5;
|
||||
|
||||
Assert.Equal(expectedMean, cma.Last.Value, 1e-6);
|
||||
_output.WriteLine($"CMA Welford stability test passed: {cma.Last.Value:G17}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WelfordAlgorithm_SmallDifferences()
|
||||
{
|
||||
// Test with values that have small differences (challenges precision)
|
||||
var cma = new Cma();
|
||||
double[] values = new double[10000];
|
||||
double baseValue = 1e8;
|
||||
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
values[i] = baseValue + (i % 2 == 0 ? 0.1 : -0.1);
|
||||
}
|
||||
|
||||
foreach (var val in values)
|
||||
{
|
||||
cma.Update(new TValue(DateTime.UtcNow, val));
|
||||
}
|
||||
|
||||
// With alternating +0.1 and -0.1, the average offset is 0
|
||||
Assert.Equal(baseValue, cma.Last.Value, 1e-7);
|
||||
_output.WriteLine($"CMA small differences test passed: {cma.Last.Value:G17}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_AgainstNaiveSum_ShortSequence()
|
||||
{
|
||||
// For short sequences, compare against naive sum/count
|
||||
double[] values = [100, 200, 150, 175, 125, 180, 160, 140, 190, 170];
|
||||
var cma = new Cma();
|
||||
|
||||
double sum = 0;
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
sum += values[i];
|
||||
cma.Update(new TValue(DateTime.UtcNow, values[i]));
|
||||
|
||||
double naiveMean = sum / (i + 1);
|
||||
Assert.Equal(naiveMean, cma.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
_output.WriteLine("CMA validated against naive sum for short sequence");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_AgainstNaiveSum_LongSequence()
|
||||
{
|
||||
// For longer sequences, verify the final value
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.0, sigma: 0.1, seed: 42);
|
||||
int count = 50000;
|
||||
double sum = 0;
|
||||
var cma = new Cma();
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
double value = gbm.Next().Close;
|
||||
sum += value;
|
||||
cma.Update(new TValue(DateTime.UtcNow, value));
|
||||
}
|
||||
|
||||
double naiveMean = sum / count;
|
||||
double welfordMean = cma.Last.Value;
|
||||
|
||||
// Both should be very close
|
||||
Assert.True(
|
||||
Math.Abs(naiveMean - welfordMean) < 1e-8,
|
||||
$"Naive={naiveMean:G17}, Welford={welfordMean:G17}, Diff={Math.Abs(naiveMean - welfordMean):G17}");
|
||||
|
||||
_output.WriteLine($"CMA long sequence: Naive={naiveMean:G10}, Welford={welfordMean:G10}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_KnownSequence_ArithmeticProgression()
|
||||
{
|
||||
// Arithmetic progression: 1, 2, 3, ..., n
|
||||
// CMA at each point: 1, 1.5, 2, 2.5, 3, ...
|
||||
// Formula: CMA_n = (n+1)/2
|
||||
|
||||
var cma = new Cma();
|
||||
|
||||
for (int n = 1; n <= 100; n++)
|
||||
{
|
||||
cma.Update(new TValue(DateTime.UtcNow, n));
|
||||
double expected = (n + 1.0) / 2.0;
|
||||
Assert.Equal(expected, cma.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
_output.WriteLine("CMA validated for arithmetic progression");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_KnownSequence_GeometricProgression()
|
||||
{
|
||||
// Geometric progression: r, r^2, r^3, ..., r^n
|
||||
// Sum = r * (r^n - 1) / (r - 1)
|
||||
// CMA = Sum / n
|
||||
|
||||
double r = 1.1;
|
||||
var cma = new Cma();
|
||||
|
||||
for (int n = 1; n <= 50; n++)
|
||||
{
|
||||
double value = Math.Pow(r, n);
|
||||
cma.Update(new TValue(DateTime.UtcNow, value));
|
||||
|
||||
// Sum of geometric series: a * (r^n - 1) / (r - 1) where a = r
|
||||
double sum = r * (Math.Pow(r, n) - 1) / (r - 1);
|
||||
double expected = sum / n;
|
||||
|
||||
Assert.Equal(expected, cma.Last.Value, 1e-9);
|
||||
}
|
||||
|
||||
_output.WriteLine("CMA validated for geometric progression");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ConstantSequence()
|
||||
{
|
||||
// CMA of constant sequence should be the constant
|
||||
double constant = 42.5;
|
||||
var cma = new Cma();
|
||||
|
||||
for (int i = 0; i < 10000; i++)
|
||||
{
|
||||
cma.Update(new TValue(DateTime.UtcNow, constant));
|
||||
}
|
||||
|
||||
Assert.Equal(constant, cma.Last.Value, 1e-10);
|
||||
_output.WriteLine("CMA validated for constant sequence");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_AllModes_Consistency()
|
||||
{
|
||||
// Verify all three calculation modes produce identical results
|
||||
var sourceData = _testData.RawData.ToArray();
|
||||
|
||||
// Mode 1: TSeries Batch
|
||||
var cma1 = new Cma();
|
||||
var batchResult = cma1.Update(_testData.Data);
|
||||
|
||||
// Mode 2: Streaming
|
||||
var cma2 = new Cma();
|
||||
var streamingResults = new List<double>();
|
||||
foreach (var item in _testData.Data)
|
||||
{
|
||||
streamingResults.Add(cma2.Update(item).Value);
|
||||
}
|
||||
|
||||
// Mode 3: Span
|
||||
var spanOutput = new double[sourceData.Length];
|
||||
Cma.Batch(sourceData.AsSpan(), spanOutput.AsSpan());
|
||||
|
||||
// Compare all three
|
||||
for (int i = 0; i < sourceData.Length; i++)
|
||||
{
|
||||
double batchVal = batchResult[i].Value;
|
||||
double streamVal = streamingResults[i];
|
||||
double spanVal = spanOutput[i];
|
||||
|
||||
Assert.Equal(batchVal, streamVal, 1e-10);
|
||||
Assert.Equal(batchVal, spanVal, 1e-10);
|
||||
}
|
||||
|
||||
_output.WriteLine("All CMA calculation modes produce consistent results");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// CMA: Cumulative Moving Average (Running Average / Cumulative Mean)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// CMA calculates the arithmetic mean of ALL data points seen so far, not just a fixed window.
|
||||
/// Uses Welford's algorithm with FMA (Fused Multiply-Add) for maximum numerical precision.
|
||||
///
|
||||
/// Calculation:
|
||||
/// M_n = M_(n-1) + α * (x_n - M_(n-1)) where α = 1/n
|
||||
///
|
||||
/// Implemented using FMA for single-rounding precision:
|
||||
/// mean = FusedMultiplyAdd(alpha, delta, mean)
|
||||
///
|
||||
/// This is equivalent to:
|
||||
/// M_n = ((n-1) * M_(n-1) + x_n) / n
|
||||
///
|
||||
/// Key Features:
|
||||
/// - Zero window: includes ALL historical data with equal weight
|
||||
/// - O(1) time complexity per update
|
||||
/// - Maximum precision: FMA avoids intermediate rounding of alpha*delta
|
||||
/// - Numerically stable: avoids overflow from summing large sequences
|
||||
/// - No buffer required: only stores count and mean
|
||||
///
|
||||
/// IsHot:
|
||||
/// Always true after the first value (no warmup period needed).
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Cma : AbstractBase
|
||||
{
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(double Mean, long Count, double LastValidValue);
|
||||
private State _state;
|
||||
private State _p_state;
|
||||
|
||||
private readonly TValuePublishedHandler _handler;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new CMA indicator instance.
|
||||
/// No period parameter required since CMA averages all values.
|
||||
/// </summary>
|
||||
public Cma()
|
||||
{
|
||||
Name = "Cma";
|
||||
WarmupPeriod = 1;
|
||||
_handler = Handle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates CMA with a source to subscribe to.
|
||||
/// </summary>
|
||||
/// <param name="source">Source to subscribe to</param>
|
||||
public Cma(ITValuePublisher source) : this()
|
||||
{
|
||||
source.Pub += _handler;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates CMA with a TSeries source to prime from and subscribe to.
|
||||
/// </summary>
|
||||
/// <param name="source">TSeries source</param>
|
||||
public Cma(TSeries source) : this()
|
||||
{
|
||||
Prime(source.Values);
|
||||
if (source.Count > 0)
|
||||
{
|
||||
Last = new TValue(source.LastTime, Last.Value);
|
||||
}
|
||||
source.Pub += _handler;
|
||||
}
|
||||
|
||||
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Mode B: Streaming (Stateful)
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/// <summary>
|
||||
/// True if the CMA has enough data to produce valid results.
|
||||
/// CMA is "hot" after the first value since no warmup is needed.
|
||||
/// </summary>
|
||||
public override bool IsHot => _state.Count > 0;
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Mode C: Priming (The Bridge)
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the indicator state using the provided history.
|
||||
/// </summary>
|
||||
/// <param name="source">Historical data</param>
|
||||
/// <param name="step">Time interval between values (not used for CMA)</param>
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
if (source.Length == 0) return;
|
||||
|
||||
// Reset state
|
||||
_state = default;
|
||||
_p_state = default;
|
||||
|
||||
// Find first valid value to seed lastValid
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
if (double.IsFinite(source[i]))
|
||||
{
|
||||
_state.LastValidValue = source[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Process all values using Welford's algorithm with FMA
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
double val = GetValidValue(source[i]);
|
||||
_state.Count++;
|
||||
double alpha = 1.0 / _state.Count;
|
||||
double delta = val - _state.Mean;
|
||||
_state.Mean = Math.FusedMultiplyAdd(alpha, delta, _state.Mean);
|
||||
}
|
||||
|
||||
Last = new TValue(DateTime.MinValue, _state.Mean);
|
||||
_p_state = _state;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double GetValidValue(double input)
|
||||
{
|
||||
if (double.IsFinite(input))
|
||||
{
|
||||
_state.LastValidValue = input;
|
||||
return input;
|
||||
}
|
||||
return _state.LastValidValue;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_p_state = _state;
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = _p_state;
|
||||
}
|
||||
|
||||
double val = GetValidValue(input.Value);
|
||||
_state.Count++;
|
||||
double alpha = 1.0 / _state.Count;
|
||||
double delta = val - _state.Mean;
|
||||
_state.Mean = Math.FusedMultiplyAdd(alpha, delta, _state.Mean);
|
||||
|
||||
Last = new TValue(input.Time, _state.Mean);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
if (source.Count == 0) return [];
|
||||
|
||||
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);
|
||||
|
||||
Batch(source.Values, vSpan);
|
||||
source.Times.CopyTo(tSpan);
|
||||
|
||||
Prime(source.Values);
|
||||
|
||||
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Mode A: Batch (Stateless)
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/// <summary>
|
||||
/// Calculates CMA for the entire series using a new instance.
|
||||
/// </summary>
|
||||
/// <param name="source">Input series</param>
|
||||
/// <returns>CMA series</returns>
|
||||
public static TSeries Batch(TSeries source)
|
||||
{
|
||||
var cma = new Cma();
|
||||
return cma.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates CMA in-place, writing results to pre-allocated output span.
|
||||
/// Zero-allocation method for maximum performance.
|
||||
/// Uses Welford's algorithm for numerical stability.
|
||||
/// </summary>
|
||||
/// <param name="source">Input values</param>
|
||||
/// <param name="output">Output span (must be same length as source)</param>
|
||||
public static void Batch(ReadOnlySpan<double> source, Span<double> output)
|
||||
{
|
||||
if (source.Length != output.Length)
|
||||
throw new ArgumentException("Source and output must have the same length", nameof(output));
|
||||
|
||||
int len = source.Length;
|
||||
if (len == 0) return;
|
||||
|
||||
double mean = 0;
|
||||
double lastValid = double.NaN;
|
||||
|
||||
// Find first valid value to seed lastValid
|
||||
for (int k = 0; k < len; k++)
|
||||
{
|
||||
if (double.IsFinite(source[k]))
|
||||
{
|
||||
lastValid = source[k];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Welford's algorithm for running mean with FMA
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
if (double.IsFinite(val))
|
||||
lastValid = val;
|
||||
else
|
||||
val = lastValid;
|
||||
|
||||
// M_n = M_(n-1) + alpha * delta using FMA for single-rounding precision
|
||||
double alpha = 1.0 / (i + 1);
|
||||
double delta = val - mean;
|
||||
mean = Math.FusedMultiplyAdd(alpha, delta, mean);
|
||||
output[i] = mean;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a batch calculation on history and returns
|
||||
/// a "Hot" Cma instance ready to process the next tick immediately.
|
||||
/// </summary>
|
||||
/// <param name="source">Historical time series</param>
|
||||
/// <returns>A tuple containing the full calculation results and the hot indicator instance</returns>
|
||||
public static (TSeries Results, Cma Indicator) Calculate(TSeries source)
|
||||
{
|
||||
var cma = new Cma();
|
||||
TSeries results = cma.Update(source);
|
||||
return (results, cma);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the CMA state.
|
||||
/// </summary>
|
||||
public override void Reset()
|
||||
{
|
||||
_state = default;
|
||||
_p_state = default;
|
||||
Last = default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
# CMA: Cumulative Moving Average
|
||||
|
||||
> "The running average that never forgets. Every single tick you've ever fed it? Still in there, affecting the result. It's like the elephant of technical indicators."
|
||||
|
||||
The Cumulative Moving Average (CMA) calculates the arithmetic mean of ALL data points seen so far, not just a fixed window. Unlike SMA or EMA which use a sliding window, CMA treats every historical value with equal weight. As the sample size grows, each new value has diminishing impact on the average.
|
||||
|
||||
## Historical Context
|
||||
|
||||
The concept of a running mean is fundamental to statistics and was formalized by B. P. Welford in 1962 for numerically stable computation. Donald Knuth popularized it in *The Art of Computer Programming*. While not a traditional trading indicator, CMA is essential for scenarios requiring the true average of all observed data: calculating session VWAP from scratch, averaging tick counts, or computing lifetime average fill prices.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
The naive approach (sum all values, divide by count) works for small datasets but fails at scale. After millions of ticks, the running sum can overflow or lose precision.
|
||||
|
||||
### Welford's Algorithm with FMA
|
||||
|
||||
QuanTAlib uses Welford's numerically stable update, enhanced with Fused Multiply-Add (FMA) for maximum precision:
|
||||
|
||||
$$ M_n = M_{n-1} + \alpha \cdot (x_n - M_{n-1}) \quad \text{where } \alpha = \frac{1}{n} $$
|
||||
|
||||
Implemented as:
|
||||
|
||||
```csharp
|
||||
double alpha = 1.0 / n;
|
||||
double delta = x - mean;
|
||||
mean = Math.FusedMultiplyAdd(alpha, delta, mean);
|
||||
```
|
||||
|
||||
This formulation:
|
||||
|
||||
1. Keeps intermediate values near the scale of the actual mean (no overflow)
|
||||
2. Requires only O(1) memory (just count and mean)
|
||||
3. Achieves O(1) time complexity per update
|
||||
4. Uses FMA for single-rounding precision (avoids rounding `alpha * delta` before adding to `mean`)
|
||||
5. Is mathematically equivalent to $M_n = \frac{(n-1) \cdot M_{n-1} + x_n}{n}$
|
||||
|
||||
### Why Not Just Sum?
|
||||
|
||||
Consider averaging 10 million tick prices around 50,000 (a futures contract). The naive sum exceeds $5 \times 10^{11}$, approaching the precision limits of `double`. Welford's algorithm keeps the working value around 50,000 throughout, maintaining full precision.
|
||||
|
||||
### The Diminishing Return Problem
|
||||
|
||||
As $n$ grows large, each new value contributes only $\frac{1}{n}$ to the mean. After 1 million samples, a new tick moves the average by roughly 0.0001% of the difference from the current mean. This is mathematically correct but may not be what traders want for responsiveness (use EMA or SMA for that).
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### 1. Incremental Update (Welford)
|
||||
|
||||
$$ M_n = M_{n-1} + \frac{x_n - M_{n-1}}{n} $$
|
||||
|
||||
Where:
|
||||
|
||||
* $M_n$ = cumulative mean after $n$ values
|
||||
* $M_{n-1}$ = previous cumulative mean
|
||||
* $x_n$ = new value
|
||||
* $n$ = total count of values
|
||||
|
||||
### 2. Algebraic Equivalence
|
||||
|
||||
$$ M_n = \frac{1}{n} \sum_{i=1}^{n} x_i = \frac{(n-1) \cdot M_{n-1} + x_n}{n} $$
|
||||
|
||||
## Performance Profile
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| **Throughput** | ~5 ns/bar | Single division per update. |
|
||||
| **Allocations** | 0 | Zero-allocation in hot paths. |
|
||||
| **Complexity** | O(1) | Constant time regardless of history length. |
|
||||
| **Accuracy** | 10 | Welford's algorithm ensures numerical stability. |
|
||||
| **Timeliness** | 1 | Maximum lag; every historical value affects output. |
|
||||
| **Overshoot** | 0 | Never overshoots the input data range. |
|
||||
| **Smoothness** | 10 | Extremely smooth as $n$ grows (almost constant). |
|
||||
|
||||
## Validation
|
||||
|
||||
| Library | Status | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| **TA-Lib** | N/A | No CMA function. |
|
||||
| **Skender** | N/A | No CMA function. |
|
||||
| **Tulip** | N/A | No CMA function. |
|
||||
| **Mathematical** | ✅ | Validated against known formulas. |
|
||||
|
||||
CMA is a fundamental statistical operation rather than a standard TA library indicator. QuanTAlib validates against mathematical proofs: arithmetic progressions, geometric series, and direct sum/count calculations.
|
||||
|
||||
## Use Cases
|
||||
|
||||
1. **Session VWAP**: Calculate volume-weighted average price from session start
|
||||
2. **Lifetime Averages**: Average fill price across all trades
|
||||
3. **Quality Metrics**: Average latency, slippage, or fill rate over time
|
||||
4. **Baseline Comparison**: Compare current price to "all-time average"
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Responsiveness**: CMA becomes nearly unresponsive after many values. For a reactive average, use SMA or EMA instead.
|
||||
2. **Memory of Bad Data**: A single extreme outlier early in the stream permanently affects the average. Consider filtering before feeding CMA.
|
||||
3. **No Period Parameter**: Unlike SMA/EMA, CMA has no period. It always includes all data. This is by design.
|
||||
4. **Session Resets**: If you need per-session averages, call `Reset()` at session boundaries.
|
||||
Reference in New Issue
Block a user