mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-04 20:17:43 +00:00
Convolution Indicator (CONV) with customizable kernel support
This commit is contained in:
@@ -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");
|
||||
@@ -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 | |
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user