mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-18 10:38:05 +00:00
next iteration
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class SimdExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void SumSIMD_EmptySpan_ReturnsZero()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
Assert.Equal(0.0, span.SumSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumSIMD_SingleElement_ReturnsElement()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(42.5, span.SumSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumSIMD_MultipleElements_ReturnsCorrectSum()
|
||||
{
|
||||
double[] data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(55.0, span.SumSIMD(), precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumSIMD_LargeArray_ReturnsCorrectSum()
|
||||
{
|
||||
double[] data = new double[1000];
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
data[i] = i + 1.0;
|
||||
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
double expected = 1000.0 * 1001.0 / 2.0; // Sum of 1..1000
|
||||
Assert.Equal(expected, span.SumSIMD(), precision: 8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinSIMD_EmptySpan_ReturnsNaN()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
Assert.True(double.IsNaN(span.MinSIMD()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinSIMD_SingleElement_ReturnsElement()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(42.5, span.MinSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinSIMD_MultipleElements_ReturnsMinimum()
|
||||
{
|
||||
double[] data = [5.0, 2.0, 8.0, 1.0, 9.0, 3.0, 7.0, 4.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(1.0, span.MinSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaxSIMD_EmptySpan_ReturnsNaN()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
Assert.True(double.IsNaN(span.MaxSIMD()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaxSIMD_SingleElement_ReturnsElement()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(42.5, span.MaxSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaxSIMD_MultipleElements_ReturnsMaximum()
|
||||
{
|
||||
double[] data = [5.0, 2.0, 8.0, 1.0, 9.0, 3.0, 7.0, 4.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(9.0, span.MaxSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AverageSIMD_EmptySpan_ReturnsNaN()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
Assert.True(double.IsNaN(span.AverageSIMD()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AverageSIMD_MultipleElements_ReturnsCorrectAverage()
|
||||
{
|
||||
double[] data = [1.0, 2.0, 3.0, 4.0, 5.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(3.0, span.AverageSIMD(), precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceSIMD_LessThanTwoElements_ReturnsNaN()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(double.IsNaN(span.VarianceSIMD()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceSIMD_MultipleElements_ReturnsCorrectVariance()
|
||||
{
|
||||
double[] data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
|
||||
// Expected variance: 4.571428... (sample variance)
|
||||
double variance = span.VarianceSIMD();
|
||||
Assert.True(Math.Abs(variance - 4.571428) < 0.0001);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StdDevSIMD_MultipleElements_ReturnsCorrectStdDev()
|
||||
{
|
||||
double[] data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
|
||||
// Expected std dev: sqrt(4.571428) ≈ 2.138
|
||||
double stdDev = span.StdDevSIMD();
|
||||
Assert.True(Math.Abs(stdDev - 2.138) < 0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinMaxSIMD_EmptySpan_ReturnsBothNaN()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
var (min, max) = span.MinMaxSIMD();
|
||||
Assert.True(double.IsNaN(min));
|
||||
Assert.True(double.IsNaN(max));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinMaxSIMD_SingleElement_ReturnsSameValue()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
var (min, max) = span.MinMaxSIMD();
|
||||
Assert.Equal(42.5, min);
|
||||
Assert.Equal(42.5, max);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinMaxSIMD_MultipleElements_ReturnsCorrectMinMax()
|
||||
{
|
||||
double[] data = [5.0, 2.0, 8.0, 1.0, 9.0, 3.0, 7.0, 4.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
var (min, max) = span.MinMaxSIMD();
|
||||
Assert.Equal(1.0, min);
|
||||
Assert.Equal(9.0, max);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SIMD_WorksWithTSeriesValues()
|
||||
{
|
||||
var series = new TSeries(100);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
series.Add(DateTime.UtcNow.Ticks + i, i + 1.0);
|
||||
}
|
||||
|
||||
var values = series.Values;
|
||||
|
||||
double sum = values.SumSIMD();
|
||||
double avg = values.AverageSIMD();
|
||||
double min = values.MinSIMD();
|
||||
double max = values.MaxSIMD();
|
||||
var (minAlt, maxAlt) = values.MinMaxSIMD();
|
||||
|
||||
Assert.Equal(5050.0, sum, precision: 8); // Sum of 1..100
|
||||
Assert.Equal(50.5, avg, precision: 8);
|
||||
Assert.Equal(1.0, min);
|
||||
Assert.Equal(100.0, max);
|
||||
Assert.Equal(min, minAlt);
|
||||
Assert.Equal(max, maxAlt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SIMD_WorksWithTBarSeriesClose()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
var bars = gbm.Fetch(1000, startTime, interval);
|
||||
|
||||
var closeValues = bars.Close.Values;
|
||||
|
||||
double sum = closeValues.SumSIMD();
|
||||
double avg = closeValues.AverageSIMD();
|
||||
double min = closeValues.MinSIMD();
|
||||
double max = closeValues.MaxSIMD();
|
||||
|
||||
Assert.True(sum > 0);
|
||||
Assert.True(avg > 0);
|
||||
Assert.True(min > 0);
|
||||
Assert.True(max > min);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SIMD_PerformanceTest_LargeDataset()
|
||||
{
|
||||
// Generate large dataset
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
var bars = gbm.Fetch(10000, startTime, interval);
|
||||
var closeValues = bars.Close.Values;
|
||||
|
||||
// Warm up
|
||||
_ = closeValues.SumSIMD();
|
||||
|
||||
// Test SIMD operations
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
double sum = closeValues.SumSIMD();
|
||||
double avg = closeValues.AverageSIMD();
|
||||
double min = closeValues.MinSIMD();
|
||||
double max = closeValues.MaxSIMD();
|
||||
var (minAlt, maxAlt) = closeValues.MinMaxSIMD();
|
||||
double variance = closeValues.VarianceSIMD();
|
||||
double stdDev = closeValues.StdDevSIMD();
|
||||
|
||||
sw.Stop();
|
||||
|
||||
// Verify results are valid
|
||||
Assert.True(sum > 0);
|
||||
Assert.True(avg > 0);
|
||||
Assert.True(min > 0);
|
||||
Assert.True(max > min);
|
||||
Assert.True(variance > 0);
|
||||
Assert.True(stdDev > 0);
|
||||
|
||||
// Performance should be sub-millisecond for 10k elements
|
||||
Assert.True(sw.ElapsedMilliseconds < 10,
|
||||
$"SIMD operations took {sw.ElapsedMilliseconds}ms, expected < 10ms");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests
|
||||
{
|
||||
public class TBarTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_SetsPropertiesCorrectly()
|
||||
{
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
double open = 100;
|
||||
double high = 110;
|
||||
double low = 90;
|
||||
double close = 105;
|
||||
double volume = 1000;
|
||||
|
||||
var bar = new TBar(time, open, high, low, close, volume);
|
||||
|
||||
Assert.Equal(time, bar.Time);
|
||||
Assert.Equal(open, bar.Open);
|
||||
Assert.Equal(high, bar.High);
|
||||
Assert.Equal(low, bar.Low);
|
||||
Assert.Equal(close, bar.Close);
|
||||
Assert.Equal(volume, bar.Volume);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HL2_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, 100, 110, 90, 105, 1000);
|
||||
Assert.Equal(100.0, bar.HL2); // (110 + 90) / 2
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OHL3_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, 100, 110, 90, 105, 1000);
|
||||
Assert.Equal(100.0, bar.OHL3); // (100 + 110 + 90) / 3
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HLC3_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, 100, 110, 90, 100, 1000);
|
||||
Assert.Equal(100.0, bar.HLC3); // (110 + 90 + 100) / 3
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OHLC4_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, 100, 110, 90, 100, 1000);
|
||||
Assert.Equal(100.0, bar.OHLC4); // (100 + 110 + 90 + 100) / 4
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HLCC4_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, 100, 110, 90, 100, 1000);
|
||||
Assert.Equal(100.0, bar.HLCC4); // (110 + 90 + 100 + 100) / 4
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImplicitConversion_ToTValue_ReturnsClosePriceWithTime()
|
||||
{
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
var bar = new TBar(time, 100, 110, 90, 105, 1000);
|
||||
|
||||
TValue tv = bar;
|
||||
|
||||
Assert.Equal(time, tv.Time);
|
||||
Assert.Equal(105.0, tv.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,12 +26,12 @@ public readonly struct TBar : IEquatable<TBar>
|
||||
public TValue V { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Time, Volume); }
|
||||
|
||||
// Computed properties (calculated on demand, no storage overhead)
|
||||
public double HL2 => (High + Low) * 0.5;
|
||||
public double OC2 => (Open + Close) * 0.5;
|
||||
public double OHL3 => (Open + High + Low) / 3.0;
|
||||
public double HLC3 => (High + Low + Close) / 3.0;
|
||||
public double OHLC4 => (Open + High + Low + Close) * 0.25;
|
||||
public double HLCC4 => (High + Low + Close + Close) * 0.25;
|
||||
public double HL2 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low) * 0.5; }
|
||||
public double OC2 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + Close) * 0.5; }
|
||||
public double OHL3 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + High + Low) / 3.0; }
|
||||
public double HLC3 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low + Close) / 3.0; }
|
||||
public double OHLC4 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + High + Low + Close) * 0.25; }
|
||||
public double HLCC4 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low + Close + Close) * 0.25; }
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TBar(long time, double open, double high, double low, double close, double volume)
|
||||
@@ -58,6 +58,9 @@ public readonly struct TBar : IEquatable<TBar>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator double(TBar bar) => bar.Close;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator TValue(TBar bar) => new(bar.Time, bar.Close);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator DateTime(TBar bar) => new(bar.Time, DateTimeKind.Utc);
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests
|
||||
{
|
||||
public class TBarSeriesTests
|
||||
{
|
||||
[Fact]
|
||||
public void Add_NewBar_IncreasesCount()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
var bar = new TBar(DateTime.UtcNow.Ticks, 100, 110, 90, 105, 1000);
|
||||
|
||||
series.Add(bar, isNew: true);
|
||||
|
||||
Assert.Single(series);
|
||||
Assert.Equal(105.0, series.Last.Close);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_UpdateBar_DoesNotIncreaseCount()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
var bar1 = new TBar(time, 100, 110, 90, 105, 1000);
|
||||
var bar2 = new TBar(time, 100, 112, 90, 108, 1200);
|
||||
|
||||
series.Add(bar1, isNew: true);
|
||||
series.Add(bar2, isNew: false);
|
||||
|
||||
Assert.Single(series);
|
||||
Assert.Equal(108.0, series.Last.Close);
|
||||
Assert.Equal(112.0, series.Last.High);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubSeries_AreUpdated()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
var bar = new TBar(DateTime.UtcNow.Ticks, 100, 110, 90, 105, 1000);
|
||||
|
||||
series.Add(bar, isNew: true);
|
||||
|
||||
Assert.Single(series.Open);
|
||||
Assert.Single(series.High);
|
||||
Assert.Single(series.Low);
|
||||
Assert.Single(series.Close);
|
||||
Assert.Single(series.Volume);
|
||||
|
||||
Assert.Equal(100.0, series.Open.Last.Value);
|
||||
Assert.Equal(110.0, series.High.Last.Value);
|
||||
Assert.Equal(90.0, series.Low.Last.Value);
|
||||
Assert.Equal(105.0, series.Close.Last.Value);
|
||||
Assert.Equal(1000.0, series.Volume.Last.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests
|
||||
{
|
||||
public class TSeriesTests
|
||||
{
|
||||
[Fact]
|
||||
public void Add_NewValue_IncreasesCount()
|
||||
{
|
||||
var series = new TSeries();
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
|
||||
series.Add(time, 10.0, isNew: true);
|
||||
|
||||
Assert.Single(series);
|
||||
Assert.Equal(10.0, series.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_UpdateValue_DoesNotIncreaseCount()
|
||||
{
|
||||
var series = new TSeries();
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
|
||||
series.Add(time, 10.0, isNew: true);
|
||||
series.Add(time, 11.0, isNew: false);
|
||||
|
||||
Assert.Single(series);
|
||||
Assert.Equal(11.0, series.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_MultipleValues_MaintainsOrder()
|
||||
{
|
||||
var series = new TSeries();
|
||||
long t0 = DateTime.UtcNow.Ticks;
|
||||
long t1 = t0 + TimeSpan.TicksPerMinute;
|
||||
|
||||
series.Add(t0, 10.0, isNew: true);
|
||||
series.Add(t1, 20.0, isNew: true);
|
||||
|
||||
Assert.Equal(2, series.Count);
|
||||
Assert.Equal(10.0, series[0].Value);
|
||||
Assert.Equal(20.0, series[1].Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests
|
||||
{
|
||||
public class TValueTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_SetsPropertiesCorrectly()
|
||||
{
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
double value = 123.45;
|
||||
|
||||
var tValue = new TValue(time, value);
|
||||
|
||||
Assert.Equal(time, tValue.Time);
|
||||
Assert.Equal(value, tValue.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AsDateTime_ReturnsCorrectDateTime()
|
||||
{
|
||||
DateTime dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
long ticks = dt.Ticks;
|
||||
var tValue = new TValue(ticks, 100.0);
|
||||
|
||||
Assert.Equal(dt, tValue.AsDateTime);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_FormatsCorrectly()
|
||||
{
|
||||
DateTime dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
var tValue = new TValue(dt.Ticks, 123.456);
|
||||
|
||||
string result = tValue.ToString();
|
||||
|
||||
Assert.Contains(dt.ToString("yyyy-MM-dd HH:mm:ss"), result);
|
||||
Assert.Contains("123.46", result); // Default formatting usually 2 decimals or similar
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImplicitConversion_ToDouble()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, 42.0);
|
||||
double val = tValue;
|
||||
Assert.Equal(42.0, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class CsvFeedTests
|
||||
{
|
||||
private const string TestCsvPath = "daily_IBM.csv";
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidFile_LoadsData()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
Assert.NotNull(feed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NonExistentFile_ThrowsFileNotFoundException()
|
||||
{
|
||||
Assert.Throws<FileNotFoundException>(() => new CsvFeed("nonexistent.csv"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NullPath_ThrowsArgumentException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new CsvFeed(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_EmptyPath_ThrowsArgumentException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new CsvFeed(""));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Next_StreamsDataChronologically()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
// Get first bar
|
||||
var bar1 = feed.Next(isNew: true);
|
||||
Assert.True(bar1.Time > 0);
|
||||
|
||||
// Get second bar - should be later in time
|
||||
var bar2 = feed.Next(isNew: true);
|
||||
Assert.True(bar2.Time > bar1.Time);
|
||||
|
||||
// Get third bar
|
||||
var bar3 = feed.Next(isNew: true);
|
||||
Assert.True(bar3.Time > bar2.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Next_WithRefParameter_StreamsCorrectly()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
bool isNew = true;
|
||||
var bar1 = feed.Next(ref isNew);
|
||||
Assert.True(isNew); // Should still be true
|
||||
Assert.True(bar1.Time > 0);
|
||||
|
||||
isNew = true;
|
||||
var bar2 = feed.Next(ref isNew);
|
||||
Assert.True(isNew);
|
||||
Assert.True(bar2.Time > bar1.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Next_UpdateCurrentBar_ReturnsSameBar()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
// Get first bar
|
||||
var bar1 = feed.Next(isNew: true);
|
||||
|
||||
// Update current bar (should return same bar)
|
||||
var bar2 = feed.Next(isNew: false);
|
||||
Assert.Equal(bar1.Time, bar2.Time);
|
||||
Assert.Equal(bar1.Close, bar2.Close);
|
||||
|
||||
// Get next bar
|
||||
var bar3 = feed.Next(isNew: true);
|
||||
Assert.True(bar3.Time > bar1.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Next_EndOfData_SignalsNoMoreData()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
// Stream through all data
|
||||
TBar lastBar = default;
|
||||
bool isNew = true;
|
||||
int count = 0;
|
||||
|
||||
while (isNew && count < 200) // Safety limit
|
||||
{
|
||||
lastBar = feed.Next(ref isNew);
|
||||
count++;
|
||||
}
|
||||
|
||||
// Should have reached end and isNew should be false
|
||||
Assert.False(isNew);
|
||||
Assert.True(lastBar.Time > 0);
|
||||
|
||||
// Calling again should return same bar with isNew=false
|
||||
isNew = true;
|
||||
var finalBar = feed.Next(ref isNew);
|
||||
Assert.False(isNew);
|
||||
Assert.Equal(lastBar.Time, finalBar.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_ReturnsCorrectNumberOfBars()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
var startTime = new DateTime(2025, 7, 1, 0, 0, 0, DateTimeKind.Utc).Ticks;
|
||||
var interval = TimeSpan.FromDays(1);
|
||||
|
||||
var series = feed.Fetch(10, startTime, interval);
|
||||
|
||||
Assert.True(series.Count > 0);
|
||||
Assert.True(series.Count <= 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_InvalidCount_ThrowsArgumentException()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
var startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromDays(1);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => feed.Fetch(0, startTime, interval));
|
||||
Assert.Throws<ArgumentException>(() => feed.Fetch(-1, startTime, interval));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_ResetsStreamingPosition()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
// Stream a few bars
|
||||
feed.Next(isNew: true);
|
||||
feed.Next(isNew: true);
|
||||
feed.Next(isNew: true);
|
||||
|
||||
// Fetch from start
|
||||
var startTime = new DateTime(2025, 7, 1, 0, 0, 0, DateTimeKind.Utc).Ticks;
|
||||
var series = feed.Fetch(5, startTime, TimeSpan.FromDays(1));
|
||||
|
||||
// Next should now stream from fetched position
|
||||
var bar = feed.Next(isNew: true);
|
||||
Assert.True(bar.Time >= startTime);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadFromCsv_ParsesValuesCorrectly()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
// Get first bar (oldest in chronological order)
|
||||
var bar = feed.Next(isNew: true);
|
||||
|
||||
// Verify it has valid OHLCV data
|
||||
Assert.True(bar.Open > 0);
|
||||
Assert.True(bar.High >= bar.Open);
|
||||
Assert.True(bar.High >= bar.Close);
|
||||
Assert.True(bar.Low <= bar.Open);
|
||||
Assert.True(bar.Low <= bar.Close);
|
||||
Assert.True(bar.Close > 0);
|
||||
Assert.True(bar.Volume > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadFromCsv_DataInChronologicalOrder()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
var bars = new List<TBar>();
|
||||
bool isNew = true;
|
||||
|
||||
// Collect first 10 bars
|
||||
for (int i = 0; i < 10 && isNew; i++)
|
||||
{
|
||||
bars.Add(feed.Next(ref isNew));
|
||||
}
|
||||
|
||||
// Verify chronological order (each bar later than previous)
|
||||
for (int i = 1; i < bars.Count; i++)
|
||||
{
|
||||
Assert.True(bars[i].Time > bars[i - 1].Time,
|
||||
$"Bar {i} time ({bars[i].AsDateTime}) should be after bar {i-1} time ({bars[i-1].AsDateTime})");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CsvFeed_WorksWithIFeedInterface()
|
||||
{
|
||||
IFeed feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
var bar1 = feed.Next(isNew: true);
|
||||
Assert.True(bar1.Time > 0);
|
||||
|
||||
var bar2 = feed.Next(isNew: true);
|
||||
Assert.True(bar2.Time > bar1.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Next_MixedNewAndUpdate_WorksCorrectly()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
var bar1 = feed.Next(isNew: true);
|
||||
var bar1Update = feed.Next(isNew: false);
|
||||
Assert.Equal(bar1.Time, bar1Update.Time);
|
||||
|
||||
var bar2 = feed.Next(isNew: true);
|
||||
Assert.True(bar2.Time > bar1.Time);
|
||||
|
||||
var bar2Update = feed.Next(isNew: false);
|
||||
Assert.Equal(bar2.Time, bar2Update.Time);
|
||||
|
||||
var bar3 = feed.Next(isNew: true);
|
||||
Assert.True(bar3.Time > bar2.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_WithEarlyStartTime_ReturnsData()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
// Start from very early date (before any data)
|
||||
var startTime = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks;
|
||||
var series = feed.Fetch(5, startTime, TimeSpan.FromDays(1));
|
||||
|
||||
// Should return data starting from first available bar
|
||||
Assert.True(series.Count > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_WithFutureStartTime_ReturnsEmpty()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
// Start from future date (after all data)
|
||||
var startTime = new DateTime(2030, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks;
|
||||
var series = feed.Fetch(5, startTime, TimeSpan.FromDays(1));
|
||||
|
||||
// Should return empty or minimal data
|
||||
Assert.True(series.Count == 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
timestamp,open,high,low,close,volume
|
||||
2025-11-25,304.1250,306.0000,297.0600,304.4800,2825322
|
||||
2025-11-24,299.1800,307.1800,297.5100,304.1200,6050640
|
||||
2025-11-21,293.4800,300.4800,291.8900,297.4400,5710903
|
||||
2025-11-20,294.6400,300.7100,290.1600,290.4000,5597028
|
||||
2025-11-19,290.5000,291.1099,288.0700,288.5300,3595912
|
||||
2025-11-18,297.0000,297.0000,289.9200,289.9500,4861928
|
||||
2025-11-17,305.5900,306.0000,296.5100,297.1700,3909741
|
||||
2025-11-14,300.0000,307.7200,297.5900,305.6900,3592455
|
||||
2025-11-13,312.2900,314.6000,303.6800,304.8600,5310150
|
||||
2025-11-12,319.8900,324.9000,314.5324,314.9800,6042686
|
||||
2025-11-11,309.0000,317.9100,308.4300,313.7200,4381913
|
||||
2025-11-10,306.8200,309.9400,304.2300,309.1300,2975188
|
||||
2025-11-07,309.6800,310.0000,302.6301,306.3800,5070773
|
||||
2025-11-06,306.7500,315.4400,301.0900,312.4200,6818521
|
||||
2025-11-05,301.3800,307.2000,299.7100,306.7700,4633195
|
||||
2025-11-04,300.0000,303.1700,296.0000,300.8500,5677330
|
||||
2025-11-03,308.0000,312.1411,304.2300,304.7300,4957958
|
||||
2025-10-31,312.0000,313.5000,301.6300,307.4100,7697499
|
||||
2025-10-30,306.6500,313.7500,305.0200,310.0600,4694275
|
||||
2025-10-29,312.7900,314.3300,307.5200,308.2100,4135948
|
||||
2025-10-28,312.6000,319.3500,311.4100,312.5700,6044770
|
||||
2025-10-27,307.8000,313.5000,302.8800,313.0900,9868151
|
||||
2025-10-24,283.7700,310.7500,282.2100,307.4600,16914243
|
||||
2025-10-23,264.9500,285.5791,263.5623,285.0000,16676394
|
||||
2025-10-22,281.9900,289.1700,281.3500,287.5100,10538480
|
||||
2025-10-21,283.3100,285.3100,281.6000,282.0500,4080981
|
||||
2025-10-20,281.2500,285.5000,280.9600,283.6500,3494336
|
||||
2025-10-17,276.1500,283.4000,275.3500,281.2800,5309565
|
||||
2025-10-16,281.1100,282.5600,275.6000,275.9700,2956923
|
||||
2025-10-15,278.3800,285.4500,277.0000,280.7500,3346753
|
||||
2025-10-14,275.5200,277.5300,272.5469,276.1500,3058149
|
||||
2025-10-13,279.7900,282.4399,274.6400,277.2200,4333836
|
||||
2025-10-10,288.9700,290.3850,277.5000,277.8200,4508506
|
||||
2025-10-09,289.8200,290.1300,283.3200,288.2300,4912375
|
||||
2025-10-08,294.1600,294.2000,286.4730,289.4600,5297030
|
||||
2025-10-07,295.5500,301.0425,293.2850,293.8700,7190126
|
||||
2025-10-06,288.6100,291.4500,287.8000,289.4200,2881947
|
||||
2025-10-03,287.5000,293.3200,287.3000,288.3700,4375082
|
||||
2025-10-02,285.7900,288.5400,282.7900,286.7200,3814232
|
||||
2025-10-01,280.2000,286.5900,280.1500,286.4900,4381338
|
||||
2025-09-30,280.8800,286.0250,280.5200,282.1600,5926924
|
||||
2025-09-29,286.0000,286.0000,279.6600,279.8000,6022125
|
||||
2025-09-26,280.5100,288.8500,280.1100,284.3100,9063938
|
||||
2025-09-25,272.9350,284.2300,271.1480,281.4400,11506192
|
||||
2025-09-24,272.6200,273.6499,267.3000,267.5300,3159924
|
||||
2025-09-23,272.7000,273.2962,269.2650,272.2400,5394121
|
||||
2025-09-22,266.6200,272.3100,266.0000,271.3700,5030540
|
||||
2025-09-19,266.0500,267.8700,263.6400,266.4000,9858112
|
||||
2025-09-18,258.8600,265.2300,256.8004,265.0000,4988421
|
||||
2025-09-17,257.4950,260.9644,257.0100,259.0800,3974785
|
||||
2025-09-16,256.2600,258.0000,254.4100,257.5200,2719918
|
||||
2025-09-15,254.0200,259.0500,254.0000,256.2400,4028365
|
||||
2025-09-12,256.9500,257.2500,252.4250,253.4400,3433300
|
||||
2025-09-11,257.5600,258.5450,255.6550,257.0100,3576048
|
||||
2025-09-10,259.6500,260.0800,254.5600,256.8800,5185420
|
||||
2025-09-09,256.1200,260.6600,254.8800,259.1100,4931105
|
||||
2025-09-08,248.6300,257.1500,247.0200,256.0900,6940270
|
||||
2025-09-05,248.2300,249.0300,245.4500,248.5300,3147478
|
||||
2025-09-04,245.4200,249.2800,242.8500,247.1800,4765087
|
||||
2025-09-03,240.0200,244.2500,239.4100,244.1000,3156289
|
||||
2025-09-02,240.9000,241.5500,238.2500,241.5000,3469501
|
||||
2025-08-29,245.2300,245.4599,241.7200,243.4900,2967558
|
||||
2025-08-28,245.4300,245.8800,243.3600,245.7300,2820817
|
||||
2025-08-27,242.8700,245.9600,242.0000,244.8400,3698372
|
||||
2025-08-26,241.0200,244.9800,240.3800,242.6300,5386582
|
||||
2025-08-25,242.5650,242.5650,239.4300,239.4300,3513327
|
||||
2025-08-22,240.7400,243.6800,240.2200,242.0900,3134882
|
||||
2025-08-21,242.2100,242.5000,238.6500,239.4000,2991902
|
||||
2025-08-20,242.1100,242.8800,240.3400,242.5500,3240064
|
||||
2025-08-19,240.0000,242.8300,239.4900,241.2800,3328305
|
||||
2025-08-18,239.5700,241.4200,239.1158,239.4500,3569594
|
||||
2025-08-15,237.6100,240.6200,236.7700,239.7200,4344322
|
||||
2025-08-14,238.2500,239.0000,235.6200,237.1100,4556725
|
||||
2025-08-13,236.2000,240.8411,236.2000,240.0700,5663562
|
||||
2025-08-12,236.5300,237.9600,233.3600,234.7700,8800597
|
||||
2025-08-11,242.2400,243.1500,234.7000,236.3000,9381960
|
||||
2025-08-08,248.8800,249.4800,241.6500,242.2700,6828390
|
||||
2025-08-07,252.8100,255.0000,248.8750,250.1600,6251285
|
||||
2025-08-06,251.5300,254.3200,249.2800,252.2800,3692105
|
||||
2025-08-05,252.0000,252.8000,248.9950,250.6700,5823016
|
||||
2025-08-04,251.0500,252.0800,248.1100,251.9800,5280588
|
||||
2025-08-01,251.4050,251.4791,245.6100,250.0500,9683404
|
||||
2025-07-31,259.5700,259.9900,252.2200,253.1500,6739092
|
||||
2025-07-30,261.6000,262.0000,258.9000,260.2600,3718290
|
||||
2025-07-29,264.3000,265.7999,261.0200,262.4100,4627265
|
||||
2025-07-28,260.3000,264.0000,259.6100,263.2100,5192516
|
||||
2025-07-25,260.0200,260.8000,256.3500,259.7200,7758653
|
||||
2025-07-24,261.2500,262.0486,252.7500,260.5100,22647720
|
||||
2025-07-23,284.3000,288.0800,281.4400,282.0100,8105906
|
||||
2025-07-22,284.7400,284.8800,281.2500,281.9600,4824219
|
||||
2025-07-21,286.2900,287.7300,284.3800,284.7100,3051791
|
||||
2025-07-18,283.3800,287.1600,282.2200,285.8700,4478165
|
||||
2025-07-17,281.5000,283.4566,280.9000,282.0000,3337168
|
||||
2025-07-16,282.7500,283.8700,279.8700,281.9200,2804831
|
||||
2025-07-15,283.7700,284.1550,280.7301,282.7000,2864106
|
||||
2025-07-14,282.8300,284.9250,281.7100,283.7900,2857401
|
||||
2025-07-11,285.0100,287.4300,282.9200,283.5900,3790679
|
||||
2025-07-10,288.9000,288.9000,282.2100,287.4300,3489068
|
||||
2025-07-09,291.3900,291.6000,288.6300,290.1400,2971309
|
||||
2025-07-08,293.1000,295.6100,289.4900,290.4200,2925329
|
||||
|
@@ -0,0 +1,283 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class GBMTests
|
||||
{
|
||||
[Fact]
|
||||
public void Next_DefaultParameter_GeneratesNewBar()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
var bar1 = gbm.Next();
|
||||
var bar2 = gbm.Next();
|
||||
|
||||
Assert.NotEqual(bar1.Time, bar2.Time);
|
||||
Assert.True(bar2.Time > bar1.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Next_IsNewTrue_AdvancesToNewBar()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
var bar1 = gbm.Next(isNew: true);
|
||||
var bar2 = gbm.Next(isNew: true);
|
||||
|
||||
Assert.NotEqual(bar1.Time, bar2.Time);
|
||||
Assert.True(bar2.Time > bar1.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Next_IsNewFalse_UpdatesCurrentBar()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
var bar1 = gbm.Next(isNew: true);
|
||||
long initialTime = bar1.Time;
|
||||
|
||||
var bar2 = gbm.Next(isNew: false);
|
||||
|
||||
Assert.Equal(initialTime, bar2.Time);
|
||||
// Price likely changed (GBM random walk)
|
||||
Assert.NotEqual(bar1.Close, bar2.Close);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Next_RefBool_HonorsRequest()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
// GBM always honors isNew - parameter should remain unchanged
|
||||
bool isNew1 = true;
|
||||
var bar1 = gbm.Next(ref isNew1);
|
||||
Assert.True(isNew1, "GBM should honor isNew=true request");
|
||||
|
||||
bool isNew2 = false;
|
||||
long time1 = bar1.Time;
|
||||
var bar2 = gbm.Next(ref isNew2);
|
||||
Assert.False(isNew2, "GBM should honor isNew=false request");
|
||||
Assert.Equal(time1, bar2.Time);
|
||||
|
||||
bool isNew3 = true;
|
||||
var bar3 = gbm.Next(ref isNew3);
|
||||
Assert.True(isNew3, "GBM should honor isNew=true request");
|
||||
Assert.NotEqual(time1, bar3.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_GeneratesCorrectCount()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
int count = 10;
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
|
||||
var series = gbm.Fetch(count, startTime, interval);
|
||||
|
||||
Assert.Equal(count, series.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_GeneratesSequentialBars()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
|
||||
var series = gbm.Fetch(5, startTime, interval);
|
||||
|
||||
// Verify time sequence
|
||||
for (int i = 1; i < series.Count; i++)
|
||||
{
|
||||
Assert.True(series[i].Time > series[i - 1].Time);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_RespectsInterval()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
var interval = TimeSpan.FromHours(1);
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
|
||||
var series = gbm.Fetch(5, startTime, interval);
|
||||
|
||||
// Verify interval spacing
|
||||
for (int i = 1; i < series.Count; i++)
|
||||
{
|
||||
long expectedDiff = interval.Ticks;
|
||||
long actualDiff = series[i].Time - series[i - 1].Time;
|
||||
Assert.Equal(expectedDiff, actualDiff);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_StartsAtSpecifiedTime()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
var startTime = new DateTime(2024, 1, 1, 9, 30, 0, DateTimeKind.Utc).Ticks;
|
||||
var interval = TimeSpan.FromMinutes(5);
|
||||
|
||||
var series = gbm.Fetch(3, startTime, interval);
|
||||
|
||||
Assert.Equal(startTime, series[0].Time);
|
||||
Assert.Equal(startTime + interval.Ticks, series[1].Time);
|
||||
Assert.Equal(startTime + 2 * interval.Ticks, series[2].Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_WithDifferentIntervals_WorksCorrectly()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
|
||||
// Test different intervals
|
||||
var intervals = new[] {
|
||||
TimeSpan.FromMinutes(1),
|
||||
TimeSpan.FromMinutes(5),
|
||||
TimeSpan.FromHours(1)
|
||||
};
|
||||
|
||||
foreach (var interval in intervals)
|
||||
{
|
||||
var series = gbm.Fetch(3, startTime, interval);
|
||||
|
||||
// Verify spacing
|
||||
for (int i = 1; i < series.Count; i++)
|
||||
{
|
||||
long expectedDiff = interval.Ticks;
|
||||
long actualDiff = series[i].Time - series[i - 1].Time;
|
||||
Assert.Equal(expectedDiff, actualDiff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GeneratesRealisticOHLCV()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
var series = gbm.Fetch(10, startTime, interval);
|
||||
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
var bar = series[i];
|
||||
|
||||
// High should be >= max(Open, Close)
|
||||
Assert.True(bar.High >= Math.Max(bar.Open, bar.Close));
|
||||
|
||||
// Low should be <= min(Open, Close)
|
||||
Assert.True(bar.Low <= Math.Min(bar.Open, bar.Close));
|
||||
|
||||
// Volume should be positive
|
||||
Assert.True(bar.Volume > 0);
|
||||
|
||||
// All prices should be positive
|
||||
Assert.True(bar.Open > 0);
|
||||
Assert.True(bar.High > 0);
|
||||
Assert.True(bar.Low > 0);
|
||||
Assert.True(bar.Close > 0);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IntraBarUpdates_ModifyCurrentBar()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
var bar1 = gbm.Next(isNew: true);
|
||||
long initialTime = bar1.Time;
|
||||
double initialClose = bar1.Close;
|
||||
|
||||
// Loop until price changes (random walk might stay same but unlikely)
|
||||
bool changed = false;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: false);
|
||||
Assert.Equal(initialTime, bar.Time);
|
||||
if (Math.Abs(bar.Close - initialClose) > double.Epsilon)
|
||||
{
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(changed, "Price should change during intra-bar updates");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MixedStreamingAndBatch_WorksCorrectly()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
// Start with streaming
|
||||
var bar1 = gbm.Next();
|
||||
var bar2 = gbm.Next();
|
||||
|
||||
// Batch generation with explicit time
|
||||
long startTime = bar2.Time + TimeSpan.FromMinutes(1).Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
var series = gbm.Fetch(3, startTime, interval);
|
||||
|
||||
Assert.True(series[0].Time > bar2.Time);
|
||||
Assert.Equal(3, series.Count);
|
||||
|
||||
// Continue streaming after batch (uses internal state)
|
||||
var bar3 = gbm.Next();
|
||||
Assert.True(bar3.Time > series[2].Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DriftAndVolatility_AffectPriceMovement()
|
||||
{
|
||||
// High volatility should produce more price variation
|
||||
var gbmLowVol = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.01);
|
||||
var gbmHighVol = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.5);
|
||||
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
var seriesLow = gbmLowVol.Fetch(100, startTime, interval);
|
||||
var seriesHigh = gbmHighVol.Fetch(100, startTime, interval);
|
||||
|
||||
// Calculate price ranges
|
||||
double rangeLow = seriesLow[99].Close - seriesLow[0].Open;
|
||||
double rangeHigh = seriesHigh[99].Close - seriesHigh[0].Open;
|
||||
|
||||
// High volatility should generally produce larger absolute movements
|
||||
Assert.True(Math.Abs(rangeHigh) > Math.Abs(rangeLow) * 0.5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConsecutiveCalls_MaintainContinuity()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
var bar1 = gbm.Next();
|
||||
var bar2 = gbm.Next();
|
||||
|
||||
// bar2.Open should equal bar1.Close (continuity)
|
||||
Assert.Equal(bar1.Close, bar2.Open);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stateless_NoHistoryStorage()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
// Generate multiple bars
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
gbm.Next();
|
||||
}
|
||||
|
||||
// GBM should not expose any history storage
|
||||
var type = gbm.GetType();
|
||||
var barsProperty = type.GetProperty("Bars");
|
||||
|
||||
Assert.Null(barsProperty);
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="**\*.cs" Exclude="obj\**\*.cs" />
|
||||
<Compile Include="**\*.cs" Exclude="**\*.Tests.cs;**\*.Benchmarks.cs;**\*.benchmark.cs;obj\**\*.cs" />
|
||||
<PackageReference Include="System.Text.Encodings.Web" Version="10.0.0-rc.1.25451.107" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
#!meta
|
||||
|
||||
{"kernelInfo":{"defaultKernelName":"csharp","items":[{"name":"csharp"},{"name":"fsharp","languageName":"F#","aliases":["f#","fs"]},{"name":"html","languageName":"HTML"},{"name":"http","languageName":"HTTP"},{"name":"javascript","languageName":"JavaScript","aliases":["js"]},{"name":"mermaid","languageName":"Mermaid"},{"name":"pwsh","languageName":"PowerShell","aliases":["powershell"]},{"name":"value"}]}}
|
||||
|
||||
#!markdown
|
||||
|
||||
# Exponential Moving Average (EMA) Examples
|
||||
|
||||
This is a **.NET Interactive** notebook. To run it, you need the [Polyglot Notebooks](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.dotnet-interactive-vscode) extension installed in VS Code.
|
||||
|
||||
For detailed documentation on the EMA indicator, including mathematical formulas and interpretation, please refer to [Ema.md](Ema.md).
|
||||
|
||||
The **Exponential Moving Average (EMA)** is a weighted moving average that gives more importance to recent price data. Unlike the Simple Moving Average (SMA), which assigns equal weight to all data points, the EMA reacts more significantly to recent price changes.
|
||||
|
||||
This notebook demonstrates:
|
||||
1. **Manual Data Processing**: Understanding Batch vs. Streaming modes.
|
||||
2. **Streaming with `isNew`**: Handling intra-bar updates.
|
||||
3. **Large Dataset Processing**: Using Geometric Brownian Motion (GBM) generated data.
|
||||
4. **Vectorized Operations**: Calculating multiple EMAs simultaneously.
|
||||
|
||||
#!csharp
|
||||
|
||||
// Reference the library
|
||||
#r "..\..\bin\QuanTAlib.dll"
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using QuanTAlib;
|
||||
|
||||
// Helper to print TSeries
|
||||
void PrintSeries(TSeries series, int count = 5)
|
||||
{
|
||||
Console.WriteLine($"Series Length: {series.Count}");
|
||||
foreach (var item in series.Take(count))
|
||||
{
|
||||
Console.WriteLine($"Time: {item.Time:HH:mm:ss}, Value: {item.Value:F2}");
|
||||
}
|
||||
if (series.Count > count) Console.WriteLine("...");
|
||||
}
|
||||
|
||||
#!markdown
|
||||
|
||||
## 1. Manual Data: Batch vs. Streaming
|
||||
|
||||
We'll start with a small, manually created dataset to clearly see how Batch and Streaming operations work.
|
||||
|
||||
### Batch Processing
|
||||
Batch processing calculates the EMA for the entire dataset at once. This is efficient for historical analysis.
|
||||
|
||||
#!csharp
|
||||
|
||||
// Create a small manual dataset
|
||||
var manualData = new TSeries();
|
||||
manualData.Add(DateTime.Now, 100.0);
|
||||
manualData.Add(DateTime.Now.AddMinutes(1), 102.0);
|
||||
manualData.Add(DateTime.Now.AddMinutes(2), 101.0);
|
||||
manualData.Add(DateTime.Now.AddMinutes(3), 103.0);
|
||||
manualData.Add(DateTime.Now.AddMinutes(4), 105.0);
|
||||
|
||||
Console.WriteLine("--- Input Data ---");
|
||||
PrintSeries(manualData, 5);
|
||||
|
||||
// Batch Calculation
|
||||
Console.WriteLine("\n--- Batch EMA (Period 3) ---");
|
||||
var emaBatch = new Ema(3);
|
||||
var resultBatch = emaBatch.Update(manualData);
|
||||
|
||||
PrintSeries(resultBatch, 5);
|
||||
|
||||
#!markdown
|
||||
|
||||
### Streaming Processing
|
||||
Streaming processing updates the EMA one data point at a time. This is essential for real-time trading systems where data arrives sequentially.
|
||||
|
||||
#!csharp
|
||||
|
||||
Console.WriteLine("\n--- Streaming EMA (Period 3) ---");
|
||||
var emaStream = new Ema(3);
|
||||
|
||||
foreach (var item in manualData)
|
||||
{
|
||||
var result = emaStream.Update(item);
|
||||
Console.WriteLine($"Time: {item.Time:HH:mm:ss}, Input: {item.Value:F2}, EMA: {result.Value:F2}, IsHot: {emaStream.IsHot}");
|
||||
}
|
||||
|
||||
// Verify that the last values match
|
||||
var batchLast = resultBatch.Last().Value;
|
||||
var streamLast = emaStream.Value.Value;
|
||||
Console.WriteLine($"\nMatch: {Math.Abs(batchLast - streamLast) < 1e-10} (Batch: {batchLast:F2}, Stream: {streamLast:F2})");
|
||||
|
||||
#!markdown
|
||||
|
||||
## 2. Streaming with `isNew` (Intra-bar Updates)
|
||||
|
||||
In real-time feeds, you often receive multiple updates for the *same* bar (e.g., price changes within the current minute) before the bar closes.
|
||||
* `isNew = true`: The input is a new bar (advances time).
|
||||
* `isNew = false`: The input is an update to the current bar (recalculates without advancing).
|
||||
|
||||
#!csharp
|
||||
|
||||
Console.WriteLine("\n--- Streaming with Intra-bar Updates ---");
|
||||
var emaIntra = new Ema(3);
|
||||
|
||||
// 1. Process the first 4 bars normally
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
emaIntra.Update(manualData[i]);
|
||||
}
|
||||
Console.WriteLine($"After 4th bar: {emaIntra.Value.Value:F2}");
|
||||
|
||||
// 2. Simulate intra-bar updates for the 5th bar (Final value is 105.0)
|
||||
// Update 1: Price moves to 104.0
|
||||
var update1 = new TValue(manualData[4].Time, 104.0);
|
||||
emaIntra.Update(update1, isNew: true); // First update for this bar is "New"
|
||||
Console.WriteLine($"Update 1 (104.0): {emaIntra.Value.Value:F2}");
|
||||
|
||||
// Update 2: Price moves to 106.0 (Same time, same bar)
|
||||
var update2 = new TValue(manualData[4].Time, 106.0);
|
||||
emaIntra.Update(update2, isNew: false); // Not new, just an update
|
||||
Console.WriteLine($"Update 2 (106.0): {emaIntra.Value.Value:F2}");
|
||||
|
||||
// Update 3: Final Close at 105.0
|
||||
var update3 = manualData[4];
|
||||
emaIntra.Update(update3, isNew: false); // Final update
|
||||
Console.WriteLine($"Update 3 (105.0): {emaIntra.Value.Value:F2}");
|
||||
|
||||
// Verify match with batch result
|
||||
Console.WriteLine($"Match with Batch: {Math.Abs(emaIntra.Value.Value - batchLast) < 1e-10}");
|
||||
|
||||
#!markdown
|
||||
|
||||
## 3. Large Dataset: Geometric Brownian Motion (GBM)
|
||||
|
||||
We'll generate a larger dataset (1000 bars) using a Geometric Brownian Motion generator to simulate realistic market data.
|
||||
|
||||
#!csharp
|
||||
|
||||
// Generate 1000 bars of data
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2);
|
||||
var gbmData = gbm.Fetch(1000, DateTime.Now.Ticks, TimeSpan.FromMinutes(1));
|
||||
var closeSeries = gbmData.Close;
|
||||
|
||||
Console.WriteLine($"Generated {closeSeries.Count} bars of GBM data.");
|
||||
Console.WriteLine($"First 5 values: {string.Join(", ", closeSeries.Take(5).Select(x => x.Value.ToString("F2")))}");
|
||||
|
||||
#!markdown
|
||||
|
||||
### Batch vs. Streaming Performance on Large Data
|
||||
|
||||
#!csharp
|
||||
|
||||
// Batch
|
||||
var emaLargeBatch = new Ema(20);
|
||||
var batchLargeResult = emaLargeBatch.Update(closeSeries);
|
||||
Console.WriteLine($"Batch Last Value: {batchLargeResult.Last().Value:F2}");
|
||||
|
||||
// Streaming
|
||||
var emaLargeStream = new Ema(20);
|
||||
TValue lastStreamVal = default;
|
||||
foreach(var item in closeSeries)
|
||||
{
|
||||
lastStreamVal = emaLargeStream.Update(item);
|
||||
}
|
||||
Console.WriteLine($"Streaming Last Value: {lastStreamVal.Value:F2}");
|
||||
|
||||
#!markdown
|
||||
|
||||
## 4. Vectorized EMA (Multiple Periods)
|
||||
|
||||
`EmaVector` allows calculating multiple EMAs (e.g., 9, 12, 26) simultaneously. This is optimized for performance using SIMD where available.
|
||||
|
||||
### Vectorized Batch
|
||||
|
||||
#!csharp
|
||||
|
||||
int[] periods = { 9, 12, 26 };
|
||||
Console.WriteLine($"\n--- Vectorized Batch EMA (Periods: {string.Join(", ", periods)}) ---");
|
||||
|
||||
var emaVectorBatch = new EmaVector(periods);
|
||||
var vectorBatchResults = emaVectorBatch.Calculate(closeSeries);
|
||||
|
||||
for (int i = 0; i < periods.Length; i++)
|
||||
{
|
||||
Console.WriteLine($"EMA({periods[i]}) Last Value: {vectorBatchResults[i].Last().Value:F2}");
|
||||
}
|
||||
|
||||
#!markdown
|
||||
|
||||
### Vectorized Streaming
|
||||
|
||||
#!csharp
|
||||
|
||||
Console.WriteLine($"\n--- Vectorized Streaming EMA (Periods: {string.Join(", ", periods)}) ---");
|
||||
|
||||
var emaVectorStream = new EmaVector(periods);
|
||||
TValue[] lastVectorVal = null;
|
||||
|
||||
foreach(var item in closeSeries)
|
||||
{
|
||||
lastVectorVal = emaVectorStream.Update(item);
|
||||
}
|
||||
|
||||
for (int i = 0; i < periods.Length; i++)
|
||||
{
|
||||
Console.WriteLine($"EMA({periods[i]}) Last Value: {lastVectorVal[i].Value:F2}");
|
||||
}
|
||||
|
||||
// Verification
|
||||
bool allMatch = true;
|
||||
for (int i = 0; i < periods.Length; i++)
|
||||
{
|
||||
if (Math.Abs(vectorBatchResults[i].Last().Value - lastVectorVal[i].Value) > 1e-10)
|
||||
{
|
||||
allMatch = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Console.WriteLine($"\nAll Vectorized Stream/Batch values match: {allMatch}");
|
||||
@@ -0,0 +1,228 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class EmaTests
|
||||
{
|
||||
[Fact]
|
||||
public void Ema_Constructor_Period_ValidatesInput()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Ema(0));
|
||||
Assert.Throws<ArgumentException>(() => new Ema(-1));
|
||||
|
||||
var ema = new Ema(10);
|
||||
Assert.NotNull(ema);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_Constructor_Alpha_ValidatesInput()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Ema(0.0));
|
||||
Assert.Throws<ArgumentException>(() => new Ema(-0.1));
|
||||
Assert.Throws<ArgumentException>(() => new Ema(1.1));
|
||||
|
||||
var ema = new Ema(0.5);
|
||||
Assert.NotNull(ema);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_Calc_ReturnsValue()
|
||||
{
|
||||
var ema = new Ema(10);
|
||||
|
||||
Assert.Equal(0, ema.Value.Value);
|
||||
|
||||
TValue result = ema.Update(new TValue(DateTime.Now, 100));
|
||||
|
||||
Assert.True(result.Value > 0);
|
||||
Assert.Equal(result.Value, ema.Value.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_Calc_IsNew_AcceptsParameter()
|
||||
{
|
||||
var ema = new Ema(10);
|
||||
|
||||
ema.Update(new TValue(DateTime.Now, 100), isNew: true);
|
||||
double value1 = ema.Value;
|
||||
|
||||
ema.Update(new TValue(DateTime.Now, 105), isNew: true);
|
||||
double value2 = ema.Value;
|
||||
|
||||
// Values should change with new bars
|
||||
Assert.NotEqual(value1, value2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_Calc_IsNew_False_UpdatesValue()
|
||||
{
|
||||
var ema = new Ema(10);
|
||||
|
||||
ema.Update(new TValue(DateTime.Now, 100));
|
||||
ema.Update(new TValue(DateTime.Now, 110), isNew: true);
|
||||
double beforeUpdate = ema.Value;
|
||||
|
||||
ema.Update(new TValue(DateTime.Now, 120), isNew: false);
|
||||
double afterUpdate = ema.Value;
|
||||
|
||||
// Update should change the value
|
||||
Assert.NotEqual(beforeUpdate, afterUpdate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_Reset_ClearsState()
|
||||
{
|
||||
var ema = new Ema(10);
|
||||
|
||||
ema.Update(new TValue(DateTime.Now, 100));
|
||||
ema.Update(new TValue(DateTime.Now, 105));
|
||||
double valueBefore = ema.Value;
|
||||
|
||||
ema.Reset();
|
||||
|
||||
Assert.Equal(0, ema.Value.Value);
|
||||
|
||||
// After reset, should accept new values
|
||||
ema.Update(new TValue(DateTime.Now, 50));
|
||||
Assert.NotEqual(0, ema.Value.Value);
|
||||
Assert.NotEqual(valueBefore, ema.Value.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_Properties_Accessible()
|
||||
{
|
||||
var ema = new Ema(10);
|
||||
|
||||
Assert.Equal(0, ema.Value.Value);
|
||||
Assert.False(ema.IsHot);
|
||||
|
||||
ema.Update(new TValue(DateTime.Now, 100));
|
||||
|
||||
Assert.NotEqual(0, ema.Value.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_IsHot_BecomesTrueAfterWarmup()
|
||||
{
|
||||
var ema = new Ema(10);
|
||||
|
||||
// Initially IsHot should be false
|
||||
Assert.False(ema.IsHot);
|
||||
|
||||
// Feed values until it warms up
|
||||
// Warmup condition is state.E <= 1e-10
|
||||
// state.E starts at 1.0 and decays by (1 - alpha) each step
|
||||
// alpha = 2 / (10 + 1) = 2/11 ~= 0.1818
|
||||
// (1 - alpha) ~= 0.8181
|
||||
// 1.0 * (0.8181)^n <= 1e-10
|
||||
// n * log(0.8181) <= log(1e-10)
|
||||
// n * -0.200 <= -23.02
|
||||
// n >= 115 steps roughly
|
||||
|
||||
int steps = 0;
|
||||
while (!ema.IsHot && steps < 1000)
|
||||
{
|
||||
ema.Update(new TValue(DateTime.Now, 100));
|
||||
steps++;
|
||||
}
|
||||
|
||||
Assert.True(ema.IsHot);
|
||||
Assert.True(steps > 0); // Should take some steps
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_PeriodEquivalence_BothConstructorsWork()
|
||||
{
|
||||
int period = 20;
|
||||
double alpha = 2.0 / (period + 1);
|
||||
|
||||
var emaPeriod = new Ema(period);
|
||||
var emaAlpha = new Ema(alpha);
|
||||
|
||||
// Both should accept Calc calls and produce same result
|
||||
TValue result1 = emaPeriod.Update(new TValue(DateTime.Now, 100));
|
||||
TValue result2 = emaAlpha.Update(new TValue(DateTime.Now, 100));
|
||||
|
||||
Assert.Equal(result1.Value, result2.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var ema = new Ema(10);
|
||||
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);
|
||||
ema.Update(tenthInput, isNew: true);
|
||||
}
|
||||
|
||||
// Remember EMA state after 10 values
|
||||
double emaAfterTen = ema.Value;
|
||||
|
||||
// Generate 9 corrections with isNew=false (different values)
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: false);
|
||||
ema.Update(new TValue(bar.Time, bar.Close), isNew: false);
|
||||
}
|
||||
|
||||
// Feed the remembered 10th input again with isNew=false
|
||||
TValue finalEma = ema.Update(tenthInput, isNew: false);
|
||||
|
||||
// EMA should match the original state after 10 values
|
||||
Assert.Equal(emaAfterTen, finalEma.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_BatchCalc_MatchesIterativeCalc()
|
||||
{
|
||||
var emaIterative = new Ema(10);
|
||||
var emaBatch = new Ema(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);
|
||||
}
|
||||
|
||||
// Calculate iteratively
|
||||
var iterativeResults = new TSeries();
|
||||
foreach (var item in series)
|
||||
{
|
||||
iterativeResults.Add(emaIterative.Update(item));
|
||||
}
|
||||
|
||||
// Calculate batch
|
||||
var batchResults = emaBatch.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 Ema_Result_ImplicitConversionToDouble()
|
||||
{
|
||||
var ema = new Ema(10);
|
||||
ema.Update(new TValue(DateTime.Now, 100));
|
||||
|
||||
// This should compile and work because TValue has implicit conversion to double
|
||||
double result = ema.Value;
|
||||
|
||||
Assert.Equal(100.0, result, 1e-10);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Skender.Stock.Indicators;
|
||||
using TALib;
|
||||
using Tulip;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class EmaValidationTests : IDisposable
|
||||
{
|
||||
private readonly TBarSeries _bars;
|
||||
private readonly TSeries _data;
|
||||
private readonly List<Quote> _skenderQuotes;
|
||||
private readonly Random _rnd = new(42);
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public EmaValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
|
||||
// 1. Generate 1000 records using GBM feed
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2);
|
||||
_bars = gbm.Fetch(1000, 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]),
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Cleanup if needed
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib EMA
|
||||
var ema = new global::QuanTAlib.Ema(period);
|
||||
var qResult = ema.Update(_data);
|
||||
|
||||
// Calculate Skender EMA
|
||||
var sResult = _skenderQuotes.GetEma(period).ToList();
|
||||
|
||||
// Compare last 100 records
|
||||
VerifyData(qResult, sResult, period);
|
||||
}
|
||||
_output.WriteLine("EMA validated successfully against Skender");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Talib()
|
||||
{
|
||||
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 EMA
|
||||
var ema = new global::QuanTAlib.Ema(period);
|
||||
var qResult = ema.Update(_data);
|
||||
|
||||
// Calculate TA-Lib EMA
|
||||
var retCode = TALib.Functions.Ema(tData, 0..^0, output, out var outRange, period);
|
||||
|
||||
// Check success
|
||||
Assert.Equal(Core.RetCode.Success, retCode);
|
||||
|
||||
// TA-Lib skips the lookback period, so output[0] corresponds to input[lookback]
|
||||
int lookback = TALib.Functions.EmaLookback(period);
|
||||
|
||||
// Compare last 100 records
|
||||
VerifyData_Talib(qResult, output, outRange, lookback, period);
|
||||
}
|
||||
_output.WriteLine("EMA validated successfully against TA-Lib");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Tulip()
|
||||
{
|
||||
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 EMA
|
||||
var ema = new global::QuanTAlib.Ema(period);
|
||||
var qResult = ema.Update(_data);
|
||||
|
||||
// Calculate Tulip EMA
|
||||
var emaIndicator = Tulip.Indicators.ema;
|
||||
double[][] inputs = { tData };
|
||||
double[] options = { (double)period };
|
||||
double[][] outputs = { new double[tData.Length] };
|
||||
|
||||
emaIndicator.Run(inputs, options, outputs);
|
||||
var tResult = outputs[0];
|
||||
|
||||
// Compare last 100 records
|
||||
VerifyData(qResult, tResult.ToList(), period);
|
||||
}
|
||||
_output.WriteLine("EMA validated successfully against Tulip");
|
||||
}
|
||||
|
||||
private void VerifyData(TSeries qSeries, List<double> tSeries, int period)
|
||||
{
|
||||
// Ensure we have enough data
|
||||
Assert.Equal(qSeries.Count, tSeries.Count);
|
||||
|
||||
int count = qSeries.Count;
|
||||
int skip = count - 100; // Last 100 records
|
||||
|
||||
for (int i = skip; i < count; i++)
|
||||
{
|
||||
double qValue = qSeries[i].Value;
|
||||
double tValue = tSeries[i];
|
||||
if (tValue == 0) continue;
|
||||
|
||||
Assert.Equal(tValue, qValue, 1e-6);
|
||||
}
|
||||
}
|
||||
|
||||
private void VerifyData(TSeries qSeries, List<EmaResult> sSeries, int period)
|
||||
{
|
||||
// Ensure we have enough data
|
||||
Assert.Equal(qSeries.Count, sSeries.Count);
|
||||
|
||||
int count = qSeries.Count;
|
||||
int skip = count - 100; // Last 100 records
|
||||
|
||||
for (int i = skip; i < count; i++)
|
||||
{
|
||||
double qValue = qSeries[i].Value;
|
||||
double? sValue = sSeries[i].Ema;
|
||||
|
||||
// Skip if Skender returns null (warmup period)
|
||||
if (!sValue.HasValue) continue;
|
||||
|
||||
// Assert equality with tolerance
|
||||
Assert.Equal(sValue.Value, qValue, 1e-6);
|
||||
}
|
||||
}
|
||||
|
||||
private void VerifyData_Talib(TSeries qSeries, double[] tOutput, Range outRange, int lookback, int period)
|
||||
{
|
||||
int count = qSeries.Count;
|
||||
int skip = count - 100; // Last 100 records
|
||||
|
||||
// outRange.End.Value is the number of elements written to tOutput
|
||||
int validCount = outRange.End.Value - outRange.Start.Value;
|
||||
|
||||
for (int i = skip; i < count; i++)
|
||||
{
|
||||
double qValue = qSeries[i].Value;
|
||||
|
||||
// Calculate index in tOutput
|
||||
// If i < lookback, we don't have a value from TA-Lib
|
||||
if (i < lookback) continue;
|
||||
|
||||
int tIndex = i - lookback;
|
||||
|
||||
// Check if tIndex is within valid range
|
||||
if (tIndex >= validCount) continue;
|
||||
|
||||
double tValue = tOutput[tIndex];
|
||||
|
||||
// Assert equality with tolerance
|
||||
Assert.Equal(tValue, qValue, 1e-6);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public struct EmaState
|
||||
{
|
||||
public double Ema;
|
||||
public double E;
|
||||
public bool IsHot;
|
||||
|
||||
public static EmaState New() => new() { Ema = 0, E = 1.0, IsHot = false };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exponential Moving Average (EMA) - IIR filter with exponential warmup compensator.
|
||||
/// Provides valid output from first bar with O(1) complexity.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Algorithm uses exponential smoothing with compensator for immediate valid results.
|
||||
/// Reference: https://github.com/mihakralj/pinescript/blob/main/indicators/trends_IIR/ema.md
|
||||
/// </remarks>
|
||||
public class Ema
|
||||
{
|
||||
private readonly double _alpha;
|
||||
private EmaState _state = EmaState.New();
|
||||
private EmaState _p_state = EmaState.New();
|
||||
|
||||
/// <summary>
|
||||
/// Creates EMA with specified period.
|
||||
/// Alpha = 2 / (period + 1)
|
||||
/// </summary>
|
||||
/// <param name="period">Period for EMA calculation (must be > 0)</param>
|
||||
public Ema(int period)
|
||||
{
|
||||
if (period <= 0)
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
|
||||
_alpha = 2.0 / (period + 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates EMA with specified alpha smoothing factor.
|
||||
/// </summary>
|
||||
/// <param name="alpha">Smoothing factor (0 < alpha <= 1)</param>
|
||||
public Ema(double alpha)
|
||||
{
|
||||
if (alpha <= 0 || alpha > 1)
|
||||
throw new ArgumentException("Alpha must be between 0 and 1", nameof(alpha));
|
||||
|
||||
_alpha = alpha;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Current EMA value.
|
||||
/// </summary>
|
||||
public TValue Value { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the EMA has warmed up and is providing valid results.
|
||||
/// </summary>
|
||||
public bool IsHot => _state.IsHot;
|
||||
|
||||
/// <summary>
|
||||
/// Core EMA calculation kernel.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double Compute(double input, double alpha, ref EmaState state)
|
||||
{
|
||||
state.Ema += alpha * (input - state.Ema);
|
||||
|
||||
if (!state.IsHot)
|
||||
{
|
||||
state.E *= (1.0 - alpha);
|
||||
state.IsHot = state.E <= 1e-10;
|
||||
return state.Ema / (1.0 - state.E);
|
||||
}
|
||||
|
||||
return state.Ema;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates EMA with the given value.
|
||||
/// </summary>
|
||||
/// <param name="input">Input value</param>
|
||||
/// <param name="isNew">True for new bar, false for update to current bar (default: true)</param>
|
||||
/// <returns>Compensated EMA value</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_p_state = _state;
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = _p_state;
|
||||
}
|
||||
|
||||
double val = Compute(input.Value, _alpha, ref _state);
|
||||
Value = new TValue(input.Time, val);
|
||||
return Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates EMA with the entire series.
|
||||
/// </summary>
|
||||
/// <param name="source">Input series</param>
|
||||
/// <returns>EMA series</returns>
|
||||
public TSeries Update(TSeries source)
|
||||
{
|
||||
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);
|
||||
var sourceValues = source.Values;
|
||||
var sourceTimes = source.Times;
|
||||
|
||||
// Local state for batch processing
|
||||
EmaState state = _state;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double val = Compute(sourceValues[i], _alpha, ref state);
|
||||
tSpan[i] = sourceTimes[i];
|
||||
vSpan[i] = val;
|
||||
}
|
||||
|
||||
// Update instance state to the final state
|
||||
_state = state;
|
||||
_p_state = state; // Assume last point is committed
|
||||
|
||||
Value = new TValue(tSpan[len - 1], vSpan[len - 1]);
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates EMA for the entire series using a new instance.
|
||||
/// </summary>
|
||||
/// <param name="source">Input series</param>
|
||||
/// <param name="period">EMA period</param>
|
||||
/// <returns>EMA series</returns>
|
||||
public static TSeries Calculate(TSeries source, int period)
|
||||
{
|
||||
var ema = new Ema(period);
|
||||
return ema.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the EMA state.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
_state = EmaState.New();
|
||||
_p_state = _state;
|
||||
Value = default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
# EMA: Exponential Moving Average
|
||||
|
||||
[Pine Script Implementation of EMA](https://github.com/mihakralj/pinescript/blob/main/indicators/trends_IIR/ema.pine)
|
||||
|
||||
## Overview and Purpose
|
||||
|
||||
The Exponential Moving Average (EMA) is a fundamental technical indicator that calculates the average price over a specific period while giving more weight to recent price data. Introduced in the 1950s, EMA has become one of the most widely used technical indicators in financial markets due to its balance of responsiveness and stability.
|
||||
|
||||
Unlike the Simple Moving Average (SMA) which assigns equal weight to all data points, the EMA emphasizes recent price action, allowing traders to identify trend changes earlier while still filtering out short-term market noise. Its mathematical elegance has made it a standard tool in signal processing beyond finance, including communications, control systems, and data analysis.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
* **Weighted price action:** EMA gives greater importance to recent prices through exponential weighting, providing a more timely response to current market conditions
|
||||
* **Smoothing mechanism:** Acts as a noise filter by reducing the impact of random price fluctuations while preserving meaningful trends
|
||||
* **Universal application:** Functions effectively across all timeframes from intraday to monthly charts, with parameter adjustments
|
||||
* **Foundation indicator:** Serves as the mathematical basis for numerous other technical indicators (MACD, PPO, etc.)
|
||||
|
||||
EMA achieves its enhanced responsiveness by applying a smoothing factor (α) that determines how quickly older data points lose influence. This approach creates a moving average that reacts faster to price changes than an SMA of the same length while maintaining enough stability to identify the underlying trend.
|
||||
|
||||
## Common Settings and Parameters
|
||||
|
||||
| Parameter | Default | Function | When to Adjust |
|
||||
|-----------|---------|----------|---------------|
|
||||
| Length | 20 | Controls responsiveness/smoothness | Shorter for faster signals in active markets, longer for stable trends in ranging markets |
|
||||
| Source | Close | Data point used for calculation | Change to HL2 or HLC3 for more balanced price representation |
|
||||
| Alpha | 2/(length+1) | Determines weighting decay | Direct alpha manipulation allows for precise tuning beyond standard length settings |
|
||||
|
||||
**Pro Tip:** Many professional traders use multiple EMAs simultaneously (e.g., 8, 21, 50) to identify potential support/resistance levels and trend strength based on their relative positioning.
|
||||
|
||||
## Calculation and Mathematical Foundation
|
||||
|
||||
**Simplified explanation:**
|
||||
EMA works by calculating a weighted average where recent prices have more influence. The implementation uses an optimized form of the EMA calculation that is both computationally efficient and numerically stable.
|
||||
|
||||
**Technical formula:**
|
||||
The optimized EMA formula used in the implementation is:
|
||||
$$EMA_t = \alpha \cdot P_t + (1 - \alpha) \cdot EMA_{t-1}$$
|
||||
|
||||
Where:
|
||||
|
||||
* $\alpha = \frac{2}{N + 1}$ is the smoothing factor ($N$ is the period)
|
||||
* $P_t$ is the current price value
|
||||
* $EMA_{t-1}$ is the previous period's EMA value
|
||||
|
||||
This form is algebraically equivalent to the traditional EMA formula but offers better computational efficiency and numerical stability.
|
||||
|
||||
> 🔍 **Technical Note:** The implementation uses a sophisticated warm-up compensation method that provides accurate EMA values from the first bar. The compensation works by tracking an error term that decays exponentially:
|
||||
> $$e_t = e_{t-1} \cdot (1 - \alpha)$$
|
||||
> $$Compensation = \frac{1}{1 - e_t}$$
|
||||
> $$EMA_{corrected} = Compensation \cdot EMA_{raw}$$
|
||||
> This compensation automatically adjusts during the warm-up phase and becomes negligible ($e \le 1e^{-10}$) once sufficient data has been processed, ensuring mathematically correct values throughout the entire data series without requiring a traditional warm-up period.
|
||||
|
||||
## C# Implementation
|
||||
|
||||
The library provides two implementations: a standard scalar version and a SIMD-optimized vector version for high-performance scenarios.
|
||||
|
||||
### Single EMA (`Ema`)
|
||||
|
||||
The `Ema` class calculates a single exponential moving average.
|
||||
|
||||
```csharp
|
||||
using QuanTAlib;
|
||||
|
||||
// Initialize with period 10
|
||||
var ema = new Ema(10);
|
||||
|
||||
// Or initialize with specific alpha
|
||||
var emaAlpha = new Ema(0.5);
|
||||
|
||||
// Streaming update
|
||||
TValue result = ema.Update(new TValue(time, price));
|
||||
Console.WriteLine($"Current EMA: {result.Value}");
|
||||
|
||||
// Access current value property
|
||||
Console.WriteLine($"Current Value: {ema.Value.Value}");
|
||||
|
||||
// Batch calculation
|
||||
TSeries source = ...;
|
||||
TSeries results = Ema.Calculate(source, 10);
|
||||
```
|
||||
|
||||
### Multi-Alpha EMA (`EmaVector`)
|
||||
|
||||
The `EmaVector` class is a SIMD-optimized implementation for calculating multiple EMAs with different periods on the same input series simultaneously. It leverages hardware intrinsics (AVX/SSE) for high performance.
|
||||
|
||||
```csharp
|
||||
using QuanTAlib;
|
||||
|
||||
// Initialize with multiple periods
|
||||
int[] periods = { 9, 12, 26 };
|
||||
var emaVector = new EmaVector(periods);
|
||||
|
||||
// Streaming update
|
||||
TValue[] results = emaVector.Update(new TValue(time, price));
|
||||
|
||||
// Access values
|
||||
Console.WriteLine($"EMA(9): {results[0].Value}");
|
||||
Console.WriteLine($"EMA(12): {results[1].Value}");
|
||||
Console.WriteLine($"EMA(26): {results[2].Value}");
|
||||
|
||||
// Batch calculation
|
||||
TSeries source = ...;
|
||||
TSeries[] seriesResults = emaVector.Calculate(source);
|
||||
```
|
||||
|
||||
### Performance Characteristics
|
||||
|
||||
* **O(1) Complexity:** The calculation time is constant regardless of the period length.
|
||||
* **SIMD Optimization:** `EmaVector` processes multiple periods in parallel using vector instructions, significantly reducing CPU cycles for multi-timeframe analysis.
|
||||
* **Zero Allocation:** The streaming `Update` method is designed to be allocation-free (excluding the return struct).
|
||||
|
||||
## Interpretation Details
|
||||
|
||||
The EMA's primary value comes from its ability to identify trend direction and potential reversal points:
|
||||
|
||||
* When price is above EMA, the short-term trend is generally bullish
|
||||
* When price is below EMA, the short-term trend is generally bearish
|
||||
* When a shorter-period EMA crosses above a longer-period EMA, it often signals the beginning of an uptrend
|
||||
* When a shorter-period EMA crosses below a longer-period EMA, it often signals the beginning of a downtrend
|
||||
* The slope of the EMA indicates trend strength and momentum
|
||||
|
||||
EMAs work particularly well in trending markets but may generate false signals during sideways or choppy conditions. For optimal results, traders typically use EMA crossovers or EMA-price crossovers as part of a broader system that includes volume and momentum confirmation.
|
||||
|
||||
## Limitations and Considerations
|
||||
|
||||
* **Market conditions:** Less effective in choppy, sideways markets where price constantly crosses the average
|
||||
* **Lag factor:** While less significant than SMA, EMA still exhibits some lag, especially with longer lookback periods
|
||||
* **False signals:** Can produce whipsaws during consolidation phases or range-bound conditions
|
||||
* **Parameter sensitivity:** Small changes in length or alpha can significantly alter behavior
|
||||
* **Complementary tools:** Should be used with momentum indicators (RSI, MACD) or volume indicators for confirmation
|
||||
|
||||
## References
|
||||
|
||||
1. Murphy, J.J. (1999). *Technical Analysis of the Financial Markets*. New York Institute of Finance.
|
||||
2. Kaufman, P. (2013). *Trading Systems and Methods*, 5th Edition. Wiley Trading.
|
||||
3. Ehlers, J. (2001). *Rocket Science for Traders*. John Wiley & Sons.
|
||||
@@ -0,0 +1,138 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class EmaVectorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Initialization_WithPeriods_SetsCorrectAlphas()
|
||||
{
|
||||
int[] periods = { 10, 20 };
|
||||
var emaVector = new EmaVector(periods);
|
||||
|
||||
// We can't check private fields directly, but we can check results after 1 step
|
||||
// Alpha = 2 / (P + 1)
|
||||
// P=10 -> A=2/11
|
||||
// P=20 -> A=2/21
|
||||
|
||||
var res = emaVector.Update(new TValue(DateTime.Now, 100.0));
|
||||
|
||||
// First value should be 100.0 due to compensation
|
||||
Assert.Equal(100.0, res[0].Value, 1e-9);
|
||||
Assert.Equal(100.0, res[1].Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_Streaming_MatchesSingleEma()
|
||||
{
|
||||
int[] periods = { 5, 10, 20 };
|
||||
var emaVector = new EmaVector(periods);
|
||||
var emaSingles = periods.Select(p => new Ema(p)).ToArray();
|
||||
|
||||
var values = new double[] { 10, 20, 30, 40, 50, 40, 30, 20, 10 };
|
||||
var time = DateTime.Now;
|
||||
|
||||
foreach (var val in values)
|
||||
{
|
||||
var tVal = new TValue(time, val);
|
||||
var multiRes = emaVector.Update(tVal);
|
||||
|
||||
for (int i = 0; i < periods.Length; i++)
|
||||
{
|
||||
var singleRes = emaSingles[i].Update(tVal);
|
||||
Assert.Equal(singleRes.Value, multiRes[i].Value, 1e-9);
|
||||
Assert.Equal(singleRes.Time, multiRes[i].Time);
|
||||
}
|
||||
|
||||
time = time.AddMinutes(1);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_Series_MatchesSingleEma()
|
||||
{
|
||||
int[] periods = { 5, 10, 20 };
|
||||
var emaVector = new EmaVector(periods);
|
||||
var emaSingles = periods.Select(p => new Ema(p)).ToArray();
|
||||
|
||||
int len = 100;
|
||||
var t = new System.Collections.Generic.List<long>(len);
|
||||
var v = new System.Collections.Generic.List<double>(len);
|
||||
var now = DateTime.Now;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
t.Add(now.AddMinutes(i).Ticks);
|
||||
v.Add(Math.Sin(i * 0.1) * 100);
|
||||
}
|
||||
|
||||
var series = new TSeries(t, v);
|
||||
|
||||
var multiRes = emaVector.Calculate(series);
|
||||
|
||||
for (int i = 0; i < periods.Length; i++)
|
||||
{
|
||||
var singleRes = emaSingles[i].Update(series);
|
||||
|
||||
Assert.Equal(singleRes.Count, multiRes[i].Count);
|
||||
for (int j = 0; j < len; j++)
|
||||
{
|
||||
Assert.Equal(singleRes.Values[j], multiRes[i].Values[j], 1e-8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_Series_MatchesStreaming()
|
||||
{
|
||||
int[] periods = { 5, 10, 20 };
|
||||
var emaVectorBatch = new EmaVector(periods);
|
||||
var emaVectorStream = new EmaVector(periods);
|
||||
|
||||
int len = 100;
|
||||
var t = new System.Collections.Generic.List<long>(len);
|
||||
var v = new System.Collections.Generic.List<double>(len);
|
||||
var now = DateTime.Now;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
t.Add(now.AddMinutes(i).Ticks);
|
||||
v.Add(Math.Sin(i * 0.1) * 100);
|
||||
}
|
||||
|
||||
var series = new TSeries(t, v);
|
||||
|
||||
// Batch calculation
|
||||
var batchRes = emaVectorBatch.Calculate(series);
|
||||
|
||||
// Streaming calculation
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
var tVal = new TValue(new DateTime(t[i]), v[i]);
|
||||
var streamRes = emaVectorStream.Update(tVal);
|
||||
|
||||
for (int j = 0; j < periods.Length; j++)
|
||||
{
|
||||
Assert.Equal(batchRes[j].Values[i], streamRes[j].Value, 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
int[] periods = { 10 };
|
||||
var emaVector = new EmaVector(periods);
|
||||
|
||||
emaVector.Update(new TValue(DateTime.Now, 100.0));
|
||||
emaVector.Reset();
|
||||
|
||||
// After reset, next calculation should treat it as first value (warmup)
|
||||
var res = emaVector.Update(new TValue(DateTime.Now, 200.0));
|
||||
|
||||
Assert.Equal(200.0, res[0].Value, 1e-9);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Multi-Alpha Exponential Moving Average (EMA) - SIMD optimized.
|
||||
/// Calculates multiple EMAs with different periods/alphas for the same input series in parallel.
|
||||
/// </summary>
|
||||
public class EmaVector
|
||||
{
|
||||
private readonly double[] _alphas;
|
||||
private readonly double[] _emas;
|
||||
private readonly double[] _Es;
|
||||
private readonly int _count;
|
||||
|
||||
/// <summary>
|
||||
/// Current EMA values for all periods.
|
||||
/// </summary>
|
||||
public TValue[] Values { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes EmaVector with specified periods.
|
||||
/// </summary>
|
||||
/// <param name="periods">Array of periods</param>
|
||||
public EmaVector(int[] periods)
|
||||
{
|
||||
_count = periods.Length;
|
||||
_alphas = new double[_count];
|
||||
_emas = new double[_count];
|
||||
_Es = new double[_count];
|
||||
Values = new TValue[_count];
|
||||
|
||||
for (int i = 0; i < _count; i++)
|
||||
{
|
||||
if (periods[i] <= 0) throw new ArgumentException("Period must be greater than 0", nameof(periods));
|
||||
_alphas[i] = 2.0 / (periods[i] + 1);
|
||||
ResetAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes EmaVector with specified alphas.
|
||||
/// </summary>
|
||||
/// <param name="alphas">Array of alphas</param>
|
||||
public EmaVector(double[] alphas)
|
||||
{
|
||||
_count = alphas.Length;
|
||||
_alphas = new double[_count];
|
||||
_emas = new double[_count];
|
||||
_Es = new double[_count];
|
||||
Values = new TValue[_count];
|
||||
|
||||
for (int i = 0; i < _count; i++)
|
||||
{
|
||||
if (alphas[i] <= 0 || alphas[i] > 1) throw new ArgumentException("Alpha must be between 0 and 1", nameof(alphas));
|
||||
_alphas[i] = alphas[i];
|
||||
ResetAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetAt(int index)
|
||||
{
|
||||
_emas[index] = 0.0;
|
||||
_Es[index] = 1.0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets all EMA states.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
for (int i = 0; i < _count; i++)
|
||||
{
|
||||
ResetAt(i);
|
||||
}
|
||||
Array.Clear(Values);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates EMAs with the given value.
|
||||
/// </summary>
|
||||
/// <param name="input">Input value</param>
|
||||
/// <returns>Array of compensated EMA values</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue[] Update(TValue input)
|
||||
{
|
||||
double val = input.Value;
|
||||
|
||||
// SIMD Loop
|
||||
int vecCount = Vector<double>.Count;
|
||||
int i = 0;
|
||||
|
||||
if (Vector.IsHardwareAccelerated && _count >= vecCount)
|
||||
{
|
||||
var vecInput = new Vector<double>(val);
|
||||
var vecOne = Vector<double>.One;
|
||||
var vecEpsilon = new Vector<double>(1e-10);
|
||||
|
||||
for (; i <= _count - vecCount; i += vecCount)
|
||||
{
|
||||
// Load state
|
||||
var vecAlpha = new Vector<double>(_alphas, i);
|
||||
var vecEma = new Vector<double>(_emas, i);
|
||||
var vecE = new Vector<double>(_Es, i);
|
||||
|
||||
// Update EMA
|
||||
// ema += alpha * (input - ema)
|
||||
vecEma += vecAlpha * (vecInput - vecEma);
|
||||
|
||||
// Update E (warmup factor)
|
||||
// E *= (1 - alpha)
|
||||
vecE *= (vecOne - vecAlpha);
|
||||
|
||||
// Calculate compensated result
|
||||
// res = ema / (1 - E)
|
||||
var vecCompensated = vecEma / (vecOne - vecE);
|
||||
|
||||
// Check warmup condition: E > 1e-10
|
||||
var warmupMask = Vector.GreaterThan(vecE, vecEpsilon);
|
||||
|
||||
// Select result
|
||||
// Vector.ConditionalSelect requires Vector<T> mask.
|
||||
// Vector.GreaterThan returns Vector<long> for double.
|
||||
// We cast Vector<long> to Vector<double> to use as mask.
|
||||
var vecResult = Vector.ConditionalSelect(Vector.AsVectorDouble(warmupMask), vecCompensated, vecEma);
|
||||
|
||||
// Store state
|
||||
vecEma.CopyTo(_emas, i);
|
||||
vecE.CopyTo(_Es, i);
|
||||
|
||||
// Store result
|
||||
for (int j = 0; j < vecCount; j++)
|
||||
{
|
||||
Values[i + j] = new TValue(input.Time, vecResult[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scalar fallback for remaining items
|
||||
for (; i < _count; i++)
|
||||
{
|
||||
double alpha = _alphas[i];
|
||||
_emas[i] += alpha * (val - _emas[i]);
|
||||
|
||||
double result = _emas[i];
|
||||
if (_Es[i] > 1e-10)
|
||||
{
|
||||
_Es[i] *= (1.0 - alpha);
|
||||
if (_Es[i] > 1e-10)
|
||||
{
|
||||
result = _emas[i] / (1.0 - _Es[i]);
|
||||
}
|
||||
}
|
||||
|
||||
Values[i] = new TValue(input.Time, result);
|
||||
}
|
||||
|
||||
return Values;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates EMAs for the entire series.
|
||||
/// </summary>
|
||||
/// <param name="source">Input series</param>
|
||||
/// <returns>Array of EMA series</returns>
|
||||
public TSeries[] Calculate(TSeries source)
|
||||
{
|
||||
int len = source.Count;
|
||||
var resultSeries = new TSeries[_count];
|
||||
|
||||
// Pre-allocate lists
|
||||
var tLists = new List<long>[_count];
|
||||
var vLists = new List<double>[_count];
|
||||
|
||||
for (int i = 0; i < _count; i++)
|
||||
{
|
||||
tLists[i] = new List<long>(len);
|
||||
vLists[i] = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(tLists[i], len);
|
||||
CollectionsMarshal.SetCount(vLists[i], len);
|
||||
}
|
||||
|
||||
var sourceValues = source.Values;
|
||||
var sourceTimes = source.Times;
|
||||
|
||||
int vecCount = Vector<double>.Count;
|
||||
var vecOne = Vector<double>.One;
|
||||
var vecEpsilon = new Vector<double>(1e-10);
|
||||
|
||||
for (int t = 0; t < len; t++)
|
||||
{
|
||||
double val = sourceValues[t];
|
||||
long time = sourceTimes[t];
|
||||
var vecInput = new Vector<double>(val);
|
||||
|
||||
int i = 0;
|
||||
if (Vector.IsHardwareAccelerated && _count >= vecCount)
|
||||
{
|
||||
for (; i <= _count - vecCount; i += vecCount)
|
||||
{
|
||||
var vecAlpha = new Vector<double>(_alphas, i);
|
||||
var vecEma = new Vector<double>(_emas, i);
|
||||
var vecE = new Vector<double>(_Es, i);
|
||||
|
||||
vecEma += vecAlpha * (vecInput - vecEma);
|
||||
vecE *= (vecOne - vecAlpha);
|
||||
|
||||
var vecCompensated = vecEma / (vecOne - vecE);
|
||||
var warmupMask = Vector.GreaterThan(vecE, vecEpsilon);
|
||||
var vecResult = Vector.ConditionalSelect(Vector.AsVectorDouble(warmupMask), vecCompensated, vecEma);
|
||||
|
||||
vecEma.CopyTo(_emas, i);
|
||||
vecE.CopyTo(_Es, i);
|
||||
|
||||
// Scatter results to lists
|
||||
for (int j = 0; j < vecCount; j++)
|
||||
{
|
||||
CollectionsMarshal.AsSpan(tLists[i + j])[t] = time;
|
||||
CollectionsMarshal.AsSpan(vLists[i + j])[t] = vecResult[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (; i < _count; i++)
|
||||
{
|
||||
double alpha = _alphas[i];
|
||||
_emas[i] += alpha * (val - _emas[i]);
|
||||
|
||||
double result = _emas[i];
|
||||
if (_Es[i] > 1e-10)
|
||||
{
|
||||
_Es[i] *= (1.0 - alpha);
|
||||
if (_Es[i] > 1e-10)
|
||||
{
|
||||
result = _emas[i] / (1.0 - _Es[i]);
|
||||
}
|
||||
}
|
||||
|
||||
CollectionsMarshal.AsSpan(tLists[i])[t] = time;
|
||||
CollectionsMarshal.AsSpan(vLists[i])[t] = result;
|
||||
}
|
||||
}
|
||||
|
||||
// Create TSeries and update Values
|
||||
for (int i = 0; i < _count; i++)
|
||||
{
|
||||
resultSeries[i] = new TSeries(tLists[i], vLists[i]);
|
||||
var lastT = CollectionsMarshal.AsSpan(tLists[i])[len - 1];
|
||||
var lastV = CollectionsMarshal.AsSpan(vLists[i])[len - 1];
|
||||
Values[i] = new TValue(lastT, lastV);
|
||||
}
|
||||
|
||||
return resultSeries;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates EMAs for the entire series using specified periods.
|
||||
/// </summary>
|
||||
/// <param name="source">Input series</param>
|
||||
/// <param name="periods">Array of periods</param>
|
||||
/// <returns>Array of EMA series</returns>
|
||||
public static TSeries[] Calculate(TSeries source, int[] periods)
|
||||
{
|
||||
var emaVector = new EmaVector(periods);
|
||||
return emaVector.Calculate(source);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user