Add VWAPSD (Volume Weighted Average Price with Standard Deviation Bands) implementation and validation tests

- Implemented Vwapsd class for calculating VWAP with configurable standard deviation bands.
- Added methods for updating the indicator with new bars and calculating VWAPSD using both bar series and span arrays.
- Created comprehensive validation tests for VWAPSD, including checks for consistency between streaming and batch modes, mathematical correctness, and handling of edge cases such as NaN values and zero volume bars.
- Ensured that the implementation adheres to performance standards with tests for large datasets and fractional numDevs values.
This commit is contained in:
Miha Kralj
2026-01-24 19:07:52 -08:00
parent fd6c80e8db
commit 744d680435
32 changed files with 9090 additions and 538 deletions
@@ -0,0 +1,102 @@
using Xunit;
namespace QuanTAlib.Tests;
public class UchannelQuantowerTests
{
[Fact]
public void UchannelIndicator_Constructor_SetsDefaults()
{
var indicator = new UchannelIndicator();
Assert.Equal(20, indicator.StrPeriod);
Assert.Equal(20, indicator.CenterPeriod);
Assert.Equal(1.0, indicator.Multiplier);
Assert.True(indicator.ShowColdValues);
Assert.Equal("UCHANNEL - Ehlers Ultimate Channel", indicator.Name);
}
[Fact]
public void UchannelIndicator_MinHistoryDepths_ReturnsMaxOfPeriods()
{
var indicator1 = new UchannelIndicator { StrPeriod = 10, CenterPeriod = 20 };
Assert.Equal(20, indicator1.MinHistoryDepths);
var indicator2 = new UchannelIndicator { StrPeriod = 30, CenterPeriod = 15 };
Assert.Equal(30, indicator2.MinHistoryDepths);
var indicator3 = new UchannelIndicator { StrPeriod = 25, CenterPeriod = 25 };
Assert.Equal(25, indicator3.MinHistoryDepths);
}
[Fact]
public void UchannelIndicator_ShortName_FormatsCorrectly()
{
var indicator = new UchannelIndicator
{
StrPeriod = 15,
CenterPeriod = 25,
Multiplier = 2.5
};
Assert.Equal("UCHANNEL (15,25,2.5)", indicator.ShortName);
}
[Fact]
public void UchannelIndicator_SourceCodeLink_IsValid()
{
var indicator = new UchannelIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.OrdinalIgnoreCase);
Assert.Contains("Uchannel.cs", indicator.SourceCodeLink, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void UchannelIndicator_OnInit_CreatesInternalIndicator()
{
var indicator = new UchannelIndicator
{
StrPeriod = 10,
CenterPeriod = 15,
Multiplier = 1.5
};
// OnInit is protected, but we can verify it doesn't throw
// by checking the indicator state after construction
Assert.NotNull(indicator);
}
[Fact]
public void UchannelIndicator_Parameters_CanBeModified()
{
var indicator = new UchannelIndicator();
indicator.StrPeriod = 30;
indicator.CenterPeriod = 40;
indicator.Multiplier = 2.0;
indicator.ShowColdValues = false;
Assert.Equal(30, indicator.StrPeriod);
Assert.Equal(40, indicator.CenterPeriod);
Assert.Equal(2.0, indicator.Multiplier);
Assert.False(indicator.ShowColdValues);
}
[Fact]
public void UchannelIndicator_Description_IsNotEmpty()
{
var indicator = new UchannelIndicator();
Assert.False(string.IsNullOrWhiteSpace(indicator.Description));
Assert.Contains("Ultrasmooth", indicator.Description, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void UchannelIndicator_HasCorrectLineSeries()
{
var indicator = new UchannelIndicator();
// The indicator should have 5 line series: Middle, Upper, Lower, STR, Width
Assert.NotNull(indicator);
}
}
@@ -0,0 +1,78 @@
using System.Drawing;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
public class UchannelIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("STR Period", sortIndex: 1, minimum: 1, maximum: 1000, increment: 1, decimalPlaces: 0)]
public int StrPeriod { get; set; } = 20;
[InputParameter("Center Period", sortIndex: 2, minimum: 1, maximum: 1000, increment: 1, decimalPlaces: 0)]
public int CenterPeriod { get; set; } = 20;
[InputParameter("Multiplier", sortIndex: 3, minimum: 0.1, maximum: 10.0, increment: 0.1, decimalPlaces: 1)]
public double Multiplier { get; set; } = 1.0;
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private Uchannel? uchannel;
protected LineSeries? MiddleSeries;
protected LineSeries? UpperSeries;
protected LineSeries? LowerSeries;
protected LineSeries? StrSeries;
protected LineSeries? WidthSeries;
public int MinHistoryDepths => Math.Max(StrPeriod, CenterPeriod);
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => $"UCHANNEL ({StrPeriod},{CenterPeriod},{Multiplier:F1})";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/channels/uchannel/Uchannel.cs";
public UchannelIndicator()
{
Name = "UCHANNEL - Ehlers Ultimate Channel";
Description = "Volatility channel using the Ehlers Ultrasmooth Filter (USF) for both centerline and True Range smoothing";
MiddleSeries = new("Middle", Color.Blue, 2, LineStyle.Solid);
UpperSeries = new("Upper", Color.Red, 1, LineStyle.Solid);
LowerSeries = new("Lower", Color.Green, 1, LineStyle.Solid);
StrSeries = new("STR", Color.Orange, 1, LineStyle.Dot);
WidthSeries = new("Width", Color.Gray, 1, LineStyle.Dot);
AddLineSeries(MiddleSeries);
AddLineSeries(UpperSeries);
AddLineSeries(LowerSeries);
AddLineSeries(StrSeries);
AddLineSeries(WidthSeries);
SeparateWindow = false;
OnBackGround = true;
}
protected override void OnInit()
{
uchannel = new(StrPeriod, CenterPeriod, Multiplier);
base.OnInit();
}
protected override void OnUpdate(UpdateArgs args)
{
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
double open = item[PriceType.Open];
double high = item[PriceType.High];
double low = item[PriceType.Low];
double close = item[PriceType.Close];
var time = HistoricalData.Time();
TBar input = new(time, open, high, low, close, item[PriceType.Volume]);
TValue result = uchannel!.Update(input, args.IsNewBar());
MiddleSeries!.SetValue(result.Value, uchannel.IsHot, ShowColdValues);
UpperSeries!.SetValue(uchannel.Upper.Value, uchannel.IsHot, ShowColdValues);
LowerSeries!.SetValue(uchannel.Lower.Value, uchannel.IsHot, ShowColdValues);
StrSeries!.SetValue(uchannel.STR.Value, uchannel.IsHot, ShowColdValues);
WidthSeries!.SetValue(uchannel.Width.Value, uchannel.IsHot, ShowColdValues);
}
}
+500
View File
@@ -0,0 +1,500 @@
namespace QuanTAlib.Tests;
public class UchannelTests
{
[Fact]
public void Uchannel_Constructor_ValidatesInput()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new Uchannel(0));
Assert.Throws<ArgumentOutOfRangeException>(() => new Uchannel(-1));
Assert.Throws<ArgumentOutOfRangeException>(() => new Uchannel(10, 0));
Assert.Throws<ArgumentOutOfRangeException>(() => new Uchannel(10, -1));
Assert.Throws<ArgumentOutOfRangeException>(() => new Uchannel(10, 10, 0));
Assert.Throws<ArgumentOutOfRangeException>(() => new Uchannel(10, 10, -1));
var uchannel = new Uchannel(10, 10, 1.0);
Assert.NotNull(uchannel);
}
[Fact]
public void Uchannel_Update_ReturnsValue()
{
var uchannel = new Uchannel(10, 10, 1.0);
var bar = new TBar(DateTime.UtcNow, 102.0, 98.0, 100.0, 101.0, 1000);
var result = uchannel.Update(bar);
Assert.True(double.IsFinite(result.Value));
Assert.True(double.IsFinite(uchannel.Upper.Value));
Assert.True(double.IsFinite(uchannel.Middle.Value));
Assert.True(double.IsFinite(uchannel.Lower.Value));
Assert.True(double.IsFinite(uchannel.STR.Value));
}
[Fact]
public void Uchannel_FirstValue_InitializesCorrectly()
{
var uchannel = new Uchannel(10, 10, 1.0);
var bar = new TBar(DateTime.UtcNow, 102.0, 98.0, 100.0, 101.0, 1000);
_ = uchannel.Update(bar);
// First value should be the close (USF returns input initially)
Assert.Equal(101.0, uchannel.Middle.Value, precision: 10);
// First TR = high - low = 102 - 98 = 4 (no prevClose yet)
Assert.True(double.IsFinite(uchannel.STR.Value));
}
[Fact]
public void Uchannel_Properties_Accessible()
{
var uchannel = new Uchannel(10, 10, 1.0);
Assert.False(uchannel.IsHot);
Assert.Contains("Uchannel", uchannel.Name, StringComparison.Ordinal);
Assert.Equal(10, uchannel.WarmupPeriod);
}
[Fact]
public void Uchannel_Update_IsNew_AcceptsParameter()
{
var uchannel = new Uchannel(10, 10, 1.0);
var bar1 = new TBar(DateTime.UtcNow, 102.0, 98.0, 100.0, 101.0, 1000);
var bar2 = new TBar(DateTime.UtcNow, 103.0, 99.0, 101.0, 102.0, 1000);
var result1 = uchannel.Update(bar1, isNew: true);
var result2 = uchannel.Update(bar2, isNew: false);
Assert.True(double.IsFinite(result1.Value));
Assert.True(double.IsFinite(result2.Value));
}
[Fact]
public void Uchannel_Update_IsNew_False_UpdatesValue()
{
var uchannel = new Uchannel(10, 10, 1.0);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
var bars = gbm.Fetch(15, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Process several bars
foreach (var bar in bars)
{
uchannel.Update(bar, isNew: true);
}
double beforeCorrection = uchannel.Middle.Value;
// Correct last bar with different value
var correctionBar = new TBar(DateTime.UtcNow, 250.0, 150.0, 200.0, 200.0, 1000);
uchannel.Update(correctionBar, isNew: false);
double afterCorrection = uchannel.Middle.Value;
Assert.NotEqual(beforeCorrection, afterCorrection);
}
[Fact]
public void Uchannel_IterativeCorrections_RestoreToOriginalState()
{
var uchannel = new Uchannel(5, 5, 1.0);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Process all bars
foreach (var bar in bars)
{
uchannel.Update(bar);
}
double originalMiddle = uchannel.Middle.Value;
double originalUpper = uchannel.Upper.Value;
double originalLower = uchannel.Lower.Value;
// Make multiple corrections
for (int i = 0; i < 10; i++)
{
var correctionBar = new TBar(DateTime.UtcNow, 200.0 + i, 140.0 + i, 150.0 + i, 190.0 + i, 1000);
uchannel.Update(correctionBar, isNew: false);
}
// Restore original
var lastBar = bars[^1];
uchannel.Update(lastBar, isNew: false);
double restoredMiddle = uchannel.Middle.Value;
double restoredUpper = uchannel.Upper.Value;
double restoredLower = uchannel.Lower.Value;
Assert.Equal(originalMiddle, restoredMiddle, precision: 8);
Assert.Equal(originalUpper, restoredUpper, precision: 8);
Assert.Equal(originalLower, restoredLower, precision: 8);
}
[Fact]
public void Uchannel_Reset_ClearsState()
{
var uchannel = new Uchannel(10, 10, 1.0);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
var bars = gbm.Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars)
{
uchannel.Update(bar);
}
Assert.True(uchannel.IsHot);
uchannel.Reset();
Assert.False(uchannel.IsHot);
}
[Fact]
public void Uchannel_IsHot_BecomesTrueAfterWarmup()
{
var uchannel = new Uchannel(5, 5, 1.0);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < 4; i++)
{
uchannel.Update(bars[i]);
Assert.False(uchannel.IsHot);
}
uchannel.Update(bars[4]);
Assert.True(uchannel.IsHot);
}
[Fact]
public void Uchannel_WarmupPeriod_IsMaxOfPeriods()
{
var uchannel1 = new Uchannel(5, 10, 1.0);
Assert.Equal(10, uchannel1.WarmupPeriod);
var uchannel2 = new Uchannel(20, 10, 2.0);
Assert.Equal(20, uchannel2.WarmupPeriod);
var uchannel3 = new Uchannel(15, 15, 1.5);
Assert.Equal(15, uchannel3.WarmupPeriod);
}
[Fact]
public void Uchannel_NaN_Input_UsesLastValidValue()
{
var uchannel = new Uchannel(5, 5, 1.0);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars)
{
uchannel.Update(bar);
}
var nanBar = new TBar(DateTime.UtcNow, double.NaN, double.NaN, double.NaN, double.NaN, 1000);
uchannel.Update(nanBar);
double afterNaN = uchannel.Middle.Value;
Assert.True(double.IsFinite(afterNaN));
Assert.True(double.IsFinite(uchannel.Upper.Value));
Assert.True(double.IsFinite(uchannel.Lower.Value));
}
[Fact]
public void Uchannel_Infinity_Input_UsesLastValidValue()
{
var uchannel = new Uchannel(5, 5, 1.0);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars)
{
uchannel.Update(bar);
}
var infBar = new TBar(DateTime.UtcNow, double.PositiveInfinity, double.NegativeInfinity, 100.0, double.PositiveInfinity, 1000);
uchannel.Update(infBar);
Assert.True(double.IsFinite(uchannel.Middle.Value));
Assert.True(double.IsFinite(uchannel.Upper.Value));
Assert.True(double.IsFinite(uchannel.Lower.Value));
}
[Fact]
public void Uchannel_BandRelationship_UpperGreaterThanLower()
{
var uchannel = new Uchannel(10, 10, 1.0);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars)
{
uchannel.Update(bar);
Assert.True(uchannel.Upper.Value >= uchannel.Lower.Value,
$"Upper ({uchannel.Upper.Value}) should be >= Lower ({uchannel.Lower.Value})");
}
}
[Fact]
public void Uchannel_MiddleBetweenBands()
{
var uchannel = new Uchannel(10, 10, 1.0);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars)
{
uchannel.Update(bar);
Assert.True(uchannel.Middle.Value <= uchannel.Upper.Value,
$"Middle ({uchannel.Middle.Value}) should be <= Upper ({uchannel.Upper.Value})");
Assert.True(uchannel.Middle.Value >= uchannel.Lower.Value,
$"Middle ({uchannel.Middle.Value}) should be >= Lower ({uchannel.Lower.Value})");
}
}
[Fact]
public void Uchannel_Width_EqualsUpperMinusLower()
{
var uchannel = new Uchannel(10, 10, 1.0);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars)
{
uchannel.Update(bar);
double expectedWidth = uchannel.Upper.Value - uchannel.Lower.Value;
Assert.Equal(expectedWidth, uchannel.Width.Value, precision: 10);
}
}
[Fact]
public void Uchannel_BatchCalc_MatchesIterativeCalc()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Iterative
var uchannelIterative = new Uchannel(10, 10, 1.0);
var iterativeMiddle = new List<double>();
foreach (var bar in bars)
{
uchannelIterative.Update(bar);
iterativeMiddle.Add(uchannelIterative.Middle.Value);
}
// Batch
var (_, batchMiddle, _, _) = Uchannel.Calculate(bars, 10, 10, 1.0);
// Compare last 50 values
for (int i = 50; i < 100; i++)
{
Assert.Equal(iterativeMiddle[i], batchMiddle[i].Value, precision: 10);
}
}
[Fact]
public void Uchannel_AllModes_ProduceSameResult()
{
int strPeriod = 10;
int centerPeriod = 10;
double multiplier = 1.0;
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// 1. Batch Mode
var (_, batchMiddle, _, _) = Uchannel.Calculate(bars, strPeriod, centerPeriod, multiplier);
double batchLast = batchMiddle.Last.Value;
// 2. Span Mode
double[] highArr = bars.High.Values.ToArray();
double[] lowArr = bars.Low.Values.ToArray();
double[] closeArr = bars.Close.Values.ToArray();
double[] spanUpper = new double[highArr.Length];
double[] spanMiddle = new double[highArr.Length];
double[] spanLower = new double[highArr.Length];
Uchannel.Calculate(highArr.AsSpan(), lowArr.AsSpan(), closeArr.AsSpan(),
spanUpper.AsSpan(), spanMiddle.AsSpan(), spanLower.AsSpan(),
strPeriod, centerPeriod, multiplier);
double spanLast = spanMiddle[^1];
// 3. Streaming Mode
var streamingInd = new Uchannel(strPeriod, centerPeriod, multiplier);
foreach (var bar in bars)
{
streamingInd.Update(bar);
}
double streamingLast = streamingInd.Middle.Value;
Assert.Equal(batchLast, spanLast, precision: 10);
Assert.Equal(batchLast, streamingLast, precision: 10);
}
[Fact]
public void Uchannel_SpanCalculate_ValidatesInput()
{
double[] high = [102, 103, 104, 105, 106];
double[] low = [98, 99, 100, 101, 102];
double[] close = [100, 101, 102, 103, 104];
double[] upper = new double[5];
double[] middle = new double[5];
double[] lower = new double[5];
double[] wrongSize = new double[3];
// Period must be >= 1
Assert.Throws<ArgumentOutOfRangeException>(() =>
Uchannel.Calculate(high.AsSpan(), low.AsSpan(), close.AsSpan(),
upper.AsSpan(), middle.AsSpan(), lower.AsSpan(), 0));
Assert.Throws<ArgumentOutOfRangeException>(() =>
Uchannel.Calculate(high.AsSpan(), low.AsSpan(), close.AsSpan(),
upper.AsSpan(), middle.AsSpan(), lower.AsSpan(), -1));
// All arrays must be same length
Assert.Throws<ArgumentException>(() =>
Uchannel.Calculate(high.AsSpan(), wrongSize.AsSpan(), close.AsSpan(),
upper.AsSpan(), middle.AsSpan(), lower.AsSpan(), 3));
}
[Fact]
public void Uchannel_SpanCalculate_HandlesNaN()
{
double[] high = [102, 103, double.NaN, 105, 106];
double[] low = [98, 99, double.NaN, 101, 102];
double[] close = [100, 101, double.NaN, 103, 104];
double[] upper = new double[5];
double[] middle = new double[5];
double[] lower = new double[5];
Uchannel.Calculate(high.AsSpan(), low.AsSpan(), close.AsSpan(),
upper.AsSpan(), middle.AsSpan(), lower.AsSpan(), 3, 3, 1.0);
foreach (var val in middle)
{
Assert.True(double.IsFinite(val), $"Middle should be finite, got {val}");
}
}
[Fact]
public void Uchannel_FlatLine_ReturnsSameValueForMiddle()
{
var uchannel = new Uchannel(10, 10, 1.0);
for (int i = 0; i < 30; i++)
{
var bar = new TBar(DateTime.UtcNow, 100.0, 100.0, 100.0, 100.0, 1000);
uchannel.Update(bar);
}
// After warmup with constant input, middle should equal input
Assert.Equal(100.0, uchannel.Middle.Value, precision: 6);
// TR of flat bars = 0, so STR = 0, upper = lower = middle
Assert.Equal(uchannel.Middle.Value, uchannel.Upper.Value, precision: 6);
Assert.Equal(uchannel.Middle.Value, uchannel.Lower.Value, precision: 6);
}
[Fact]
public void Uchannel_HigherMultiplier_WiderBands()
{
int period = 10;
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var uchannel1 = new Uchannel(period, period, 1.0);
var uchannel2 = new Uchannel(period, period, 2.0);
foreach (var bar in bars)
{
uchannel1.Update(bar);
uchannel2.Update(bar);
}
// Same middle (USF is the same)
Assert.Equal(uchannel1.Middle.Value, uchannel2.Middle.Value, precision: 10);
// Higher multiplier = wider bands
Assert.True(uchannel2.Width.Value > uchannel1.Width.Value,
$"Width with mult=2 ({uchannel2.Width.Value}) should be > width with mult=1 ({uchannel1.Width.Value})");
}
[Fact]
public void Uchannel_STR_IsAlwaysNonNegative()
{
var uchannel = new Uchannel(10, 10, 1.0);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars)
{
uchannel.Update(bar);
Assert.True(uchannel.STR.Value >= 0,
$"STR ({uchannel.STR.Value}) should be >= 0");
}
}
[Fact]
public void Uchannel_TrueRange_UsedCorrectly()
{
var uchannel = new Uchannel(10, 10, 1.0);
// First bar: TR = High - Low (no prevClose)
var bar1 = new TBar(DateTime.UtcNow, 105.0, 95.0, 100.0, 102.0, 1000);
uchannel.Update(bar1);
// TR = 105 - 95 = 10
// Second bar: Gap up, TR should use prevClose
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 120.0, 110.0, 115.0, 118.0, 1000);
uchannel.Update(bar2);
// TrueHigh = max(120, 102) = 120
// TrueLow = min(110, 102) = 102
// TR = 120 - 102 = 18
Assert.True(uchannel.STR.Value > 0);
}
[Fact]
public void Uchannel_StaticCalculate_Works()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var (upper, middle, lower, str) = Uchannel.Calculate(bars, 10, 10, 1.0);
Assert.Equal(50, upper.Count);
Assert.Equal(50, middle.Count);
Assert.Equal(50, lower.Count);
Assert.Equal(50, str.Count);
Assert.True(double.IsFinite(middle.Last.Value));
}
[Fact]
public void Uchannel_DifferentStrAndCenterPeriods_Work()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Different periods for STR and centerline
var uchannel = new Uchannel(5, 20, 1.0);
foreach (var bar in bars)
{
uchannel.Update(bar);
}
Assert.True(uchannel.IsHot);
Assert.True(double.IsFinite(uchannel.Middle.Value));
Assert.True(double.IsFinite(uchannel.STR.Value));
}
[Fact]
public void Uchannel_UpdateWithLongTime_MatchesUpdateWithTBar()
{
var uchannel1 = new Uchannel(10, 10, 1.0);
var uchannel2 = new Uchannel(10, 10, 1.0);
var time = DateTime.UtcNow;
long timeTicks = time.Ticks;
double open = 100.0, high = 105.0, low = 95.0, close = 102.0;
// TBar constructor is: (time, open, high, low, close, volume)
var bar = new TBar(time, open, high, low, close, 1000);
var result1 = uchannel1.Update(bar);
var result2 = uchannel2.Update(timeTicks, high, low, close, isNew: true);
Assert.Equal(result1.Value, result2.Value, precision: 10);
Assert.Equal(uchannel1.Upper.Value, uchannel2.Upper.Value, precision: 10);
Assert.Equal(uchannel1.Lower.Value, uchannel2.Lower.Value, precision: 10);
}
}
@@ -0,0 +1,439 @@
namespace QuanTAlib.Tests;
/// <summary>
/// Validation tests for UCHANNEL (Ehlers Ultimate Channel).
/// Since this is a proprietary Ehlers indicator (2024), no external library implementations exist.
/// These tests validate internal consistency, mathematical properties, and behavior characteristics.
/// </summary>
public class UchannelValidationTests
{
private const int DefaultStrPeriod = 20;
private const int DefaultCenterPeriod = 20;
private const double DefaultMultiplier = 1.0;
/// <summary>
/// Validates that streaming and batch calculations produce identical results.
/// </summary>
[Fact]
public void Uchannel_StreamingVsBatch_MatchWithinTolerance()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Streaming
var streaming = new Uchannel(DefaultStrPeriod, DefaultCenterPeriod, DefaultMultiplier);
var streamMiddle = new List<double>();
var streamUpper = new List<double>();
var streamLower = new List<double>();
foreach (var bar in bars)
{
streaming.Update(bar);
streamMiddle.Add(streaming.Middle.Value);
streamUpper.Add(streaming.Upper.Value);
streamLower.Add(streaming.Lower.Value);
}
// Batch
var (batchUpper, batchMiddle, batchLower, _) = Uchannel.Calculate(bars, DefaultStrPeriod, DefaultCenterPeriod, DefaultMultiplier);
// Compare all values (skip first few for warmup)
for (int i = DefaultCenterPeriod; i < bars.Count; i++)
{
Assert.Equal(streamMiddle[i], batchMiddle[i].Value, precision: 10);
Assert.Equal(streamUpper[i], batchUpper[i].Value, precision: 10);
Assert.Equal(streamLower[i], batchLower[i].Value, precision: 10);
}
}
/// <summary>
/// Validates that span-based calculation matches streaming calculation.
/// </summary>
[Fact]
public void Uchannel_SpanVsStreaming_MatchWithinTolerance()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Streaming
var streaming = new Uchannel(DefaultStrPeriod, DefaultCenterPeriod, DefaultMultiplier);
var streamMiddle = new List<double>();
foreach (var bar in bars)
{
streaming.Update(bar);
streamMiddle.Add(streaming.Middle.Value);
}
// Span
double[] highArr = bars.High.Values.ToArray();
double[] lowArr = bars.Low.Values.ToArray();
double[] closeArr = bars.Close.Values.ToArray();
double[] spanUpper = new double[highArr.Length];
double[] spanMiddle = new double[highArr.Length];
double[] spanLower = new double[highArr.Length];
Uchannel.Calculate(highArr.AsSpan(), lowArr.AsSpan(), closeArr.AsSpan(),
spanUpper.AsSpan(), spanMiddle.AsSpan(), spanLower.AsSpan(),
DefaultStrPeriod, DefaultCenterPeriod, DefaultMultiplier);
// Compare all values
for (int i = DefaultCenterPeriod; i < bars.Count; i++)
{
Assert.Equal(streamMiddle[i], spanMiddle[i], precision: 10);
}
}
/// <summary>
/// Validates USF (Ultrasmooth Filter) mathematical properties:
/// The middle line should lag less than a simple moving average.
/// </summary>
[Fact]
public void Uchannel_USF_HasLessLagThanSMA()
{
int period = 20;
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.1, seed: 42);
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var uchannel = new Uchannel(period, period, 1.0);
var sma = new Sma(period);
double uchannelLagSum = 0;
double smaLagSum = 0;
foreach (var bar in bars)
{
uchannel.Update(bar);
sma.Update(new TValue(bar.Time, bar.Close));
if (uchannel.IsHot && sma.IsHot)
{
// Measure deviation from close (proxy for lag in trending market)
uchannelLagSum += Math.Abs(uchannel.Middle.Value - bar.Close);
smaLagSum += Math.Abs(sma.Last.Value - bar.Close);
}
}
// USF should have less overall deviation (implying less lag)
Assert.True(uchannelLagSum < smaLagSum,
$"USF lag sum ({uchannelLagSum:F4}) should be less than SMA lag sum ({smaLagSum:F4})");
}
/// <summary>
/// Validates that the channel bands are symmetric around the middle.
/// Upper - Middle should equal Middle - Lower.
/// </summary>
[Fact]
public void Uchannel_Bands_AreSymmetric()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var uchannel = new Uchannel(DefaultStrPeriod, DefaultCenterPeriod, DefaultMultiplier);
foreach (var bar in bars)
{
uchannel.Update(bar);
double upperDist = uchannel.Upper.Value - uchannel.Middle.Value;
double lowerDist = uchannel.Middle.Value - uchannel.Lower.Value;
Assert.Equal(upperDist, lowerDist, precision: 10);
}
}
/// <summary>
/// Validates that band width equals 2 × STR × multiplier.
/// </summary>
[Fact]
public void Uchannel_Width_Equals2xSTRxMultiplier()
{
double multiplier = 1.5;
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var uchannel = new Uchannel(DefaultStrPeriod, DefaultCenterPeriod, multiplier);
foreach (var bar in bars)
{
uchannel.Update(bar);
double expectedWidth = 2 * uchannel.STR.Value * multiplier;
Assert.Equal(expectedWidth, uchannel.Width.Value, precision: 10);
}
}
/// <summary>
/// Validates that STR (Smoothed True Range) is always non-negative.
/// </summary>
[Fact]
public void Uchannel_STR_AlwaysNonNegative()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.2, seed: 42);
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var uchannel = new Uchannel(DefaultStrPeriod, DefaultCenterPeriod, DefaultMultiplier);
foreach (var bar in bars)
{
uchannel.Update(bar);
Assert.True(uchannel.STR.Value >= 0,
$"STR ({uchannel.STR.Value}) should always be non-negative");
}
}
/// <summary>
/// Validates that True Range calculation handles gaps correctly.
/// True Range should account for gap between prev close and current high/low.
/// </summary>
[Fact]
public void Uchannel_TrueRange_HandlesGapsCorrectly()
{
var uchannel = new Uchannel(3, 3, 1.0);
// Day 1: Normal bar
var bar1 = new TBar(DateTime.UtcNow, 102.0, 98.0, 100.0, 100.0, 1000);
uchannel.Update(bar1);
// TR = 102 - 98 = 4
// Day 2: Gap up (open above prev close)
var bar2 = new TBar(DateTime.UtcNow.AddDays(1), 115.0, 110.0, 112.0, 114.0, 1000);
uchannel.Update(bar2);
// True High = max(115, 100) = 115
// True Low = min(110, 100) = 100
// TR = 115 - 100 = 15
// Day 3: Gap down (open below prev close)
var bar3 = new TBar(DateTime.UtcNow.AddDays(2), 108.0, 90.0, 95.0, 92.0, 1000);
uchannel.Update(bar3);
// True High = max(108, 114) = 114
// True Low = min(90, 114) = 90
// TR = 114 - 90 = 24
// STR should reflect these larger TR values due to gaps
Assert.True(uchannel.STR.Value > 0);
}
/// <summary>
/// Validates that different STR and center periods work independently.
/// </summary>
[Fact]
public void Uchannel_DifferentPeriods_ProduceDifferentResults()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var uchannel1 = new Uchannel(10, 20, 1.0); // Short STR, long center
var uchannel2 = new Uchannel(20, 10, 1.0); // Long STR, short center
var uchannel3 = new Uchannel(15, 15, 1.0); // Equal periods
foreach (var bar in bars)
{
uchannel1.Update(bar);
uchannel2.Update(bar);
uchannel3.Update(bar);
}
// Middle lines should differ (different center periods)
Assert.NotEqual(uchannel1.Middle.Value, uchannel2.Middle.Value);
// STR should differ (different STR periods)
Assert.NotEqual(uchannel1.STR.Value, uchannel2.STR.Value);
}
/// <summary>
/// Validates that the multiplier scales the band width proportionally.
/// </summary>
[Fact]
public void Uchannel_Multiplier_ScalesBandWidthProportionally()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var uchannel1 = new Uchannel(DefaultStrPeriod, DefaultCenterPeriod, 1.0);
var uchannel2 = new Uchannel(DefaultStrPeriod, DefaultCenterPeriod, 2.0);
var uchannel3 = new Uchannel(DefaultStrPeriod, DefaultCenterPeriod, 0.5);
foreach (var bar in bars)
{
uchannel1.Update(bar);
uchannel2.Update(bar);
uchannel3.Update(bar);
}
// Width should scale proportionally with multiplier
Assert.Equal(uchannel1.Width.Value * 2, uchannel2.Width.Value, precision: 10);
Assert.Equal(uchannel1.Width.Value / 2, uchannel3.Width.Value, precision: 10);
// Middle should be the same (same center period)
Assert.Equal(uchannel1.Middle.Value, uchannel2.Middle.Value, precision: 10);
Assert.Equal(uchannel1.Middle.Value, uchannel3.Middle.Value, precision: 10);
}
/// <summary>
/// Validates that bar correction (isNew=false) works correctly.
/// </summary>
[Fact]
public void Uchannel_BarCorrection_WorksCorrectly()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var uchannel = new Uchannel(DefaultStrPeriod, DefaultCenterPeriod, DefaultMultiplier);
// Process all bars
foreach (var bar in bars)
{
uchannel.Update(bar);
}
double originalMiddle = uchannel.Middle.Value;
double originalUpper = uchannel.Upper.Value;
double originalSTR = uchannel.STR.Value;
// Simulate tick corrections
for (int tick = 0; tick < 20; tick++)
{
var correctionBar = new TBar(DateTime.UtcNow, 150.0 + tick, 140.0, 145.0, 148.0, 1000);
uchannel.Update(correctionBar, isNew: false);
}
// Restore with original last bar
uchannel.Update(bars[^1], isNew: false);
Assert.Equal(originalMiddle, uchannel.Middle.Value, precision: 10);
Assert.Equal(originalUpper, uchannel.Upper.Value, precision: 10);
Assert.Equal(originalSTR, uchannel.STR.Value, precision: 10);
}
/// <summary>
/// Validates that the indicator converges to stable values.
/// </summary>
[Fact]
public void Uchannel_ConvergesToStableValues()
{
var uchannel = new Uchannel(DefaultStrPeriod, DefaultCenterPeriod, DefaultMultiplier);
// Feed constant bars
for (int i = 0; i < 100; i++)
{
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), 105.0, 95.0, 100.0, 100.0, 1000);
uchannel.Update(bar);
}
double middle50 = uchannel.Middle.Value;
// Feed more constant bars
for (int i = 0; i < 100; i++)
{
var bar = new TBar(DateTime.UtcNow.AddMinutes(100 + i), 105.0, 95.0, 100.0, 100.0, 1000);
uchannel.Update(bar);
}
double middle100 = uchannel.Middle.Value;
// Should converge to close value (100.0)
Assert.True(Math.Abs(middle50 - 100.0) < 0.1);
Assert.True(Math.Abs(middle100 - 100.0) < 0.01);
}
/// <summary>
/// Validates that STR converges to the True Range value for constant volatility.
/// </summary>
[Fact]
public void Uchannel_STR_ConvergesToTrueRange()
{
var uchannel = new Uchannel(10, 10, 1.0);
// Feed bars with constant TR = 10 (high=105, low=95)
// TBar constructor: (time, open, high, low, close, volume)
for (int i = 0; i < 100; i++)
{
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), 100.0, 105.0, 95.0, 100.0, 1000);
uchannel.Update(bar);
}
// STR should converge to TR value (10.0)
Assert.True(Math.Abs(uchannel.STR.Value - 10.0) < 0.1,
$"STR ({uchannel.STR.Value}) should converge to TR (10.0)");
}
/// <summary>
/// Validates behavior with high volatility data.
/// </summary>
[Fact]
public void Uchannel_HighVolatility_ProducesWiderBands()
{
var gbmLow = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.05, seed: 42);
var gbmHigh = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.30, seed: 42);
var barsLow = gbmLow.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var barsHigh = gbmHigh.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var uchannelLow = new Uchannel(DefaultStrPeriod, DefaultCenterPeriod, DefaultMultiplier);
var uchannelHigh = new Uchannel(DefaultStrPeriod, DefaultCenterPeriod, DefaultMultiplier);
foreach (var bar in barsLow)
{
uchannelLow.Update(bar);
}
foreach (var bar in barsHigh)
{
uchannelHigh.Update(bar);
}
// High volatility should produce wider bands
Assert.True(uchannelHigh.Width.Value > uchannelLow.Width.Value,
$"High vol width ({uchannelHigh.Width.Value:F4}) should be > low vol width ({uchannelLow.Width.Value:F4})");
}
/// <summary>
/// Validates that the indicator handles edge case with period = 1.
/// </summary>
[Fact]
public void Uchannel_Period1_Works()
{
var uchannel = new Uchannel(1, 1, 1.0);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars)
{
uchannel.Update(bar);
Assert.True(double.IsFinite(uchannel.Middle.Value));
Assert.True(double.IsFinite(uchannel.Upper.Value));
Assert.True(double.IsFinite(uchannel.Lower.Value));
}
}
/// <summary>
/// Validates that reset properly clears all state.
/// </summary>
[Fact]
public void Uchannel_Reset_ClearsAllState()
{
var uchannel = new Uchannel(DefaultStrPeriod, DefaultCenterPeriod, DefaultMultiplier);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Process bars
foreach (var bar in bars)
{
uchannel.Update(bar);
}
double valueBefore = uchannel.Middle.Value;
// Reset
uchannel.Reset();
// Process same bars again
foreach (var bar in bars)
{
uchannel.Update(bar);
}
double valueAfter = uchannel.Middle.Value;
// Should produce same results
Assert.Equal(valueBefore, valueAfter, precision: 10);
}
}
+506
View File
@@ -0,0 +1,506 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// UCHANNEL: Ehlers Ultimate Channel
/// A volatility channel using the Ehlers Ultrasmooth Filter (USF) for both
/// centerline smoothing and True Range smoothing (STR). The channel width is
/// determined by the smoothed True Range multiplied by a factor.
/// </summary>
/// <remarks>
/// The Ultimate Channel provides smooth, low-lag channel boundaries by applying
/// the USF to both the price centerline and the True Range. This creates channels
/// that adapt to volatility while maintaining minimal lag.
///
/// Key characteristics:
/// - Middle band: USF of close prices
/// - Band width: Smoothed True Range (USF of TR) × multiplier
/// - Both smoothers use the same 2-pole IIR Ultrasmooth Filter
///
/// Sources:
/// John F. Ehlers - "Ultimate Channel" (2024)
///
/// Formula:
/// TR = max(high, prev_close) - min(low, prev_close)
/// STR = USF(TR, strPeriod)
/// Middle = USF(close, centerPeriod)
/// Upper = Middle + (multiplier × STR)
/// Lower = Middle - (multiplier × STR)
///
/// USF coefficients (same as Ehlers Ultrasmooth Filter):
/// arg = sqrt(2) × π / period
/// c2 = 2 × exp(-arg) × cos(arg)
/// c3 = -exp(-arg)²
/// c1 = (1 + c2 - c3) / 4
///
/// USF formula:
/// usf = (1 - c1) × val + (2×c1 - c2) × val₋₁ - (c1 + c3) × val₋₂ + c2 × usf₋₁ + c3 × usf₋₂
/// </remarks>
[SkipLocalsInit]
public sealed class Uchannel : AbstractBase
{
private readonly double _multiplier;
private const int DefaultStrPeriod = 20;
private const int DefaultCenterPeriod = 20;
private const double DefaultMultiplier = 1.0;
private const double MinMultiplier = 0.001;
private const int MinPeriod = 1;
// USF coefficients for STR
private readonly double _c1_str, _c2_str, _c3_str;
// USF coefficients for centerline
private readonly double _c1_cen, _c2_cen, _c3_cen;
// State for streaming with bar correction
[StructLayout(LayoutKind.Auto)]
private record struct State(
double PrevClose,
double UsStr1, double UsStr2,
double Str1, double Str2,
double UsCen1, double UsCen2,
double Cen1, double Cen2,
double LastValidClose, double LastValidHigh, double LastValidLow,
int BarCount,
bool IsInitialized);
private State _state;
private State _p_state;
private int _index;
public override bool IsHot => _index >= WarmupPeriod;
/// <summary>Gets the upper band value.</summary>
public TValue Upper { get; private set; }
/// <summary>Gets the middle band (USF smoothed centerline) value.</summary>
public TValue Middle { get; private set; }
/// <summary>Gets the lower band value.</summary>
public TValue Lower { get; private set; }
/// <summary>Gets the current Smoothed True Range value.</summary>
public TValue STR { get; private set; }
/// <summary>Gets the channel width (Upper - Lower).</summary>
public TValue Width => new(Upper.Time, Upper.Value - Lower.Value);
/// <param name="strPeriod">Period for smoothing True Range. Must be >= 1.</param>
/// <param name="centerPeriod">Period for smoothing centerline. Must be >= 1.</param>
/// <param name="multiplier">Band multiplier for STR. Must be > 0.</param>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when strPeriod &lt; 1, centerPeriod &lt; 1, or multiplier &lt;= 0.
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Uchannel(int strPeriod = DefaultStrPeriod, int centerPeriod = DefaultCenterPeriod, double multiplier = DefaultMultiplier)
{
if (strPeriod < MinPeriod)
{
throw new ArgumentOutOfRangeException(nameof(strPeriod),
$"STR period must be at least {MinPeriod}.");
}
if (centerPeriod < MinPeriod)
{
throw new ArgumentOutOfRangeException(nameof(centerPeriod),
$"Center period must be at least {MinPeriod}.");
}
if (multiplier < MinMultiplier)
{
throw new ArgumentOutOfRangeException(nameof(multiplier),
$"Multiplier must be at least {MinMultiplier}.");
}
_multiplier = multiplier;
WarmupPeriod = Math.Max(strPeriod, centerPeriod);
Name = $"Uchannel({strPeriod},{centerPeriod},{multiplier:F1})";
// Compute USF coefficients for STR
double arg_str = Math.Sqrt(2) * Math.PI / strPeriod;
double exp_str = Math.Exp(-arg_str);
_c2_str = 2 * exp_str * Math.Cos(arg_str);
_c3_str = -exp_str * exp_str;
_c1_str = (1 + _c2_str - _c3_str) / 4.0;
// Compute USF coefficients for centerline
double arg_cen = Math.Sqrt(2) * Math.PI / centerPeriod;
double exp_cen = Math.Exp(-arg_cen);
_c2_cen = 2 * exp_cen * Math.Cos(arg_cen);
_c3_cen = -exp_cen * exp_cen;
_c1_cen = (1 + _c2_cen - _c3_cen) / 4.0;
Init();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Init()
{
_index = 0;
_state = new State(
PrevClose: double.NaN,
UsStr1: 0, UsStr2: 0,
Str1: 0, Str2: 0,
UsCen1: 0, UsCen2: 0,
Cen1: 0, Cen2: 0,
LastValidClose: 0, LastValidHigh: 0, LastValidLow: 0,
BarCount: 0,
IsInitialized: false);
_p_state = _state;
Upper = new TValue(DateTime.UtcNow, 0);
Middle = new TValue(DateTime.UtcNow, 0);
Lower = new TValue(DateTime.UtcNow, 0);
STR = new TValue(DateTime.UtcNow, 0);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static double GetFiniteValue(double value, double fallback) =>
double.IsFinite(value) ? value : fallback;
/// <summary>
/// Processes a single bar and updates the indicator.
/// </summary>
/// <param name="bar">The input bar containing OHLC data.</param>
/// <param name="isNew">True if this is a new bar, false for correction.</param>
/// <returns>The middle band value as TValue.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TBar bar, bool isNew = true)
{
return Update(bar.Time, bar.High, bar.Low, bar.Close, isNew);
}
/// <summary>
/// Processes OHLC values and updates the indicator.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(long time, double high, double low, double close, bool isNew = true)
{
// State management for bar correction
if (isNew)
{
_p_state = _state;
_index++;
}
else
{
_state = _p_state;
}
// Handle non-finite values
double validClose = GetFiniteValue(close, _state.LastValidClose);
double validHigh = GetFiniteValue(high, _state.LastValidHigh);
double validLow = GetFiniteValue(low, _state.LastValidLow);
// Compute True Range
double prevClose = double.IsNaN(_state.PrevClose) ? validClose : _state.PrevClose;
double trueHigh = Math.Max(validHigh, prevClose);
double trueLow = Math.Min(validLow, prevClose);
double tr = trueHigh - trueLow;
// USF for STR
double str_s0 = tr;
double str_s1 = _state.BarCount >= 1 ? _state.Str1 : str_s0;
double str_s2 = _state.BarCount >= 2 ? _state.Str2 : str_s1;
double usStr1 = _state.UsStr1;
double usStr2 = _state.UsStr2;
double strValue;
if (_state.BarCount < 2)
{
strValue = str_s0;
}
else
{
// USF: (1-c1)*s0 + (2*c1-c2)*s1 - (c1+c3)*s2 + c2*usf1 + c3*usf2
strValue = Math.FusedMultiplyAdd(1 - _c1_str, str_s0,
Math.FusedMultiplyAdd(2 * _c1_str - _c2_str, str_s1,
Math.FusedMultiplyAdd(-(_c1_str + _c3_str), str_s2,
Math.FusedMultiplyAdd(_c2_str, usStr1, _c3_str * usStr2))));
}
// USF for centerline
double cen_s0 = validClose;
double cen_s1 = _state.BarCount >= 1 ? _state.Cen1 : cen_s0;
double cen_s2 = _state.BarCount >= 2 ? _state.Cen2 : cen_s1;
double usCen1 = _state.UsCen1;
double usCen2 = _state.UsCen2;
double cenValue;
if (_state.BarCount < 2)
{
cenValue = cen_s0;
}
else
{
cenValue = Math.FusedMultiplyAdd(1 - _c1_cen, cen_s0,
Math.FusedMultiplyAdd(2 * _c1_cen - _c2_cen, cen_s1,
Math.FusedMultiplyAdd(-(_c1_cen + _c3_cen), cen_s2,
Math.FusedMultiplyAdd(_c2_cen, usCen1, _c3_cen * usCen2))));
}
// Compute bands
double bandwidth = _multiplier * strValue;
double upper = cenValue + bandwidth;
double lower = cenValue - bandwidth;
// Update state
_state = new State(
PrevClose: validClose,
UsStr1: strValue, UsStr2: usStr1,
Str1: str_s0, Str2: str_s1,
UsCen1: cenValue, UsCen2: usCen1,
Cen1: cen_s0, Cen2: cen_s1,
LastValidClose: validClose, LastValidHigh: validHigh, LastValidLow: validLow,
BarCount: _state.BarCount + (isNew ? 1 : 0),
IsInitialized: true);
Upper = new TValue(time, upper);
Middle = new TValue(time, cenValue);
Lower = new TValue(time, lower);
STR = new TValue(time, strValue);
Last = Middle;
return Last;
}
/// <summary>
/// Updates with TValue - requires High, Low, Close data so this uses the value as Close
/// with High = Low = Close (not recommended, use TBar overload instead)
/// </summary>
public override TValue Update(TValue input, bool isNew = true)
{
// Convert to TBar with O=H=L=C=value, V=0
TBar bar = new(input.Time, input.Value, input.Value, input.Value, input.Value, 0);
return Update(bar, isNew);
}
/// <summary>
/// Processes a series of bars.
/// </summary>
public TSeries Update(TBarSeries series)
{
if (series == null)
{
throw new ArgumentNullException(nameof(series));
}
int len = series.Count;
TSeries result = new(capacity: len);
for (int i = 0; i < len; i++)
{
var bar = series[i];
Update(bar, isNew: true);
result.Add(Last.Time, Last.Value, isNew: true);
}
return result;
}
/// <summary>
/// Updates the indicator with a new time series and returns the result series.
/// </summary>
public override TSeries Update(TSeries source)
{
if (source == null)
{
throw new ArgumentNullException(nameof(source));
}
int len = source.Count;
TSeries result = new(capacity: len);
for (int i = 0; i < len; i++)
{
var item = source[i];
Update(item, isNew: true);
result.Add(Last.Time, Last.Value, isNew: true);
}
return result;
}
public override void Reset()
{
Init();
}
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
step ??= TimeSpan.FromSeconds(1);
DateTime startTime = DateTime.UtcNow;
for (int i = 0; i < source.Length; i++)
{
// Treat as close price only
Update(new TValue(startTime + i * step.Value, source[i]), isNew: true);
}
}
/// <summary>
/// Static method to calculate Ultimate Channel for a bar series.
/// </summary>
public static (TSeries Upper, TSeries Middle, TSeries Lower, TSeries STR) Calculate(
TBarSeries source, int strPeriod = DefaultStrPeriod, int centerPeriod = DefaultCenterPeriod, double multiplier = DefaultMultiplier)
{
var indicator = new Uchannel(strPeriod, centerPeriod, multiplier);
var upper = new TSeries(source.Count);
var middle = new TSeries(source.Count);
var lower = new TSeries(source.Count);
var str = new TSeries(source.Count);
foreach (var bar in source)
{
indicator.Update(bar);
upper.Add(indicator.Upper);
middle.Add(indicator.Middle);
lower.Add(indicator.Lower);
str.Add(indicator.STR);
}
return (upper, middle, lower, str);
}
/// <summary>
/// Static span-based calculation for maximum performance.
/// </summary>
/// <param name="high">Source high prices.</param>
/// <param name="low">Source low prices.</param>
/// <param name="close">Source close prices.</param>
/// <param name="upper">Output upper band.</param>
/// <param name="middle">Output middle band.</param>
/// <param name="lower">Output lower band.</param>
/// <param name="strPeriod">Period for STR smoothing.</param>
/// <param name="centerPeriod">Period for centerline smoothing.</param>
/// <param name="multiplier">Band multiplier.</param>
public static void Calculate(
ReadOnlySpan<double> high,
ReadOnlySpan<double> low,
ReadOnlySpan<double> close,
Span<double> upper,
Span<double> middle,
Span<double> lower,
int strPeriod = DefaultStrPeriod,
int centerPeriod = DefaultCenterPeriod,
double multiplier = DefaultMultiplier)
{
if (strPeriod < MinPeriod)
{
throw new ArgumentOutOfRangeException(nameof(strPeriod),
$"STR period must be at least {MinPeriod}.");
}
if (centerPeriod < MinPeriod)
{
throw new ArgumentOutOfRangeException(nameof(centerPeriod),
$"Center period must be at least {MinPeriod}.");
}
if (multiplier < MinMultiplier)
{
throw new ArgumentOutOfRangeException(nameof(multiplier),
$"Multiplier must be at least {MinMultiplier}.");
}
int length = close.Length;
if (high.Length != length || low.Length != length)
throw new ArgumentException("All input arrays must have the same length", nameof(high));
if (upper.Length != length || middle.Length != length || lower.Length != length)
throw new ArgumentException("Output arrays must match input length", nameof(upper));
if (length == 0) return;
// Compute USF coefficients
double arg_str = Math.Sqrt(2) * Math.PI / strPeriod;
double exp_str = Math.Exp(-arg_str);
double c2_str = 2 * exp_str * Math.Cos(arg_str);
double c3_str = -exp_str * exp_str;
double c1_str = (1 + c2_str - c3_str) / 4.0;
double arg_cen = Math.Sqrt(2) * Math.PI / centerPeriod;
double exp_cen = Math.Exp(-arg_cen);
double c2_cen = 2 * exp_cen * Math.Cos(arg_cen);
double c3_cen = -exp_cen * exp_cen;
double c1_cen = (1 + c2_cen - c3_cen) / 4.0;
double prevClose = close[0];
double lastValidClose = close[0];
double lastValidHigh = high[0];
double lastValidLow = low[0];
double usStr1 = 0, usStr2 = 0;
double str1 = 0, str2 = 0;
double usCen1 = 0, usCen2 = 0;
double cen1 = 0, cen2 = 0;
for (int i = 0; i < length; i++)
{
double h = double.IsFinite(high[i]) ? high[i] : lastValidHigh;
double l = double.IsFinite(low[i]) ? low[i] : lastValidLow;
double c = double.IsFinite(close[i]) ? close[i] : lastValidClose;
lastValidHigh = h;
lastValidLow = l;
lastValidClose = c;
// True Range
double trueHigh = Math.Max(h, prevClose);
double trueLow = Math.Min(l, prevClose);
double tr = trueHigh - trueLow;
// USF for STR
double str_s0 = tr;
double str_s1 = i >= 1 ? str1 : str_s0;
double str_s2 = i >= 2 ? str2 : str_s1;
double strValue;
if (i < 2)
{
strValue = str_s0;
}
else
{
strValue = Math.FusedMultiplyAdd(1 - c1_str, str_s0,
Math.FusedMultiplyAdd(2 * c1_str - c2_str, str_s1,
Math.FusedMultiplyAdd(-(c1_str + c3_str), str_s2,
Math.FusedMultiplyAdd(c2_str, usStr1, c3_str * usStr2))));
}
// USF for centerline
double cen_s0 = c;
double cen_s1 = i >= 1 ? cen1 : cen_s0;
double cen_s2 = i >= 2 ? cen2 : cen_s1;
double cenValue;
if (i < 2)
{
cenValue = cen_s0;
}
else
{
cenValue = Math.FusedMultiplyAdd(1 - c1_cen, cen_s0,
Math.FusedMultiplyAdd(2 * c1_cen - c2_cen, cen_s1,
Math.FusedMultiplyAdd(-(c1_cen + c3_cen), cen_s2,
Math.FusedMultiplyAdd(c2_cen, usCen1, c3_cen * usCen2))));
}
// Bands
double bandwidth = multiplier * strValue;
upper[i] = cenValue + bandwidth;
middle[i] = cenValue;
lower[i] = cenValue - bandwidth;
// Update state
str2 = str1;
str1 = str_s0;
usStr2 = usStr1;
usStr1 = strValue;
cen2 = cen1;
cen1 = cen_s0;
usCen2 = usCen1;
usCen1 = cenValue;
prevClose = c;
}
}
}
+143 -102
View File
@@ -1,147 +1,188 @@
# UCHANNEL: Ultimate Channel
# UCHANNEL: Ehlers Ultimate Channel
## Overview and Purpose
> "The best volatility channel uses the best smoother—applied twice."
The Ultimate Channel, developed by John F. Ehlers, is a channel indicator designed to offer minimal lag. It draws inspiration from Keltner Channels, which typically use an Exponential Moving Average (EMA) for the centerline and Average True Range (ATR) to establish channel width. Both the EMA and the ATR's own averaging introduce lag. The Ultimate Channel aims to mitigate this by replacing these averaging processes with Ehlers' Ultrasmooth Filter.
The Ehlers Ultimate Channel combines the Ultrasmooth Filter (USF) for both the centerline and volatility measurement, creating a channel that adapts to price movements with minimal lag while maintaining smooth, responsive boundaries. Unlike traditional channels that use standard deviation or ATR, UCHANNEL employs the Smoothed True Range (STR)—the USF applied to True Range—for band width calculation.
The channel is constructed by:
1. Calculating a "Smoothed True Range" (STR). The "True Range" for this indicator is specifically defined by Ehlers as `TrueHigh - TrueLow`.
* `TrueHigh (TH)`: The Close of the previous bar if it is higher than the High of the current bar; otherwise, it is the High of the current bar. (`TH = Max(High, Close[1])`)
* `TrueLow (TL)`: The Close of the previous bar if it is lower than the Low of the current bar; otherwise, it is the Low of the current bar. (`TL = Min(Low, Close[1])`)
This `TH - TL` range is then smoothed using the Ultrasmooth Filter with a dedicated length (`STRLength`).
2. Calculating a centerline by applying the Ultrasmooth Filter to the source price (typically `close`) with its own length (`Length`).
3. Plotting the upper and lower channel bands by adding/subtracting a multiple (`NumSTRs`) of the Smoothed True Range (STR) from the centerline.
## Historical Context
The primary purpose is to provide traders with dynamic support and resistance levels that are highly reactive to price action, aiming for nearly zero lag due to the comprehensive use of the Ultrasmooth Filter.
John F. Ehlers introduced the Ultimate Channel in 2024 as the natural companion to his Ultimate Bands indicator. While Ultimate Bands (UBANDS) uses RMS of price deviations from the USF centerline to determine band width, the Ultimate Channel takes a different approach: it smooths True Range directly with the USF to create the band width multiplier.
## Core Concepts
This distinction matters for several reasons:
* **Dual Ultrasmooth Filtering:** Both the centerline and the range component (STR) are smoothed using the Ehlers Ultrasmooth Filter, contributing to the indicator's responsiveness and reduced lag.
* **Ehlers' True Range Definition:** Utilizes a specific definition of True Range (`Max(High, Close[1]) - Min(Low, Close[1])`) as the basis for volatility measurement, which is then smoothed to create STR, rather than using a traditional ATR calculation.
* **Volatility-Adaptive Width:** The channel width is directly proportional to the Smoothed True Range (STR), causing it to expand in volatile markets and contract in calmer ones.
* **Minimal Lag:** A key design goal, aiming to provide more timely signals compared to traditional channel indicators like Keltner Channels.
1. **True Range captures gaps**: Unlike simple high-low range, True Range accounts for overnight gaps by comparing current high/low to the previous close
2. **USF smoothing on TR**: Applying USF to True Range produces a volatility measure with the same low-lag characteristics as the centerline
3. **Independent tuning**: Separate periods for STR and centerline smoothing allow traders to optimize each component independently
## Common Settings and Parameters
The design philosophy reflects Ehlers' preference for using the same high-quality filter throughout an indicator system, ensuring consistent lag characteristics across all components.
| Parameter | Default | Function | When to Adjust |
| :-------- | :------ | :------- | :------------- |
| Source | close | The price series for the centerline calculation (e.g., `Close`). | Typically `close`, but can be adjusted. |
| High Source | high | The high price series for True High calculation. | Standard `high`. |
| Low Source | low | The low price series for True Low calculation. | Standard `low`. |
| STR Length | 20 | Lookback period for smoothing the `TH - TL` range to get STR. | Shorter lengths make STR more reactive; longer lengths make STR smoother. |
| Length | 20 | Lookback period for smoothing the `Source` (e.g., `Close`) to get the centerline. | Shorter lengths make the centerline more responsive; longer lengths provide smoother channel limits but will moderately increase indicator lag. |
| STR Multiplier | 1.0 | Multiplier for the Smoothed True Range (STR) to determine channel width. | Smaller values create tighter channels; larger values create wider channels. |
## Architecture & Physics
## Calculation and Mathematical Foundation
### 1. True Range Calculation
**Simplified explanation:**
1. Determine the True High (TH) for each bar: `TH = Max(Current High, Previous Close)`.
2. Determine the True Low (TL) for each bar: `TL = Min(Current Low, Previous Close)`.
3. Calculate the bar's specific range: `Range = TH - TL`.
4. Smooth this `Range` series using the Ehlers Ultrasmooth Filter with `STRLength` to get the Smoothed True Range (STR).
5. Smooth the `Source` price (e.g., `Close`) using the Ehlers Ultrasmooth Filter with `Length` to get the `Centerline`.
6. The Upper Channel is `Centerline + (NumSTRs × STR)`.
7. The Lower Channel is `Centerline - (NumSTRs × STR)`.
True Range extends the simple high-low range to capture gaps:
**Technical formula (based on Ehlers' description):**
1. **True High (TH):**
`TH[i] = Max(High[i], Close[i-1])`
*(Note: The Pine Script implementation uses `src_centerline[i-1]` which is typically `Close[i-1]`)*
$$
TR_t = \max(H_t, C_{t-1}) - \min(L_t, C_{t-1})
$$
2. **True Low (TL):**
`TL[i] = Min(Low[i], Close[i-1])`
where:
- $H_t$ = current high
- $L_t$ = current low
- $C_{t-1}$ = previous close
3. **Range Series (RS):**
`RS[i] = TH[i] - TL[i]`
This formulation ensures that a gap up (where today's low exceeds yesterday's close) or gap down (where today's high falls below yesterday's close) is fully captured in the volatility measurement.
4. **Smoothed True Range (STR):**
`STR = UltrasmoothFilter(RS, STRLength)`
### 2. Ultrasmooth Filter (USF) Coefficients
5. **Centerline:**
`Centerline = UltrasmoothFilter(Close, Length)` (or specified `Source`)
The USF is a 2-pole IIR filter with coefficients derived from the period parameter:
6. **Upper Channel:**
`UpperChannel = Centerline + (NumSTRs × STR)`
$$
\text{arg} = \frac{\sqrt{2} \cdot \pi}{\text{period}}
$$
7. **Lower Channel:**
`LowerChannel = Centerline - (NumSTRs × STR)`
$$
c_2 = 2 \cdot e^{-\text{arg}} \cdot \cos(\text{arg})
$$
> 🔍 **Technical Note:** The Ehlers Ultrasmooth Filter is the core engine, applied independently to two different series: the calculated `TH-TL` range and the input `Source` price. The responsiveness of the channel comes from this dual application of a low-lag filter, aiming to mitigate lag found in traditional ATR and EMA calculations of Keltner Channels.
$$
c_3 = -e^{-2 \cdot \text{arg}}
$$
## Interpretation Details
$$
c_1 = \frac{1 + c_2 - c_3}{4}
$$
* **Reduced Lag:** The primary characteristic, offering quicker signals than traditional Keltner Channels. The channel aims for "nearly zero lag."
* **Dynamic Support/Resistance:** The Upper Channel can act as resistance, and the Lower Channel as support.
* **Volatility Indication:** The width of the channel (determined by STR) reflects market volatility. Wider channels mean higher volatility.
* **Trend Following:** Trades can be initiated based on breakouts from the channel or by following the direction of the centerline.
* **Smoothing Channel Limits:** The channel limits can be made smoother by increasing the input `Length` parameter (for the centerline). Doing this will moderately increase the indicator lag.
* **Comparison to Ultimate Bands:** Ehlers notes that the Ultimate Channel indicator does not differ from the Ultimate Band indicator in any major fashion.
### 3. USF Recursion Formula
## Use and Application
The filter is applied using a 2-pole IIR structure:
The Ultimate Channel can be used similarly to Keltner Channels for interpreting price action, with the key advantage of reduced lag.
$$
\text{USF}_t = (1 - c_1) \cdot X_t + (2c_1 - c_2) \cdot X_{t-1} - (c_1 + c_3) \cdot X_{t-2} + c_2 \cdot \text{USF}_{t-1} + c_3 \cdot \text{USF}_{t-2}
$$
**Example Trading Strategy (from John F. Ehlers, applicable to both Ultimate Channel and Bands):**
* Hold a position in the direction of the Ultimate Smoother (the centerline).
* Exit that position when the price "pops" outside the channel in the opposite direction of the trade.
* This is described as a trend-following strategy with an automatic following stop.
This formula is applied twice:
- Once to close prices to produce the centerline (Middle)
- Once to True Range to produce the Smoothed True Range (STR)
### 4. Channel Construction
With both smoothed values computed:
$$
\text{Middle}_t = \text{USF}(\text{Close}, \text{centerPeriod})
$$
$$
\text{STR}_t = \text{USF}(\text{TR}, \text{strPeriod})
$$
$$
\text{Upper}_t = \text{Middle}_t + (\text{multiplier} \times \text{STR}_t)
$$
$$
\text{Lower}_t = \text{Middle}_t - (\text{multiplier} \times \text{STR}_t)
$$
## Mathematical Foundation
### Transfer Function
The USF can be expressed in the z-domain as:
$$
H(z) = \frac{(1 - c_1) + (2c_1 - c_2)z^{-1} - (c_1 + c_3)z^{-2}}{1 - c_2 z^{-1} - c_3 z^{-2}}
$$
### State-Space Form
For efficient computation, the filter maintains state variables:
**For STR smoothing:**
- `UsStr1`, `UsStr2`: Previous USF outputs
- `Str1`, `Str2`: Previous True Range values
**For centerline smoothing:**
- `UsCen1`, `UsCen2`: Previous USF outputs
- `Cen1`, `Cen2`: Previous close values
### FMA Optimization
The USF formula is implemented using Fused Multiply-Add for maximum precision and performance:
```csharp
cenValue = Math.FusedMultiplyAdd(1 - c1_cen, cen_s0,
Math.FusedMultiplyAdd(2 * c1_cen - c2_cen, cen_s1,
Math.FusedMultiplyAdd(-(c1_cen + c3_cen), cen_s2,
Math.FusedMultiplyAdd(c2_cen, usCen1, c3_cen * usCen2))));
```
## Performance Profile
### Operation Count (Streaming Mode, per Bar)
Ultimate Channel uses dual Ultrasmooth Filters (4-pole IIR) for centerline and STR:
### Operation Count (Streaming Mode, Scalar)
| Operation | Count | Cost (cycles) | Subtotal |
| :--- | :---: | :---: | :---: |
| ADD/SUB | 12 | 1 | 12 |
| MUL | 18 | 3 | 54 |
| CMP/MAX/MIN | 2 | 1 | 2 |
| **Total** | **32** | | **~68 cycles** |
| MUL | 14 | 3 | 42 |
| MAX/MIN | 2 | 1 | 2 |
| FMA | 8 | 4 | 32 |
| **Total** | **36** | — | **~88 cycles** |
**Breakdown:**
- True High/Low (2 comparisons): 2 CMP = 2 cycles
- Range calculation: 1 SUB = 1 cycle
- Ultrasmooth Filter #1 (STR, 4-pole): 4 ADD + 8 MUL = 28 cycles
- Ultrasmooth Filter #2 (Centerline, 4-pole): 4 ADD + 8 MUL = 28 cycles
- Band calculation: 2 ADD + 2 MUL = 8 cycles
The dominant cost is the 8 FMA operations (4 for STR + 4 for centerline).
### Complexity Analysis
### Batch Mode (512 values, SIMD/FMA)
| Mode | Complexity | Notes |
| :--- | :---: | :--- |
| Streaming | O(1) | Two IIR filters with constant state |
| Batch | O(n) | Linear scan, IIR sequential |
Due to the recursive IIR structure, SIMD vectorization is limited. However, FMA instructions provide measurable improvement:
**Memory**: ~96 bytes (two 4-pole filter states, previous close)
| Operation | Scalar Ops | With FMA | Improvement |
| :--- | :---: | :---: | :---: |
| MUL+ADD chains | 16 | 8 FMA | ~15% |
### SIMD Analysis
| Optimization | Applicable | Notes |
| :--- | :---: | :--- |
| AVX2 vectorization | ❌ | Dual 4-pole IIR recursive dependencies |
| FMA | ✅ | IIR coefficients benefit from FMA |
| Batch parallelism | ❌ | IIR filters inherently sequential |
**Note:** Both Ultrasmooth Filters use 4-pole IIR structures with strong recursive dependencies, preventing SIMD parallelization.
**Per-bar estimate:** ~75 cycles with FMA optimization
### Quality Metrics
| Metric | Score | Notes |
| :--- | :---: | :--- |
| **Accuracy** | 9/10 | Dual Ehlers filters provide excellent smoothing |
| **Timeliness** | 9/10 | Designed for near-zero lag |
| **Overshoot** | 8/10 | Ultrasmooth minimizes overshoot |
| **Smoothness** | 9/10 | 4-pole filters extremely smooth |
| **Accuracy** | 10/10 | Exact implementation per Ehlers specification |
| **Timeliness** | 9/10 | USF provides near-zero lag response |
| **Overshoot** | 8/10 | Minimal overshoot in trending markets |
| **Smoothness** | 9/10 | Very smooth bands due to 2-pole filtering |
| **Gap Handling** | 10/10 | True Range properly captures overnight gaps |
## Limitations and Considerations
## Validation
* **Lag (Minimized but Present):** While designed for minimal lag, some inherent delay from the smoothing process will still exist, especially if `Length` is increased for smoother bands.
* **Parameter Sensitivity:** Performance can be sensitive to the `STRLength`, `Length`, and `NumSTRs` parameters. These may need tuning for different instruments or timeframes.
* **Whipsaws:** In choppy or sideways markets, the high responsiveness might lead to more frequent false signals or whipsaws.
* **Not a Standalone System:** It's generally advisable to use the Ultimate Channel in conjunction with other indicators or analytical techniques for confirmation.
| Library | Status | Notes |
| :--- | :---: | :--- |
| **TA-Lib** | N/A | Not implemented (proprietary Ehlers indicator) |
| **Skender** | N/A | Not implemented |
| **Tulip** | N/A | Not implemented |
| **Ooples** | N/A | Not implemented |
| **PineScript** | ✅ | Reference implementation in `uchannel.pine` |
| **Self-consistency** | ✅ | Streaming, batch, and span modes match |
## Common Pitfalls
1. **Warmup Period**: The indicator requires `max(strPeriod, centerPeriod)` bars before producing stable values. Using results during warmup can lead to erratic signals.
2. **Parameter Confusion**: Unlike UBANDS (which uses a single period), UCHANNEL accepts two periods:
- `strPeriod`: Controls how quickly the band width responds to volatility changes
- `centerPeriod`: Controls how quickly the centerline follows price
3. **Gap Sensitivity**: True Range includes gap size, so significant overnight gaps will widen the channel. This is intended behavior but may surprise traders expecting simple high-low range.
4. **Memory Footprint**: Each instance requires ~200 bytes for state (record struct with 13 fields plus coefficients). At 1000 symbols: ~200KB.
5. **Different from UBANDS**: While both use USF for the centerline:
- UBANDS: Band width = RMS of price deviations from centerline
- UCHANNEL: Band width = USF-smoothed True Range × multiplier
6. **Bar Correction (`isNew=false`)**: When correcting the current bar, ensure you pass the complete updated OHLC values. Partial corrections may produce inconsistent results.
## References
* Ehlers, J. F. (2024, April). The Ultimate Smoother. *Stocks & Commodities Magazine*. (This article is referenced in the context of the Ultimate Channel's components).
* Ehlers, J. F. (General). *Various publications on advanced filtering and cycle analysis.* (e.g., "Rocket Science for Traders", "Cycle Analytics for Traders").
- Ehlers, John F. (2024). "Ultimate Channel." *Technical Analysis of Stocks & Commodities*.
- Ehlers, John F. (2013). *Cycle Analytics for Traders*. Wiley Trading.
- Ehlers, John F. (2001). *Rocket Science for Traders*. Wiley Trading.