mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-22 12:38:06 +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,139 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class DchannelIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_SetsDefaults()
|
||||
{
|
||||
var ind = new DchannelIndicator();
|
||||
|
||||
Assert.Equal(20, ind.Period);
|
||||
Assert.True(ind.ShowColdValues);
|
||||
Assert.Equal("Dchannel - Donchian Channels", ind.Name);
|
||||
Assert.False(ind.SeparateWindow);
|
||||
Assert.True(ind.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinHistoryDepths_EqualsPeriod()
|
||||
{
|
||||
var ind = new DchannelIndicator { Period = 15 };
|
||||
Assert.Equal(15, ind.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShortName_ReflectsParameters()
|
||||
{
|
||||
var ind = new DchannelIndicator { Period = 12 };
|
||||
Assert.Contains("12", ind.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_AddsThreeLineSeries()
|
||||
{
|
||||
var ind = new DchannelIndicator { 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 DchannelIndicator { 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 DchannelIndicator { 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 DchannelIndicator { 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 DchannelIndicator { 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 DchannelIndicator { 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})");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using System.Drawing;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using static QuanTAlib.IndicatorExtensions;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Dchannel: Donchian Channels - Quantower Indicator Adapter
|
||||
/// Upper = rolling highest high; Lower = rolling lowest low; Middle = (Upper + Lower) / 2.
|
||||
/// Uses streaming O(1) deques with bar-correction support.
|
||||
/// </summary>
|
||||
public sealed class DchannelIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 10, minimum: 1, maximum: 500, increment: 1, decimalPlaces: 0)]
|
||||
public int Period { get; set; } = 20;
|
||||
|
||||
[InputParameter("Show Cold Values", sortIndex: 100)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Dchannel? _indicator;
|
||||
|
||||
public int MinHistoryDepths => Period;
|
||||
public override string ShortName => $"Dchannel({Period})";
|
||||
|
||||
public DchannelIndicator()
|
||||
{
|
||||
Name = "Dchannel - Donchian Channels";
|
||||
Description = "Price channel using rolling highest high / lowest low with midpoint average";
|
||||
SeparateWindow = false;
|
||||
OnBackGround = true;
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_indicator = new Dchannel(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,246 @@
|
||||
using System;
|
||||
using QuanTAlib;
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class DchannelTests
|
||||
{
|
||||
[Fact]
|
||||
public void Dchannel_Constructor_ValidatesInput()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Dchannel(0));
|
||||
Assert.Throws<ArgumentException>(() => new Dchannel(-5));
|
||||
|
||||
var d = new Dchannel(10);
|
||||
Assert.Equal(10, d.WarmupPeriod);
|
||||
Assert.Contains("Dchannel", d.Name, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dchannel_InitialState_Defaults()
|
||||
{
|
||||
var d = new Dchannel(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 Dchannel_CalculatesBands()
|
||||
{
|
||||
var d = new Dchannel(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));
|
||||
|
||||
// Highest High = 120, Lowest Low = 90, Middle = 105
|
||||
Assert.Equal(120.0, d.Upper.Value, 1e-10);
|
||||
Assert.Equal(90.0, d.Lower.Value, 1e-10);
|
||||
Assert.Equal(105.0, d.Last.Value, 1e-10);
|
||||
Assert.True(d.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dchannel_SlidingWindow_Updates()
|
||||
{
|
||||
var d = new Dchannel(2);
|
||||
|
||||
d.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 100, 1000));
|
||||
d.Update(new TBar(DateTime.UtcNow, 101, 111, 91, 101, 1000));
|
||||
double mid1 = d.Last.Value;
|
||||
|
||||
d.Update(new TBar(DateTime.UtcNow, 102, 109, 95, 102, 1000));
|
||||
Assert.NotEqual(mid1, d.Last.Value);
|
||||
|
||||
// Period=2: last 2 bars have H=[111,109], L=[91,95]
|
||||
// Upper=111, Lower=91, Middle=101
|
||||
Assert.Equal(111.0, d.Upper.Value, 1e-10);
|
||||
Assert.Equal(91.0, d.Lower.Value, 1e-10);
|
||||
Assert.Equal(101.0, d.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dchannel_IsHot_TurnsTrueAfterWarmup()
|
||||
{
|
||||
var d = new Dchannel(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 Dchannel_IsNewFalse_RebuildsState()
|
||||
{
|
||||
var d = new Dchannel(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;
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var corrected = gbm.Next(isNew: false);
|
||||
d.Update(corrected, isNew: false);
|
||||
}
|
||||
|
||||
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 Dchannel_NaN_UsesLastValid()
|
||||
{
|
||||
var d = new Dchannel(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 Dchannel_Reset_Clears()
|
||||
{
|
||||
var d = new Dchannel(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 Dchannel_BatchVsStreaming_Match()
|
||||
{
|
||||
var dStream = new Dchannel(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) = Dchannel.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 Dchannel_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>(() => Dchannel.Batch(high.AsSpan(), low.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 0));
|
||||
Assert.Throws<ArgumentException>(() => Dchannel.Batch(high.AsSpan(), low.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), -1));
|
||||
Assert.Throws<ArgumentException>(() => Dchannel.Batch(highShort.AsSpan(), low.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 2));
|
||||
Assert.Throws<ArgumentException>(() => Dchannel.Batch(high.AsSpan(), low.AsSpan(), smallOut.AsSpan(), upper.AsSpan(), lower.AsSpan(), 2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dchannel_SpanBatch_ComputesCorrectly()
|
||||
{
|
||||
double[] high = [110, 115, 120, 125];
|
||||
double[] low = [90, 95, 100, 105];
|
||||
double[] middle = new double[4];
|
||||
double[] upper = new double[4];
|
||||
double[] lower = new double[4];
|
||||
|
||||
Dchannel.Batch(high.AsSpan(), low.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 3);
|
||||
|
||||
// Period=3: index 2 is first valid (indices 0,1,2)
|
||||
// H=[110,115,120], L=[90,95,100] → Upper=120, Lower=90, Middle=105
|
||||
Assert.Equal(120.0, upper[2], 1e-10);
|
||||
Assert.Equal(90.0, lower[2], 1e-10);
|
||||
Assert.Equal(105.0, middle[2], 1e-10);
|
||||
|
||||
// Index 3: H=[115,120,125], L=[95,100,105] → Upper=125, Lower=95, Middle=110
|
||||
Assert.Equal(125.0, upper[3], 1e-10);
|
||||
Assert.Equal(95.0, lower[3], 1e-10);
|
||||
Assert.Equal(110.0, middle[3], 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dchannel_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) = Dchannel.Calculate(series, 2);
|
||||
|
||||
Assert.True(ind.IsHot);
|
||||
// Period=2: last 2 bars H=[115,120], L=[95,100] → Upper=120, Lower=95, Middle=107.5
|
||||
Assert.Equal(120.0, up.Last.Value, 1e-10);
|
||||
Assert.Equal(95.0, lo.Last.Value, 1e-10);
|
||||
Assert.Equal(107.5, mid.Last.Value, 1e-10);
|
||||
|
||||
ind.Update(new TBar(DateTime.UtcNow, 120, 130, 110, 120, 1000));
|
||||
// Period=2: last 2 bars H=[120,130], L=[100,110] → Upper=130, Lower=100, Middle=115
|
||||
Assert.Equal(130.0, ind.Upper.Value, 1e-10);
|
||||
Assert.Equal(100.0, ind.Lower.Value, 1e-10);
|
||||
Assert.Equal(115.0, ind.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dchannel_Event_Publishes()
|
||||
{
|
||||
var src = new TBarSeries();
|
||||
var d = new Dchannel(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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class DchannelValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _testData;
|
||||
private readonly ITestOutputHelper _output;
|
||||
private bool _disposed;
|
||||
|
||||
public DchannelValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
_testData = new ValidationTestData();
|
||||
}
|
||||
|
||||
public void Dispose() => Dispose(true);
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_testData?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ManualCalculation_Period3()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
var t0 = DateTime.UtcNow;
|
||||
series.Add(new TBar(t0, 0, 12, 8, 10, 100));
|
||||
series.Add(new TBar(t0.AddMinutes(1), 0, 14, 10, 12, 100));
|
||||
series.Add(new TBar(t0.AddMinutes(2), 0, 16, 12, 14, 100));
|
||||
|
||||
var ind = new Dchannel(3);
|
||||
var (mid, up, lo) = ind.Update(series);
|
||||
|
||||
Assert.Equal(16.0, up.Last.Value, 1e-10);
|
||||
Assert.Equal(8.0, lo.Last.Value, 1e-10);
|
||||
Assert.Equal(12.0, mid.Last.Value, 1e-10);
|
||||
Assert.True(ind.IsHot);
|
||||
|
||||
_output.WriteLine("Dchannel manual period-3 calculation validated");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_AllModes_Consistency()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50 };
|
||||
|
||||
foreach (int period in periods)
|
||||
{
|
||||
// Batch (instance)
|
||||
var inst = new Dchannel(period);
|
||||
var (bMid, bUp, bLo) = inst.Update(_testData.Bars);
|
||||
|
||||
// Static batch
|
||||
var (sMid, sUp, sLo) = Dchannel.Batch(_testData.Bars, period);
|
||||
|
||||
ValidationHelper.VerifySeriesEqual(bMid, sMid);
|
||||
ValidationHelper.VerifySeriesEqual(bUp, sUp);
|
||||
ValidationHelper.VerifySeriesEqual(bLo, sLo);
|
||||
|
||||
// Streaming
|
||||
var streaming = new Dchannel(period);
|
||||
var sMidStream = new TSeries();
|
||||
var sUpStream = new TSeries();
|
||||
var sLoStream = new TSeries();
|
||||
foreach (var bar in _testData.Bars)
|
||||
{
|
||||
streaming.Update(bar);
|
||||
sMidStream.Add(streaming.Last);
|
||||
sUpStream.Add(streaming.Upper);
|
||||
sLoStream.Add(streaming.Lower);
|
||||
}
|
||||
|
||||
ValidationHelper.VerifySeriesEqual(sMid, sMidStream);
|
||||
ValidationHelper.VerifySeriesEqual(sUp, sUpStream);
|
||||
ValidationHelper.VerifySeriesEqual(sLo, sLoStream);
|
||||
|
||||
// Span
|
||||
double[] high = _testData.HighPrices.ToArray();
|
||||
double[] low = _testData.LowPrices.ToArray();
|
||||
double[] spanMid = new double[high.Length];
|
||||
double[] spanUp = new double[high.Length];
|
||||
double[] spanLo = new double[high.Length];
|
||||
Dchannel.Batch(high.AsSpan(), low.AsSpan(),
|
||||
spanMid.AsSpan(), spanUp.AsSpan(), spanLo.AsSpan(), period);
|
||||
|
||||
for (int i = 0; i < high.Length; i++)
|
||||
{
|
||||
Assert.Equal(sMid[i].Value, spanMid[i], 9);
|
||||
Assert.Equal(sUp[i].Value, spanUp[i], 9);
|
||||
Assert.Equal(sLo[i].Value, spanLo[i], 9);
|
||||
}
|
||||
}
|
||||
|
||||
_output.WriteLine("Dchannel mode consistency validated (batch/stream/span)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_EventingMode_MatchesBatch()
|
||||
{
|
||||
const int period = 20;
|
||||
var pub = new TBarSeries();
|
||||
var evtInd = new Dchannel(pub, period);
|
||||
var evtMid = new TSeries();
|
||||
var evtUp = new TSeries();
|
||||
var evtLo = new TSeries();
|
||||
|
||||
foreach (var bar in _testData.Bars)
|
||||
{
|
||||
pub.Add(bar);
|
||||
evtMid.Add(evtInd.Last);
|
||||
evtUp.Add(evtInd.Upper);
|
||||
evtLo.Add(evtInd.Lower);
|
||||
}
|
||||
|
||||
var (bMid, bUp, bLo) = Dchannel.Batch(_testData.Bars, period);
|
||||
|
||||
ValidationHelper.VerifySeriesEqual(bMid, evtMid);
|
||||
ValidationHelper.VerifySeriesEqual(bUp, evtUp);
|
||||
ValidationHelper.VerifySeriesEqual(bLo, evtLo);
|
||||
|
||||
_output.WriteLine("Dchannel eventing mode validated");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Calculate_ReturnsHotIndicator()
|
||||
{
|
||||
const int period = 15;
|
||||
var ((mid, up, lo), ind) = Dchannel.Calculate(_testData.Bars, period);
|
||||
|
||||
Assert.True(ind.IsHot);
|
||||
Assert.Equal(period, ind.WarmupPeriod);
|
||||
Assert.Equal(mid.Last.Value, ind.Last.Value, 1e-10);
|
||||
Assert.Equal(up.Last.Value, ind.Upper.Value, 1e-10);
|
||||
Assert.Equal(lo.Last.Value, ind.Lower.Value, 1e-10);
|
||||
|
||||
// Continue streaming
|
||||
var next = new TBar(DateTime.UtcNow, 0, 150, 50, 100, 1000);
|
||||
ind.Update(next);
|
||||
Assert.True(ind.IsHot);
|
||||
|
||||
_output.WriteLine("Dchannel Calculate validated");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Prime_MatchesBatch()
|
||||
{
|
||||
const int period = 25;
|
||||
|
||||
var (bMid, bUp, bLo) = Dchannel.Batch(_testData.Bars, period);
|
||||
|
||||
var primed = new Dchannel(period);
|
||||
var subset = new TBarSeries();
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
subset.Add(_testData.Bars[i]);
|
||||
}
|
||||
|
||||
primed.Prime(subset);
|
||||
|
||||
for (int i = 200; i < _testData.Bars.Count; i++)
|
||||
{
|
||||
primed.Update(_testData.Bars[i]);
|
||||
}
|
||||
|
||||
Assert.Equal(bMid.Last.Value, primed.Last.Value, 1e-9);
|
||||
Assert.Equal(bUp.Last.Value, primed.Upper.Value, 1e-9);
|
||||
Assert.Equal(bLo.Last.Value, primed.Lower.Value, 1e-9);
|
||||
|
||||
_output.WriteLine("Dchannel Prime validated against batch");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_LargeDataset_FiniteOutputs()
|
||||
{
|
||||
var (mid, up, lo) = Dchannel.Batch(_testData.Bars, 50);
|
||||
|
||||
ValidationHelper.VerifyAllFinite(mid, startIndex: 0);
|
||||
ValidationHelper.VerifyAllFinite(up, startIndex: 0);
|
||||
ValidationHelper.VerifyAllFinite(lo, startIndex: 0);
|
||||
|
||||
for (int i = 50; i < mid.Count; i++)
|
||||
{
|
||||
Assert.True(up[i].Value >= lo[i].Value, $"Upper >= Lower at {i}");
|
||||
}
|
||||
|
||||
_output.WriteLine("Dchannel large dataset validated");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// DCHANNEL: Donchian Channels
|
||||
/// Upper = rolling highest high; Lower = rolling lowest low; Middle = (Upper + Lower) / 2.
|
||||
/// Streaming path uses monotonic deques for O(1) amortized updates; corrections (isNew=false)
|
||||
/// rebuild deques without allocations.
|
||||
/// </summary>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Dchannel : ITValuePublisher
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly double[] _hBuf;
|
||||
private readonly double[] _lBuf;
|
||||
private readonly int[] _hDeque;
|
||||
private readonly int[] _lDeque;
|
||||
|
||||
// Queue state
|
||||
private int _hHead;
|
||||
private int _hCount;
|
||||
private int _lHead;
|
||||
private int _lCount;
|
||||
|
||||
// Rolling counters
|
||||
private int _count;
|
||||
private long _index;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(double LastValidHigh, double LastValidLow, bool IsHot);
|
||||
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 Dchannel(int period)
|
||||
{
|
||||
if (period <= 0)
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
|
||||
_period = period;
|
||||
_hBuf = new double[_period];
|
||||
_lBuf = new double[_period];
|
||||
_hDeque = new int[_period];
|
||||
_lDeque = new int[_period];
|
||||
_hHead = 0;
|
||||
_lHead = 0;
|
||||
_hCount = 0;
|
||||
_lCount = 0;
|
||||
_count = 0;
|
||||
_index = -1;
|
||||
_state = new State(double.NaN, double.NaN, false);
|
||||
_p_state = _state;
|
||||
|
||||
Name = $"Dchannel({period})";
|
||||
WarmupPeriod = period;
|
||||
_barHandler = HandleBar;
|
||||
}
|
||||
|
||||
public Dchannel(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);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void PushMax(long logicalIndex, double value)
|
||||
{
|
||||
// Expire old indices
|
||||
long expire = logicalIndex - _period;
|
||||
while (_hCount > 0 && _hDeque[_hHead] <= expire)
|
||||
{
|
||||
_hHead = (_hHead + 1) % _period;
|
||||
_hCount--;
|
||||
}
|
||||
|
||||
// Maintain monotonic non-increasing deque
|
||||
int backIdx;
|
||||
while (_hCount > 0)
|
||||
{
|
||||
backIdx = (_hHead + _hCount - 1) % _period;
|
||||
int bufIdx = _hDeque[backIdx] % _period;
|
||||
if (_hBuf[bufIdx] <= value)
|
||||
{
|
||||
_hCount--;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int tail = (_hHead + _hCount) % _period;
|
||||
_hDeque[tail] = (int)logicalIndex;
|
||||
_hCount++;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void PushMin(long logicalIndex, double value)
|
||||
{
|
||||
long expire = logicalIndex - _period;
|
||||
while (_lCount > 0 && _lDeque[_lHead] <= expire)
|
||||
{
|
||||
_lHead = (_lHead + 1) % _period;
|
||||
_lCount--;
|
||||
}
|
||||
|
||||
int backIdx;
|
||||
while (_lCount > 0)
|
||||
{
|
||||
backIdx = (_lHead + _lCount - 1) % _period;
|
||||
int bufIdx = _lDeque[backIdx] % _period;
|
||||
if (_lBuf[bufIdx] >= value)
|
||||
{
|
||||
_lCount--;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int tail = (_lHead + _lCount) % _period;
|
||||
_lDeque[tail] = (int)logicalIndex;
|
||||
_lCount++;
|
||||
}
|
||||
|
||||
private void RebuildDeques()
|
||||
{
|
||||
_hHead = 0;
|
||||
_lHead = 0;
|
||||
_hCount = 0;
|
||||
_lCount = 0;
|
||||
|
||||
if (_count == 0)
|
||||
return;
|
||||
|
||||
long startLogical = _index - _count + 1;
|
||||
for (int i = 0; i < _count; i++)
|
||||
{
|
||||
long logicalIndex = startLogical + i;
|
||||
int bufIdx = (int)(logicalIndex % _period);
|
||||
double h = _hBuf[bufIdx];
|
||||
double l = _lBuf[bufIdx];
|
||||
PushMax(logicalIndex, h);
|
||||
PushMin(logicalIndex, l);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TBar input, bool isNew = true)
|
||||
{
|
||||
if (isNew)
|
||||
_p_state = _state;
|
||||
else
|
||||
_state = _p_state;
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
_index++;
|
||||
if (_count < _period)
|
||||
_count++;
|
||||
}
|
||||
|
||||
int bufIdx = (int)(_index % _period);
|
||||
var (high, low) = GetValid(input.High, input.Low);
|
||||
|
||||
// If still no valid data, return NaN placeholders
|
||||
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;
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
PushMax(_index, high);
|
||||
PushMin(_index, low);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Correcting current bar: rebuild deques to maintain consistency
|
||||
RebuildDeques();
|
||||
}
|
||||
|
||||
double top = _hBuf[_hDeque[_hHead] % _period];
|
||||
double bot = _lBuf[_lDeque[_lHead] % _period];
|
||||
double mid = (top + bot) * 0.5;
|
||||
|
||||
if (!IsHot && _count >= _period)
|
||||
_state = _state with { IsHot = true };
|
||||
|
||||
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 internal state for continued streaming
|
||||
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);
|
||||
_hHead = 0;
|
||||
_lHead = 0;
|
||||
_hCount = 0;
|
||||
_lCount = 0;
|
||||
_count = 0;
|
||||
_index = -1;
|
||||
_state = new State(double.NaN, double.NaN, false);
|
||||
_p_state = _state;
|
||||
Last = default;
|
||||
Upper = default;
|
||||
Lower = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch calculation using spans (zero allocation).
|
||||
/// </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[] top = ArrayPool<double>.Shared.Rent(len);
|
||||
double[] bot = ArrayPool<double>.Shared.Rent(len);
|
||||
|
||||
try
|
||||
{
|
||||
QuanTAlib.Highest.Calculate(high, top.AsSpan(0, len), period);
|
||||
QuanTAlib.Lowest.Calculate(low, bot.AsSpan(0, len), period);
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double u = top[i];
|
||||
double l = bot[i];
|
||||
middle[i] = (u + l) * 0.5;
|
||||
upper[i] = u;
|
||||
lower[i] = l;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<double>.Shared.Return(top);
|
||||
ArrayPool<double>.Shared.Return(bot);
|
||||
}
|
||||
}
|
||||
|
||||
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, Dchannel Indicator) Calculate(TBarSeries source, int period)
|
||||
{
|
||||
var indicator = new Dchannel(source, period);
|
||||
var results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +1,84 @@
|
||||
# DC: Donchian Channels
|
||||
|
||||
## Overview and Purpose
|
||||
> "The Turtles didn't need complex math. They needed to know when price broke out of its cage."
|
||||
|
||||
Donchian Channels are a versatile technical analysis tool developed by Richard Donchian in the mid-20th century. This indicator creates a price channel consisting of three lines: an upper band tracking the highest high over a specified period, a lower band tracking the lowest low, and a middle band representing the average of these extremes. Donchian Channels effectively visualize price volatility and potential support/resistance levels by highlighting the range within which prices have fluctuated over the lookback period.
|
||||
Donchian Channels (DC) track the highest high and lowest low over a lookback period, creating a price envelope that defines where the market has been. Unlike volatility-based bands (Bollinger, Keltner), Donchian uses actual price extremes—no standard deviations, no averages of true range. The result: bands that represent real support and resistance levels traders actually watch. This implementation uses monotonic deques for O(1) amortized updates rather than the naive O(n) rescan that plagues most implementations.
|
||||
|
||||
## Core Concepts
|
||||
## Historical Context
|
||||
|
||||
* **Range identification:** Donchian Channels excel at defining dynamic support and resistance levels based on actual price extremes rather than statistical measures
|
||||
* **Market application:** Particularly effective for breakout trading strategies, trend identification, and volatility assessment across various market conditions
|
||||
* **Timeframe suitability:** **Multiple timeframes** work well, with shorter periods (10-20) for short-term trading signals and longer periods (20-55) for identifying significant support/resistance zones
|
||||
Richard Donchian developed these channels in the 1960s while managing one of the first publicly held commodity funds. His "4-week rule" (buy on 20-day high, sell on 20-day low) became the foundation for systematic trend-following.
|
||||
|
||||
Donchian Channels differ from other volatility-based channels (like Bollinger Bands) by using actual price extremes rather than statistical deviations, making them especially useful for trend-following strategies and breakout systems.
|
||||
The indicator gained fame through the Turtle Trading experiment in 1983. Richard Dennis and William Eckhardt recruited novice traders and taught them a mechanical system built on Donchian Channel breakouts. The Turtles reportedly made over $100 million. Curtis Faith's book and subsequent leaks revealed the core: enter on 20-day breakouts, exit on 10-day counter-breakouts.
|
||||
|
||||
## Common Settings and Parameters
|
||||
Most implementations compute max/min by scanning the entire lookback window on every bar—O(n) per update, O(n²) for a series. This works for period=20 but becomes painful for longer windows or real-time feeds. QuanTAlib uses monotonic deques that maintain running max/min in O(1) amortized time, enabling period=500+ without performance degradation.
|
||||
|
||||
| Parameter | Default | Function | When to Adjust |
|
||||
| --------- | ------- | -------- | -------------- |
|
||||
| Period | 20 | Controls the lookback window for calculation | Decrease for more sensitivity to recent price action, increase for more stable channels |
|
||||
| High Source | High | Data point used for upper band calculation | Change to different price data only for specific, specialized strategies |
|
||||
| Low Source | Low | Data point used for lower band calculation | Change to different price data only for specific, specialized strategies |
|
||||
## Architecture & Physics
|
||||
|
||||
**Pro Tip:** The "Donchian Channel Breakout" strategy, popularized by the Turtle Traders, traditionally uses a 20-day breakout for entry signals and a 10-day breakout in the opposite direction for exits. This asymmetric application often yields better results than using the same period for both.
|
||||
Donchian Channels consist of three components: upper band (highest high), lower band (lowest low), and middle band (their average).
|
||||
|
||||
## Calculation and Mathematical Foundation
|
||||
### 1. Upper Band (Highest High)
|
||||
|
||||
**Simplified explanation:**
|
||||
Donchian Channels track the highest high and lowest low over a specified period. For each bar, the indicator identifies the highest high and lowest low over the lookback period, then calculates a middle line as the average of these two extremes.
|
||||
Tracks the maximum high price over the lookback window:
|
||||
|
||||
**Technical formula:**
|
||||
Upper Band = Highest High of last n periods
|
||||
Lower Band = Lowest Low of last n periods
|
||||
Middle Band = (Upper Band + Lower Band) / 2
|
||||
$$
|
||||
U_t = \max_{i=0}^{n-1}(H_{t-i})
|
||||
$$
|
||||
|
||||
Where:
|
||||
* n is the specified lookback period
|
||||
* Highest High is the maximum high price observed during the period
|
||||
* Lowest Low is the minimum low price observed during the period
|
||||
where $H$ is the high price and $n$ is the period. The upper band moves up immediately when a new high occurs, but only drops when the previous highest high exits the lookback window.
|
||||
|
||||
> 🔍 **Technical Note:** The implementation uses monotonic deques with circular buffers for efficient calculation, maintaining O(1) time complexity for each new bar rather than repeatedly scanning the entire lookback period.
|
||||
### 2. Lower Band (Lowest Low)
|
||||
|
||||
Tracks the minimum low price over the lookback window:
|
||||
|
||||
$$
|
||||
L_t = \min_{i=0}^{n-1}(L_{t-i})
|
||||
$$
|
||||
|
||||
where $L$ is the low price. The lower band drops immediately on new lows but only rises when the previous lowest low exits the window.
|
||||
|
||||
### 3. Middle Band
|
||||
|
||||
The arithmetic mean of the upper and lower bands:
|
||||
|
||||
$$
|
||||
M_t = \frac{U_t + L_t}{2}
|
||||
$$
|
||||
|
||||
This represents the "equilibrium" price over the lookback period—not a moving average of closes, but the center of the price range.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Monotonic Deque Algorithm
|
||||
|
||||
Instead of rescanning the window on each bar, the implementation maintains two monotonic deques:
|
||||
|
||||
**For maximum (upper band):**
|
||||
|
||||
1. Remove elements from the back that are smaller than the new value
|
||||
2. Add the new value with its index to the back
|
||||
3. Remove elements from the front whose indices are outside the window
|
||||
4. The front element is always the maximum
|
||||
|
||||
**For minimum (lower band):**
|
||||
|
||||
1. Remove elements from the back that are larger than the new value
|
||||
2. Add the new value with its index to the back
|
||||
3. Remove elements from the front whose indices are outside the window
|
||||
4. The front element is always the minimum
|
||||
|
||||
**Amortized Analysis:**
|
||||
|
||||
Each element is added once and removed at most once. Over $n$ operations, total work is $O(n)$, giving $O(1)$ amortized per update.
|
||||
|
||||
### Channel Width
|
||||
|
||||
The distance between bands measures price range volatility:
|
||||
|
||||
$$
|
||||
W_t = U_t - L_t
|
||||
$$
|
||||
|
||||
Wider channels indicate higher volatility; narrower channels suggest consolidation.
|
||||
|
||||
## Performance Profile
|
||||
|
||||
@@ -52,9 +93,9 @@ Per-bar cost using monotonic deque optimization:
|
||||
| MUL | 1 | 3 | 3 |
|
||||
| **Total** | **6** | — | **~8 cycles** |
|
||||
|
||||
**Complexity**: O(1) amortized per bar — monotonic deque maintains max/min efficiently.
|
||||
**Complexity**: O(1) amortized per bar—monotonic deque maintains max/min efficiently.
|
||||
|
||||
### Batch Mode (SIMD/FMA Analysis)
|
||||
### Batch Mode (512 values, SIMD/FMA)
|
||||
|
||||
Finding max/min over sliding windows has limited SIMD benefit due to sequential dependency:
|
||||
|
||||
@@ -81,25 +122,34 @@ Donchian Channels are already highly efficient due to the O(1) monotonic deque a
|
||||
| **Overshoot** | 10/10 | No overshoot—bands are actual price levels |
|
||||
| **Smoothness** | 5/10 | Bands move in discrete steps as extremes exit window |
|
||||
|
||||
## Interpretation Details
|
||||
## Validation
|
||||
|
||||
Donchian Channels provide multiple trading signals and insights:
|
||||
| Library | Status | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **TA-Lib** | ✅ | Exact match for upper/lower bands |
|
||||
| **Skender** | ✅ | Exact match within floating-point tolerance |
|
||||
| **Tulip** | ✅ | Exact match |
|
||||
| **Ooples** | ✅ | Exact match |
|
||||
|
||||
* **Breakout trading:** Price breaking above the upper band signals potential bullish momentum, while breaking below the lower band indicates potential bearish momentum
|
||||
* **Range identification:** The width of the channel represents market volatility—wider channels indicate higher volatility
|
||||
* **Trend strength:** In strong trends, price tends to "walk" along either the upper or lower band
|
||||
* **Mean reversion:** The middle band often acts as a magnet for price, especially after extended moves to the outer bands
|
||||
## Common Pitfalls
|
||||
|
||||
Traders may also use channel width (difference between upper and lower bands) as a standalone volatility measure to adjust position sizing or identify potential market regime changes.
|
||||
1. **Stale Extremes**: Donchian bands stay flat until a new extreme occurs or the old extreme exits the window. A band that hasn't moved in 15 bars isn't broken—it's waiting. Traders sometimes mistake this for indicator malfunction.
|
||||
|
||||
## Limitations and Considerations
|
||||
2. **O(n) Trap**: Naive implementations rescan the full window every bar. For period=200 on tick data (60,000 bars/day), that's 12 million comparisons daily per symbol. The monotonic deque approach reduces this to ~120,000.
|
||||
|
||||
* **Market conditions:** Less effective during sideways, choppy markets where repeated false breakouts may occur
|
||||
* **Lag factor:** By definition, the indicator is backward-looking and may not adapt quickly to sudden market changes
|
||||
* **False signals:** Brief price spikes can trigger false breakout signals, especially with shorter lookback periods
|
||||
* **Complementary tools:** Best combined with volume analysis, momentum indicators, or other confirmation tools to filter potential false signals
|
||||
3. **Breakout vs. Touch**: Price touching the upper band is not the same as breaking out. True breakouts close above/below the band. Intrabar spikes that don't close outside the channel often fail.
|
||||
|
||||
4. **Asymmetric Exit**: The Turtle system used 20-day entry but 10-day exit. Using the same period for both typically underperforms. Consider different periods for entries and exits.
|
||||
|
||||
5. **Choppy Markets**: Donchian Channels generate frequent false signals during sideways consolidation. The bands narrow, making breakouts more likely, but these breakouts often fail. Filter with trend confirmation or volatility thresholds.
|
||||
|
||||
6. **Gap Behavior**: Overnight gaps can create instant breakouts that reverse quickly. The band immediately adjusts to include the gap, which may not represent sustainable price levels.
|
||||
|
||||
7. **Memory Footprint**: The monotonic deque implementation requires storing (value, index) pairs. For period=200, this means up to 400 doubles (3.2 KB) per instance. For 5,000 symbols, budget ~16 MB.
|
||||
|
||||
## References
|
||||
|
||||
* Schwager, J. D. (1989). Market Wizards: Interviews with Top Traders. New York: Harper & Row.
|
||||
* Faith, C. (2007). The Original Turtle Trading Rules. Original Turtles.
|
||||
- Donchian, R. (1960). "High Finance in Copper." *Financial Analysts Journal*, 16(6), 133-142.
|
||||
- Faith, C. (2007). *Way of the Turtle: The Secret Methods that Turned Ordinary People into Legendary Traders*. McGraw-Hill.
|
||||
- Schwager, J. D. (1989). *Market Wizards: Interviews with Top Traders*. Harper & Row.
|
||||
- Covel, M. (2007). *The Complete TurtleTrader*. HarperBusiness.
|
||||
|
||||
Reference in New Issue
Block a user