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,139 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class PchannelIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_SetsDefaults()
|
||||
{
|
||||
var ind = new PchannelIndicator();
|
||||
|
||||
Assert.Equal(20, ind.Period);
|
||||
Assert.True(ind.ShowColdValues);
|
||||
Assert.Equal("Pchannel - Price Channel", ind.Name);
|
||||
Assert.False(ind.SeparateWindow);
|
||||
Assert.True(ind.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinHistoryDepths_EqualsPeriod()
|
||||
{
|
||||
var ind = new PchannelIndicator { Period = 15 };
|
||||
Assert.Equal(15, ind.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShortName_ReflectsParameters()
|
||||
{
|
||||
var ind = new PchannelIndicator { Period = 12 };
|
||||
Assert.Contains("12", ind.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_AddsThreeLineSeries()
|
||||
{
|
||||
var ind = new PchannelIndicator { 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 PchannelIndicator { 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 PchannelIndicator { 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 PchannelIndicator { 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 PchannelIndicator { 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 PchannelIndicator { 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>
|
||||
/// Pchannel: Price Channel - 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 PchannelIndicator : 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 Pchannel? _indicator;
|
||||
|
||||
public int MinHistoryDepths => Period;
|
||||
public override string ShortName => $"Pchannel({Period})";
|
||||
|
||||
public PchannelIndicator()
|
||||
{
|
||||
Name = "Pchannel - Price Channel";
|
||||
Description = "Price channel using rolling highest high / lowest low with midpoint average";
|
||||
SeparateWindow = false;
|
||||
OnBackGround = true;
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_indicator = new Pchannel(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,274 @@
|
||||
using System;
|
||||
using QuanTAlib;
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class PchannelTests
|
||||
{
|
||||
[Fact]
|
||||
public void Pchannel_Constructor_ValidatesInput()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Pchannel(0));
|
||||
Assert.Throws<ArgumentException>(() => new Pchannel(-5));
|
||||
|
||||
var pc = new Pchannel(10);
|
||||
Assert.Equal(10, pc.WarmupPeriod);
|
||||
Assert.Contains("Pchannel", pc.Name, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pchannel_InitialState_Defaults()
|
||||
{
|
||||
var pc = new Pchannel(5);
|
||||
|
||||
Assert.Equal(0, pc.Last.Value);
|
||||
Assert.Equal(0, pc.Upper.Value);
|
||||
Assert.Equal(0, pc.Lower.Value);
|
||||
Assert.False(pc.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pchannel_CalculatesBands()
|
||||
{
|
||||
var pc = new Pchannel(3);
|
||||
|
||||
pc.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
|
||||
pc.Update(new TBar(DateTime.UtcNow, 105, 115, 95, 110, 1000));
|
||||
pc.Update(new TBar(DateTime.UtcNow, 110, 120, 100, 115, 1000));
|
||||
|
||||
// Highest High = 120, Lowest Low = 90, Middle = 105
|
||||
Assert.Equal(120.0, pc.Upper.Value, 1e-10);
|
||||
Assert.Equal(90.0, pc.Lower.Value, 1e-10);
|
||||
Assert.Equal(105.0, pc.Last.Value, 1e-10);
|
||||
Assert.True(pc.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pchannel_SlidingWindow_Updates()
|
||||
{
|
||||
var pc = new Pchannel(2);
|
||||
|
||||
pc.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 100, 1000));
|
||||
pc.Update(new TBar(DateTime.UtcNow, 101, 111, 91, 101, 1000));
|
||||
double lo1 = pc.Lower.Value;
|
||||
|
||||
pc.Update(new TBar(DateTime.UtcNow, 102, 109, 95, 102, 1000));
|
||||
|
||||
// Period=2: last 2 bars have H=[111,109], L=[91,95]
|
||||
// Upper=111, Lower=91, Middle=101
|
||||
Assert.Equal(111.0, pc.Upper.Value, 1e-10);
|
||||
Assert.Equal(91.0, pc.Lower.Value, 1e-10);
|
||||
Assert.Equal(101.0, pc.Last.Value, 1e-10);
|
||||
|
||||
Assert.NotEqual(lo1, pc.Lower.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pchannel_IsHot_TurnsTrueAfterWarmup()
|
||||
{
|
||||
var pc = new Pchannel(4);
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
pc.Update(new TBar(DateTime.UtcNow, 100 + i, 101 + i, 99 + i, 100 + i, 1000));
|
||||
Assert.False(pc.IsHot);
|
||||
}
|
||||
|
||||
pc.Update(new TBar(DateTime.UtcNow, 200, 201, 199, 200, 1000));
|
||||
Assert.True(pc.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pchannel_IsNewFalse_RebuildsState()
|
||||
{
|
||||
var pc = new Pchannel(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);
|
||||
pc.Update(remembered, isNew: true);
|
||||
}
|
||||
|
||||
double mid = pc.Last.Value;
|
||||
double up = pc.Upper.Value;
|
||||
double lo = pc.Lower.Value;
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var corrected = gbm.Next(isNew: false);
|
||||
pc.Update(corrected, isNew: false);
|
||||
}
|
||||
|
||||
pc.Update(remembered, isNew: false);
|
||||
|
||||
Assert.Equal(mid, pc.Last.Value, 1e-10);
|
||||
Assert.Equal(up, pc.Upper.Value, 1e-10);
|
||||
Assert.Equal(lo, pc.Lower.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pchannel_NaN_UsesLastValid()
|
||||
{
|
||||
var pc = new Pchannel(3);
|
||||
|
||||
pc.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
|
||||
pc.Update(new TBar(DateTime.UtcNow, 101, 111, 91, 106, 1000));
|
||||
|
||||
var result = pc.Update(new TBar(DateTime.UtcNow, 102, double.NaN, 92, 107, 1000));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.True(double.IsFinite(pc.Upper.Value));
|
||||
Assert.True(double.IsFinite(pc.Lower.Value));
|
||||
|
||||
var result2 = pc.Update(new TBar(DateTime.UtcNow, 103, 113, double.PositiveInfinity, 108, 1000));
|
||||
Assert.True(double.IsFinite(result2.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pchannel_Reset_Clears()
|
||||
{
|
||||
var pc = new Pchannel(3);
|
||||
pc.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 100, 1000));
|
||||
pc.Update(new TBar(DateTime.UtcNow, 101, 111, 91, 101, 1000));
|
||||
|
||||
pc.Reset();
|
||||
|
||||
Assert.Equal(0, pc.Last.Value);
|
||||
Assert.Equal(0, pc.Upper.Value);
|
||||
Assert.Equal(0, pc.Lower.Value);
|
||||
Assert.False(pc.IsHot);
|
||||
|
||||
pc.Update(new TBar(DateTime.UtcNow, 50, 60, 40, 55, 1000));
|
||||
Assert.NotEqual(0, pc.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pchannel_BatchVsStreaming_Match()
|
||||
{
|
||||
var pcStream = new Pchannel(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);
|
||||
pcStream.Update(bar, isNew: true);
|
||||
}
|
||||
|
||||
double expectedMid = pcStream.Last.Value;
|
||||
double expectedUp = pcStream.Upper.Value;
|
||||
double expectedLo = pcStream.Lower.Value;
|
||||
|
||||
var (midBatch, upBatch, loBatch) = Pchannel.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 Pchannel_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>(() => Pchannel.Batch(high.AsSpan(), low.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 0));
|
||||
Assert.Throws<ArgumentException>(() => Pchannel.Batch(high.AsSpan(), low.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), -1));
|
||||
Assert.Throws<ArgumentException>(() => Pchannel.Batch(highShort.AsSpan(), low.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 2));
|
||||
Assert.Throws<ArgumentException>(() => Pchannel.Batch(high.AsSpan(), low.AsSpan(), smallOut.AsSpan(), upper.AsSpan(), lower.AsSpan(), 2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pchannel_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];
|
||||
|
||||
Pchannel.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 Pchannel_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) = Pchannel.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 Pchannel_Event_Publishes()
|
||||
{
|
||||
var src = new TBarSeries();
|
||||
var pc = new Pchannel(src, 2);
|
||||
bool fired = false;
|
||||
pc.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 Pchannel_MiddleIsMidpoint()
|
||||
{
|
||||
var pc = new Pchannel(3);
|
||||
pc.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
|
||||
pc.Update(new TBar(DateTime.UtcNow, 105, 115, 95, 110, 1000));
|
||||
pc.Update(new TBar(DateTime.UtcNow, 110, 120, 100, 115, 1000));
|
||||
|
||||
double expectedMiddle = (pc.Upper.Value + pc.Lower.Value) / 2.0;
|
||||
Assert.Equal(expectedMiddle, pc.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pchannel_UpperGreaterOrEqualLower()
|
||||
{
|
||||
var pc = new Pchannel(5);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.2, seed: 123);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
pc.Update(bar);
|
||||
Assert.True(pc.Upper.Value >= pc.Lower.Value,
|
||||
$"Bar {i}: Upper ({pc.Upper.Value}) should be >= Lower ({pc.Lower.Value})");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class PchannelValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _testData;
|
||||
private readonly ITestOutputHelper _output;
|
||||
private bool _disposed;
|
||||
|
||||
public PchannelValidationTests(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 Pchannel(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("Pchannel manual period-3 calculation validated");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_AllModes_Consistency()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50 };
|
||||
|
||||
foreach (int period in periods)
|
||||
{
|
||||
var inst = new Pchannel(period);
|
||||
var (bMid, bUp, bLo) = inst.Update(_testData.Bars);
|
||||
|
||||
var (sMid, sUp, sLo) = Pchannel.Batch(_testData.Bars, period);
|
||||
|
||||
ValidationHelper.VerifySeriesEqual(bMid, sMid);
|
||||
ValidationHelper.VerifySeriesEqual(bUp, sUp);
|
||||
ValidationHelper.VerifySeriesEqual(bLo, sLo);
|
||||
|
||||
var streaming = new Pchannel(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);
|
||||
|
||||
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];
|
||||
Pchannel.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("Pchannel mode consistency validated (batch/stream/span)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_EventingMode_MatchesBatch()
|
||||
{
|
||||
const int period = 20;
|
||||
var pub = new TBarSeries();
|
||||
var evtInd = new Pchannel(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) = Pchannel.Batch(_testData.Bars, period);
|
||||
|
||||
ValidationHelper.VerifySeriesEqual(bMid, evtMid);
|
||||
ValidationHelper.VerifySeriesEqual(bUp, evtUp);
|
||||
ValidationHelper.VerifySeriesEqual(bLo, evtLo);
|
||||
|
||||
_output.WriteLine("Pchannel eventing mode validated");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_AgainstDchannel_ExactMatch()
|
||||
{
|
||||
// Pchannel should produce identical results to Dchannel
|
||||
int[] periods = { 10, 20, 50 };
|
||||
|
||||
foreach (int period in periods)
|
||||
{
|
||||
var (dcMid, dcUp, dcLo) = Dchannel.Batch(_testData.Bars, period);
|
||||
var (pcMid, pcUp, pcLo) = Pchannel.Batch(_testData.Bars, period);
|
||||
|
||||
ValidationHelper.VerifySeriesEqual(dcMid, pcMid);
|
||||
ValidationHelper.VerifySeriesEqual(dcUp, pcUp);
|
||||
ValidationHelper.VerifySeriesEqual(dcLo, pcLo);
|
||||
}
|
||||
|
||||
_output.WriteLine("Pchannel matches Dchannel exactly");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Calculate_ReturnsHotIndicator()
|
||||
{
|
||||
const int period = 15;
|
||||
var ((mid, up, lo), ind) = Pchannel.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);
|
||||
|
||||
var next = new TBar(DateTime.UtcNow, 0, 150, 50, 100, 1000);
|
||||
ind.Update(next);
|
||||
Assert.True(ind.IsHot);
|
||||
|
||||
_output.WriteLine("Pchannel Calculate validated");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Prime_MatchesBatch()
|
||||
{
|
||||
const int period = 25;
|
||||
|
||||
var (bMid, bUp, bLo) = Pchannel.Batch(_testData.Bars, period);
|
||||
|
||||
var primed = new Pchannel(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("Pchannel Prime validated against batch");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_LargeDataset_FiniteOutputs()
|
||||
{
|
||||
var (mid, up, lo) = Pchannel.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("Pchannel large dataset validated");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_StateRestoration_Iterative()
|
||||
{
|
||||
var ind = new Pchannel(15);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
ind.Update(gbm.Next(isNew: true), isNew: true);
|
||||
}
|
||||
|
||||
var remembered = gbm.Next(isNew: true);
|
||||
ind.Update(remembered, isNew: true);
|
||||
|
||||
var savedMid = ind.Last.Value;
|
||||
var savedUp = ind.Upper.Value;
|
||||
var savedLo = ind.Lower.Value;
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var corrected = gbm.Next(isNew: false);
|
||||
ind.Update(corrected, isNew: false);
|
||||
}
|
||||
|
||||
ind.Update(remembered, isNew: false);
|
||||
|
||||
Assert.Equal(savedMid, ind.Last.Value, 1e-10);
|
||||
Assert.Equal(savedUp, ind.Upper.Value, 1e-10);
|
||||
Assert.Equal(savedLo, ind.Lower.Value, 1e-10);
|
||||
|
||||
_output.WriteLine("Pchannel state restoration validated");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// PCHANNEL: Price Channel
|
||||
/// Upper = rolling highest high; Lower = rolling lowest low; Middle = (Upper + Lower) / 2.
|
||||
/// Functionally equivalent to Donchian Channels (DCHANNEL).
|
||||
/// Streaming path uses monotonic deques for O(1) amortized updates; corrections (isNew=false)
|
||||
/// rebuild deques without allocations.
|
||||
/// </summary>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Pchannel : 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 Pchannel(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 = $"Pchannel({period})";
|
||||
WarmupPeriod = period;
|
||||
_barHandler = HandleBar;
|
||||
}
|
||||
|
||||
public Pchannel(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)
|
||||
{
|
||||
long expire = logicalIndex - _period;
|
||||
while (_hCount > 0 && _hDeque[_hHead] <= expire)
|
||||
{
|
||||
_hHead = (_hHead + 1) % _period;
|
||||
_hCount--;
|
||||
}
|
||||
|
||||
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 (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
|
||||
{
|
||||
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(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
|
||||
{
|
||||
Highest.Calculate(high, top.AsSpan(0, len), period);
|
||||
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, Pchannel Indicator) Calculate(TBarSeries source, int period)
|
||||
{
|
||||
var indicator = new Pchannel(source, period);
|
||||
var results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
}
|
||||
@@ -1,41 +1,84 @@
|
||||
# PCHANNEL: Price Channel
|
||||
# PC: Price Channel
|
||||
|
||||
## Overview and Purpose
|
||||
> "The Turtles didn't need complex math. They needed to know when price broke out of its cage."
|
||||
|
||||
The Price Channel is a simple volatility-based indicator that plots the highest high and the lowest low over a user-defined lookback period. It is very similar in concept and application to Donchian Channels. The channel visually represents the trading range of an asset over the specified period.
|
||||
Price Channel (PC) tracks the highest high and lowest low over a lookback period, creating a price envelope that defines where the market has been. Functionally identical to Donchian Channels—same algorithm, different name. Unlike volatility-based bands (Bollinger, Keltner), Price Channel 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.
|
||||
|
||||
A middle line, typically the average of the upper and lower channel lines, can also be plotted to serve as a mean reference.
|
||||
## Historical Context
|
||||
|
||||
## Core Concepts
|
||||
Price Channel is the generic name for what Richard Donchian formalized in the 1960s while managing one of the first publicly held commodity funds. The indicator is also known as Donchian Channels, N-period high/low channels, or simply "breakout bands."
|
||||
|
||||
* **Highest High:** The upper band represents the highest price reached during the lookback period.
|
||||
* **Lowest Low:** The lower band represents the lowest price reached during thelookback period.
|
||||
* **Trading Range:** The channel effectively shows the price extremes for the chosen period.
|
||||
* **Breakout Indication:** Prices moving above the upper channel or below the lower channel can signal potential breakouts and the start of new trends.
|
||||
The "4-week rule" (buy on 20-day high, sell on 20-day low) became the foundation for systematic trend-following. 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 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 |
|
||||
| :-------- | :------ | :------- | :------------- |
|
||||
| Length | 20 | Lookback period for determining the highest high and lowest low. | Shorter lengths make the channel more reactive to recent price action; longer lengths create a wider, smoother channel representing longer-term ranges. |
|
||||
## Architecture & Physics
|
||||
|
||||
## Calculation and Mathematical Foundation
|
||||
Price Channel consists of three components: upper band (highest high), lower band (lowest low), and middle band (their average).
|
||||
|
||||
**Simplified explanation:**
|
||||
1. For each bar, look back over the specified `Length`.
|
||||
2. Identify the absolute highest `high` price during that period. This forms the Upper Channel line.
|
||||
3. Identify the absolute lowest `low` price during that period. This forms the Lower Channel line.
|
||||
4. (Optional) The Middle Channel line is the average of the Upper and Lower Channel lines: `(Upper Channel + Lower Channel) / 2`.
|
||||
### 1. Upper Band (Highest High)
|
||||
|
||||
**Technical formula:**
|
||||
1. **Upper Channel:**
|
||||
`UpperChannel = Highest(High, Length)`
|
||||
Tracks the maximum high price over the lookback window:
|
||||
|
||||
2. **Lower Channel:**
|
||||
`LowerChannel = Lowest(Low, Length)`
|
||||
$$
|
||||
U_t = \max_{i=0}^{n-1}(H_{t-i})
|
||||
$$
|
||||
|
||||
3. **Middle Channel (optional):**
|
||||
`MiddleChannel = (UpperChannel + LowerChannel) / 2`
|
||||
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.
|
||||
|
||||
### 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
|
||||
|
||||
@@ -48,55 +91,66 @@ Per-bar cost using monotonic deque optimization:
|
||||
| CMP | 4 | 1 | 4 |
|
||||
| ADD | 1 | 1 | 1 |
|
||||
| MUL | 1 | 3 | 3 |
|
||||
| **Total** | **6** | | **~8 cycles** |
|
||||
| **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:
|
||||
Finding max/min over sliding windows has limited SIMD benefit due to sequential dependency:
|
||||
|
||||
| Operation | Scalar Ops | SIMD Benefit | Notes |
|
||||
| :--- | :---: | :---: | :--- |
|
||||
| Max/Min update | 4 | 1× | Deque-based, sequential |
|
||||
| Middle band | 2 | 2× | ADD + MUL parallelizable |
|
||||
| Max/Min update | 4 | 1× | Deque-based, sequential |
|
||||
| Middle band | 2 | 2× | ADD + MUL parallelizable |
|
||||
|
||||
**Batch efficiency (512 bars):**
|
||||
|
||||
| Mode | Cycles/bar | Total (512 bars) | Improvement |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| Scalar streaming | 8 | 4,096 | |
|
||||
| Scalar streaming | 8 | 4,096 | — |
|
||||
| Partial SIMD | ~7 | ~3,584 | **~12%** |
|
||||
|
||||
Price Channel is already highly efficient due to the O(1) monotonic deque algorithm.
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **Accuracy** | 10/10 | Exact max/min calculation |
|
||||
| **Timeliness** | 6/10 | Tracks past extremes, inherently lagging |
|
||||
| **Overshoot** | 10/10 | No overshootbands are actual price levels |
|
||||
| **Smoothness** | 5/10 | Bands move in discrete steps |
|
||||
| **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
|
||||
|
||||
* **Support and Resistance:** The upper band can act as resistance, and the lower band as support.
|
||||
* **Breakouts:**
|
||||
* A close above the Upper Channel suggests bullish strength and a potential upside breakout.
|
||||
* A close below the Lower Channel suggests bearish pressure and a potential downside breakout.
|
||||
* **Trend Identification:**
|
||||
* In an uptrend, prices may consistently touch or "ride" the Upper Channel.
|
||||
* In a downtrend, prices may consistently touch or "ride" the Lower Channel.
|
||||
* **Volatility:** The width of the channel can give an indication of volatility. Wider channels suggest higher volatility over the lookback period.
|
||||
* **"Turtle Trading" Strategy:** Price Channels (like Donchian Channels) were famously used in the "Turtle Trading" system, where breakouts from the channel were used as entry signals.
|
||||
| Library | Status | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **TA-Lib** | - | No implementation |
|
||||
| **Skender** | - | No implementation (uses Donchian) |
|
||||
| **Tulip** | - | No implementation |
|
||||
| **Ooples** | ✅ | Cross-validated via Donchian equivalence |
|
||||
| **Dchannel** | ✅ | Exact match—identical algorithm |
|
||||
|
||||
## Limitations and Considerations
|
||||
## Common Pitfalls
|
||||
|
||||
* **Lag:** Like all indicators based on lookback periods, there's an inherent lag. The channel reflects past price action.
|
||||
* **Whipsaws:** In choppy, non-trending markets, breakouts can be false, leading to whipsaws.
|
||||
* **Parameter Choice:** The `Length` parameter is crucial. A length too short may generate many false signals, while one too long may miss timely entries.
|
||||
* **Not a Standalone System:** Best used in conjunction with other indicators (e.g., volume, trend indicators) or price action analysis for confirmation.
|
||||
1. **Stale Extremes**: Price Channel 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.
|
||||
|
||||
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.
|
||||
|
||||
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**: Price Channel generates 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
|
||||
|
||||
* Donchian, R. D. (Various). (Conceptual basis for channel breakouts).
|
||||
* Faith, C. (2007). *Way of the Turtle*. McGraw-Hill. (Describes trading systems using similar channels).
|
||||
- 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