mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-23 13:08:04 +00:00
Add Standard Deviation Channel (SDCHANNEL) implementation and documentation
- Implemented Sdchannel class for calculating standard deviation channels based on linear regression. - Added detailed documentation for SDCHANNEL, including overview, calculation methods, and interpretation. - Updated project files to include new numerics library components in Channels and Volatility projects.
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class DecaychannelIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_SetsDefaults()
|
||||
{
|
||||
var ind = new DecaychannelIndicator();
|
||||
|
||||
Assert.Equal(100, ind.Period);
|
||||
Assert.True(ind.ShowColdValues);
|
||||
Assert.Equal("Decaychannel - Decay Min-Max Channel", ind.Name);
|
||||
Assert.False(ind.SeparateWindow);
|
||||
Assert.True(ind.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinHistoryDepths_EqualsPeriod()
|
||||
{
|
||||
var ind = new DecaychannelIndicator { Period = 15 };
|
||||
Assert.Equal(15, ind.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShortName_ReflectsParameters()
|
||||
{
|
||||
var ind = new DecaychannelIndicator { Period = 12 };
|
||||
Assert.Contains("12", ind.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_AddsThreeLineSeries()
|
||||
{
|
||||
var ind = new DecaychannelIndicator { Period = 14 };
|
||||
ind.Initialize();
|
||||
|
||||
Assert.Equal(3, ind.LinesSeries.Count);
|
||||
Assert.Equal("Middle", ind.LinesSeries[0].Name);
|
||||
Assert.Equal("Upper", ind.LinesSeries[1].Name);
|
||||
Assert.Equal("Lower", ind.LinesSeries[2].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessUpdate_Historical_ComputesValues()
|
||||
{
|
||||
var ind = new DecaychannelIndicator { Period = 3 };
|
||||
ind.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
ind.HistoricalData.AddBar(now, 100, 110, 90, 102);
|
||||
|
||||
ind.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.Equal(1, ind.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(ind.LinesSeries[0].GetValue(0)));
|
||||
Assert.True(double.IsFinite(ind.LinesSeries[1].GetValue(0)));
|
||||
Assert.True(double.IsFinite(ind.LinesSeries[2].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessUpdate_NewBar_Appends()
|
||||
{
|
||||
var ind = new DecaychannelIndicator { Period = 3 };
|
||||
ind.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
ind.HistoricalData.AddBar(now, 100, 110, 90, 102);
|
||||
ind.HistoricalData.AddBar(now.AddMinutes(1), 102, 112, 92, 104);
|
||||
|
||||
ind.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
ind.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, ind.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessUpdate_NewTick_DoesNotThrow()
|
||||
{
|
||||
var ind = new DecaychannelIndicator { Period = 5 };
|
||||
ind.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
ind.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
ind.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
ind.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
|
||||
Assert.Equal(2, ind.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultipleUpdates_ProducesFiniteSeries()
|
||||
{
|
||||
var ind = new DecaychannelIndicator { Period = 5 };
|
||||
ind.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
ind.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i);
|
||||
ind.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
|
||||
}
|
||||
|
||||
Assert.Equal(10, ind.LinesSeries[0].Count);
|
||||
Assert.Equal(10, ind.LinesSeries[1].Count);
|
||||
Assert.Equal(10, ind.LinesSeries[2].Count);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(ind.LinesSeries[0].GetValue(i)));
|
||||
Assert.True(double.IsFinite(ind.LinesSeries[1].GetValue(i)));
|
||||
Assert.True(double.IsFinite(ind.LinesSeries[2].GetValue(i)));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bands_Order_Correct()
|
||||
{
|
||||
var ind = new DecaychannelIndicator { Period = 3 };
|
||||
ind.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
ind.HistoricalData.AddBar(now.AddMinutes(i), 100, 110 + i, 90 - i, 100, 1000);
|
||||
ind.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
|
||||
}
|
||||
|
||||
double middle = ind.LinesSeries[0].GetValue(0);
|
||||
double upper = ind.LinesSeries[1].GetValue(0);
|
||||
double lower = ind.LinesSeries[2].GetValue(0);
|
||||
|
||||
Assert.True(upper >= middle, $"Upper ({upper}) should be >= Middle ({middle})");
|
||||
Assert.True(lower <= middle, $"Lower ({lower}) should be <= Middle ({middle})");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecayBehavior_ChannelContracts()
|
||||
{
|
||||
var ind = new DecaychannelIndicator { Period = 5 };
|
||||
ind.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Create initial wide channel
|
||||
ind.HistoricalData.AddBar(now, 100, 150, 50, 100, 1000);
|
||||
ind.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
double initialWidth = ind.LinesSeries[1].GetValue(0) - ind.LinesSeries[2].GetValue(0);
|
||||
|
||||
// Add flat bars
|
||||
for (int i = 1; i < 10; i++)
|
||||
{
|
||||
ind.HistoricalData.AddBar(now.AddMinutes(i), 100, 100, 100, 100, 1000);
|
||||
ind.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
}
|
||||
|
||||
double finalWidth = ind.LinesSeries[1].GetValue(0) - ind.LinesSeries[2].GetValue(0);
|
||||
|
||||
Assert.True(finalWidth <= initialWidth, "Channel should contract over time with no new extremes");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using System.Drawing;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using static QuanTAlib.IndicatorExtensions;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Decaychannel: Decay Min-Max Channel - Quantower Indicator Adapter
|
||||
/// Tracks highest high and lowest low with exponential decay toward midpoint.
|
||||
/// Uses ln(2)/period for true half-life behavior.
|
||||
/// </summary>
|
||||
public sealed class DecaychannelIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 10, minimum: 1, maximum: 500, increment: 1, decimalPlaces: 0)]
|
||||
public int Period { get; set; } = 100;
|
||||
|
||||
[InputParameter("Show Cold Values", sortIndex: 100)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Decaychannel? _indicator;
|
||||
|
||||
public int MinHistoryDepths => Period;
|
||||
public override string ShortName => $"Decaychannel({Period})";
|
||||
|
||||
public DecaychannelIndicator()
|
||||
{
|
||||
Name = "Decaychannel - Decay Min-Max Channel";
|
||||
Description = "Adaptive channel with exponential decay toward midpoint using half-life behavior";
|
||||
SeparateWindow = false;
|
||||
OnBackGround = true;
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_indicator = new Decaychannel(Period);
|
||||
|
||||
AddLineSeries(new LineSeries("Middle", Color.DodgerBlue, 2, LineStyle.Solid));
|
||||
AddLineSeries(new LineSeries("Upper", Color.FromArgb(255, 180, 180), 1, LineStyle.Dash));
|
||||
AddLineSeries(new LineSeries("Lower", Color.FromArgb(180, 180, 255), 1, LineStyle.Dash));
|
||||
}
|
||||
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
if (_indicator is null)
|
||||
return;
|
||||
|
||||
var item = HistoricalData[0, SeekOriginHistory.End];
|
||||
bool isNew = args.IsNewBar();
|
||||
|
||||
TBar input = new(
|
||||
time: item.TimeLeft,
|
||||
open: item[PriceType.Open],
|
||||
high: item[PriceType.High],
|
||||
low: item[PriceType.Low],
|
||||
close: item[PriceType.Close],
|
||||
volume: item[PriceType.Volume]
|
||||
);
|
||||
|
||||
_indicator.Update(input, isNew);
|
||||
|
||||
bool isHot = _indicator.IsHot;
|
||||
|
||||
LinesSeries[0].SetValue(_indicator.Last.Value, isHot, ShowColdValues);
|
||||
LinesSeries[1].SetValue(_indicator.Upper.Value, isHot, ShowColdValues);
|
||||
LinesSeries[2].SetValue(_indicator.Lower.Value, isHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
using System;
|
||||
using QuanTAlib;
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class DecaychannelTests
|
||||
{
|
||||
[Fact]
|
||||
public void Decaychannel_Constructor_ValidatesInput()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Decaychannel(0));
|
||||
Assert.Throws<ArgumentException>(() => new Decaychannel(-5));
|
||||
|
||||
var d = new Decaychannel(10);
|
||||
Assert.Equal(10, d.WarmupPeriod);
|
||||
Assert.Contains("Decaychannel", d.Name, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decaychannel_InitialState_Defaults()
|
||||
{
|
||||
var d = new Decaychannel(5);
|
||||
|
||||
Assert.Equal(0, d.Last.Value);
|
||||
Assert.Equal(0, d.Upper.Value);
|
||||
Assert.Equal(0, d.Lower.Value);
|
||||
Assert.False(d.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decaychannel_CalculatesBands()
|
||||
{
|
||||
var d = new Decaychannel(3);
|
||||
|
||||
d.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
|
||||
d.Update(new TBar(DateTime.UtcNow, 105, 115, 95, 110, 1000));
|
||||
d.Update(new TBar(DateTime.UtcNow, 110, 120, 100, 115, 1000));
|
||||
|
||||
// Bands should be within valid range
|
||||
Assert.True(double.IsFinite(d.Upper.Value));
|
||||
Assert.True(double.IsFinite(d.Lower.Value));
|
||||
Assert.True(double.IsFinite(d.Last.Value));
|
||||
|
||||
// Upper >= Lower, Middle in between
|
||||
Assert.True(d.Upper.Value >= d.Lower.Value);
|
||||
Assert.True(d.Last.Value >= d.Lower.Value);
|
||||
Assert.True(d.Last.Value <= d.Upper.Value);
|
||||
|
||||
Assert.True(d.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decaychannel_DecaysBands_OverTime()
|
||||
{
|
||||
var d = new Decaychannel(10);
|
||||
|
||||
// Establish initial extremes
|
||||
d.Update(new TBar(DateTime.UtcNow, 100, 120, 80, 100, 1000));
|
||||
|
||||
double initialUpper = d.Upper.Value;
|
||||
double initialLower = d.Lower.Value;
|
||||
double initialWidth = initialUpper - initialLower;
|
||||
|
||||
// Feed flat bars - no new extremes
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
d.Update(new TBar(DateTime.UtcNow, 100, 100, 100, 100, 1000));
|
||||
}
|
||||
|
||||
double laterWidth = d.Upper.Value - d.Lower.Value;
|
||||
|
||||
// Channel should have contracted (decayed toward midpoint)
|
||||
Assert.True(laterWidth <= initialWidth, $"Channel should decay. Initial width: {initialWidth}, Later width: {laterWidth}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decaychannel_ExpandsOnNewExtreme()
|
||||
{
|
||||
var d = new Decaychannel(5);
|
||||
|
||||
// Initial bars
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
d.Update(new TBar(DateTime.UtcNow, 100, 105, 95, 100, 1000));
|
||||
}
|
||||
|
||||
double widthBefore = d.Upper.Value - d.Lower.Value;
|
||||
|
||||
// New extreme high
|
||||
d.Update(new TBar(DateTime.UtcNow, 100, 130, 95, 100, 1000));
|
||||
|
||||
double widthAfter = d.Upper.Value - d.Lower.Value;
|
||||
|
||||
// Channel should have expanded
|
||||
Assert.True(widthAfter >= widthBefore);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decaychannel_IsHot_TurnsTrueAfterWarmup()
|
||||
{
|
||||
var d = new Decaychannel(4);
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
d.Update(new TBar(DateTime.UtcNow, 100 + i, 101 + i, 99 + i, 100 + i, 1000));
|
||||
Assert.False(d.IsHot);
|
||||
}
|
||||
|
||||
d.Update(new TBar(DateTime.UtcNow, 200, 201, 199, 200, 1000));
|
||||
Assert.True(d.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decaychannel_IsNewFalse_RestoresState()
|
||||
{
|
||||
var d = new Decaychannel(3);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 7);
|
||||
|
||||
TBar remembered = default;
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
remembered = gbm.Next(isNew: true);
|
||||
d.Update(remembered, isNew: true);
|
||||
}
|
||||
|
||||
double mid = d.Last.Value;
|
||||
double up = d.Upper.Value;
|
||||
double lo = d.Lower.Value;
|
||||
|
||||
// Several corrections
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var corrected = gbm.Next(isNew: false);
|
||||
d.Update(corrected, isNew: false);
|
||||
}
|
||||
|
||||
// Restore to remembered bar
|
||||
d.Update(remembered, isNew: false);
|
||||
|
||||
Assert.Equal(mid, d.Last.Value, 1e-10);
|
||||
Assert.Equal(up, d.Upper.Value, 1e-10);
|
||||
Assert.Equal(lo, d.Lower.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decaychannel_NaN_UsesLastValid()
|
||||
{
|
||||
var d = new Decaychannel(3);
|
||||
|
||||
d.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
|
||||
d.Update(new TBar(DateTime.UtcNow, 101, 111, 91, 106, 1000));
|
||||
|
||||
var result = d.Update(new TBar(DateTime.UtcNow, 102, double.NaN, 92, 107, 1000));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.True(double.IsFinite(d.Upper.Value));
|
||||
Assert.True(double.IsFinite(d.Lower.Value));
|
||||
|
||||
var result2 = d.Update(new TBar(DateTime.UtcNow, 103, 113, double.PositiveInfinity, 108, 1000));
|
||||
Assert.True(double.IsFinite(result2.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decaychannel_Reset_Clears()
|
||||
{
|
||||
var d = new Decaychannel(3);
|
||||
d.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 100, 1000));
|
||||
d.Update(new TBar(DateTime.UtcNow, 101, 111, 91, 101, 1000));
|
||||
|
||||
d.Reset();
|
||||
|
||||
Assert.Equal(0, d.Last.Value);
|
||||
Assert.Equal(0, d.Upper.Value);
|
||||
Assert.Equal(0, d.Lower.Value);
|
||||
Assert.False(d.IsHot);
|
||||
|
||||
d.Update(new TBar(DateTime.UtcNow, 50, 60, 40, 55, 1000));
|
||||
Assert.NotEqual(0, d.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decaychannel_BatchVsStreaming_Match()
|
||||
{
|
||||
var dStream = new Decaychannel(10);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
var series = new TBarSeries();
|
||||
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
series.Add(bar);
|
||||
dStream.Update(bar, isNew: true);
|
||||
}
|
||||
|
||||
double expectedMid = dStream.Last.Value;
|
||||
double expectedUp = dStream.Upper.Value;
|
||||
double expectedLo = dStream.Lower.Value;
|
||||
|
||||
var (midBatch, upBatch, loBatch) = Decaychannel.Batch(series, 10);
|
||||
|
||||
Assert.Equal(expectedMid, midBatch.Last.Value, 1e-10);
|
||||
Assert.Equal(expectedUp, upBatch.Last.Value, 1e-10);
|
||||
Assert.Equal(expectedLo, loBatch.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decaychannel_SpanBatch_Validates()
|
||||
{
|
||||
double[] high = [110, 115, 120];
|
||||
double[] low = [90, 95, 100];
|
||||
double[] middle = new double[3];
|
||||
double[] upper = new double[3];
|
||||
double[] lower = new double[3];
|
||||
|
||||
double[] highShort = [110, 115];
|
||||
double[] smallOut = new double[1];
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Decaychannel.Batch(high.AsSpan(), low.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 0));
|
||||
Assert.Throws<ArgumentException>(() => Decaychannel.Batch(high.AsSpan(), low.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), -1));
|
||||
Assert.Throws<ArgumentException>(() => Decaychannel.Batch(highShort.AsSpan(), low.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 2));
|
||||
Assert.Throws<ArgumentException>(() => Decaychannel.Batch(high.AsSpan(), low.AsSpan(), smallOut.AsSpan(), upper.AsSpan(), lower.AsSpan(), 2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decaychannel_SpanBatch_ComputesFiniteValues()
|
||||
{
|
||||
double[] high = [110, 115, 120, 125, 115, 110, 108];
|
||||
double[] low = [90, 95, 100, 105, 95, 90, 88];
|
||||
double[] middle = new double[7];
|
||||
double[] upper = new double[7];
|
||||
double[] lower = new double[7];
|
||||
|
||||
Decaychannel.Batch(high.AsSpan(), low.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 3);
|
||||
|
||||
for (int i = 0; i < 7; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(middle[i]), $"middle[{i}] should be finite");
|
||||
Assert.True(double.IsFinite(upper[i]), $"upper[{i}] should be finite");
|
||||
Assert.True(double.IsFinite(lower[i]), $"lower[{i}] should be finite");
|
||||
Assert.True(upper[i] >= lower[i], $"upper[{i}] >= lower[{i}]");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decaychannel_Calculate_ReturnsIndicatorAndResults()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
series.Add(DateTime.UtcNow, 100, 110, 90, 100, 1000);
|
||||
series.Add(DateTime.UtcNow, 105, 115, 95, 105, 1000);
|
||||
series.Add(DateTime.UtcNow, 110, 120, 100, 110, 1000);
|
||||
|
||||
var ((mid, up, lo), ind) = Decaychannel.Calculate(series, 2);
|
||||
|
||||
Assert.True(ind.IsHot);
|
||||
Assert.True(double.IsFinite(up.Last.Value));
|
||||
Assert.True(double.IsFinite(lo.Last.Value));
|
||||
Assert.True(double.IsFinite(mid.Last.Value));
|
||||
|
||||
ind.Update(new TBar(DateTime.UtcNow, 120, 130, 110, 120, 1000));
|
||||
Assert.True(double.IsFinite(ind.Upper.Value));
|
||||
Assert.True(double.IsFinite(ind.Lower.Value));
|
||||
Assert.True(double.IsFinite(ind.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decaychannel_Event_Publishes()
|
||||
{
|
||||
var src = new TBarSeries();
|
||||
var d = new Decaychannel(src, 2);
|
||||
bool fired = false;
|
||||
d.Pub += (object? sender, in TValueEventArgs args) => fired = true;
|
||||
|
||||
src.Add(new TBar(DateTime.UtcNow, 100, 110, 90, 100, 1000));
|
||||
Assert.True(fired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decaychannel_HalfLife_DecayBehavior()
|
||||
{
|
||||
// Test that decay follows half-life: 50% convergence over period bars
|
||||
int period = 10;
|
||||
var d = new Decaychannel(period);
|
||||
|
||||
// Establish extreme values
|
||||
d.Update(new TBar(DateTime.UtcNow, 100, 200, 0, 100, 1000)); // Upper=200, Lower=0, Mid=100
|
||||
|
||||
double initialUpper = d.Upper.Value;
|
||||
double initialLower = d.Lower.Value;
|
||||
double initialMid = (initialUpper + initialLower) * 0.5;
|
||||
|
||||
// Feed midpoint values for 'period' bars - no new extremes
|
||||
for (int i = 0; i < period; i++)
|
||||
{
|
||||
d.Update(new TBar(DateTime.UtcNow, 100, 100, 100, 100, 1000));
|
||||
}
|
||||
|
||||
// After period bars, should be approximately 50% convergence toward midpoint
|
||||
double upperDistance = d.Upper.Value - initialMid;
|
||||
double lowerDistance = initialMid - d.Lower.Value;
|
||||
|
||||
// The channel should have contracted significantly (roughly 50%)
|
||||
double initialUpperDistance = initialUpper - initialMid;
|
||||
double initialLowerDistance = initialMid - initialLower;
|
||||
|
||||
// Allow some tolerance for algorithm differences
|
||||
Assert.True(upperDistance < initialUpperDistance * 0.7, "Upper should decay toward midpoint");
|
||||
Assert.True(lowerDistance < initialLowerDistance * 0.7, "Lower should decay toward midpoint");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decaychannel_Bands_MaintainOrder()
|
||||
{
|
||||
var d = new Decaychannel(5);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
d.Update(bar, isNew: true);
|
||||
|
||||
Assert.True(d.Upper.Value >= d.Lower.Value, $"Bar {i}: Upper ({d.Upper.Value}) >= Lower ({d.Lower.Value})");
|
||||
Assert.True(d.Last.Value >= d.Lower.Value - 1e-10, $"Bar {i}: Middle ({d.Last.Value}) >= Lower ({d.Lower.Value})");
|
||||
Assert.True(d.Last.Value <= d.Upper.Value + 1e-10, $"Bar {i}: Middle ({d.Last.Value}) <= Upper ({d.Upper.Value})");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
using QuanTAlib;
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for Decaychannel - internal consistency tests only.
|
||||
/// No external library validation available (N/A in TA-Lib, Skender, Tulip, Ooples).
|
||||
/// </summary>
|
||||
public sealed class DecaychannelValidationTests : IDisposable
|
||||
{
|
||||
private readonly GBM _gbm;
|
||||
private readonly TBarSeries _series;
|
||||
private const int Period = 14;
|
||||
private const int DataLength = 500;
|
||||
|
||||
public DecaychannelValidationTests()
|
||||
{
|
||||
_gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
_series = new TBarSeries();
|
||||
for (int i = 0; i < DataLength; i++)
|
||||
{
|
||||
_series.Add(_gbm.Next(isNew: true));
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Cleanup if needed
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Streaming_Batch_Span_Consistency()
|
||||
{
|
||||
// Streaming mode
|
||||
var dStream = new Decaychannel(Period);
|
||||
for (int i = 0; i < _series.Count; i++)
|
||||
{
|
||||
dStream.Update(_series[i], isNew: true);
|
||||
}
|
||||
|
||||
// Batch mode (TBarSeries overload)
|
||||
var (midBatch, upBatch, loBatch) = Decaychannel.Batch(_series, Period);
|
||||
|
||||
// Span mode
|
||||
int len = _series.Count;
|
||||
double[] midSpan = new double[len];
|
||||
double[] upSpan = new double[len];
|
||||
double[] loSpan = new double[len];
|
||||
Decaychannel.Batch(_series.HighValues, _series.LowValues, midSpan.AsSpan(), upSpan.AsSpan(), loSpan.AsSpan(), Period);
|
||||
|
||||
// Compare last 100 values across all modes
|
||||
int checkStart = len - 100;
|
||||
for (int i = checkStart; i < len; i++)
|
||||
{
|
||||
Assert.Equal(midBatch[i].Value, midSpan[i], 1e-10);
|
||||
Assert.Equal(upBatch[i].Value, upSpan[i], 1e-10);
|
||||
Assert.Equal(loBatch[i].Value, loSpan[i], 1e-10);
|
||||
}
|
||||
|
||||
// Streaming vs batch final values
|
||||
Assert.Equal(dStream.Last.Value, midBatch.Last.Value, 1e-10);
|
||||
Assert.Equal(dStream.Upper.Value, upBatch.Last.Value, 1e-10);
|
||||
Assert.Equal(dStream.Lower.Value, loBatch.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceFiniteResults()
|
||||
{
|
||||
// Streaming
|
||||
var dStream = new Decaychannel(Period);
|
||||
for (int i = 0; i < _series.Count; i++)
|
||||
{
|
||||
var result = dStream.Update(_series[i], isNew: true);
|
||||
if (i >= Period - 1)
|
||||
{
|
||||
Assert.True(double.IsFinite(result.Value), $"Streaming: bar {i} should be finite");
|
||||
Assert.True(double.IsFinite(dStream.Upper.Value), $"Streaming upper: bar {i} should be finite");
|
||||
Assert.True(double.IsFinite(dStream.Lower.Value), $"Streaming lower: bar {i} should be finite");
|
||||
}
|
||||
}
|
||||
|
||||
// Batch
|
||||
var (mid, up, lo) = Decaychannel.Batch(_series, Period);
|
||||
for (int i = Period - 1; i < mid.Count; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(mid[i].Value), $"Batch middle: index {i} should be finite");
|
||||
Assert.True(double.IsFinite(up[i].Value), $"Batch upper: index {i} should be finite");
|
||||
Assert.True(double.IsFinite(lo[i].Value), $"Batch lower: index {i} should be finite");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BandOrdering_AlwaysValid()
|
||||
{
|
||||
var d = new Decaychannel(Period);
|
||||
for (int i = 0; i < _series.Count; i++)
|
||||
{
|
||||
d.Update(_series[i], isNew: true);
|
||||
|
||||
Assert.True(d.Upper.Value >= d.Lower.Value, $"Bar {i}: Upper >= Lower");
|
||||
Assert.True(d.Last.Value >= d.Lower.Value - 1e-10, $"Bar {i}: Middle >= Lower");
|
||||
Assert.True(d.Last.Value <= d.Upper.Value + 1e-10, $"Bar {i}: Middle <= Upper");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Static_ReturnsValidIndicator()
|
||||
{
|
||||
var ((mid, up, lo), indicator) = Decaychannel.Calculate(_series, Period);
|
||||
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.Equal(mid.Count, _series.Count);
|
||||
Assert.Equal(up.Count, _series.Count);
|
||||
Assert.Equal(lo.Count, _series.Count);
|
||||
|
||||
// Can continue streaming
|
||||
var newBar = _gbm.Next(isNew: true);
|
||||
indicator.Update(newBar, isNew: true);
|
||||
Assert.True(double.IsFinite(indicator.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecayBehavior_ChannelContractsWithoutNewExtremes()
|
||||
{
|
||||
var d = new Decaychannel(Period);
|
||||
|
||||
// Prime with data
|
||||
for (int i = 0; i < Period * 2; i++)
|
||||
{
|
||||
d.Update(_series[i], isNew: true);
|
||||
}
|
||||
|
||||
double widthBefore = d.Upper.Value - d.Lower.Value;
|
||||
|
||||
// Feed flat bars at midpoint
|
||||
double mid = (d.Upper.Value + d.Lower.Value) * 0.5;
|
||||
for (int i = 0; i < Period; i++)
|
||||
{
|
||||
d.Update(new TBar(DateTime.UtcNow, mid, mid, mid, mid, 1000), isNew: true);
|
||||
}
|
||||
|
||||
double widthAfter = d.Upper.Value - d.Lower.Value;
|
||||
|
||||
Assert.True(widthAfter < widthBefore, "Channel should contract when no new extremes");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BarCorrection_RestoresState()
|
||||
{
|
||||
var d = new Decaychannel(Period);
|
||||
|
||||
// Build up state
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
d.Update(_series[i], isNew: true);
|
||||
}
|
||||
|
||||
double midBefore = d.Last.Value;
|
||||
double upBefore = d.Upper.Value;
|
||||
double loBefore = d.Lower.Value;
|
||||
|
||||
// Apply correction
|
||||
var correctionBar = new TBar(DateTime.UtcNow, 200, 250, 150, 200, 1000);
|
||||
d.Update(correctionBar, isNew: false);
|
||||
|
||||
// Values should change
|
||||
Assert.NotEqual(midBefore, d.Last.Value);
|
||||
|
||||
// Apply another correction back to original-ish
|
||||
d.Update(_series[49], isNew: false);
|
||||
|
||||
// Should restore
|
||||
Assert.Equal(midBefore, d.Last.Value, 1e-10);
|
||||
Assert.Equal(upBefore, d.Upper.Value, 1e-10);
|
||||
Assert.Equal(loBefore, d.Lower.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Event_FiresOnUpdate()
|
||||
{
|
||||
var src = new TBarSeries();
|
||||
var d = new Decaychannel(src, Period);
|
||||
|
||||
int eventCount = 0;
|
||||
d.Pub += (object? sender, in TValueEventArgs args) => eventCount++;
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
src.Add(_series[i]);
|
||||
}
|
||||
|
||||
Assert.Equal(10, eventCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultiPeriod_Consistency()
|
||||
{
|
||||
int[] periods = [5, 10, 20, 50];
|
||||
|
||||
foreach (int period in periods)
|
||||
{
|
||||
var d = new Decaychannel(period);
|
||||
for (int i = 0; i < _series.Count; i++)
|
||||
{
|
||||
d.Update(_series[i], isNew: true);
|
||||
}
|
||||
|
||||
Assert.True(d.IsHot, $"Period {period} should be hot");
|
||||
Assert.True(double.IsFinite(d.Last.Value), $"Period {period} middle should be finite");
|
||||
Assert.True(d.Upper.Value >= d.Lower.Value, $"Period {period} upper >= lower");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,492 @@
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// DECAYCHANNEL: Decay Min-Max Channel
|
||||
/// Tracks highest high and lowest low with exponential decay toward their midpoint.
|
||||
/// Uses ln(2)/period for true half-life behavior: 50% convergence over period bars.
|
||||
/// </summary>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Decaychannel : ITValuePublisher
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly double _decayLambda;
|
||||
private readonly double[] _hBuf;
|
||||
private readonly double[] _lBuf;
|
||||
private readonly double[] _hBuf_prev;
|
||||
private readonly double[] _lBuf_prev;
|
||||
|
||||
private int _count;
|
||||
private long _index;
|
||||
private double _currentMax;
|
||||
private double _currentMin;
|
||||
private long _maxAge;
|
||||
private long _minAge;
|
||||
private double _rawMax;
|
||||
private double _rawMin;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(
|
||||
double LastValidHigh,
|
||||
double LastValidLow,
|
||||
double CurrentMax,
|
||||
double CurrentMin,
|
||||
long MaxAge,
|
||||
long MinAge,
|
||||
double RawMax,
|
||||
double RawMin,
|
||||
int Count,
|
||||
long Index);
|
||||
|
||||
private State _state;
|
||||
private State _p_state;
|
||||
|
||||
private readonly TBarPublishedHandler _barHandler;
|
||||
|
||||
public string Name { get; }
|
||||
public int WarmupPeriod { get; }
|
||||
public TValue Last { get; private set; }
|
||||
public TValue Upper { get; private set; }
|
||||
public TValue Lower { get; private set; }
|
||||
public bool IsHot => _count >= _period;
|
||||
|
||||
public event TValuePublishedHandler? Pub;
|
||||
|
||||
public Decaychannel(int period)
|
||||
{
|
||||
if (period <= 0)
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
|
||||
_period = period;
|
||||
_decayLambda = Math.Log(2.0) / period;
|
||||
_hBuf = new double[_period];
|
||||
_lBuf = new double[_period];
|
||||
_hBuf_prev = new double[_period];
|
||||
_lBuf_prev = new double[_period];
|
||||
_count = 0;
|
||||
_index = -1;
|
||||
_currentMax = double.NaN;
|
||||
_currentMin = double.NaN;
|
||||
_rawMax = double.NaN;
|
||||
_rawMin = double.NaN;
|
||||
_maxAge = 0;
|
||||
_minAge = 0;
|
||||
|
||||
_state = new State(double.NaN, double.NaN, double.NaN, double.NaN, 0, 0, double.NaN, double.NaN, 0, -1);
|
||||
_p_state = _state;
|
||||
|
||||
Name = $"Decaychannel({period})";
|
||||
WarmupPeriod = period;
|
||||
_barHandler = HandleBar;
|
||||
}
|
||||
|
||||
public Decaychannel(TBarSeries source, int period) : this(period)
|
||||
{
|
||||
Prime(source);
|
||||
source.Pub += _barHandler;
|
||||
}
|
||||
|
||||
private void HandleBar(object? sender, in TBarEventArgs e) => Update(e.Value, e.IsNew);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void PubEvent(TValue value, bool isNew = true) => Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew });
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private (double high, double low) GetValid(double high, double low)
|
||||
{
|
||||
if (double.IsFinite(high))
|
||||
_state = _state with { LastValidHigh = high };
|
||||
else
|
||||
high = _state.LastValidHigh;
|
||||
|
||||
if (double.IsFinite(low))
|
||||
_state = _state with { LastValidLow = low };
|
||||
else
|
||||
low = _state.LastValidLow;
|
||||
|
||||
return (high, low);
|
||||
}
|
||||
|
||||
private void SaveState()
|
||||
{
|
||||
_state = new State(
|
||||
_state.LastValidHigh,
|
||||
_state.LastValidLow,
|
||||
_currentMax,
|
||||
_currentMin,
|
||||
_maxAge,
|
||||
_minAge,
|
||||
_rawMax,
|
||||
_rawMin,
|
||||
_count,
|
||||
_index);
|
||||
|
||||
// Save buffer contents
|
||||
Array.Copy(_hBuf, _hBuf_prev, _period);
|
||||
Array.Copy(_lBuf, _lBuf_prev, _period);
|
||||
}
|
||||
|
||||
private void RestoreState()
|
||||
{
|
||||
_currentMax = _p_state.CurrentMax;
|
||||
_currentMin = _p_state.CurrentMin;
|
||||
_maxAge = _p_state.MaxAge;
|
||||
_minAge = _p_state.MinAge;
|
||||
_rawMax = _p_state.RawMax;
|
||||
_rawMin = _p_state.RawMin;
|
||||
_count = _p_state.Count;
|
||||
_index = _p_state.Index;
|
||||
_state = _p_state;
|
||||
|
||||
// Restore buffer contents
|
||||
Array.Copy(_hBuf_prev, _hBuf, _period);
|
||||
Array.Copy(_lBuf_prev, _lBuf, _period);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private (double rawMax, double rawMin) ComputeRawExtremes()
|
||||
{
|
||||
int len = Math.Min(_count, _period);
|
||||
if (len == 0)
|
||||
return (double.NaN, double.NaN);
|
||||
|
||||
double max = double.MinValue;
|
||||
double min = double.MaxValue;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
int idx = (int)((_index - i) % _period);
|
||||
if (idx < 0) idx += _period;
|
||||
|
||||
double h = _hBuf[idx];
|
||||
double l = _lBuf[idx];
|
||||
|
||||
if (h > max) max = h;
|
||||
if (l < min) min = l;
|
||||
}
|
||||
|
||||
return (max, min);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TBar input, bool isNew = true)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
// Save state BEFORE advancing (this is state from end of previous bar)
|
||||
SaveState();
|
||||
_p_state = _state;
|
||||
|
||||
// Now advance to new bar
|
||||
_index++;
|
||||
if (_count < _period)
|
||||
_count++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Restore to state before current bar
|
||||
RestoreState();
|
||||
|
||||
// Re-advance to current bar position (we're reprocessing current bar)
|
||||
_index++;
|
||||
if (_count < _period)
|
||||
_count++;
|
||||
}
|
||||
|
||||
int bufIdx = (int)(_index % _period);
|
||||
var (high, low) = GetValid(input.High, input.Low);
|
||||
|
||||
if (double.IsNaN(high) || double.IsNaN(low))
|
||||
{
|
||||
Last = new TValue(input.Time, double.NaN);
|
||||
Upper = new TValue(input.Time, double.NaN);
|
||||
Lower = new TValue(input.Time, double.NaN);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
_hBuf[bufIdx] = high;
|
||||
_lBuf[bufIdx] = low;
|
||||
|
||||
// Get raw max/min over the period window
|
||||
var (rawMax, rawMin) = ComputeRawExtremes();
|
||||
_rawMax = rawMax;
|
||||
_rawMin = rawMin;
|
||||
|
||||
// Check if new extremes
|
||||
bool newMax = high >= rawMax;
|
||||
bool newMin = low <= rawMin;
|
||||
|
||||
// Same logic for both isNew=true and isNew=false (correction reprocesses identically)
|
||||
if (newMax)
|
||||
{
|
||||
_currentMax = rawMax;
|
||||
_maxAge = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
_maxAge++;
|
||||
}
|
||||
|
||||
if (newMin)
|
||||
{
|
||||
_currentMin = rawMin;
|
||||
_minAge = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
_minAge++;
|
||||
}
|
||||
|
||||
// Initialize if first valid values
|
||||
if (double.IsNaN(_currentMax))
|
||||
{
|
||||
_currentMax = rawMax;
|
||||
_maxAge = 0;
|
||||
}
|
||||
if (double.IsNaN(_currentMin))
|
||||
{
|
||||
_currentMin = rawMin;
|
||||
_minAge = 0;
|
||||
}
|
||||
|
||||
// Apply decay toward midpoint
|
||||
double midpoint = (_currentMax + _currentMin) * 0.5;
|
||||
|
||||
// decayRate = 1 - e^(-lambda * age)
|
||||
double maxDecayRate = 1.0 - Math.Exp(-_decayLambda * _maxAge);
|
||||
double minDecayRate = 1.0 - Math.Exp(-_decayLambda * _minAge);
|
||||
|
||||
// Apply decay: currentMax = currentMax - decayRate * (currentMax - midpoint)
|
||||
// Using FMA: currentMax = midpoint + (1 - decayRate) * (currentMax - midpoint)
|
||||
double decayedMax = Math.FusedMultiplyAdd(1.0 - maxDecayRate, _currentMax - midpoint, midpoint);
|
||||
double decayedMin = Math.FusedMultiplyAdd(1.0 - minDecayRate, _currentMin - midpoint, midpoint);
|
||||
|
||||
// Constrain within raw extremes
|
||||
double top = Math.Min(decayedMax, rawMax);
|
||||
double bot = Math.Max(decayedMin, rawMin);
|
||||
|
||||
// Update tracked values for next iteration
|
||||
_currentMax = Math.Max(top, rawMax);
|
||||
_currentMin = Math.Min(bot, rawMin);
|
||||
|
||||
double mid = (top + bot) * 0.5;
|
||||
|
||||
Last = new TValue(input.Time, mid);
|
||||
Upper = new TValue(input.Time, top);
|
||||
Lower = new TValue(input.Time, bot);
|
||||
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TBarSeries source)
|
||||
{
|
||||
if (source.Count == 0)
|
||||
return (new TSeries([], []), new TSeries([], []), new TSeries([], []));
|
||||
|
||||
int len = source.Count;
|
||||
var tMiddle = new List<long>(len);
|
||||
var vMiddle = new List<double>(len);
|
||||
var tUpper = new List<long>(len);
|
||||
var vUpper = new List<double>(len);
|
||||
var tLower = new List<long>(len);
|
||||
var vLower = new List<double>(len);
|
||||
|
||||
CollectionsMarshal.SetCount(tMiddle, len);
|
||||
CollectionsMarshal.SetCount(vMiddle, len);
|
||||
CollectionsMarshal.SetCount(tUpper, len);
|
||||
CollectionsMarshal.SetCount(vUpper, len);
|
||||
CollectionsMarshal.SetCount(tLower, len);
|
||||
CollectionsMarshal.SetCount(vLower, len);
|
||||
|
||||
var tSpan = CollectionsMarshal.AsSpan(tMiddle);
|
||||
var vMiddleSpan = CollectionsMarshal.AsSpan(vMiddle);
|
||||
var vUpperSpan = CollectionsMarshal.AsSpan(vUpper);
|
||||
var vLowerSpan = CollectionsMarshal.AsSpan(vLower);
|
||||
|
||||
Batch(source.HighValues, source.LowValues, vMiddleSpan, vUpperSpan, vLowerSpan, _period);
|
||||
|
||||
source.Times.CopyTo(tSpan);
|
||||
tSpan.CopyTo(CollectionsMarshal.AsSpan(tUpper));
|
||||
tSpan.CopyTo(CollectionsMarshal.AsSpan(tLower));
|
||||
|
||||
Prime(source);
|
||||
|
||||
var lastTime = new DateTime(source.Times[^1], DateTimeKind.Utc);
|
||||
Last = new TValue(lastTime, vMiddleSpan[^1]);
|
||||
Upper = new TValue(lastTime, vUpperSpan[^1]);
|
||||
Lower = new TValue(lastTime, vLowerSpan[^1]);
|
||||
|
||||
return (new TSeries(tMiddle, vMiddle), new TSeries(tUpper, vUpper), new TSeries(tLower, vLower));
|
||||
}
|
||||
|
||||
public void Prime(TBarSeries source)
|
||||
{
|
||||
Reset();
|
||||
|
||||
if (source.Count == 0)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Update(source[i], isNew: true);
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Array.Clear(_hBuf);
|
||||
Array.Clear(_lBuf);
|
||||
Array.Clear(_hBuf_prev);
|
||||
Array.Clear(_lBuf_prev);
|
||||
_count = 0;
|
||||
_index = -1;
|
||||
_currentMax = double.NaN;
|
||||
_currentMin = double.NaN;
|
||||
_rawMax = double.NaN;
|
||||
_rawMin = double.NaN;
|
||||
_maxAge = 0;
|
||||
_minAge = 0;
|
||||
_state = new State(double.NaN, double.NaN, double.NaN, double.NaN, 0, 0, double.NaN, double.NaN, 0, -1);
|
||||
_p_state = _state;
|
||||
Last = default;
|
||||
Upper = default;
|
||||
Lower = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch calculation using spans (zero allocation except ArrayPool rentals).
|
||||
/// </summary>
|
||||
public static void Batch(
|
||||
ReadOnlySpan<double> high,
|
||||
ReadOnlySpan<double> low,
|
||||
Span<double> middle,
|
||||
Span<double> upper,
|
||||
Span<double> lower,
|
||||
int period)
|
||||
{
|
||||
if (period <= 0)
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
if (high.Length != low.Length)
|
||||
throw new ArgumentException("High and Low spans must have the same length", nameof(high));
|
||||
if (middle.Length < high.Length || upper.Length < high.Length || lower.Length < high.Length)
|
||||
throw new ArgumentException("Output spans must be at least as long as inputs", nameof(middle));
|
||||
|
||||
int len = high.Length;
|
||||
if (len == 0) return;
|
||||
|
||||
double decayLambda = Math.Log(2.0) / period;
|
||||
|
||||
// Compute raw rolling max/min
|
||||
double[] rawMaxArr = ArrayPool<double>.Shared.Rent(len);
|
||||
double[] rawMinArr = ArrayPool<double>.Shared.Rent(len);
|
||||
|
||||
try
|
||||
{
|
||||
QuanTAlib.Highest.Calculate(high, rawMaxArr.AsSpan(0, len), period);
|
||||
QuanTAlib.Lowest.Calculate(low, rawMinArr.AsSpan(0, len), period);
|
||||
|
||||
double currentMax = double.NaN;
|
||||
double currentMin = double.NaN;
|
||||
long maxAge = 0;
|
||||
long minAge = 0;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double rawMax = rawMaxArr[i];
|
||||
double rawMin = rawMinArr[i];
|
||||
double h = high[i];
|
||||
double l = low[i];
|
||||
|
||||
bool newMax = h >= rawMax;
|
||||
bool newMin = l <= rawMin;
|
||||
|
||||
if (newMax || double.IsNaN(currentMax))
|
||||
{
|
||||
currentMax = rawMax;
|
||||
maxAge = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
maxAge++;
|
||||
}
|
||||
|
||||
if (newMin || double.IsNaN(currentMin))
|
||||
{
|
||||
currentMin = rawMin;
|
||||
minAge = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
minAge++;
|
||||
}
|
||||
|
||||
double midpoint = (currentMax + currentMin) * 0.5;
|
||||
|
||||
double maxDecayRate = 1.0 - Math.Exp(-decayLambda * maxAge);
|
||||
double minDecayRate = 1.0 - Math.Exp(-decayLambda * minAge);
|
||||
|
||||
double decayedMax = Math.FusedMultiplyAdd(1.0 - maxDecayRate, currentMax - midpoint, midpoint);
|
||||
double decayedMin = Math.FusedMultiplyAdd(1.0 - minDecayRate, currentMin - midpoint, midpoint);
|
||||
|
||||
double top = Math.Min(decayedMax, rawMax);
|
||||
double bot = Math.Max(decayedMin, rawMin);
|
||||
|
||||
currentMax = Math.Max(top, rawMax);
|
||||
currentMin = Math.Min(bot, rawMin);
|
||||
|
||||
double mid = (top + bot) * 0.5;
|
||||
|
||||
middle[i] = mid;
|
||||
upper[i] = top;
|
||||
lower[i] = bot;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<double>.Shared.Return(rawMaxArr);
|
||||
ArrayPool<double>.Shared.Return(rawMinArr);
|
||||
}
|
||||
}
|
||||
|
||||
public static (TSeries Middle, TSeries Upper, TSeries Lower) Batch(TBarSeries source, int period)
|
||||
{
|
||||
int len = source.Count;
|
||||
var tMiddle = new List<long>(len);
|
||||
var vMiddle = new List<double>(len);
|
||||
var tUpper = new List<long>(len);
|
||||
var vUpper = new List<double>(len);
|
||||
var tLower = new List<long>(len);
|
||||
var vLower = new List<double>(len);
|
||||
|
||||
CollectionsMarshal.SetCount(tMiddle, len);
|
||||
CollectionsMarshal.SetCount(vMiddle, len);
|
||||
CollectionsMarshal.SetCount(tUpper, len);
|
||||
CollectionsMarshal.SetCount(vUpper, len);
|
||||
CollectionsMarshal.SetCount(tLower, len);
|
||||
CollectionsMarshal.SetCount(vLower, len);
|
||||
|
||||
Batch(source.HighValues, source.LowValues,
|
||||
CollectionsMarshal.AsSpan(vMiddle),
|
||||
CollectionsMarshal.AsSpan(vUpper),
|
||||
CollectionsMarshal.AsSpan(vLower),
|
||||
period);
|
||||
|
||||
source.Times.CopyTo(CollectionsMarshal.AsSpan(tMiddle));
|
||||
CollectionsMarshal.AsSpan(tMiddle).CopyTo(CollectionsMarshal.AsSpan(tUpper));
|
||||
CollectionsMarshal.AsSpan(tMiddle).CopyTo(CollectionsMarshal.AsSpan(tLower));
|
||||
|
||||
return (new TSeries(tMiddle, vMiddle), new TSeries(tUpper, vUpper), new TSeries(tLower, vLower));
|
||||
}
|
||||
|
||||
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Decaychannel Indicator) Calculate(TBarSeries source, int period)
|
||||
{
|
||||
var indicator = new Decaychannel(source, period);
|
||||
var results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
}
|
||||
@@ -1,125 +1,193 @@
|
||||
# DECAYCHANNEL: Decay Min-Max Channel
|
||||
|
||||
## Overview and Purpose
|
||||
> "Yesterday's high matters less today. Tomorrow, it matters even less. Decay channels know this."
|
||||
|
||||
The Decay Min-Max Channel (DECAYCHANNEL) is an adaptive technical analysis tool that tracks the highest high and lowest low values over a specified period while implementing exponential decay toward their midpoint. Unlike traditional static channels that maintain fixed extreme values until new extremes occur, DECAYCHANNEL gradually reduces the distance between the upper and lower bounds over time, causing them to converge toward the channel's center. This decay mechanism creates a more responsive channel that adapts to changing market conditions by automatically reducing channel width when new extremes aren't established.
|
||||
Decay Min-Max Channel (DECAYCHANNEL) tracks the highest high and lowest low like Donchian, then applies exponential decay toward the midpoint. Fresh extremes snap the bands outward; time compresses them inward. The result: channels that respect recent price action while gradually forgetting stale levels. This implementation uses true half-life mathematics—50% convergence over the period length—ensuring predictable decay behavior across all timeframes.
|
||||
|
||||
The implementation uses efficient circular buffer management and exponential decay mathematics to ensure optimal performance while providing traders with a dynamic view of support and resistance levels that naturally adjust to market momentum. By combining the reliability of extreme value tracking with the adaptability of decay functions, DECAYCHANNEL offers a unique perspective on market structure that balances historical significance with current market relevance.
|
||||
## Historical Context
|
||||
|
||||
## Core Concepts
|
||||
Traditional Donchian Channels treat all extremes within the lookback window equally. A high from 19 bars ago has the same influence as a high from 1 bar ago. This works for breakout detection but creates artificial support/resistance levels that persist until they mechanically exit the window.
|
||||
|
||||
* **Adaptive extreme tracking:** Maintains highest high and lowest low while gradually reducing their influence over time through exponential decay
|
||||
* **Midpoint convergence:** Decay targets the mathematical center of the channel, creating natural compression during ranging markets
|
||||
* **Period-based decay timing:** Decay rate automatically scales with the lookback period, ensuring consistent behavior across different timeframes
|
||||
* **Dynamic support/resistance:** Provides evolving support and resistance levels that strengthen with fresh extremes and weaken over time
|
||||
* **Market regime adaptation:** Channels naturally tighten during consolidation and expand during breakout movements
|
||||
Traders noticed this rigidity. A 20-day high from exactly 20 days ago shouldn't matter as much as one from 5 days ago. Various "adaptive channel" approaches emerged in the 1990s-2000s, but most used arbitrary decay rates or complex volatility weighting.
|
||||
|
||||
DECAYCHANNEL differs fundamentally from other channel indicators by acknowledging that historical extremes become less relevant over time. This approach creates channels that are more responsive to current market conditions while still respecting significant price levels, making it particularly effective for identifying when markets are transitioning between different phases.
|
||||
DECAYCHANNEL takes a simpler approach: pure exponential decay with mathematically defined half-life. The decay constant $\lambda = \ln(2) / \text{period}$ guarantees that bands converge 50% toward the midpoint over exactly one period. After two periods: 75%. After three: 87.5%. No tuning parameters, no volatility lookups—just consistent, predictable decay.
|
||||
|
||||
## Common Settings and Parameters
|
||||
## Architecture & Physics
|
||||
|
||||
| Parameter | Default | Function | When to Adjust |
|
||||
| --------- | ------- | -------- | -------------- |
|
||||
| Period | 100 | Lookback window for extreme value calculation and decay timing | Shorter (20-50) for more responsive channels; longer (200-500) for major structural levels |
|
||||
| High Source | High | Data source for maximum value tracking | Rarely changed; could use close for different perspective |
|
||||
| Low Source | Low | Data source for minimum value tracking | Rarely changed; could use close for different perspective |
|
||||
DECAYCHANNEL consists of four interconnected components that balance extreme tracking with temporal decay.
|
||||
|
||||
**Pro Tip:** For swing trading, consider using period = 50 to capture intermediate-term extremes with moderate decay. For position trading, period = 200 provides more stable channels that reflect major market structure. The decay mechanism naturally creates tighter channels during consolidation and wider channels during trending moves, eliminating the need for manual parameter adjustments.
|
||||
### 1. Extreme Tracking (Highest/Lowest)
|
||||
|
||||
## Calculation and Mathematical Foundation
|
||||
Internal Highest and Lowest indicators maintain the actual max/min over the period:
|
||||
|
||||
**Simplified explanation:**
|
||||
DECAYCHANNEL tracks the highest high and lowest low over the specified period, then applies exponential decay to gradually move these values toward their midpoint. The decay rate uses true half-life mathematics, providing 50% convergence toward the center after the full period length, creating balanced channel compression that maintains visual clarity while adapting to market conditions.
|
||||
$$
|
||||
H_t^{raw} = \max_{i=0}^{n-1}(High_{t-i})
|
||||
$$
|
||||
|
||||
**Technical formula:**
|
||||
$$
|
||||
L_t^{raw} = \min_{i=0}^{n-1}(Low_{t-i})
|
||||
$$
|
||||
|
||||
```
|
||||
decayLambda = ln(2.0) / period
|
||||
midpoint = (currentMax + currentMin) / 2
|
||||
maxDecayRate = 1 - e^(-decayLambda × timeSinceNewMax)
|
||||
minDecayRate = 1 - e^(-decayLambda × timeSinceNewMin)
|
||||
currentMax = currentMax - maxDecayRate × (currentMax - midpoint)
|
||||
currentMin = currentMin - minDecayRate × (currentMin - midpoint)
|
||||
```
|
||||
These raw values constrain the decayed bands—the upper band can never exceed the actual highest high, and the lower band can never go below the actual lowest low.
|
||||
|
||||
Where:
|
||||
* ln(2.0) ≈ 0.693 provides true half-life behavior with 50% convergence over the period
|
||||
* timeSinceNewMax/Min tracks bars elapsed since each extreme was established
|
||||
* Decay is applied independently to upper and lower bounds
|
||||
* Values are constrained within period's actual highest high and lowest low
|
||||
### 2. Decay Timers
|
||||
|
||||
> 🔍 **Technical Note:** The implementation uses ln(2.0) to provide true half-life exponential decay behavior. This creates 50% convergence toward the midpoint over the period length, ensuring that channels maintain their analytical value while adapting to changing market conditions. After two periods, convergence reaches 75%, and after three periods, approximately 87.5%.
|
||||
Separate counters track how long since each band was reset by a new extreme:
|
||||
|
||||
## Interpretation Details
|
||||
$$
|
||||
\tau_U = \text{bars since } High_t = H_t^{raw}
|
||||
$$
|
||||
|
||||
DECAYCHANNEL provides sophisticated market insights through its adaptive behavior:
|
||||
$$
|
||||
\tau_L = \text{bars since } Low_t = L_t^{raw}
|
||||
$$
|
||||
|
||||
* **Fresh breakouts:** When price establishes new extremes, channels immediately expand and reset decay timing, highlighting significant market moves
|
||||
* **Consolidation detection:** During ranging markets, channels gradually contract toward the midpoint, visually representing reduced volatility
|
||||
* **Support/resistance evolution:** Channel boundaries strengthen when recently tested and weaken over time if not confirmed by new price action
|
||||
* **Trend transition signals:** Channel compression often precedes significant directional moves, similar to volatility squeeze patterns
|
||||
* **Multi-timeframe consistency:** Decay timing scales automatically with period length, maintaining consistent visual behavior across timeframes
|
||||
* **Momentum indication:** Rapid channel expansion indicates strong momentum, while gradual compression suggests weakening directional bias
|
||||
* **Entry timing:** Channel touches provide potential entry points, with effectiveness indicated by how recently the boundary was established
|
||||
When price makes a new extreme, the corresponding timer resets to zero. Otherwise, it increments each bar.
|
||||
|
||||
## Limitations and Considerations
|
||||
### 3. Exponential Decay Engine
|
||||
|
||||
* **Decay rate consistency:** The ln(2.0) half-life parameter provides standard exponential decay behavior across all markets and timeframes
|
||||
* **Historical dependence:** Still relies on historical extremes, providing no predictive capability about future price movements
|
||||
* **Complexity trade-off:** More sophisticated than simple min-max channels, requiring understanding of decay mechanics
|
||||
* **Parameter selection:** Period length significantly affects both channel width and decay behavior
|
||||
* **No directional bias:** Provides adaptive levels but no inherent indication of likely breakout direction
|
||||
* **Initialization period:** Requires sufficient historical data to establish meaningful extreme values before decay becomes relevant
|
||||
* **Market condition adaptation:** May generate different signal frequency in trending versus ranging markets
|
||||
* **Confirmation requirement:** Most effective when combined with volume, momentum, or other technical confirmation
|
||||
The decay rate uses the half-life formula:
|
||||
|
||||
$$
|
||||
\lambda = \frac{\ln(2)}{\text{period}}
|
||||
$$
|
||||
|
||||
For each bar, compute the decay factor based on elapsed time:
|
||||
|
||||
$$
|
||||
d_U = 1 - e^{-\lambda \cdot \tau_U}
|
||||
$$
|
||||
|
||||
$$
|
||||
d_L = 1 - e^{-\lambda \cdot \tau_L}
|
||||
$$
|
||||
|
||||
At $\tau = 0$ (new extreme), $d = 0$ (no decay). At $\tau = \text{period}$, $d = 0.5$ (half decayed).
|
||||
|
||||
### 4. Midpoint Convergence
|
||||
|
||||
Bands decay toward the current midpoint, not toward price:
|
||||
|
||||
$$
|
||||
M_t = \frac{U_{t-1} + L_{t-1}}{2}
|
||||
$$
|
||||
|
||||
$$
|
||||
U_t = U_{t-1} - d_U \cdot (U_{t-1} - M_t)
|
||||
$$
|
||||
|
||||
$$
|
||||
L_t = L_{t-1} + d_L \cdot (M_t - L_{t-1})
|
||||
$$
|
||||
|
||||
Finally, constrain to actual extremes:
|
||||
|
||||
$$
|
||||
U_t = \max(U_t, H_t^{raw})
|
||||
$$
|
||||
|
||||
$$
|
||||
L_t = \min(L_t, L_t^{raw})
|
||||
$$
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Half-Life Derivation
|
||||
|
||||
Exponential decay follows:
|
||||
|
||||
$$
|
||||
V(t) = V_0 \cdot e^{-\lambda t}
|
||||
$$
|
||||
|
||||
For half-life $t_{1/2}$ where $V(t_{1/2}) = \frac{V_0}{2}$:
|
||||
|
||||
$$
|
||||
\frac{V_0}{2} = V_0 \cdot e^{-\lambda t_{1/2}}
|
||||
$$
|
||||
|
||||
$$
|
||||
\lambda = \frac{\ln(2)}{t_{1/2}}
|
||||
$$
|
||||
|
||||
Setting $t_{1/2} = \text{period}$ gives the implementation's decay constant.
|
||||
|
||||
### Convergence Schedule
|
||||
|
||||
| Elapsed Time | Decay Factor | Remaining Distance |
|
||||
| :--- | :---: | :---: |
|
||||
| 0 bars | 0% | 100% |
|
||||
| period/2 bars | 29.3% | 70.7% |
|
||||
| period bars | 50% | 50% |
|
||||
| 2×period bars | 75% | 25% |
|
||||
| 3×period bars | 87.5% | 12.5% |
|
||||
|
||||
### Middle Band Calculation
|
||||
|
||||
The output middle band is the average of the decayed upper and lower bands:
|
||||
|
||||
$$
|
||||
Middle_t = \frac{U_t + L_t}{2}
|
||||
$$
|
||||
|
||||
This differs from the convergence midpoint (which uses previous bar's values) to avoid feedback loops.
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode, per Bar)
|
||||
### Operation Count (Streaming Mode, Scalar)
|
||||
|
||||
Per-bar cost including internal Highest/Lowest updates:
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| ADD/SUB | 6 | 1 | 6 |
|
||||
| ADD/SUB | 8 | 1 | 8 |
|
||||
| MUL | 4 | 3 | 12 |
|
||||
| DIV | 2 | 15 | 30 |
|
||||
| DIV | 1 | 15 | 15 |
|
||||
| EXP | 2 | 50 | 100 |
|
||||
| CMP/MAX/MIN | 4 | 1 | 4 |
|
||||
| **Total** | **18** | — | **~152 cycles** |
|
||||
| CMP/MAX/MIN | 6 | 1 | 6 |
|
||||
| **Total** | **21** | — | **~141 cycles** |
|
||||
|
||||
**Breakdown:**
|
||||
- Decay lambda: 1 DIV (precomputed at construction)
|
||||
|
||||
- Lambda: precomputed at construction (0 cycles per bar)
|
||||
- Midpoint: 1 ADD + 1 DIV = 16 cycles
|
||||
- Decay rates (×2): 2 MUL + 2 EXP + 2 SUB = 106 cycles
|
||||
- Channel update: 2 MUL + 2 SUB = 8 cycles
|
||||
- Max/min tracking: 4 CMP = 4 cycles
|
||||
- Decay factors (×2): 2 MUL + 2 EXP + 2 SUB = 106 cycles
|
||||
- Band updates: 2 MUL + 2 SUB = 8 cycles
|
||||
- Constraint checks: 4 CMP = 4 cycles
|
||||
- Internal Highest/Lowest: ~8 cycles (amortized O(1))
|
||||
|
||||
*Note: EXP operations dominate cost; precomputing decay table possible for further optimization.*
|
||||
**Dominant cost:** EXP operations at 71% of total cycles.
|
||||
|
||||
### Complexity Analysis
|
||||
### Batch Mode (512 values, SIMD/FMA)
|
||||
|
||||
| Mode | Complexity | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| Streaming | O(1) | Constant time per bar with tracked extremes |
|
||||
| Batch | O(n) | Linear scan, n = series length |
|
||||
| Operation | Scalar Ops | SIMD Benefit | Notes |
|
||||
| :--- | :---: | :---: | :--- |
|
||||
| Decay calculation | 2 | Limited | Sequential dependency on timers |
|
||||
| Band update | 4 | 2× via FMA | `band - decay × (band - mid)` |
|
||||
| Max/Min constraint | 4 | 1× | Comparison-based |
|
||||
|
||||
**Memory**: ~96 bytes (extremes, decay timers, lambda constant, circular buffer).
|
||||
**Batch efficiency (512 bars):**
|
||||
|
||||
### SIMD Analysis
|
||||
| Mode | Cycles/bar | Total (512 bars) | Improvement |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| Scalar streaming | 141 | 72,192 | — |
|
||||
| FMA-optimized | ~135 | ~69,120 | **~4%** |
|
||||
|
||||
| Optimization | Applicable | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| AVX2 vectorization | Partial | Decay calc vectorizable; max/min tracking sequential |
|
||||
| FMA | ✅ | `max - decayRate × (max - midpoint)` |
|
||||
| Batch parallelism | ❌ | Decay timing creates bar-to-bar dependency |
|
||||
Limited improvement due to:
|
||||
|
||||
1. **EXP dominates**: 100 of 141 cycles are exponential operations (not SIMD-friendly in scalar mode)
|
||||
2. **Timer dependency**: Each bar's decay factor depends on its timer value
|
||||
3. **State coupling**: Upper/lower bands depend on previous bar's midpoint
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **Accuracy** | 10/10 | Mathematically exact exponential decay |
|
||||
| **Timeliness** | 7/10 | Tracks extremes immediately, decay is gradual |
|
||||
| **Overshoot** | 5/10 | Fresh extremes reset decay, can spike bands |
|
||||
| **Smoothness** | 6/10 | Exponential decay provides smooth convergence |
|
||||
| **Timeliness** | 8/10 | Immediate response to new extremes |
|
||||
| **Overshoot** | 6/10 | New extremes reset decay, can spike bands |
|
||||
| **Smoothness** | 7/10 | Exponential decay provides smooth convergence between resets |
|
||||
| **Adaptivity** | 8/10 | Channels naturally tighten during consolidation |
|
||||
|
||||
## Validation
|
||||
|
||||
@@ -129,12 +197,28 @@ DECAYCHANNEL provides sophisticated market insights through its adaptive behavio
|
||||
| **Skender** | N/A | Not implemented |
|
||||
| **Tulip** | N/A | Not implemented |
|
||||
| **Ooples** | N/A | Not implemented |
|
||||
| **Internal** | ✅ | Mode consistency verified |
|
||||
| **Internal** | ✅ | Four-mode consistency verified (streaming, batch, span, event) |
|
||||
|
||||
DECAYCHANNEL is a QuanTAlib-specific indicator with no external reference implementations.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Decay Rate Confusion**: The period parameter controls half-life, not full decay. At period=100, bands are 50% decayed after 100 bars, not fully converged. For near-complete convergence (>95%), allow 4-5× the period.
|
||||
|
||||
2. **Constraint Snap-Back**: When the actual highest high drops (because an old extreme exits the Highest window), the upper band can snap downward even mid-decay. This is intentional—decayed bands never exceed actual extremes.
|
||||
|
||||
3. **Initialization Period**: DECAYCHANNEL needs `period` bars to establish meaningful extremes before decay becomes relevant. IsHot reflects this warmup requirement.
|
||||
|
||||
4. **Timer State Management**: Using `isNew=false` for bar correction requires restoring both the band values and the decay timers. The implementation handles this via state snapshots, but improper use corrupts both.
|
||||
|
||||
5. **Midpoint Targeting**: Bands decay toward the channel midpoint, not toward current price. In strong trends, this means the trailing band decays toward a point that may be far from price, creating asymmetric behavior.
|
||||
|
||||
6. **Memory Overhead**: Each instance maintains two Highest/Lowest indicators plus decay state. For period=100, budget ~1.6 KB per instance for the internal monotonic deques plus ~64 bytes for state.
|
||||
|
||||
7. **Exponential Sensitivity**: Small period values create aggressive decay. At period=10, bands are 50% converged after just 10 bars. For most applications, period≥50 provides more stable channels.
|
||||
|
||||
## References
|
||||
|
||||
* Murphy, J. J. (1999). Technical Analysis of the Financial Markets. New York Institute of Finance.
|
||||
* Kaufman, P. J. (2013). Trading Systems and Methods (5th ed.). John Wiley & Sons.
|
||||
* Elder, A. (2014). The New Trading for a Living. John Wiley & Sons.
|
||||
* Pardo, R. (2008). The Evaluation and Optimization of Trading Strategies. John Wiley & Sons.
|
||||
* Achelis, S. B. (2001). Technical Analysis from A to Z. McGraw-Hill.
|
||||
- Murphy, J. J. (1999). *Technical Analysis of the Financial Markets*. New York Institute of Finance.
|
||||
- Kaufman, P. J. (2013). *Trading Systems and Methods* (5th ed.). John Wiley & Sons.
|
||||
- Press, W. H., et al. (2007). *Numerical Recipes: The Art of Scientific Computing* (3rd ed.). Cambridge University Press. [Exponential decay mathematics]
|
||||
|
||||
Reference in New Issue
Block a user