mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-23 21:18:04 +00:00
SIMD Refactor: Merge simd-dev into dev (#55)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) <aider@aider.chat> Co-authored-by: Warp <agent@warp.dev>
This commit is contained in:
co-authored by
Claude Opus 4.5
aider
Warp
parent
5bcdf8d614
commit
86fe32a682
@@ -0,0 +1,175 @@
|
||||
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(0, ConvIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConvIndicator_ShortName_IncludesSource()
|
||||
{
|
||||
var indicator = new ConvIndicator();
|
||||
|
||||
Assert.Contains("CONV", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("Close", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConvIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new ConvIndicator();
|
||||
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Conv.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[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_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, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
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 = null!;
|
||||
protected LineSeries Series;
|
||||
protected string SourceName = null!;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 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 LineSeries(name: "CONV", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(Series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
try
|
||||
{
|
||||
var weightStrings = WeightsInput.Split(',');
|
||||
var weights = new double[weightStrings.Length];
|
||||
for (int i = 0; i < weightStrings.Length; i++)
|
||||
{
|
||||
weights[i] = double.Parse(weightStrings[i].Trim(), System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
_conv = new Conv(weights.Length == 0 ? [1.0] : weights);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
_conv = new Conv([1.0]);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
_conv = new Conv([1.0]);
|
||||
}
|
||||
|
||||
SourceName = Source.ToString();
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
|
||||
|
||||
TValue result = _conv.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), isNew: args.IsNewBar());
|
||||
|
||||
Series.SetValue(result.Value, _conv.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
|
||||
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
|
||||
|
||||
double[] kernel = [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()
|
||||
{
|
||||
double[] kernel = [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()
|
||||
{
|
||||
double[] kernel = [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()
|
||||
{
|
||||
double[] kernel = [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.Batch(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()
|
||||
{
|
||||
double[] kernel = [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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LeadingNaN_RemainsNaN()
|
||||
{
|
||||
double[] kernel = [1.0];
|
||||
var conv = new Conv(kernel);
|
||||
var res = conv.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
Assert.True(double.IsNaN(res.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
double[] kernel = [0.5, 1.0];
|
||||
var conv = new Conv(kernel);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
|
||||
|
||||
// Feed 10 new values
|
||||
TValue tenthInput = default;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
tenthInput = new TValue(bar.Time, bar.Close);
|
||||
conv.Update(tenthInput, isNew: true);
|
||||
}
|
||||
|
||||
// Remember state after 10 values
|
||||
double valueAfterTen = conv.Last.Value;
|
||||
|
||||
// Generate 9 corrections with isNew=false (different values)
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: false);
|
||||
conv.Update(new TValue(bar.Time, bar.Close), isNew: false);
|
||||
}
|
||||
|
||||
// Feed the remembered 10th input again with isNew=false
|
||||
TValue finalValue = conv.Update(tenthInput, isNew: false);
|
||||
|
||||
// Should match the original state after 10 values
|
||||
Assert.Equal(valueAfterTen, finalValue.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceSameResult()
|
||||
{
|
||||
// Arrange
|
||||
double[] kernel = [0.1, 0.2, 0.3, 0.4];
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
|
||||
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
// 1. Batch Mode
|
||||
var batchSeries = Conv.Batch(series, kernel);
|
||||
double expected = batchSeries.Last.Value;
|
||||
|
||||
// 2. Span Mode
|
||||
var tValues = series.Values.ToArray();
|
||||
var spanInput = new ReadOnlySpan<double>(tValues);
|
||||
var spanOutput = new double[tValues.Length];
|
||||
Conv.Batch(spanInput, spanOutput, kernel);
|
||||
double spanResult = spanOutput[^1];
|
||||
|
||||
// 3. Streaming Mode
|
||||
var streamingInd = new Conv(kernel);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
streamingInd.Update(series[i]);
|
||||
}
|
||||
double streamingResult = streamingInd.Last.Value;
|
||||
|
||||
// 4. Eventing Mode
|
||||
var pubSource = new TSeries();
|
||||
var eventingInd = new Conv(pubSource, kernel);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
pubSource.Add(series[i]);
|
||||
}
|
||||
double eventingResult = eventingInd.Last.Value;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, spanResult, 1e-9);
|
||||
Assert.Equal(expected, streamingResult, 1e-9);
|
||||
Assert.Equal(expected, eventingResult, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanCalc_ValidatesInput()
|
||||
{
|
||||
double[] source = [1, 2, 3, 4, 5];
|
||||
double[] output = new double[5];
|
||||
double[] wrongSizeOutput = new double[3];
|
||||
double[] kernel = [0.5, 0.5];
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Conv.Batch(source.AsSpan(), output.AsSpan(), Array.Empty<double>()));
|
||||
Assert.Throws<ArgumentException>(() => Conv.Batch(source.AsSpan(), wrongSizeOutput.AsSpan(), kernel));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanCalc_HandlesNaN()
|
||||
{
|
||||
double[] source = [100, 110, double.NaN, 120, 130];
|
||||
double[] output = new double[5];
|
||||
double[] kernel = [0.5, 0.5];
|
||||
|
||||
Conv.Batch(source.AsSpan(), output.AsSpan(), kernel);
|
||||
|
||||
foreach (var val in output)
|
||||
{
|
||||
Assert.True(double.IsFinite(val));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
using QuanTAlib.Tests;
|
||||
using Skender.Stock.Indicators;
|
||||
using OoplesFinance.StockIndicators;
|
||||
using OoplesFinance.StockIndicators.Models;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public sealed class ConvValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _testData;
|
||||
private bool _disposed;
|
||||
|
||||
public ConvValidationTests()
|
||||
{
|
||||
_testData = new ValidationTestData(count: 1000, seed: 123);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_testData?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static double[] GenerateWmaKernel(int period)
|
||||
{
|
||||
double divisor = period * (period + 1) / 2.0;
|
||||
double[] kernel = new double[period];
|
||||
for (int i = 0; i < period; i++)
|
||||
{
|
||||
kernel[i] = (i + 1) / divisor;
|
||||
}
|
||||
return kernel;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Against_Sma()
|
||||
{
|
||||
// SMA(10) is equivalent to Conv with 10 weights of 1/10
|
||||
const 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);
|
||||
|
||||
for (int i = 0; i < _testData.Data.Count; i++)
|
||||
{
|
||||
var item = _testData.Data[i];
|
||||
var smaVal = sma.Update(item);
|
||||
var convVal = conv.Update(item);
|
||||
|
||||
if (i >= period) // Skip warmup
|
||||
{
|
||||
Assert.Equal(smaVal.Value, convVal.Value, ValidationHelper.DefaultTolerance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Against_Wma()
|
||||
{
|
||||
int period = 10;
|
||||
double[] kernel = GenerateWmaKernel(period);
|
||||
|
||||
var wma = new Wma(period);
|
||||
var conv = new Conv(kernel);
|
||||
|
||||
for (int i = 0; i < _testData.Data.Count; i++)
|
||||
{
|
||||
var item = _testData.Data[i];
|
||||
var wmaVal = wma.Update(item);
|
||||
var convVal = conv.Update(item);
|
||||
|
||||
if (i >= period) // Skip warmup
|
||||
{
|
||||
Assert.Equal(wmaVal.Value, convVal.Value, ValidationHelper.DefaultTolerance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[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++)
|
||||
{
|
||||
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);
|
||||
|
||||
for (int i = 0; i < _testData.Data.Count; i++)
|
||||
{
|
||||
var item = _testData.Data[i];
|
||||
var trimaVal = trima.Update(item);
|
||||
var convVal = conv.Update(item);
|
||||
|
||||
if (i >= period) // Skip warmup
|
||||
{
|
||||
Assert.Equal(trimaVal.Value, convVal.Value, ValidationHelper.DefaultTolerance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Against_Skender_Wma()
|
||||
{
|
||||
int period = 14;
|
||||
var skenderWma = _testData.SkenderQuotes.GetWma(period).ToList();
|
||||
double[] kernel = GenerateWmaKernel(period);
|
||||
var conv = new Conv(kernel);
|
||||
var result = conv.Update(_testData.Data);
|
||||
|
||||
ValidationHelper.VerifyData(result, skenderWma, (s) => s.Wma, skip: period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Against_TALib_Wma()
|
||||
{
|
||||
int period = 14;
|
||||
double[] input = _testData.Data.Values.ToArray();
|
||||
double[] output = new double[input.Length];
|
||||
|
||||
var retCode = TALib.Functions.Wma<double>(input, 0..^0, output, out var outRange, period);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
double[] kernel = GenerateWmaKernel(period);
|
||||
var conv = new Conv(kernel);
|
||||
var result = conv.Update(_testData.Data);
|
||||
|
||||
ValidationHelper.VerifyData(result, output, outRange, lookback: period - 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Against_Tulip_Wma()
|
||||
{
|
||||
int period = 14;
|
||||
double[] input = _testData.Data.Values.ToArray();
|
||||
|
||||
var wmaIndicator = Tulip.Indicators.wma;
|
||||
double[][] inputs = { input };
|
||||
double[] options = { period };
|
||||
double[][] outputs = { new double[input.Length - period + 1] };
|
||||
|
||||
wmaIndicator.Run(inputs, options, outputs);
|
||||
double[] output = outputs[0];
|
||||
|
||||
double[] kernel = GenerateWmaKernel(period);
|
||||
var conv = new Conv(kernel);
|
||||
var result = conv.Update(_testData.Data);
|
||||
|
||||
ValidationHelper.VerifyData(result, output, lookback: period - 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Against_Ooples_Wma()
|
||||
{
|
||||
int period = 14;
|
||||
var ooplesData = _testData.SkenderQuotes.Select(q => new TickerData
|
||||
{
|
||||
Date = q.Date,
|
||||
Open = (double)q.Open,
|
||||
High = (double)q.High,
|
||||
Low = (double)q.Low,
|
||||
Close = (double)q.Close,
|
||||
Volume = (double)q.Volume
|
||||
}).ToList();
|
||||
|
||||
var stockData = new StockData(ooplesData);
|
||||
var ooplesWma = stockData.CalculateWeightedMovingAverage(length: period).OutputValues["Wma"];
|
||||
|
||||
double[] kernel = GenerateWmaKernel(period);
|
||||
var conv = new Conv(kernel);
|
||||
var result = conv.Update(_testData.Data);
|
||||
|
||||
ValidationHelper.VerifyData(result, ooplesWma, (s) => s, skip: period, tolerance: ValidationHelper.OoplesTolerance);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
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.
|
||||
///
|
||||
/// IMPORTANT: This class implements IDisposable. When using the constructor with ITValuePublisher,
|
||||
/// you MUST dispose the instance to unsubscribe from the source event and prevent memory leaks.
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Conv : AbstractBase
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly double[] _kernel;
|
||||
private readonly RingBuffer _buffer;
|
||||
private readonly ITValuePublisher? _source;
|
||||
private readonly TValuePublishedHandler? _subHandler;
|
||||
private bool _isNew = true;
|
||||
|
||||
private record struct State(double LastValidValue);
|
||||
private State _state;
|
||||
private State _p_state;
|
||||
|
||||
public bool IsNew => _isNew;
|
||||
public override bool IsHot => _buffer.IsFull;
|
||||
|
||||
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})";
|
||||
WarmupPeriod = _period;
|
||||
_state.LastValidValue = double.NaN;
|
||||
_p_state.LastValidValue = double.NaN;
|
||||
}
|
||||
|
||||
public Conv(ITValuePublisher source, double[] kernel) : this(kernel)
|
||||
{
|
||||
_source = source;
|
||||
_subHandler = Handle;
|
||||
_source.Pub += _subHandler;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (_source != null && _subHandler != null)
|
||||
{
|
||||
_source.Pub -= _subHandler;
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double GetValidValue(double input)
|
||||
{
|
||||
if (double.IsFinite(input))
|
||||
{
|
||||
_state.LastValidValue = input;
|
||||
return input;
|
||||
}
|
||||
return _state.LastValidValue;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
_isNew = isNew;
|
||||
if (isNew)
|
||||
{
|
||||
_p_state = _state;
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = _p_state;
|
||||
}
|
||||
|
||||
double val = GetValidValue(input.Value);
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
_buffer.Add(val);
|
||||
}
|
||||
else
|
||||
{
|
||||
_buffer.UpdateNewest(val);
|
||||
}
|
||||
|
||||
double result = 0;
|
||||
if (_buffer.Count > 0)
|
||||
{
|
||||
int count = _buffer.Count;
|
||||
int kernelOffset = _period - count;
|
||||
ReadOnlySpan<double> kernelSpan = _kernel.AsSpan()[kernelOffset..];
|
||||
ReadOnlySpan<double> internalBuf = _buffer.InternalBuffer;
|
||||
|
||||
if (count < _period)
|
||||
{
|
||||
result = internalBuf[..count].DotProduct(kernelSpan);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Full: data is split at StartIndex (which points to oldest)
|
||||
int head = _buffer.StartIndex;
|
||||
int part1Len = _period - head;
|
||||
result = internalBuf.Slice(head, part1Len).DotProduct(kernelSpan[..part1Len])
|
||||
+ internalBuf[..head].DotProduct(kernelSpan[part1Len..]);
|
||||
}
|
||||
}
|
||||
|
||||
Last = new TValue(input.Time, result);
|
||||
PubEvent(Last);
|
||||
return Last;
|
||||
}
|
||||
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
if (source.Count == 0) return [];
|
||||
|
||||
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;
|
||||
|
||||
Batch(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)
|
||||
{
|
||||
_state.LastValidValue = double.NaN;
|
||||
for (int i = startIndex - 1; i >= 0; i--)
|
||||
{
|
||||
if (double.IsFinite(sourceValues[i]))
|
||||
{
|
||||
_state.LastValidValue = sourceValues[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_state.LastValidValue = double.NaN;
|
||||
}
|
||||
|
||||
_buffer.Clear();
|
||||
|
||||
// Replay
|
||||
for (int i = startIndex; i < len; i++)
|
||||
{
|
||||
double val = GetValidValue(sourceValues[i]);
|
||||
_buffer.Add(val);
|
||||
}
|
||||
|
||||
// Set Last
|
||||
Last = new TValue(source.Times[len - 1], vSpan[len - 1]);
|
||||
|
||||
// Save state for isNew=false
|
||||
_p_state = _state;
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
foreach (var value in source)
|
||||
{
|
||||
Update(new TValue(DateTime.MinValue, value));
|
||||
}
|
||||
}
|
||||
|
||||
public static TSeries Batch(TSeries source, double[] kernel)
|
||||
{
|
||||
var conv = new Conv(kernel);
|
||||
return conv.Update(source);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Batch(ReadOnlySpan<double> source, Span<double> output, double[] kernel)
|
||||
{
|
||||
if (source.Length != output.Length)
|
||||
throw new ArgumentException("Source and output must have the same length", nameof(output));
|
||||
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 = double.NaN;
|
||||
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 = window[..count].DotProduct(kernelSpan[kernelOffset..]);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Full buffer - branchless version
|
||||
int part1Len = period - windowIdx;
|
||||
sum = window.Slice(windowIdx, part1Len).DotProduct(kernelSpan[..part1Len])
|
||||
+ window[..windowIdx].DotProduct(kernelSpan[part1Len..]);
|
||||
}
|
||||
|
||||
output[i] = sum;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
_buffer.Clear();
|
||||
_state.LastValidValue = double.NaN;
|
||||
_p_state.LastValidValue = double.NaN;
|
||||
Last = default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
# CONV: Convolution Moving Average
|
||||
|
||||
> "If you want a moving average that behaves exactly how you want it to, build it yourself. CONV is the 'Bring Your Own Kernel' of indicators."
|
||||
|
||||
CONV (Convolution Moving Average) is the ultimate tool for the signal processing purist. It doesn't presume to know what kind of smoothing you need; it simply asks for a kernel (a set of weights) and applies it to the data. Want a Gaussian filter? A Sinc filter? A custom edge-detection filter? CONV runs them all.
|
||||
|
||||
## Historical Context
|
||||
|
||||
Convolution is the fundamental operation of digital signal processing (DSP). While traders were busy inventing "new" moving averages by tweaking alpha values, engineers were using convolution to process audio, images, and radar signals for decades. CONV brings this raw power to financial time series, allowing for arbitrary FIR (Finite Impulse Response) filtering.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
CONV applies a sliding dot product between the data window and your custom kernel. The "physics" are entirely defined by the kernel you provide.
|
||||
|
||||
* **Symmetric Kernel**: Zero phase shift (if centered correctly).
|
||||
* **Asymmetric Kernel**: Introduces lag or lead.
|
||||
* **Positive Weights**: Smoothing.
|
||||
* **Mixed Weights**: Differentiation or band-pass filtering.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
The value at time $t$ is the sum of the element-wise product of the kernel $K$ and the price vector $P$:
|
||||
|
||||
$$ \text{CONV}_t = \sum_{i=0}^{N-1} P_{t-i} \cdot K_i $$
|
||||
|
||||
Where:
|
||||
|
||||
* $N$ is the length of the kernel.
|
||||
* $K_0$ multiplies the most recent price (or oldest, depending on convention; the QuanTAlib implementation aligns $K_0$ with the oldest data in the window and $K_{N-1}$ with the newest).
|
||||
|
||||
## Performance Profile
|
||||
|
||||
Performance depends linearly on the kernel length ($N$).
|
||||
|
||||
### Operation Count (Streaming Mode, Scalar)
|
||||
|
||||
Per-bar cost for kernel length $N$:
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| MUL | N | 3 | 3N |
|
||||
| ADD | N | 1 | N |
|
||||
| **Total** | **2N** | — | **~4N cycles** |
|
||||
|
||||
For a typical kernel length of 14:
|
||||
- **Total**: ~56 cycles per bar
|
||||
|
||||
**Complexity**: O(N) — linear with kernel length. No recursion, pure FIR convolution.
|
||||
|
||||
### Batch Mode (SIMD/FMA Analysis)
|
||||
|
||||
CONV's dot product structure is ideal for SIMD vectorization:
|
||||
|
||||
| Operation | Scalar Ops | SIMD Ops (AVX2) | Speedup |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| MUL+ADD (FMA) | 2N | N/4 (FMA256) | 8× |
|
||||
| Horizontal sum | — | 1 | — |
|
||||
|
||||
**Batch efficiency (512 bars, N=14):**
|
||||
|
||||
| Mode | Cycles/bar | Total (512 bars) | Improvement |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| Scalar streaming | 56 | 28,672 | — |
|
||||
| SIMD batch (FMA) | ~10 | ~5,120 | **~82%** |
|
||||
|
||||
SIMD achieves excellent speedup because the dot product is embarrassingly parallel.
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **Accuracy** | 10/10 | Exact convolution to double precision |
|
||||
| **Timeliness** | Variable | Depends on kernel (symmetric = lag N/2) |
|
||||
| **Overshoot** | Variable | Depends on kernel design |
|
||||
| **Smoothness** | Variable | Kernel-dependent |
|
||||
|
||||
Quality characteristics are entirely determined by the user-provided kernel.
|
||||
|
||||
### Zero-Allocation Design
|
||||
|
||||
CONV stores the kernel in a pre-allocated array. The `Update` method performs a dot product using a circular buffer for the price history, requiring no new allocations.
|
||||
|
||||
## Validation
|
||||
|
||||
Validation is performed by reproducing standard moving averages (SMA, WMA, TRIMA) using their equivalent kernels and comparing against external libraries.
|
||||
|
||||
| Library | Status | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| **QuanTAlib** | ✅ | Validated against internal SMA, WMA, TRIMA. |
|
||||
| **Skender** | ✅ | Validated against WMA (using WMA kernel). |
|
||||
| **TA-Lib** | ✅ | Validated against WMA (using WMA kernel). |
|
||||
| **Tulip** | ✅ | Validated against WMA (using WMA kernel). |
|
||||
| **Ooples** | ✅ | Validated against WMA (using WMA kernel). |
|
||||
|
||||
### C# Implementation Considerations
|
||||
|
||||
The QuanTAlib CONV implementation optimizes convolution through pre-allocation and SIMD-accelerated dot products:
|
||||
|
||||
**Defensive Kernel Copy**
|
||||
```csharp
|
||||
_kernel = new double[_period];
|
||||
Array.Copy(kernel, _kernel, _period);
|
||||
```
|
||||
The kernel is copied to prevent external mutation. This one-time allocation at construction ensures the indicator owns its weight array.
|
||||
|
||||
**State Record Struct**
|
||||
```csharp
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(double LastValidValue);
|
||||
private State _state;
|
||||
private State _p_state;
|
||||
```
|
||||
Minimal state (just last valid value) enables efficient bar correction via `_p_state` snapshot/restore.
|
||||
|
||||
**Circular Buffer Dot Product**
|
||||
```csharp
|
||||
int head = _buffer.StartIndex;
|
||||
int part1Len = _period - head;
|
||||
result = internalBuf.Slice(head, part1Len).DotProduct(kernelSpan[..part1Len])
|
||||
+ internalBuf[..head].DotProduct(kernelSpan[part1Len..]);
|
||||
```
|
||||
Full buffer requires two `DotProduct` calls to handle the circular wrap. The `DotProduct` extension leverages AVX2/FMA intrinsics when available.
|
||||
|
||||
**Stackalloc for Batch Processing**
|
||||
```csharp
|
||||
Span<double> window = period <= 256 ? stackalloc double[period] : new double[period];
|
||||
```
|
||||
Small kernels (≤256 elements) use stack allocation to avoid heap pressure during batch operations.
|
||||
|
||||
**Pre-sized Output Collections**
|
||||
```csharp
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
```
|
||||
Batch processing pre-sizes lists to avoid reallocation during population.
|
||||
|
||||
**Memory Layout**
|
||||
|
||||
| Field | Type | Size | Notes |
|
||||
|:------|:-----|-----:|:------|
|
||||
| `_period` | int | 4B | Kernel length |
|
||||
| `_kernel` | double[] | 8B + N×8B | Weight array reference + data |
|
||||
| `_buffer` | RingBuffer | ~40B + N×8B | Circular data buffer |
|
||||
| `_state` | State | 8B | Current last valid value |
|
||||
| `_p_state` | State | 8B | Previous state for rollback |
|
||||
| **Total** | | ~68B + 2N×8B | Plus object overhead |
|
||||
|
||||
For a typical 14-period kernel: ~68 + 224 ≈ **292 bytes** per instance.
|
||||
|
||||
### Common Pitfalls
|
||||
|
||||
1. **Kernel Direction**: Our implementation applies the kernel such that the last element of the kernel multiplies the most recent data point. If you import kernels from other DSP libraries, you might need to reverse them.
|
||||
2. **Normalization**: Kernel weights are *not* automatically normalized. If the sum of the weights is not 1.0, the output scale will be different from the input scale. This is a feature, not a bug (allows for differential filters).
|
||||
3. **Performance**: A kernel size of 1000 will be 100x slower than a kernel size of 10. Use FFT-based convolution for massive kernels (not implemented here; this is for trading, not searching for extraterrestrial life).
|
||||
@@ -0,0 +1,48 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Convolution Moving Average (CONV)", "CONV", overlay=true)
|
||||
|
||||
//@function Calculates a convolution MA using any custom kernel
|
||||
//@param source Series to calculate CONV from
|
||||
//@param kernel Array of weights to use as convolution kernel
|
||||
//@returns CONV value, calculates from first bar using available data
|
||||
//@optimized Uses custom kernel convolution with O(n) complexity per bar due to lookback loop
|
||||
conv(series float source, simple array<float> kernel) =>
|
||||
int kernel_size = array.size(kernel)
|
||||
if kernel_size <= 0
|
||||
runtime.error("Kernel must not be empty")
|
||||
var array<float> norm_kernel = array.new_float(1, 1.0)
|
||||
var int last_kernel_size = 1
|
||||
if last_kernel_size != kernel_size
|
||||
norm_kernel := array.copy(kernel)
|
||||
float kernel_sum = 0.0
|
||||
for i = 0 to kernel_size - 1
|
||||
kernel_sum += array.get(kernel, i)
|
||||
if kernel_sum != 0.0
|
||||
float inv_sum = 1.0 / kernel_sum
|
||||
for i = 0 to kernel_size - 1
|
||||
array.set(norm_kernel, i, array.get(kernel, i) * inv_sum)
|
||||
last_kernel_size := kernel_size
|
||||
int p = math.min(bar_index + 1, kernel_size)
|
||||
float sum = 0.0
|
||||
float weight_sum = 0.0
|
||||
for i = 0 to p - 1
|
||||
float price = source[i]
|
||||
if not na(price)
|
||||
float w = array.get(norm_kernel, i)
|
||||
sum += price * w
|
||||
weight_sum += w
|
||||
nz(sum / weight_sum, source)
|
||||
|
||||
// ---------- Main loop ----------
|
||||
|
||||
// Inputs
|
||||
i_source = input.source(close, "Source")
|
||||
i_kernel = array.from(1.0, 2.5, -3.14, 0.0, 1.0)
|
||||
|
||||
// Calculation
|
||||
conv_value = conv(i_source, i_kernel)
|
||||
|
||||
// Plot
|
||||
plot(conv_value, "CONV", color=color.yellow, linewidth=2)
|
||||
Reference in New Issue
Block a user