diff --git a/lib/core/simd/SimdExtensions.Notebook.dib b/lib/core/simd/SimdExtensions.Notebook.dib deleted file mode 100644 index f8812b2c..00000000 --- a/lib/core/simd/SimdExtensions.Notebook.dib +++ /dev/null @@ -1,91 +0,0 @@ -#!meta - -{"kernelInfo":{"defaultKernelName":"csharp","items":[{"name":"csharp","languageName":"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"}]}} - -#!csharp - -// Reference the library -#r "..\..\bin\QuanTAlib.dll" - -using System; -using System.Linq; -using System.Numerics; -using QuanTAlib; - -// 1. Check Hardware Support -Console.WriteLine($"SIMD Hardware Acceleration: {Vector.IsHardwareAccelerated}"); -Console.WriteLine($"Vector Count: {Vector.Count}"); - -#!csharp - -// 2. Basic Operations -// Demonstrate Sum, Min, Max, Average using SIMD extensions - -// Define data locally in this cell -double[] data = new double[1000]; -for (int i = 0; i < data.Length; i++) data[i] = i; - -// We use explicit static method calls with .AsSpan() to ensure correct overload resolution -// and avoid creating top-level ReadOnlySpan variables (which causes CS8345). - -double sum = SimdExtensions.SumSIMD(data.AsSpan()); -double minVal = SimdExtensions.MinSIMD(data.AsSpan()); -double maxVal = SimdExtensions.MaxSIMD(data.AsSpan()); -double avg = SimdExtensions.AverageSIMD(data.AsSpan()); - -Console.WriteLine($"Sum: {sum}"); -Console.WriteLine($"Min: {minVal}"); -Console.WriteLine($"Max: {maxVal}"); -Console.WriteLine($"Average: {avg}"); - -#!csharp - -// 3. Advanced Statistics - -double[] dataStats = new double[1000]; -for (int i = 0; i < dataStats.Length; i++) dataStats[i] = i; - -double variance = SimdExtensions.VarianceSIMD(dataStats.AsSpan()); -double stdDev = SimdExtensions.StdDevSIMD(dataStats.AsSpan()); - -Console.WriteLine($"Variance: {variance:F4}"); -Console.WriteLine($"Standard Deviation: {stdDev:F4}"); - -#!csharp - -// 4. Combined Operations - -double[] dataComb = new double[1000]; -for (int i = 0; i < dataComb.Length; i++) dataComb[i] = i; - -var (min, max) = SimdExtensions.MinMaxSIMD(dataComb.AsSpan()); -Console.WriteLine($"Min: {min}, Max: {max}"); - -#!csharp - -// 5. Performance Comparison (Simple Benchmark) - -int size = 1_000_000; -double[] largeData = new double[size]; -Random rnd = new Random(42); -for (int i = 0; i < size; i++) largeData[i] = rnd.NextDouble(); - -// Warmup -SimdExtensions.SumSIMD(largeData.AsSpan()); - -// Measure SIMD -long start = DateTime.UtcNow.Ticks; -double sumSimd = SimdExtensions.SumSIMD(largeData.AsSpan()); -long end = DateTime.UtcNow.Ticks; -double simdTime = (end - start) / 10000.0; // ms - -// Measure Scalar (LINQ Sum as proxy for scalar loop) -start = DateTime.UtcNow.Ticks; -double sumScalar = largeData.Sum(); -end = DateTime.UtcNow.Ticks; -double scalarTime = (end - start) / 10000.0; // ms - -Console.WriteLine($"Array Size: {size:N0}"); -Console.WriteLine($"SIMD Time: {simdTime:F4} ms"); -Console.WriteLine($"Scalar Time: {scalarTime:F4} ms"); -Console.WriteLine($"Speedup: {scalarTime / simdTime:F2}x"); diff --git a/lib/trends/_index.md b/lib/trends/_index.md index 9f55cebf..ab3e3464 100644 --- a/lib/trends/_index.md +++ b/lib/trends/_index.md @@ -13,7 +13,7 @@ Trend indicators help identify the direction and strength of a market trend. Mov | BWMA | Bessel-Weighted MA | | | CHEBY1 | Chebyshev Type I Filter | | | CHEBY2 | Chebyshev Type II Filter | | -| CONV | Convolution MA with any kernel | | +| [CONV](trends/conv/Conv.md) | Convolution Indicator | Applies a custom kernel (weights) to the data window. | | [DEMA](trends/dema/Dema.md) | Double Exponential Moving Average | Reduces lag by placing more weight on recent data than a standard EMA. | | DSMA | Deviation-Scaled MA | | | DWMA | Double Weighted MA | | diff --git a/lib/trends/conv/Conv.Quantower.Tests.cs b/lib/trends/conv/Conv.Quantower.Tests.cs new file mode 100644 index 00000000..f953d86d --- /dev/null +++ b/lib/trends/conv/Conv.Quantower.Tests.cs @@ -0,0 +1,187 @@ +using Xunit; +using TradingPlatform.BusinessLayer; + +namespace QuanTAlib.Tests; + +public class ConvIndicatorTests +{ + [Fact] + public void ConvIndicator_Constructor_SetsDefaults() + { + var indicator = new ConvIndicator(); + + Assert.Equal("0.1, 0.2, 0.3, 0.4", indicator.WeightsInput); + Assert.Equal(SourceType.Close, indicator.Source); + Assert.True(indicator.ShowColdValues); + Assert.Equal("Conv - Convolution", indicator.Name); + Assert.False(indicator.SeparateWindow); + Assert.True(indicator.OnBackGround); + } + + [Fact] + public void ConvIndicator_MinHistoryDepths_EqualsWeightsLength() + { + var indicator = new ConvIndicator { WeightsInput = "1, 2, 3, 4, 5" }; + indicator.Initialize(); // Initialize to parse weights + + Assert.Equal(5, indicator.MinHistoryDepths); + Assert.Equal(5, ((IWatchlistIndicator)indicator).MinHistoryDepths); + } + + [Fact] + public void ConvIndicator_ShortName_IncludesSource() + { + var indicator = new ConvIndicator(); + + Assert.Contains("Conv", indicator.ShortName); + Assert.Contains("Close", indicator.ShortName); + } + + [Fact] + public void ConvIndicator_SourceCodeLink_IsValid() + { + var indicator = new ConvIndicator(); + + Assert.Contains("github.com", indicator.SourceCodeLink); + Assert.Contains("Conv.Quantower.cs", indicator.SourceCodeLink); + } + + [Fact] + public void ConvIndicator_Initialize_CreatesInternalConv() + { + var indicator = new ConvIndicator(); + + // Initialize should not throw + indicator.Initialize(); + + // After init, line series should exist + Assert.Single(indicator.LinesSeries); + } + + [Fact] + public void ConvIndicator_ProcessUpdate_HistoricalBar_ComputesValue() + { + var indicator = new ConvIndicator { WeightsInput = "0.5, 0.5" }; + 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 ConvIndicator_ProcessUpdate_NewBar_ComputesValue() + { + var indicator = new ConvIndicator { WeightsInput = "0.5, 0.5" }; + 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 ConvIndicator_ProcessUpdate_NewTick_ProcessesWithoutError() + { + var indicator = new ConvIndicator { WeightsInput = "0.5, 0.5" }; + 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 ConvIndicator_OnPaintChart_DoesNotThrow() + { + var indicator = new ConvIndicator(); + indicator.Initialize(); + + var method = indicator.GetType().GetMethod("OnPaintChart"); + Assert.NotNull(method); + Assert.Equal(typeof(ConvIndicator), method.DeclaringType); + } + + [Fact] + public void ConvIndicator_MultipleUpdates_ProducesCorrectSequence() + { + // Weights [0.5, 1.0] + var indicator = new ConvIndicator { WeightsInput = "0.5, 1.0" }; + 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))); + } + } + + [Fact] + public void ConvIndicator_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 ConvIndicator { WeightsInput = "0.5, 0.5", 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 ConvIndicator_InvalidWeights_FallsBackToDefault() + { + var indicator = new ConvIndicator { WeightsInput = "invalid" }; + + // Should not throw, but fallback + indicator.Initialize(); + + Assert.Single(indicator.LinesSeries); + } + + [Fact] + public void ConvIndicator_DescriptionIsSet() + { + var indicator = new ConvIndicator(); + + Assert.Contains("Convolution", indicator.Description); + } +} diff --git a/lib/trends/conv/Conv.Quantower.cs b/lib/trends/conv/Conv.Quantower.cs new file mode 100644 index 00000000..47fe9269 --- /dev/null +++ b/lib/trends/conv/Conv.Quantower.cs @@ -0,0 +1,84 @@ +using System; +using System.Drawing; +using System.Linq; +using TradingPlatform.BusinessLayer; + +namespace QuanTAlib; + +public class ConvIndicator : Indicator, IWatchlistIndicator +{ + [InputParameter("Weights (comma separated)", sortIndex: 1)] + public string WeightsInput { get; set; } = "0.1, 0.2, 0.3, 0.4"; + + [IndicatorExtensions.DataSourceInput] + public SourceType Source { get; set; } = SourceType.Close; + + [InputParameter("Show cold values", sortIndex: 21)] + public bool ShowColdValues { get; set; } = true; + + private Conv? _conv; + private int _warmupBarIndex = -1; + protected LineSeries? Series; + protected string? SourceName; + + public int MinHistoryDepths => _conv != null ? WeightsInput.Split(',').Length : 0; + int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths; + + public override string ShortName => $"Conv:{SourceName}"; + public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/trends/conv/Conv.Quantower.cs"; + + public ConvIndicator() + { + OnBackGround = true; + SeparateWindow = false; + SourceName = Source.ToString(); + Name = "Conv - Convolution"; + Description = "Convolution with custom kernel"; + Series = new(name: "Conv", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid); + AddLineSeries(Series); + } + + protected override void OnInit() + { + try + { + var weights = WeightsInput.Split(',') + .Select(s => double.Parse(s.Trim())) + .ToArray(); + + if (weights.Length == 0) + throw new ArgumentException("Weights cannot be empty"); + + _conv = new Conv(weights); + } + catch (FormatException) + { + _conv = new Conv([1.0]); + } + catch (ArgumentException) + { + _conv = new Conv([1.0]); + } + + _warmupBarIndex = -1; + SourceName = Source.ToString(); + base.OnInit(); + } + + protected override void OnUpdate(UpdateArgs args) + { + TValue input = this.GetInputValue(args, Source); + bool isNew = args.Reason == UpdateReason.NewBar || args.Reason == UpdateReason.HistoricalBar; + TValue result = _conv!.Update(input, isNew); + if (_warmupBarIndex < 0 && _conv!.IsHot) + _warmupBarIndex = Count; + Series!.SetValue(result.Value); + Series!.SetMarker(0, Color.Transparent); //OnPaintChart draws the line, hidden here + } + + public override void OnPaintChart(PaintChartEventArgs args) + { + base.OnPaintChart(args); + this.PaintSmoothCurve(args, Series!, _warmupBarIndex, showColdValues: ShowColdValues, tension: 0.2); + } +} diff --git a/lib/trends/conv/Conv.Tests.cs b/lib/trends/conv/Conv.Tests.cs new file mode 100644 index 00000000..c68663c2 --- /dev/null +++ b/lib/trends/conv/Conv.Tests.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace QuanTAlib; + +public class ConvTests +{ + [Fact] + public void Constructor_EmptyKernel_ThrowsArgumentException() + { + Assert.Throws(() => new Conv(Array.Empty())); + Assert.Throws(() => new Conv(null!)); + } + + [Fact] + public void BasicCalculation_MatchesExpected() + { + // Kernel: [0.5, 1.0] + // Data: [1, 2, 3, 4] + // 1: 1*1.0 = 1.0 (partial) + // 2: 1*0.5 + 2*1.0 = 2.5 + // 3: 2*0.5 + 3*1.0 = 4.0 + // 4: 3*0.5 + 4*1.0 = 5.5 + + var kernel = new double[] { 0.5, 1.0 }; + var conv = new Conv(kernel); + + var result1 = conv.Update(new TValue(DateTime.UtcNow, 1)); + Assert.Equal(1.0, result1.Value); + + var result2 = conv.Update(new TValue(DateTime.UtcNow, 2)); + Assert.Equal(2.5, result2.Value); + + var result3 = conv.Update(new TValue(DateTime.UtcNow, 3)); + Assert.Equal(4.0, result3.Value); + + var result4 = conv.Update(new TValue(DateTime.UtcNow, 4)); + Assert.Equal(5.5, result4.Value); + } + + [Fact] + public void BarCorrection_UpdatesCorrectly() + { + var kernel = new double[] { 0.5, 1.0 }; + var conv = new Conv(kernel); + + // 1 + conv.Update(new TValue(DateTime.UtcNow, 1)); + + // 2 (isNew=true) -> 2.5 + var res1 = conv.Update(new TValue(DateTime.UtcNow, 2), isNew: true); + Assert.Equal(2.5, res1.Value); + + // Update 2 to 3 (isNew=false) + // Buffer was [1, 2]. Now [1, 3]. + // 1*0.5 + 3*1.0 = 3.5 + var res2 = conv.Update(new TValue(DateTime.UtcNow, 3), isNew: false); + Assert.Equal(3.5, res2.Value); + + // New bar 4 (isNew=true) + // Buffer was [1, 3]. New bar 4. Buffer becomes [3, 4]. + // 3*0.5 + 4*1.0 = 1.5 + 4 = 5.5 + var res3 = conv.Update(new TValue(DateTime.UtcNow, 4), isNew: true); + Assert.Equal(5.5, res3.Value); + } + + [Fact] + public void NanHandling_UsesLastValid() + { + var kernel = new double[] { 1.0, 1.0 }; // Sum of last 2 + var conv = new Conv(kernel); + + // 1 -> 1 + conv.Update(new TValue(DateTime.UtcNow, 1)); + + // NaN -> treated as 1. Buffer: [1, 1]. Result: 2. + var res = conv.Update(new TValue(DateTime.UtcNow, double.NaN)); + Assert.Equal(2.0, res.Value); + + // 2 -> Buffer: [1, 2]. Result: 3. + res = conv.Update(new TValue(DateTime.UtcNow, 2)); + Assert.Equal(3.0, res.Value); + } + + [Fact] + public void StaticCalculate_MatchesObjectApi() + { + var kernel = new double[] { 0.5, 1.0 }; + var source = new TSeries(); + source.Add(new TValue(DateTime.UtcNow, 1)); + source.Add(new TValue(DateTime.UtcNow, 2)); + source.Add(new TValue(DateTime.UtcNow, 3)); + source.Add(new TValue(DateTime.UtcNow, 4)); + + var result = Conv.Calculate(source, kernel); + + Assert.Equal(1.0, result.Values[0]); + Assert.Equal(2.5, result.Values[1]); + Assert.Equal(4.0, result.Values[2]); + Assert.Equal(5.5, result.Values[3]); + } + + [Fact] + public void Reset_ClearsState() + { + var kernel = new double[] { 1.0, 1.0 }; + var conv = new Conv(kernel); + + conv.Update(new TValue(DateTime.UtcNow, 1)); + conv.Update(new TValue(DateTime.UtcNow, 2)); + Assert.True(conv.IsHot); + + conv.Reset(); + Assert.False(conv.IsHot); + Assert.Equal(0, conv.Last.Value); + + // Should behave as new + var res = conv.Update(new TValue(DateTime.UtcNow, 1)); + Assert.Equal(1.0, res.Value); + } +} diff --git a/lib/trends/conv/Conv.Validation.Tests.cs b/lib/trends/conv/Conv.Validation.Tests.cs new file mode 100644 index 00000000..6f1305a4 --- /dev/null +++ b/lib/trends/conv/Conv.Validation.Tests.cs @@ -0,0 +1,123 @@ +using System; +using Xunit; + +namespace QuanTAlib; + +public class ConvValidationTests +{ + [Fact] + public void Validate_Against_Sma() + { + // SMA(10) is equivalent to Conv with 10 weights of 1/10 + int period = 10; + double weight = 1.0 / period; + double[] kernel = new double[period]; + Array.Fill(kernel, weight); + + var sma = new Sma(period); + var conv = new Conv(kernel); + + var rnd = new Random(123); + for (int i = 0; i < 1000; i++) + { + double price = rnd.NextDouble() * 100; + var tValue = new TValue(DateTime.UtcNow, price); + + var smaVal = sma.Update(tValue); + var convVal = conv.Update(tValue); + + if (i >= period) // Skip warmup + { + Assert.Equal(smaVal.Value, convVal.Value, 1e-9); + } + } + } + + [Fact] + public void Validate_Against_Wma() + { + // WMA(10) weights are 1, 2, ..., 10 divided by sum(1..10) + int period = 10; + double divisor = period * (period + 1) / 2.0; + double[] kernel = new double[period]; + for (int i = 0; i < period; i++) + { + kernel[i] = (i + 1) / divisor; + } + + var wma = new Wma(period); + var conv = new Conv(kernel); + + var rnd = new Random(123); + for (int i = 0; i < 1000; i++) + { + double price = rnd.NextDouble() * 100; + var tValue = new TValue(DateTime.UtcNow, price); + + var wmaVal = wma.Update(tValue); + var convVal = conv.Update(tValue); + + if (i >= period) // Skip warmup + { + Assert.Equal(wmaVal.Value, convVal.Value, 1e-9); + } + } + } + + [Fact] + public void Validate_Against_Trima() + { + // TRIMA(10) - Even period + // Weights: 1, 2, 3, 4, 5, 5, 4, 3, 2, 1 + // Sum: 30 + int period = 10; + double[] kernel = new double[period]; + double sum = 0; + + // Generate triangular weights + int mid = period / 2; + for (int i = 0; i < period; i++) + { + // For even period 10: + // i=0 -> 1 + // i=4 -> 5 + // i=5 -> 5 + // i=9 -> 1 + + // Distance from ends? + // 0 -> 1 + // 1 -> 2 + // ... + // mid-1 -> mid + // mid -> mid + + double val = (i < mid) ? (i + 1) : (period - i); + kernel[i] = val; + sum += val; + } + + // Normalize + for (int i = 0; i < period; i++) + { + kernel[i] /= sum; + } + + var trima = new Trima(period); + var conv = new Conv(kernel); + + var rnd = new Random(123); + for (int i = 0; i < 1000; i++) + { + double price = rnd.NextDouble() * 100; + var tValue = new TValue(DateTime.UtcNow, price); + + var trimaVal = trima.Update(tValue); + var convVal = conv.Update(tValue); + + if (i >= period) // Skip warmup + { + Assert.Equal(trimaVal.Value, convVal.Value, 1e-9); + } + } + } +} diff --git a/lib/trends/conv/Conv.cs b/lib/trends/conv/Conv.cs new file mode 100644 index 00000000..2ee4941b --- /dev/null +++ b/lib/trends/conv/Conv.cs @@ -0,0 +1,571 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Arm; +using System.Runtime.Intrinsics.X86; + +namespace QuanTAlib; + +/// +/// Convolution Indicator +/// +/// +/// Applies a custom kernel (weights) to the data window. +/// The kernel is applied such that kernel[0] multiplies the oldest data point in the window, +/// and kernel[n-1] multiplies the newest data point. +/// +/// Calculation: +/// Result = Sum(kernel[i] * data[i]) for i = 0 to n-1 +/// +/// Complexity: +/// Update: O(K) where K is kernel length. +/// +[SkipLocalsInit] +public sealed class Conv : ITValuePublisher +{ + private readonly int _period; + private readonly double[] _kernel; + private readonly RingBuffer _buffer; + + private double _lastValidValue; + private int _head; + + // State for bar correction + private double _p_lastValidValue; + + public string Name { get; } + public TValue Last { get; private set; } + public bool IsHot => _buffer.IsFull; + public event Action? Pub; + + public Conv(double[] kernel) + { + if (kernel == null || kernel.Length == 0) + throw new ArgumentException("Kernel must not be empty", nameof(kernel)); + + _period = kernel.Length; + _kernel = new double[_period]; + Array.Copy(kernel, _kernel, _period); + _buffer = new RingBuffer(_period); + Name = $"Conv({_period})"; + } + + public Conv(ITValuePublisher source, double[] kernel) : this(kernel) + { + source.Pub += (item) => Update(item); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double GetValidValue(double input) + { + if (double.IsFinite(input)) + { + _lastValidValue = input; + return input; + } + return _lastValidValue; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TValue Update(TValue input, bool isNew = true) + { + if (isNew) + { + _p_lastValidValue = _lastValidValue; + } + else + { + _lastValidValue = _p_lastValidValue; + } + + double val = GetValidValue(input.Value); + + if (isNew) + { + _buffer.Add(val); + _head = (_head + 1 == _period) ? 0 : _head + 1; + } + else + { + _buffer.UpdateNewest(val); + } + + double result = 0; + if (_buffer.Count > 0) + { + int count = _buffer.Count; + int kernelOffset = _period - count; + ReadOnlySpan kernelSpan = _kernel.AsSpan().Slice(kernelOffset); + ReadOnlySpan internalBuf = _buffer.InternalBuffer; + + if (count < _period) + { + result = DotProduct(internalBuf.Slice(0, count), kernelSpan); + } + else + { + // Full: data is split at _head (which points to oldest) + int part1Len = _period - _head; + result = DotProduct(internalBuf.Slice(_head, part1Len), kernelSpan.Slice(0, part1Len)) + + DotProduct(internalBuf.Slice(0, _head), kernelSpan.Slice(part1Len)); + } + } + + Last = new TValue(input.Time, result); + Pub?.Invoke(Last); + return Last; + } + + public TSeries Update(TSeries source) + { + if (source.Count == 0) return new TSeries(); + + int len = source.Count; + List t = new(len); + List v = new(len); + CollectionsMarshal.SetCount(t, len); + CollectionsMarshal.SetCount(v, len); + + var tSpan = CollectionsMarshal.AsSpan(t); + var vSpan = CollectionsMarshal.AsSpan(v); + + source.Times.CopyTo(tSpan); + var sourceValues = source.Values; + + Calculate(sourceValues, vSpan, _kernel); + + // Restore state + // We need to replay the last few updates to restore _buffer and _lastValidValue + int windowSize = Math.Min(len, _period); + int startIndex = len - windowSize; + + // Find last valid value before the window if possible + if (startIndex > 0) + { + for (int i = startIndex - 1; i >= 0; i--) + { + if (double.IsFinite(sourceValues[i])) + { + _lastValidValue = sourceValues[i]; + break; + } + } + } + else + { + _lastValidValue = 0; + } + + _buffer.Clear(); + + // Replay + for (int i = startIndex; i < len; i++) + { + double val = GetValidValue(sourceValues[i]); + _buffer.Add(val); + } + + // Sync _head with buffer state + _head = windowSize % _period; + + // Set Last + Last = new TValue(source.Times[len - 1], vSpan[len - 1]); + + // Save state for isNew=false + _p_lastValidValue = _lastValidValue; + + return new TSeries(t, v); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double DotProduct(ReadOnlySpan a, ReadOnlySpan b) + { + if (a.Length != b.Length || a.Length == 0) return 0; + + int len = a.Length; + + // Fast path for very small kernels (avoid SIMD overhead) + if (len <= 3) + { + ref double aRef = ref MemoryMarshal.GetReference(a); + ref double bRef = ref MemoryMarshal.GetReference(b); + + double sum = aRef * bRef; + if (len > 1) sum += Unsafe.Add(ref aRef, 1) * Unsafe.Add(ref bRef, 1); + if (len > 2) sum += Unsafe.Add(ref aRef, 2) * Unsafe.Add(ref bRef, 2); + return sum; + } + + if (Avx512F.IsSupported) + return DotProductAvx512(a, b); + + if (Avx2.IsSupported) + return DotProductAvx2(a, b); + + if (Sse2.IsSupported) + return DotProductSse2(a, b); + + if (AdvSimd.Arm64.IsSupported) + return DotProductNeon(a, b); + + double s = 0; + ref double ar = ref MemoryMarshal.GetReference(a); + ref double br = ref MemoryMarshal.GetReference(b); + + int i = 0; + // Unroll scalar loop + for (; i <= len - 4; i += 4) + { + s += Unsafe.Add(ref ar, i) * Unsafe.Add(ref br, i); + s += Unsafe.Add(ref ar, i + 1) * Unsafe.Add(ref br, i + 1); + s += Unsafe.Add(ref ar, i + 2) * Unsafe.Add(ref br, i + 2); + s += Unsafe.Add(ref ar, i + 3) * Unsafe.Add(ref br, i + 3); + } + + for (; i < len; i++) + { + s += Unsafe.Add(ref ar, i) * Unsafe.Add(ref br, i); + } + return s; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double DotProductAvx512(ReadOnlySpan a, ReadOnlySpan b) + { + int len = a.Length; + int i = 0; + Vector512 vSum = Vector512.Zero; + Vector512 vSum2 = Vector512.Zero; + Vector512 vSum3 = Vector512.Zero; + Vector512 vSum4 = Vector512.Zero; + + ref double aRef = ref MemoryMarshal.GetReference(a); + ref double bRef = ref MemoryMarshal.GetReference(b); + + // Unroll loop: Process 32 doubles (4 vectors) at a time + if (len >= 32) + { + for (; i <= len - 32; i += 32) + { + var va1 = Vector512.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); + var vb1 = Vector512.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); + + var va2 = Vector512.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 8)); + var vb2 = Vector512.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 8)); + + var va3 = Vector512.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 16)); + var vb3 = Vector512.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 16)); + + var va4 = Vector512.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 24)); + var vb4 = Vector512.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 24)); + + vSum = Avx512F.FusedMultiplyAdd(va1, vb1, vSum); + vSum2 = Avx512F.FusedMultiplyAdd(va2, vb2, vSum2); + vSum3 = Avx512F.FusedMultiplyAdd(va3, vb3, vSum3); + vSum4 = Avx512F.FusedMultiplyAdd(va4, vb4, vSum4); + } + } + + // Process remaining vectors (8 doubles at a time) + for (; i <= len - 8; i += 8) + { + var va = Vector512.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); + var vb = Vector512.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); + vSum = Avx512F.FusedMultiplyAdd(va, vb, vSum); + } + + // Combine accumulators + vSum = Avx512F.Add(vSum, vSum2); + vSum3 = Avx512F.Add(vSum3, vSum4); + vSum = Avx512F.Add(vSum, vSum3); + + // Horizontal sum - reduce to Vector256, then Vector128 + Vector256 v256 = Avx512F.Add(vSum.GetLower(), vSum.GetUpper()); + Vector128 lower = v256.GetLower(); + Vector128 upper = v256.GetUpper(); + Vector128 combined = Sse2.Add(lower, upper); + double sum = combined.GetElement(0) + combined.GetElement(1); + + // Scalar remainder + for (; i < len; i++) + { + sum += Unsafe.Add(ref aRef, i) * Unsafe.Add(ref bRef, i); + } + + return sum; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double DotProductAvx2(ReadOnlySpan a, ReadOnlySpan b) + { + int len = a.Length; + int i = 0; + Vector256 vSum = Vector256.Zero; + Vector256 vSum2 = Vector256.Zero; + Vector256 vSum3 = Vector256.Zero; + Vector256 vSum4 = Vector256.Zero; + + ref double aRef = ref MemoryMarshal.GetReference(a); + ref double bRef = ref MemoryMarshal.GetReference(b); + + // Unroll loop: Process 16 doubles (4 vectors) at a time + if (len >= 16) + { + for (; i <= len - 16; i += 16) + { + var va1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); + var vb1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); + + var va2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 4)); + var vb2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 4)); + + var va3 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 8)); + var vb3 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 8)); + + var va4 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 12)); + var vb4 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 12)); + + if (Fma.IsSupported) + { + vSum = Fma.MultiplyAdd(va1, vb1, vSum); + vSum2 = Fma.MultiplyAdd(va2, vb2, vSum2); + vSum3 = Fma.MultiplyAdd(va3, vb3, vSum3); + vSum4 = Fma.MultiplyAdd(va4, vb4, vSum4); + } + else + { + vSum = Avx.Add(vSum, Avx.Multiply(va1, vb1)); + vSum2 = Avx.Add(vSum2, Avx.Multiply(va2, vb2)); + vSum3 = Avx.Add(vSum3, Avx.Multiply(va3, vb3)); + vSum4 = Avx.Add(vSum4, Avx.Multiply(va4, vb4)); + } + } + } + + // Process remaining vectors (4 doubles at a time) + for (; i <= len - 4; i += 4) + { + var va = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); + var vb = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); + + vSum = Fma.IsSupported + ? Fma.MultiplyAdd(va, vb, vSum) + : Avx.Add(vSum, Avx.Multiply(va, vb)); + } + + // Combine accumulators + vSum = Avx.Add(vSum, vSum2); + vSum3 = Avx.Add(vSum3, vSum4); + vSum = Avx.Add(vSum, vSum3); + + // Horizontal sum + Vector128 lower = vSum.GetLower(); + Vector128 upper = vSum.GetUpper(); + Vector128 combined = Sse2.Add(lower, upper); + double sum = combined.GetElement(0) + combined.GetElement(1); + + // Process remaining elements (scalar) + for (; i < len; i++) + { + sum += Unsafe.Add(ref aRef, i) * Unsafe.Add(ref bRef, i); + } + + return sum; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double DotProductNeon(ReadOnlySpan a, ReadOnlySpan b) + { + int len = a.Length; + int i = 0; + Vector128 vSum = Vector128.Zero; + Vector128 vSum2 = Vector128.Zero; + Vector128 vSum3 = Vector128.Zero; + Vector128 vSum4 = Vector128.Zero; + + ref double aRef = ref MemoryMarshal.GetReference(a); + ref double bRef = ref MemoryMarshal.GetReference(b); + + // Unroll loop: Process 8 doubles (4 vectors) at a time + if (len >= 8) + { + for (; i <= len - 8; i += 8) + { + var va1 = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); + var vb1 = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); + + var va2 = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 2)); + var vb2 = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 2)); + + var va3 = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 4)); + var vb3 = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 4)); + + var va4 = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 6)); + var vb4 = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 6)); + + // NEON has FMA on ARM64 + // Since we are inside DotProductNeon which is guarded by AdvSimd.Arm64.IsSupported, + // we can assume Arm64 support. + vSum = AdvSimd.Arm64.FusedMultiplyAdd(vSum, va1, vb1); + vSum2 = AdvSimd.Arm64.FusedMultiplyAdd(vSum2, va2, vb2); + vSum3 = AdvSimd.Arm64.FusedMultiplyAdd(vSum3, va3, vb3); + vSum4 = AdvSimd.Arm64.FusedMultiplyAdd(vSum4, va4, vb4); + } + } + + // Process remaining vectors (2 doubles at a time) + for (; i <= len - 2; i += 2) + { + var va = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); + var vb = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); + + vSum = AdvSimd.Arm64.FusedMultiplyAdd(vSum, va, vb); + } + + // Combine accumulators + vSum = AdvSimd.Arm64.Add(vSum, vSum2); + vSum3 = AdvSimd.Arm64.Add(vSum3, vSum4); + vSum = AdvSimd.Arm64.Add(vSum, vSum3); + + // Horizontal sum (NEON has pairwise add) + double sum = AdvSimd.Arm64.AddPairwiseScalar(vSum).ToScalar(); + + // Scalar remainder (0-1 elements) + for (; i < len; i++) + { + sum += Unsafe.Add(ref aRef, i) * Unsafe.Add(ref bRef, i); + } + + return sum; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double DotProductSse2(ReadOnlySpan a, ReadOnlySpan b) + { + int len = a.Length; + int i = 0; + Vector128 vSum = Vector128.Zero; + Vector128 vSum2 = Vector128.Zero; + + ref double aRef = ref MemoryMarshal.GetReference(a); + ref double bRef = ref MemoryMarshal.GetReference(b); + + // Process 4 doubles at a time using 2 accumulators + for (; i <= len - 4; i += 4) + { + var va1 = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); + var vb1 = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); + var va2 = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 2)); + var vb2 = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 2)); + + if (Fma.IsSupported) + { + vSum = Fma.MultiplyAdd(va1, vb1, vSum); + vSum2 = Fma.MultiplyAdd(va2, vb2, vSum2); + } + else + { + vSum = Sse2.Add(vSum, Sse2.Multiply(va1, vb1)); + vSum2 = Sse2.Add(vSum2, Sse2.Multiply(va2, vb2)); + } + } + + // Process remaining 2 doubles if available + if (i <= len - 2) + { + var va = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); + var vb = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); + + vSum = Fma.IsSupported + ? Fma.MultiplyAdd(va, vb, vSum) + : Sse2.Add(vSum, Sse2.Multiply(va, vb)); + i += 2; + } + + vSum = Sse2.Add(vSum, vSum2); + double sum = vSum.GetElement(0) + vSum.GetElement(1); + + // Scalar remainder (0-1 elements) + for (; i < len; i++) + { + sum += Unsafe.Add(ref aRef, i) * Unsafe.Add(ref bRef, i); + } + + return sum; + } + + public static TSeries Calculate(TSeries source, double[] kernel) + { + var conv = new Conv(kernel); + return conv.Update(source); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Calculate(ReadOnlySpan source, Span output, double[] kernel) + { + if (source.Length != output.Length) + throw new ArgumentException("Source and output must have the same length"); + if (kernel == null || kernel.Length == 0) + throw new ArgumentException("Kernel must not be empty", nameof(kernel)); + + int len = source.Length; + int period = kernel.Length; + if (len == 0) return; + + // Use stackalloc for small kernels to avoid heap allocation + Span window = period <= 256 ? stackalloc double[period] : new double[period]; + + double lastValid = 0; + int windowIdx = 0; // Points to where the NEXT value goes (circular) + int count = 0; + + ReadOnlySpan kernelSpan = kernel.AsSpan(); + + for (int i = 0; i < len; i++) + { + double val = source[i]; + if (double.IsFinite(val)) + { + lastValid = val; + } + else + { + val = lastValid; + } + + window[windowIdx] = val; + windowIdx = (windowIdx + 1); + if (windowIdx >= period) windowIdx = 0; + + if (count < period) count++; + + double sum = 0; + + if (count < period) + { + int kernelOffset = period - count; + // Window is [0..count-1] + sum = DotProduct(window.Slice(0, count), kernelSpan.Slice(kernelOffset)); + } + else + { + // Full buffer - branchless version + int part1Len = period - windowIdx; + sum = DotProduct(window.Slice(windowIdx, part1Len), kernelSpan.Slice(0, part1Len)) + + DotProduct(window.Slice(0, windowIdx), kernelSpan.Slice(part1Len)); + } + + output[i] = sum; + } + } + + public void Reset() + { + _buffer.Clear(); + _lastValidValue = 0; + _p_lastValidValue = 0; + _head = 0; + Last = default; + } +} diff --git a/lib/trends/conv/Conv.md b/lib/trends/conv/Conv.md new file mode 100644 index 00000000..55b398b5 --- /dev/null +++ b/lib/trends/conv/Conv.md @@ -0,0 +1,97 @@ +# CONV: Convolution + +[Pine Script Implementation of CONV](https://github.com/mihakralj/pinescript/blob/main/indicators/trends_FIR/conv.pine) + +## Overview and Purpose + +The Convolution (CONV) is a flexible technical indicator that allows traders to apply any arbitrary weighting scheme (kernel) to price data. Rooted in signal processing principles developed in the 1950-60s, convolution filtering was later adapted to financial markets in the 1990s as digital signal processing techniques gained popularity in technical analysis. Convolution provides a generalized framework that enables traders to create customized moving averages with specific filtering characteristics, either by designing their own weight distributions or using predefined kernels. + +## Core Concepts + +* **Customizable weighting:** Convolution allows any sequence of weights to be applied to price data, enabling precise control over filtering behavior. +* **Kernel flexibility:** Supports both simple weight distributions (like those used in SMA) and complex multi-lobe designs with specialized filtering properties. +* **Market application:** Particularly valuable for traders who need to design specialized filters for specific market conditions or trading strategies. +* **Raw Dot Product:** The indicator calculates the dot product of the kernel and the price window. It does not automatically normalize the result, giving the user complete control over the magnitude. + +The core innovation of convolution is its implementation of the fundamental convolution operation from signal processing. This provides a unified framework that can replicate many standard moving averages through appropriate kernel selection, while also allowing for experimentation with novel weight distributions that aren't available in standard indicators. + +## Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `kernel` | `double[]` | Array of weights defining the filter. `kernel[0]` applies to the oldest data, `kernel[n-1]` to the newest. | + +**Note:** The `period` or `length` of the indicator is determined automatically by the length of the provided kernel array. + +## Formula + +$$ +Conv_t = \sum_{i=0}^{n-1} (kernel_i \times P_{t-(n-1)+i}) +$$ + +Where: + +* $n$ is the length of the kernel. +* $P$ is the price series. +* $kernel_i$ is the weight at index $i$. + +> ⚠️ **Important:** The implementation calculates the raw dot product. If you intend to create a Moving Average, ensure your kernel weights sum to 1.0. If they sum to something else, the output will be scaled accordingly. + +## C# Implementation + +### Standard Usage + +```csharp +// Create a custom weighted moving average (weights sum to 1.0) +double[] weights = { 0.1, 0.2, 0.3, 0.4 }; +var conv = new Conv(weights); + +TValue result = conv.Update(new TValue(DateTime.Now, 100.0)); +Console.WriteLine(result.Value); +``` + +### Span API (High Performance) + +```csharp +double[] weights = { 0.1, 0.2, 0.3, 0.4 }; +ReadOnlySpan input = ...; +Span output = new double[input.Length]; + +Conv.Calculate(input, output, weights); +``` + +### Bar Correction + +```csharp +var conv = new Conv(weights); + +// Initial update for the bar +conv.Update(new TValue(time, 100.0), isNew: true); + +// Update with corrected price for the same bar +conv.Update(new TValue(time, 101.0), isNew: false); +``` + +## Interpretation Details + +Convolution can be used in various ways depending on the kernel design: + +* **Trend identification:** With appropriate kernels (e.g., Gaussian, SMA weights), convolution can identify trends while filtering out noise. +* **Specialized filtering:** Custom kernels can be designed to target specific price patterns or cycles. +* **Moving average replication:** Convolution can replicate virtually any other moving average by using the appropriate kernel. +* **Differentiation:** If weights sum to 0 (e.g., `[-1, 1]`), it acts as a momentum or rate-of-change indicator. +* **Experimental strategies:** Enables testing of novel filtering approaches not available in standard indicators. + +## Limitations and Considerations + +* **Knowledge requirement:** Requires understanding of convolution and filter design principles. +* **Parameter complexity:** More parameters to optimize compared to standard moving averages. +* **Potential overfitting:** Easy to create kernels that work well on historical data but fail on future data. +* **Computational demands:** Slightly higher computational requirements than hardcoded implementations, though optimized with SIMD in this library. +* **Validation necessity:** Custom kernels require thorough testing to ensure desired filtering characteristics. + +## References + +* Smith, S.W. "The Scientist and Engineer's Guide to Digital Signal Processing," Chapter 7: Properties of Convolution +* Ehlers, J.F. "Cycle Analytics for Traders," Wiley, 2013 +* [Convolution on Wikipedia](https://en.wikipedia.org/wiki/Convolution)