2026-01-24 19:07:52 -08:00
|
|
|
|
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];
|
2026-02-10 21:33:16 -08:00
|
|
|
|
Uchannel.Batch(highArr.AsSpan(), lowArr.AsSpan(), closeArr.AsSpan(),
|
2026-01-24 19:07:52 -08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-01-25 16:01:45 -08:00
|
|
|
|
}
|