Merge branch 'dev'

This commit is contained in:
Miha Kralj
2026-03-10 20:34:18 -07:00
623 changed files with 191935 additions and 163878 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)
+5 -5
View File
@@ -4,17 +4,17 @@
| ---------------- | -------------------------------- |
| **Category** | Channel |
| **Inputs** | OHLCV bar (TBar) |
| **Parameters** | None |
| **Outputs** | Single series (Apchannel) |
| **Parameters** | `alpha` (default 0.2) |
| **Outputs** | Multiple series (Upper, Lower) |
| **Output range** | Tracks input |
| **Warmup** | 1 bar |
| **Warmup** | `⌈3/alpha⌉` bars (default 15) |
### TL;DR
- APCHANNEL applies exponential smoothing independently to price highs and lows, creating a dynamic envelope that "remembers" significant extremes wh...
- No configurable parameters; computation is stateless per bar.
- Parameterized by `alpha` (default 0.2).
- Output range: Tracks input.
- Requires 1 bar of warmup before first valid output (IsHot = true).
- Requires `⌈3/alpha⌉` bars (default 15) of warmup before first valid output (IsHot = true).
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
APCHANNEL applies exponential smoothing independently to price highs and lows, creating a dynamic envelope that "remembers" significant extremes while gradually fading their influence over time. Unlike rigid Donchian channels that drop price extremes abruptly when they exit the lookback window (the "cliff effect"), APCHANNEL decays them smoothly through leaky integration. The result is a channel with continuously sloping boundaries that responds to volatility without the discontinuous jumps that plague fixed-window approaches. The algorithm is $O(1)$ per bar with only two state variables and no buffers.
+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)
+2 -2
View File
@@ -7,14 +7,14 @@
| **Parameters** | `period`, `phase` (default 0) |
| **Outputs** | Multiple series (Upper, Lower) |
| **Output range** | Tracks input |
| **Warmup** | 1 bar |
| **Warmup** | `⌈20 + 80 × period^0.36⌉` bars |
### TL;DR
- JBANDS expose the internal adaptive envelope mechanism of the Jurik Moving Average (JMA), producing asymmetric bands that snap instantly to new pri...
- Parameterized by `period`, `phase` (default 0).
- Output range: Tracks input.
- Requires 1 bar of warmup before first valid output (IsHot = true).
- Requires `⌈20 + 80 × period^0.36⌉` bars of warmup before first valid output (IsHot = true).
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
JBANDS expose the internal adaptive envelope mechanism of the Jurik Moving Average (JMA), producing asymmetric bands that snap instantly to new price extremes and decay exponentially during consolidation. Unlike standard volatility bands (Bollinger, Keltner) which maintain symmetric width around a center line, JBANDS feature "snap-and-decay" hysteresis: expansion is instantaneous (plasticity), contraction is gradual (elasticity). The decay rate is dynamically modulated by a two-stage volatility estimator — a 10-bar SMA feeding a 128-bar trimmed mean — making the bands tight during quiet markets and expansive during trends. The center line is the full JMA: a 2-pole IIR filter with phase control and adaptive alpha.
+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
+2 -2
View File
@@ -7,14 +7,14 @@
| **Parameters** | `strPeriod` (default DefaultStrPeriod), `centerPeriod` (default DefaultCenterPeriod), `multiplier` (default DefaultMultiplier) |
| **Outputs** | Multiple series (Upper, Middle, Lower, STR) |
| **Output range** | Tracks input |
| **Warmup** | 1 bar |
| **Warmup** | `Math.Max(strPeriod, centerPeriod)` bars |
### TL;DR
- Ehlers Ultimate Channel applies the Ultrasmooth Filter (USF) twice: once to the close price for the centerline and once to True Range for band widt...
- Parameterized by `strperiod` (default defaultstrperiod), `centerperiod` (default defaultcenterperiod), `multiplier` (default defaultmultiplier).
- Output range: Tracks input.
- Requires 1 bar of warmup before first valid output (IsHot = true).
- Requires `Math.Max(strPeriod, centerPeriod)` bars of warmup before first valid output (IsHot = true).
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
Ehlers Ultimate Channel applies the Ultrasmooth Filter (USF) twice: once to the close price for the centerline and once to True Range for band width, creating a channel where both the trend estimate and the volatility measure share the same low-lag, zero-overshoot filter characteristics. Unlike UBANDS which uses RMS of price residuals, UCHANNEL uses Smoothed True Range (STR) for band width, making it responsive to gap-inclusive volatility. Separate period parameters allow independent tuning of centerline smoothness and band-width responsiveness.
+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)