feat: add new indicators (Decay, Edecay, MinusDi, MinusDm, PlusDi, PlusDm, Maxindex, Minindex, Sarext) and update pine scripts, core libs, validation tests, and python bindings

This commit is contained in:
Miha Kralj
2026-03-09 13:45:46 -07:00
parent 8e43d62cbb
commit 031f1b5fe6
491 changed files with 6156 additions and 5590 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
// The MIT License (MIT)
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Aberration (ABERR)", "ABERR", overlay=true)
+1 -1
View File
@@ -1,4 +1,4 @@
// The MIT License (MIT)
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Acceleration Bands (ACCBANDS)", "ACCBANDS", overlay=true)
+1 -1
View File
@@ -1,4 +1,4 @@
// The MIT License (MIT)
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Adaptive Price Channel (APCHANNEL)", "APCHANNEL", overlay=true)
+1 -1
View File
@@ -1,4 +1,4 @@
// The MIT License (MIT)
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Adaptive Price Zone", "APZ", overlay=true)
+1 -1
View File
@@ -1,4 +1,4 @@
// The MIT License (MIT)
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("ATR Bands (ATRBANDS)", "ATRBANDS", overlay=true)
+1 -1
View File
@@ -1,4 +1,4 @@
// The MIT License (MIT)
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Bollinger Bands (BBANDS)", "BBANDS", overlay=true)
+1 -1
View File
@@ -1,4 +1,4 @@
// The MIT License (MIT)
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Donchian Channels (DCHANNEL)", "DCHANNEL", overlay=true)
+1 -1
View File
@@ -1,4 +1,4 @@
// The MIT License (MIT)
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Decay Min-Max Channel (DECAYCHANNEL)", "DECAYCHANNEL", overlay=true)
+1 -1
View File
@@ -1,4 +1,4 @@
// The MIT License (MIT)
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Fractal Chaos Bands (FCB)", "FCB", overlay=true)
+1 -1
View File
@@ -1,4 +1,4 @@
// The MIT License (MIT)
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Jurik Adaptive Envelope Bands", "JBANDS", overlay=true)
+1 -1
View File
@@ -1,4 +1,4 @@
// The MIT License (MIT)
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Keltner Channel (KCHANNEL)", "KCHANNEL", overlay=true)
+1 -1
View File
@@ -1,4 +1,4 @@
// The MIT License (MIT)
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("MA Envelope (MAE)", "MAE", overlay=true)
+1 -1
View File
@@ -1,4 +1,4 @@
// The MIT License (MIT)
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Min-Max Channel (MMCHANNEL)", "MMCHANNEL", overlay=true)
+1 -1
View File
@@ -1,4 +1,4 @@
// The MIT License (MIT)
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Price Channel (PCHANNEL)", "PCHANNEL", overlay=true)
+109 -101
View File
@@ -1,3 +1,4 @@
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -206,7 +207,7 @@ public sealed class Regchannel : ITValuePublisher
for (int i = 0; i < count; i++)
{
sumY += values[i];
sumXY += i * values[i];
sumXY = Math.FusedMultiplyAdd((double)i, values[i], sumXY);
}
double n = count;
@@ -325,7 +326,11 @@ public sealed class Regchannel : ITValuePublisher
/// <summary>
/// Batch calculation using spans.
/// O(period) per bar: sums recomputed from circular buffer each bar for numerical
/// consistency with the streaming path, plus closed-form residual variance:
/// sumResiduals² = sumY² intercept·sumY slope·sumXY, eliminating a second O(period) pass.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public static void Batch(
ReadOnlySpan<double> source,
Span<double> middle,
@@ -360,121 +365,124 @@ public sealed class Regchannel : ITValuePublisher
double sumX2Full = (period - 1.0) * period * (2.0 * period - 1.0) / 6.0;
double denomFull = period * sumX2Full - sumXFull * sumXFull;
// Track last valid value for NaN substitution
double lastValid = double.NaN;
for (int i = 0; i < len; i++)
// Circular buffer of NaN-sanitised values for O(1) sliding-window recurrences.
const int StackAllocThreshold = 256;
double[]? rentedWindow = null;
scoped Span<double> window;
if (period <= StackAllocThreshold)
{
// Get valid value with last-valid substitution
double currentValue = source[i];
if (double.IsFinite(currentValue))
{
lastValid = currentValue;
}
else
{
currentValue = lastValid;
}
window = stackalloc double[period];
}
else
{
rentedWindow = ArrayPool<double>.Shared.Rent(period);
window = rentedWindow.AsSpan(0, period);
}
// If still NaN (no valid value seen yet), output NaN
if (!double.IsFinite(currentValue))
try
{
window.Clear();
double lastValid = double.NaN;
int head = 0, count = 0;
for (int i = 0; i < len; i++)
{
middle[i] = double.NaN;
upper[i] = double.NaN;
lower[i] = double.NaN;
continue;
}
int count = Math.Min(i + 1, period);
int start = i - count + 1;
if (count <= 1)
{
middle[i] = currentValue;
upper[i] = currentValue;
lower[i] = currentValue;
continue;
}
// Calculate sums for linear regression with NaN handling
double sumY = 0;
double sumXY = 0;
double lastValidInWindow = double.NaN;
for (int j = 0; j < count; j++)
{
double rawY = source[start + j];
double y;
if (double.IsFinite(rawY))
// NaN substitution
double y = source[i];
if (double.IsFinite(y))
{
lastValidInWindow = rawY;
y = rawY;
lastValid = y;
}
else
{
y = double.IsFinite(lastValidInWindow) ? lastValidInWindow : 0.0;
y = lastValid;
}
sumY += y;
sumXY += j * y;
}
double n = count;
double sx, denom;
if (count < period)
{
sx = 0.5 * n * (n - 1);
double sx2 = (n - 1.0) * n * (2.0 * n - 1.0) / 6.0;
denom = n * sx2 - sx * sx;
}
else
{
sx = sumXFull;
denom = denomFull;
}
double slope, intercept, regression;
if (Math.Abs(denom) < 1e-10)
{
slope = 0;
intercept = sumY / n;
regression = intercept;
}
else
{
slope = (n * sumXY - sx * sumY) / denom;
intercept = (sumY - slope * sx) / n;
regression = Math.FusedMultiplyAdd(slope, count - 1, intercept);
}
// Calculate standard deviation of residuals with NaN handling
double sumResiduals2 = 0;
lastValidInWindow = double.NaN;
for (int j = 0; j < count; j++)
{
double rawY = source[start + j];
double y;
if (double.IsFinite(rawY))
// No valid value seen yet
if (!double.IsFinite(y))
{
lastValidInWindow = rawY;
y = rawY;
middle[i] = double.NaN;
upper[i] = double.NaN;
lower[i] = double.NaN;
continue;
}
// Store in circular buffer
window[head] = y;
head = (head + 1) % period;
if (count < period)
{
count++;
}
if (count <= 1)
{
middle[i] = y;
upper[i] = y;
lower[i] = y;
continue;
}
// Recompute sums fresh from circular buffer (oldest-to-newest).
// This matches the streaming Update() path numerically.
double sumY = 0, sumXY = 0, sumY2 = 0;
int oldestIdx = (head - count + period) % period;
for (int k = 0; k < count; k++)
{
double wk = window[(oldestIdx + k) % period];
sumY += wk;
sumXY = Math.FusedMultiplyAdd((double)k, wk, sumXY);
sumY2 = Math.FusedMultiplyAdd(wk, wk, sumY2);
}
double n = count;
double sx, denom;
if (count < period)
{
sx = 0.5 * n * (n - 1);
double sx2 = (n - 1.0) * n * (2.0 * n - 1.0) / 6.0;
denom = n * sx2 - sx * sx;
}
else
{
y = double.IsFinite(lastValidInWindow) ? lastValidInWindow : 0.0;
sx = sumXFull;
denom = denomFull;
}
double predicted = Math.FusedMultiplyAdd(slope, j, intercept);
double residual = y - predicted;
sumResiduals2 = Math.FusedMultiplyAdd(residual, residual, sumResiduals2);
double slope, intercept, regression;
if (Math.Abs(denom) < 1e-10)
{
slope = 0;
intercept = sumY / n;
regression = intercept;
}
else
{
slope = (n * sumXY - sx * sumY) / denom;
intercept = (sumY - slope * sx) / n;
regression = Math.FusedMultiplyAdd(slope, count - 1, intercept);
}
// Closed-form residual variance (normal-equation identity):
// sumResiduals² = sumY² intercept·sumY slope·sumXY
double sumResiduals2 = Math.Max(0.0, sumY2 - intercept * sumY - slope * sumXY);
double stdDev = Math.Sqrt(sumResiduals2 / n);
double band = multiplier * stdDev;
middle[i] = regression;
upper[i] = regression + band;
lower[i] = regression - band;
}
}
finally
{
if (rentedWindow != null)
{
ArrayPool<double>.Shared.Return(rentedWindow);
}
double stdDev = Math.Sqrt(sumResiduals2 / n);
double band = multiplier * stdDev;
middle[i] = regression;
upper[i] = regression + band;
lower[i] = regression - band;
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
// The MIT License (MIT)
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Regression Channels (REGCHANNEL)", "REGCHANNEL", overlay=true)
+126 -64
View File
@@ -1,3 +1,4 @@
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -325,7 +326,11 @@ public sealed class Sdchannel : ITValuePublisher
/// <summary>
/// Batch calculation using spans.
/// O(period) per bar: sums and residuals recomputed from circular buffer each bar for numerical
/// consistency with the streaming path. Uses the same plain arithmetic as Update() for sumXY
/// and the same FMA residual loop to ensure streaming/batch agreement within 1e-9.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public static void Batch(
ReadOnlySpan<double> source,
Span<double> middle,
@@ -360,75 +365,130 @@ public sealed class Sdchannel : ITValuePublisher
double sumX2Full = (period - 1.0) * period * (2.0 * period - 1.0) / 6.0;
double denomFull = period * sumX2Full - sumXFull * sumXFull;
for (int i = 0; i < len; i++)
// Circular buffer of NaN-sanitised values for O(1) sliding-window recurrences.
const int StackAllocThreshold = 256;
double[]? rentedWindow = null;
scoped Span<double> window;
if (period <= StackAllocThreshold)
{
int count = Math.Min(i + 1, period);
int start = i - count + 1;
window = stackalloc double[period];
}
else
{
rentedWindow = ArrayPool<double>.Shared.Rent(period);
window = rentedWindow.AsSpan(0, period);
}
if (count <= 1)
try
{
window.Clear();
double lastValid = double.NaN;
int head = 0, count = 0;
for (int i = 0; i < len; i++)
{
middle[i] = source[i];
upper[i] = source[i];
lower[i] = source[i];
continue;
// NaN substitution matching GetValid() in streaming Update()
double y = source[i];
if (double.IsFinite(y))
{
lastValid = y;
}
else
{
y = lastValid;
}
// No valid value seen yet
if (!double.IsFinite(y))
{
middle[i] = double.NaN;
upper[i] = double.NaN;
lower[i] = double.NaN;
continue;
}
// Store in circular buffer
window[head] = y;
head = (head + 1) % period;
if (count < period)
{
count++;
}
if (count <= 1)
{
middle[i] = y;
upper[i] = y;
lower[i] = y;
continue;
}
// Recompute sums fresh from circular buffer (oldest-to-newest).
// Plain arithmetic for sumXY matches streaming Update() path numerically.
double sumY = 0, sumXY = 0;
int oldestIdx = (head - count + period) % period;
for (int k = 0; k < count; k++)
{
double wk = window[(oldestIdx + k) % period];
sumY += wk;
sumXY += (double)k * wk;
}
double n = count;
double sx, denom;
if (count < period)
{
sx = 0.5 * n * (n - 1);
double sx2 = (n - 1.0) * n * (2.0 * n - 1.0) / 6.0;
denom = n * sx2 - sx * sx;
}
else
{
sx = sumXFull;
denom = denomFull;
}
double slope, intercept, regression;
if (Math.Abs(denom) < 1e-10)
{
slope = 0;
intercept = sumY / n;
regression = intercept;
}
else
{
slope = (n * sumXY - sx * sumY) / denom;
intercept = (sumY - slope * sx) / n;
regression = Math.FusedMultiplyAdd(slope, count - 1, intercept);
}
// Explicit residual loop matching streaming Update() path numerically.
double sumResiduals2 = 0;
for (int k = 0; k < count; k++)
{
double wk = window[(oldestIdx + k) % period];
double predicted = Math.FusedMultiplyAdd(slope, k, intercept);
double residual = wk - predicted;
sumResiduals2 = Math.FusedMultiplyAdd(residual, residual, sumResiduals2);
}
double stdDev = Math.Sqrt(sumResiduals2 / n);
double band = multiplier * stdDev;
middle[i] = regression;
upper[i] = regression + band;
lower[i] = regression - band;
}
// Calculate sums for linear regression
double sumY = 0;
double sumXY = 0;
for (int j = 0; j < count; j++)
}
finally
{
if (rentedWindow != null)
{
double y = source[start + j];
sumY += y;
sumXY += j * y;
ArrayPool<double>.Shared.Return(rentedWindow);
}
double n = count;
double sx, denom;
if (count < period)
{
sx = 0.5 * n * (n - 1);
double sx2 = (n - 1.0) * n * (2.0 * n - 1.0) / 6.0;
denom = n * sx2 - sx * sx;
}
else
{
sx = sumXFull;
denom = denomFull;
}
double slope, intercept, regression;
if (Math.Abs(denom) < 1e-10)
{
slope = 0;
intercept = sumY / n;
regression = intercept;
}
else
{
slope = (n * sumXY - sx * sumY) / denom;
intercept = (sumY - slope * sx) / n;
regression = Math.FusedMultiplyAdd(slope, count - 1, intercept);
}
// Calculate standard deviation of residuals
double sumResiduals2 = 0;
for (int j = 0; j < count; j++)
{
double predicted = Math.FusedMultiplyAdd(slope, j, intercept);
double residual = source[start + j] - predicted;
sumResiduals2 = Math.FusedMultiplyAdd(residual, residual, sumResiduals2);
}
double stdDev = Math.Sqrt(sumResiduals2 / n);
double band = multiplier * stdDev;
middle[i] = regression;
upper[i] = regression + band;
lower[i] = regression - band;
}
}
@@ -464,7 +524,9 @@ public sealed class Sdchannel : ITValuePublisher
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Sdchannel Indicator) Calculate(TSeries source, int period = 20, double multiplier = 2.0)
{
var indicator = new Sdchannel(source, period, multiplier);
// Use parameterless constructor to avoid double-processing: new Sdchannel(source, ...) calls Prime(source),
// then Update(source) would call Prime again.
var indicator = new Sdchannel(period, multiplier);
var results = indicator.Update(source);
return (results, indicator);
}
+1 -1
View File
@@ -1,4 +1,4 @@
// The MIT License (MIT)
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Standard Deviation Channel (SDCHANNEL)", "SDCHANNEL", overlay=true)
+1 -1
View File
@@ -1,4 +1,4 @@
// The MIT License (MIT)
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Stoller Average Range Channel (STARCHANNEL)", "STARCHANNEL", overlay=true)
+1 -1
View File
@@ -1,4 +1,4 @@
// The MIT License (MIT)
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Super Trend Bands (STBANDS)", "STBANDS", overlay=true)
+1 -1
View File
@@ -1,4 +1,4 @@
// The MIT License (MIT)
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
// Ultimate Bands logic based on work by John F. Ehlers (c) 2024
+1 -1
View File
@@ -1,4 +1,4 @@
// The MIT License (MIT)
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
// Ultimate Channel logic based on work by John F. Ehlers (c) 2024
+1 -1
View File
@@ -1,4 +1,4 @@
// The MIT License (MIT)
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("VWAP Bands (VWAPBANDS)", "VWAPBANDS", overlay=true)
+1 -1
View File
@@ -1,4 +1,4 @@
// The MIT License (MIT)
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("VWAP with Standard Deviation Bands", "VWAPSD", overlay=true)