Add Choppiness Index (CHOP) implementation and tests

- Implemented ChopIndicator for Quantower with configurable period and cold value display.
- Created Chop class for calculating the Choppiness Index with detailed documentation.
- Added comprehensive unit tests for Chop functionality, covering various market conditions and edge cases.
- Developed markdown documentation for CHOP, detailing its historical context, mathematical foundation, and usage examples.
- Established a remediation plan for channel indicators documentation, identifying gaps and prioritizing updates.
This commit is contained in:
Miha Kralj
2026-02-05 19:42:49 -08:00
parent 95838a6435
commit 26280ce80b
73 changed files with 8485 additions and 5254 deletions
@@ -0,0 +1,120 @@
using TradingPlatform.BusinessLayer;
namespace QuanTAlib.Quantower.Tests;
public class HtDcphaseIndicatorTests
{
[Fact]
public void HtDcphaseIndicator_Constructor_SetsDefaults()
{
var indicator = new HtDcphaseIndicator();
Assert.Equal(SourceType.Close, indicator.Source);
Assert.True(indicator.ShowColdValues);
Assert.Equal("HT_DCPHASE - Hilbert Transform Dominant Cycle Phase", indicator.Name);
Assert.True(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void HtDcphaseIndicator_MinHistoryDepths_EqualsLookback()
{
var indicator = new HtDcphaseIndicator();
Assert.Equal(63, HtDcphaseIndicator.MinHistoryDepths);
Assert.Equal(63, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
public void HtDcphaseIndicator_ShortName_IsFixed()
{
var indicator = new HtDcphaseIndicator();
Assert.Equal("HT_DCPHASE", indicator.ShortName);
}
[Fact]
public void HtDcphaseIndicator_Initialize_CreatesInternalHtDcphase()
{
var indicator = new HtDcphaseIndicator();
indicator.Initialize();
// 2 line series: DCPhase + Zero
Assert.Equal(2, indicator.LinesSeries.Count);
}
[Fact]
public void HtDcphaseIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new HtDcphaseIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
Assert.Equal(1, indicator.LinesSeries[0].Count);
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
}
[Fact]
public void HtDcphaseIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new HtDcphaseIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
[Fact]
public void HtDcphaseIndicator_ProcessUpdate_NewTick_NoThrow()
{
var indicator = new HtDcphaseIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
double first = indicator.LinesSeries[0].GetValue(0);
// simulate same-bar update should not advance or corrupt; value remains finite
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
double second = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(first));
Assert.True(double.IsFinite(second));
Assert.Equal(first, second);
}
[Fact]
public void HtDcphaseIndicator_MultipleUpdates_ProducesSequence()
{
var indicator = new HtDcphaseIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
double[] closes = { 100, 102, 104, 103, 105, 106, 107, 108 };
foreach (var close in closes)
{
indicator.HistoricalData.AddBar(now, close, close + 2, close - 2, close);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
now = now.AddMinutes(1);
}
for (int j = 0; j < indicator.LinesSeries[0].Count; j++)
{
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(j)));
}
}
}
@@ -0,0 +1,67 @@
using System.Drawing;
using System.Runtime.CompilerServices;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
[SkipLocalsInit]
public sealed class HtDcphaseIndicator : Indicator, IWatchlistIndicator
{
[IndicatorExtensions.DataSourceInput]
public SourceType Source { get; set; } = SourceType.Close;
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private HtDcphase _htDcphase = null!;
private readonly LineSeries _phaseSeries;
private readonly LineSeries _zeroLine;
private Func<IHistoryItem, double> _priceSelector = null!;
public static int MinHistoryDepths => 63;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => "HT_DCPHASE";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/cycles/ht_dcphase/HtDcphase.Quantower.cs";
public HtDcphaseIndicator()
{
OnBackGround = true;
SeparateWindow = true;
Name = "HT_DCPHASE - Hilbert Transform Dominant Cycle Phase";
Description = "Hilbert Transform Dominant Cycle Phase indicator measuring the phase angle of the dominant cycle in price data (degrees, -45 to 315)";
_phaseSeries = new LineSeries(name: "DCPhase", color: IndicatorExtensions.Oscillators, width: 2, style: LineStyle.Solid);
_zeroLine = new LineSeries(name: "Zero", color: Color.Gray, width: 1, style: LineStyle.Dash);
AddLineSeries(_phaseSeries);
AddLineSeries(_zeroLine);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnInit()
{
_htDcphase = new HtDcphase();
_priceSelector = Source.GetPriceSelector();
base.OnInit();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnUpdate(UpdateArgs args)
{
if (args.Reason != UpdateReason.NewBar && args.Reason != UpdateReason.HistoricalBar && args.Reason != UpdateReason.NewTick)
{
return;
}
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
double value = _priceSelector(item);
var time = this.HistoricalData.Time();
var input = new TValue(time, value);
TValue result = _htDcphase.Update(input, args.IsNewBar());
_phaseSeries.SetValue(result.Value, _htDcphase.IsHot, ShowColdValues);
_zeroLine.SetValue(0.0);
}
}
+90
View File
@@ -0,0 +1,90 @@
using System;
using QuanTAlib;
using Xunit;
namespace QuanTAlib.Tests.Cycles;
public class HtDcphaseTests
{
[Fact]
public void Constructor_SetsDefaults()
{
var ht = new HtDcphase();
Assert.Equal("HtDcphase", ht.Name);
Assert.Equal(63, ht.WarmupPeriod);
Assert.False(ht.IsHot);
}
[Fact]
public void Update_BecomesHotAfterWarmup()
{
var ht = new HtDcphase();
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars)
{
ht.Update(new TValue(bar.Time, bar.Close));
}
Assert.True(ht.IsHot);
Assert.True(double.IsFinite(ht.Last.Value));
}
[Fact]
public void Reset_ClearsState()
{
var ht = new HtDcphase();
var now = DateTime.UtcNow;
for (int i = 0; i < 80; i++)
{
ht.Update(new TValue(now.AddMinutes(i), 100 + i));
}
Assert.True(ht.IsHot);
ht.Reset();
Assert.False(ht.IsHot);
Assert.Equal(default, ht.Last);
}
[Fact]
public void PhaseRange_IsValid()
{
var ht = new HtDcphase();
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars)
{
ht.Update(new TValue(bar.Time, bar.Close));
}
// After warmup, phase should be in valid range
double phase = ht.Last.Value;
Assert.True(phase >= -45.0 && phase <= 315.0,
$"Phase {phase} should be in range [-45, 315]");
}
[Fact]
public void SameBarUpdate_ReturnsSameValue()
{
var ht = new HtDcphase();
var now = DateTime.UtcNow;
// Prime with data
for (int i = 0; i < 70; i++)
{
ht.Update(new TValue(now.AddMinutes(i), 100 + Math.Sin(i * 0.1) * 10));
}
Assert.True(ht.IsHot);
// First update (new bar)
var result1 = ht.Update(new TValue(now.AddMinutes(70), 105), isNew: true);
// Same bar update
var result2 = ht.Update(new TValue(now.AddMinutes(70), 106), isNew: false);
Assert.Equal(result1.Value, result2.Value);
}
}
@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using QuanTAlib;
using TALib;
using Xunit;
namespace QuanTAlib.Tests;
public sealed class HtDcphaseValidationTests : IDisposable
{
private readonly ValidationTestData _data;
private bool _disposed;
public HtDcphaseValidationTests()
{
_data = new ValidationTestData(5000);
}
public void Dispose()
{
Dispose(true);
}
private void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
_disposed = true;
if (disposing)
{
_data?.Dispose();
}
}
[Fact]
public void Validate_TaLib_Static()
{
var input = _data.RawData.Span;
var outPhase = new double[input.Length];
var rc = TALib.Functions.HtDcPhase(input, 0..^0, outPhase, out var outRange);
Assert.Equal(Core.RetCode.Success, rc);
var q = new HtDcphase();
var qSeries = q.Update(_data.Data);
int outLength = outRange.End.Value - outRange.Start.Value;
for (int i = qSeries.Count - 200; i < qSeries.Count; i++)
{
int talibIdx = i - outRange.Start.Value;
if (talibIdx >= 0 && talibIdx < outLength)
{
Assert.Equal(outPhase[talibIdx], qSeries.Values[i], ValidationHelper.TalibTolerance);
}
}
}
[Fact]
public void Validate_TaLib_Streaming()
{
var input = _data.RawData.Span;
var outPhase = new double[input.Length];
var rc = TALib.Functions.HtDcPhase(input, 0..^0, outPhase, out var outRange);
Assert.Equal(Core.RetCode.Success, rc);
var streaming = new List<double>(_data.Data.Count);
var q = new HtDcphase();
foreach (var tv in _data.Data)
{
streaming.Add(q.Update(tv).Value);
}
int outLength = outRange.End.Value - outRange.Start.Value;
for (int i = streaming.Count - 200; i < streaming.Count; i++)
{
int talibIdx = i - outRange.Start.Value;
if (talibIdx >= 0 && talibIdx < outLength)
{
Assert.Equal(outPhase[talibIdx], streaming[i], ValidationHelper.TalibTolerance);
}
}
}
[Fact]
public void Lookback_MatchesTaLib()
{
int talibLookback = TALib.Functions.HtDcPhaseLookback();
var q = new HtDcphase();
Assert.Equal(talibLookback, q.WarmupPeriod);
}
}
+492
View File
@@ -0,0 +1,492 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// HT_DCPHASE: Hilbert Transform Dominant Cycle Phase - Calculates the phase angle of the dominant market cycle.
/// </summary>
/// <remarks>
/// The Hilbert Transform Dominant Cycle Phase indicator determines the current phase of the market cycle
/// within the dominant period. It helps in identifying where the price is within the cycle (e.g., peak, valley).
///
/// Algorithm:
/// 1. Calculate the InPhase (I) and Quadrature (Q) components using the Hilbert Transform.
/// 2. Compute determining the phase angle = arctan(Q / I).
/// 3. Adjust the phase for quadrant correctness and wrap-around.
/// 4. Smoothed using the period information to provide a stable phase reading.
///
/// Properties:
/// - Output is in degrees.
/// - Typically ranges between 0 and 360 degrees (implementation details may vary regarding specific range wrapping).
/// - Helps identifying cyclic turning points independent of amplitude.
/// </remarks>
[SkipLocalsInit]
public sealed class HtDcphase : AbstractBase
{
private const int LOOKBACK = 63; // TA-Lib lookback for HT_DCPHASE
private const int SMOOTH_PRICE_SIZE = 50;
private const int CIRC_BUFFER_SIZE = 44; // 4 * 11 for Hilbert transform
private const int PRICE_HISTORY_SIZE = 64;
private const double A_CONST = 0.0962;
private const double B_CONST = 0.5769;
private const double RAD_TO_DEG = 45.0 / 0.78539816339744830962; // 45.0 / atan(1.0)
private const double DEG_TO_RAD_360 = 0.78539816339744830962 * 8.0; // atan(1.0) * 8.0
// Hilbert buffer keys (matching TA-Lib layout)
private const int KEY_DETRENDER = 6;
private const int KEY_Q1 = 17;
private const int KEY_JI = 28;
private const int KEY_JQ = 39;
[StructLayout(LayoutKind.Auto)]
private record struct State(
double PrevI2, double PrevQ2, double Re, double Im,
double Period, double SmoothPeriod, double DcPhase,
double I1ForOddPrev3, double I1ForEvenPrev3,
double I1ForOddPrev2, double I1ForEvenPrev2,
double PeriodWMASub, double PeriodWMASum, double TrailingWMAValue,
int TrailingWMAIdx, int HilbertIdx, int SmoothPriceIdx,
double LastValidPrice, int Today
)
{
public State() : this(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, double.NaN, 0) { }
}
private State _state;
private State _p_state;
private readonly double[] _circBuffer;
private readonly double[] _p_circBuffer;
private readonly double[] _smoothPrice;
private readonly double[] _p_smoothPrice;
private readonly double[] _priceHistory;
private readonly double[] _p_priceHistory;
private readonly TValuePublishedHandler _handler;
public override bool IsHot => _state.Today > LOOKBACK;
public HtDcphase()
{
Name = "HtDcphase";
WarmupPeriod = LOOKBACK;
_handler = Handle;
_circBuffer = new double[CIRC_BUFFER_SIZE];
_p_circBuffer = new double[CIRC_BUFFER_SIZE];
_smoothPrice = new double[SMOOTH_PRICE_SIZE];
_p_smoothPrice = new double[SMOOTH_PRICE_SIZE];
_priceHistory = new double[PRICE_HISTORY_SIZE];
_p_priceHistory = new double[PRICE_HISTORY_SIZE];
Init();
}
public HtDcphase(ITValuePublisher source) : this()
{
ArgumentNullException.ThrowIfNull(source);
source.Pub += _handler;
}
private void Init()
{
_state = new State();
_p_state = new State();
Array.Clear(_circBuffer);
Array.Clear(_p_circBuffer);
Array.Clear(_smoothPrice);
Array.Clear(_p_smoothPrice);
Array.Clear(_priceHistory);
Array.Clear(_p_priceHistory);
Last = default;
}
public override void Reset() => Init();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void DoHilbertTransform(
Span<double> buffer, int baseKey, double input, bool isOdd, int hilbertIdx, double adjustedPrevPeriod)
{
double hilbertTempT = A_CONST * input;
int hilbertIndex = baseKey - (isOdd ? 6 : 3) + hilbertIdx;
int prevIndex = baseKey + (isOdd ? 1 : 2);
int prevInputIndex = baseKey + (isOdd ? 3 : 4);
buffer[baseKey] = -buffer[hilbertIndex];
buffer[hilbertIndex] = hilbertTempT;
buffer[baseKey] += hilbertTempT;
buffer[baseKey] -= buffer[prevIndex];
buffer[prevIndex] = B_CONST * buffer[prevInputIndex];
buffer[baseKey] += buffer[prevIndex];
buffer[prevInputIndex] = input;
buffer[baseKey] *= adjustedPrevPeriod;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void CalcHilbertOdd(
Span<double> buffer, double smoothedValue, int hilbertIdx, double adjustedPrevPeriod,
out double i1ForEvenPrev3, double prevQ2, double prevI2, double i1ForOddPrev3,
ref double i1ForEvenPrev2, out double q2, out double i2)
{
DoHilbertTransform(buffer, KEY_DETRENDER, smoothedValue, true, hilbertIdx, adjustedPrevPeriod);
double input = buffer[KEY_DETRENDER];
DoHilbertTransform(buffer, KEY_Q1, input, true, hilbertIdx, adjustedPrevPeriod);
DoHilbertTransform(buffer, KEY_JI, i1ForOddPrev3, true, hilbertIdx, adjustedPrevPeriod);
double input1 = buffer[KEY_Q1];
DoHilbertTransform(buffer, KEY_JQ, input1, true, hilbertIdx, adjustedPrevPeriod);
q2 = 0.2 * (buffer[KEY_Q1] + buffer[KEY_JI]) + 0.8 * prevQ2;
i2 = 0.2 * (i1ForOddPrev3 - buffer[KEY_JQ]) + 0.8 * prevI2;
i1ForEvenPrev3 = i1ForEvenPrev2;
i1ForEvenPrev2 = buffer[KEY_DETRENDER];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void CalcHilbertEven(
Span<double> buffer, double smoothedValue, ref int hilbertIdx, double adjustedPrevPeriod,
double i1ForEvenPrev3, double prevQ2, double prevI2, out double i1ForOddPrev3,
ref double i1ForOddPrev2, out double q2, out double i2)
{
DoHilbertTransform(buffer, KEY_DETRENDER, smoothedValue, false, hilbertIdx, adjustedPrevPeriod);
double input = buffer[KEY_DETRENDER];
DoHilbertTransform(buffer, KEY_Q1, input, false, hilbertIdx, adjustedPrevPeriod);
DoHilbertTransform(buffer, KEY_JI, i1ForEvenPrev3, false, hilbertIdx, adjustedPrevPeriod);
double input1 = buffer[KEY_Q1];
DoHilbertTransform(buffer, KEY_JQ, input1, false, hilbertIdx, adjustedPrevPeriod);
if (++hilbertIdx == 3)
{
hilbertIdx = 0;
}
q2 = 0.2 * (buffer[KEY_Q1] + buffer[KEY_JI]) + 0.8 * prevQ2;
i2 = 0.2 * (i1ForEvenPrev3 - buffer[KEY_JQ]) + 0.8 * prevI2;
i1ForOddPrev3 = i1ForOddPrev2;
i1ForOddPrev2 = buffer[KEY_DETRENDER];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void CalcSmoothedPeriod(
ref double re, double i2, double q2, ref double prevI2, ref double prevQ2, ref double im, ref double period)
{
re = Math.FusedMultiplyAdd(0.2, (i2 * prevI2) + (q2 * prevQ2), 0.8 * re);
im = Math.FusedMultiplyAdd(0.2, (i2 * prevQ2) - (q2 * prevI2), 0.8 * im);
prevQ2 = q2;
prevI2 = i2;
double tempReal1 = period;
if (im != 0.0 && re != 0.0)
{
double angle = Math.Atan(im / re);
if (angle != 0.0)
{
period = 360.0 / (angle * RAD_TO_DEG);
}
}
double tempReal2 = 1.5 * tempReal1;
period = Math.Min(period, tempReal2);
tempReal2 = 0.67 * tempReal1;
period = Math.Max(period, tempReal2);
period = Math.Clamp(period, 6.0, 50.0);
period = Math.FusedMultiplyAdd(0.2, period, 0.8 * tempReal1);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double Step(double price, bool isNew)
{
if (isNew)
{
_p_state = _state;
Array.Copy(_circBuffer, _p_circBuffer, CIRC_BUFFER_SIZE);
Array.Copy(_smoothPrice, _p_smoothPrice, SMOOTH_PRICE_SIZE);
Array.Copy(_priceHistory, _p_priceHistory, PRICE_HISTORY_SIZE);
}
else
{
// Same-bar update: restore previous state and return cached result from Last
_state = _p_state;
Array.Copy(_p_circBuffer, _circBuffer, CIRC_BUFFER_SIZE);
Array.Copy(_p_smoothPrice, _smoothPrice, SMOOTH_PRICE_SIZE);
Array.Copy(_p_priceHistory, _priceHistory, PRICE_HISTORY_SIZE);
return Last.Value;
}
var s = _state;
s.Today++;
// Handle non-finite input
if (!double.IsFinite(price))
{
if (double.IsNaN(s.LastValidPrice))
{
_state = s;
return 0.0;
}
price = s.LastValidPrice;
}
else
{
s.LastValidPrice = price;
}
int today = s.Today - 1;
// WMA initialization phase (first 34 + 3 bars = 37 bars for lookback)
if (today < 37)
{
// Store prices for WMA initialization
if (today >= 0)
{
_priceHistory[today % PRICE_HISTORY_SIZE] = price;
}
// Initialize WMA (TA-Lib pattern: unrolled first 3, then loop for period)
if (today == 36)
{
// Now we have enough data to initialize WMA
double tempReal = _priceHistory[0];
s.PeriodWMASub = tempReal;
s.PeriodWMASum = tempReal;
tempReal = _priceHistory[1];
s.PeriodWMASub += tempReal;
s.PeriodWMASum += tempReal * 2.0;
tempReal = _priceHistory[2];
s.PeriodWMASub += tempReal;
s.PeriodWMASum += tempReal * 3.0;
s.TrailingWMAValue = 0.0;
s.TrailingWMAIdx = 0;
// Process remaining bars in period (34 iterations)
for (int i = 0; i < 34; i++)
{
int priceIdx = 3 + i;
double priceVal = _priceHistory[priceIdx];
s.PeriodWMASub += priceVal;
s.PeriodWMASub -= s.TrailingWMAValue;
s.PeriodWMASum += priceVal * 4.0;
s.TrailingWMAValue = _priceHistory[s.TrailingWMAIdx++];
double smoothedValue = s.PeriodWMASum * 0.1;
s.PeriodWMASum -= s.PeriodWMASub;
// Store smoothed values during init
_smoothPrice[i % SMOOTH_PRICE_SIZE] = smoothedValue;
}
s.SmoothPriceIdx = 34 % SMOOTH_PRICE_SIZE;
}
_state = s;
return 0.0;
}
// Calculate smoothed price using WMA
double adjustedPrevPeriod = 0.075 * s.Period + 0.54;
s.PeriodWMASub += price;
s.PeriodWMASub -= s.TrailingWMAValue;
s.PeriodWMASum += price * 4.0;
// Get trailing value (TA-Lib uses a linear trailing index)
int trailIdx = s.TrailingWMAIdx % PRICE_HISTORY_SIZE;
s.TrailingWMAValue = _priceHistory[trailIdx];
s.TrailingWMAIdx++;
int historyIdx = today % PRICE_HISTORY_SIZE;
_priceHistory[historyIdx] = price;
double smoothedValue2 = s.PeriodWMASum * 0.1;
s.PeriodWMASum -= s.PeriodWMASub;
// Store smoothed value
_smoothPrice[s.SmoothPriceIdx] = smoothedValue2;
// Extract fields for ref/out parameters
int hilbertIdx = s.HilbertIdx;
double i1ForOddPrev2 = s.I1ForOddPrev2;
double i1ForEvenPrev2 = s.I1ForEvenPrev2;
double re = s.Re;
double im = s.Im;
double prevI2 = s.PrevI2;
double prevQ2 = s.PrevQ2;
double period = s.Period;
// Perform Hilbert Transform (alternating odd/even)
double q2, i2;
if (today % 2 == 0)
{
// Even bar
CalcHilbertEven(_circBuffer.AsSpan(), smoothedValue2, ref hilbertIdx, adjustedPrevPeriod,
s.I1ForEvenPrev3, prevQ2, prevI2, out double i1ForOddPrev3,
ref i1ForOddPrev2, out q2, out i2);
s.I1ForOddPrev3 = i1ForOddPrev3;
}
else
{
// Odd bar
CalcHilbertOdd(_circBuffer.AsSpan(), smoothedValue2, hilbertIdx, adjustedPrevPeriod,
out double i1ForEvenPrev3, prevQ2, prevI2, s.I1ForOddPrev3,
ref i1ForEvenPrev2, out q2, out i2);
s.I1ForEvenPrev3 = i1ForEvenPrev3;
}
// Write back ref parameters
s.HilbertIdx = hilbertIdx;
s.I1ForOddPrev2 = i1ForOddPrev2;
s.I1ForEvenPrev2 = i1ForEvenPrev2;
// Calculate smoothed period
CalcSmoothedPeriod(ref re, i2, q2, ref prevI2, ref prevQ2, ref im, ref period);
// Write back ref parameters
s.Re = re;
s.Im = im;
s.PrevI2 = prevI2;
s.PrevQ2 = prevQ2;
s.Period = period;
s.SmoothPeriod = Math.FusedMultiplyAdd(0.33, period, 0.67 * s.SmoothPeriod);
// Calculate DC Phase using smoothed prices
double dcPeriod = s.SmoothPeriod + 0.5;
int dcPeriodInt = (int)dcPeriod;
double realPart = 0.0;
double imagPart = 0.0;
int idx = s.SmoothPriceIdx;
for (int i = 0; i < dcPeriodInt; i++)
{
double tempReal = i * DEG_TO_RAD_360 / dcPeriodInt;
double tempReal2 = _smoothPrice[idx];
realPart += Math.Sin(tempReal) * tempReal2;
imagPart += Math.Cos(tempReal) * tempReal2;
if (idx == 0)
{
idx = SMOOTH_PRICE_SIZE - 1;
}
else
{
idx--;
}
}
double dcPhase = s.DcPhase;
double absImagPart = Math.Abs(imagPart);
if (absImagPart > 0.0)
{
dcPhase = Math.Atan(realPart / imagPart) * RAD_TO_DEG;
}
else if (absImagPart <= 0.01)
{
if (realPart < 0.0)
{
dcPhase -= 90.0;
}
else if (realPart > 0.0)
{
dcPhase += 90.0;
}
}
dcPhase += 90.0;
dcPhase += 360.0 / s.SmoothPeriod;
if (imagPart < 0.0)
{
dcPhase += 180.0;
}
if (dcPhase > 315.0)
{
dcPhase -= 360.0;
}
s.DcPhase = dcPhase;
// Advance smooth price index
s.SmoothPriceIdx = (s.SmoothPriceIdx + 1) % SMOOTH_PRICE_SIZE;
// Write back state
_state = s;
return dcPhase;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override TValue Update(TValue input, bool isNew = true)
{
double result = Step(input.Value, isNew);
Last = new TValue(input.Time, result);
return Last;
}
public override TSeries Update(TSeries source)
{
if (source.Count == 0)
{
return new TSeries([], []);
}
int len = source.Count;
var t = new System.Collections.Generic.List<long>(len);
var v = new System.Collections.Generic.List<double>(len);
for (int i = 0; i < len; i++)
{
var result = Update(new TValue(source.Times[i], source.Values[i]));
t.Add(result.Time);
v.Add(result.Value);
}
return new TSeries(t, v);
}
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
long ticksStep = step?.Ticks ?? TimeSpan.FromMinutes(1).Ticks;
long t = DateTime.UtcNow.Ticks;
foreach (double value in source)
{
Update(new TValue(new DateTime(t, DateTimeKind.Utc), value));
t += ticksStep;
}
}
public static void Calculate(ReadOnlySpan<double> source, Span<double> output)
{
if (output.Length < source.Length)
{
throw new ArgumentException("output", nameof(output));
}
var ht = new HtDcphase();
for (int i = 0; i < source.Length; i++)
{
output[i] = ht.Update(new TValue(DateTime.UtcNow.AddTicks(i), source[i])).Value;
}
}
public static TSeries Calculate(TSeries source)
{
var ht = new HtDcphase();
return ht.Update(source);
}
}
+152
View File
@@ -0,0 +1,152 @@
# HT_DCPHASE: Hilbert Transform - Dominant Cycle Phase
> "The phase advances through a full 360-degree cycle as the dominant cycle completes; rapid phase changes indicate turning points."
HT_DCPHASE measures the instantaneous phase angle of the dominant market cycle using Ehlers' Hilbert Transform cascade. The output ranges from -45° to 315°, with phase discontinuities marking cycle completions. This indicator times entries/exits based on cycle position.
## Historical Context
John Ehlers developed the Hilbert Transform cycle indicators in *Rocket Science for Traders* (2001). TA-Lib implements HT_DCPHASE directly from Ehlers' coefficients (A = 0.0962, B = 0.5769) with a 4-bar WMA prefilter and DC phase extraction from smoothed price history.
QuanTAlib matches TA-Lib HT_DCPHASE output within floating-point tolerance.
## Architecture & Physics
The algorithm extracts phase from the complex analytic signal.
### 1. WMA Price Smoothing
$$
SmoothPrice_t = \frac{4P_t + 3P_{t-1} + 2P_{t-2} + P_{t-3}}{10}
$$
### 2. Hilbert Transform Cascade
- **Detrender (D)**: Removes DC component
- **Quadrature (Q1)**: 90° phase-shifted version of D
- **In-Phase (I1)**: D delayed by 3 bars
- **jI, jQ**: Hilbert transforms of I1, Q1
### 3. Phasor Components
$$
I2_t = I1_t - jQ_t
$$
$$
Q2_t = Q1_t + jI_t
$$
Smoothed with EMA (α = 0.2).
### 4. DC Phase Calculation
Via DFT-like accumulation over smoothed period:
$$
DCPhase = \arctan\left(\frac{RealPart}{ImagPart}\right) \cdot \frac{180°}{\pi}
$$
Wrapped to range [-45°, 315°].
## Performance Profile
### Operation Count (Streaming Mode, per Bar)
| Operation | Count | Cost (cycles) | Subtotal |
| :--- | :---: | :---: | :---: |
| MUL (Hilbert + DFT) | 45 | 3 | 135 |
| SIN/COS (DFT loop) | 100 | 15 | 1500 |
| ADD/SUB | 60 | 1 | 60 |
| ATAN2 | 2 | 25 | 50 |
| **Total** | **~207** | — | **~1745 cycles** |
### Complexity Analysis
- **Streaming:** O(P) per bar where P is smoothed period (~6-50)
- **Memory:** ~1.2 KB per instance
- **Warmup:** 63 bars (TA-Lib lookback)
## Validation
| Library | Status | Notes |
| :--- | :---: | :--- |
| TA-Lib | ✅ | Matches `TALib.Functions.HtDcPhase()` |
| Skender | N/A | Not implemented |
| PineScript | ✅ | Matches `ht_dcphase.pine` |
## Usage & Pitfalls
- **Phase range is -45° to 315°**—discontinuity at wrap is expected
- **63-bar warmup required**—ignore early values
- **Phase interpretation**:
- -45° to 45°: Bottom / Start of uptrend
- 45° to 135°: Rising / Mid-uptrend
- 135° to 225°: Top / Start of downtrend
- 225° to 315°: Falling / Mid-downtrend
- **Do not smooth across discontinuity**—315° to -45° jump is cycle completion
- **Strong trends** cause phase to advance slowly or get stuck
- **Rapid phase change** often precedes price reversals
## API
```mermaid
classDiagram
class HtDcphase {
+double Value
+bool IsHot
+HtDcphase()
+HtDcphase(ITValuePublisher source)
+TValue Update(TValue input, bool isNew)
+void Reset()
}
```
### Class: `HtDcphase`
| Parameter | Type | Default | Range | Description |
| :--- | :--- | :--- | :--- | :--- |
| (none) | — | — | — | No constructor parameters |
### Properties
- `Value` (`double`): DC phase in degrees (-45° to 315°)
- `IsHot` (`bool`): Returns `true` when warmup (63 bars) is complete
### Methods
- `Update(TValue input, bool isNew)`: Updates the indicator with a new data point
## C# Example
```csharp
using QuanTAlib;
// Create HT_DCPHASE
var htPhase = new HtDcphase();
// Update with streaming data
foreach (var bar in quotes)
{
var result = htPhase.Update(new TValue(bar.Date, bar.Close));
if (htPhase.IsHot)
{
double phase = result.Value;
Console.WriteLine($"{bar.Date}: Phase = {phase:F1}°");
// Cycle position detection
if (phase >= -45 && phase < 45)
Console.WriteLine(" → Cycle bottom zone");
else if (phase >= 45 && phase < 135)
Console.WriteLine(" → Rising phase");
else if (phase >= 135 && phase < 225)
Console.WriteLine(" → Cycle top zone");
else
Console.WriteLine(" → Falling phase");
}
}
// Batch calculation
var output = HtDcphase.Calculate(sourceSeries);
```