Convolution Indicator (CONV) with customizable kernel support

This commit is contained in:
Miha Kralj
2025-12-10 11:48:52 -05:00
parent b46e83475e
commit 7a4850956b
8 changed files with 1186 additions and 92 deletions
-91
View File
@@ -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<double> Count: {Vector<double>.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");
+1 -1
View File
@@ -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 | |
+187
View File
@@ -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);
}
}
+84
View File
@@ -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);
}
}
+123
View File
@@ -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<ArgumentException>(() => new Conv(Array.Empty<double>()));
Assert.Throws<ArgumentException>(() => 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);
}
}
+123
View File
@@ -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);
}
}
}
}
+571
View File
@@ -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;
/// <summary>
/// Convolution Indicator
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[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<TValue>? 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<double> kernelSpan = _kernel.AsSpan().Slice(kernelOffset);
ReadOnlySpan<double> 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<long> t = new(len);
List<double> 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<double> a, ReadOnlySpan<double> 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<double> a, ReadOnlySpan<double> b)
{
int len = a.Length;
int i = 0;
Vector512<double> vSum = Vector512<double>.Zero;
Vector512<double> vSum2 = Vector512<double>.Zero;
Vector512<double> vSum3 = Vector512<double>.Zero;
Vector512<double> vSum4 = Vector512<double>.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<double> v256 = Avx512F.Add(vSum.GetLower(), vSum.GetUpper());
Vector128<double> lower = v256.GetLower();
Vector128<double> upper = v256.GetUpper();
Vector128<double> 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<double> a, ReadOnlySpan<double> b)
{
int len = a.Length;
int i = 0;
Vector256<double> vSum = Vector256<double>.Zero;
Vector256<double> vSum2 = Vector256<double>.Zero;
Vector256<double> vSum3 = Vector256<double>.Zero;
Vector256<double> vSum4 = Vector256<double>.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<double> lower = vSum.GetLower();
Vector128<double> upper = vSum.GetUpper();
Vector128<double> 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<double> a, ReadOnlySpan<double> b)
{
int len = a.Length;
int i = 0;
Vector128<double> vSum = Vector128<double>.Zero;
Vector128<double> vSum2 = Vector128<double>.Zero;
Vector128<double> vSum3 = Vector128<double>.Zero;
Vector128<double> vSum4 = Vector128<double>.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<double> a, ReadOnlySpan<double> b)
{
int len = a.Length;
int i = 0;
Vector128<double> vSum = Vector128<double>.Zero;
Vector128<double> vSum2 = Vector128<double>.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<double> source, Span<double> 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<double> 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<double> 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;
}
}
+97
View File
@@ -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<double> input = ...;
Span<double> 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)