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,62 @@
using System.Drawing;
using System.Runtime.CompilerServices;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
[SkipLocalsInit]
public sealed class HtDcperiodIndicator : Indicator, IWatchlistIndicator
{
[IndicatorExtensions.DataSourceInput]
public SourceType Source { get; set; } = SourceType.Close;
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private HtDcperiod _htDcperiod = null!;
private readonly LineSeries _periodSeries;
private Func<IHistoryItem, double> _priceSelector = null!;
public static int MinHistoryDepths => 32;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => "HT_DCPERIOD";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/cycles/ht_dcperiod/HtDcperiod.Quantower.cs";
public HtDcperiodIndicator()
{
OnBackGround = true;
SeparateWindow = true;
Name = "HT_DCPERIOD - Hilbert Transform Dominant Cycle Period";
Description = "Hilbert Transform Dominant Cycle Period indicator measuring the dominant cycle period in price data";
_periodSeries = new LineSeries(name: "DCPeriod", color: IndicatorExtensions.Oscillators, width: 2, style: LineStyle.Solid);
AddLineSeries(_periodSeries);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnInit()
{
_htDcperiod = new HtDcperiod();
_priceSelector = Source.GetPriceSelector();
base.OnInit();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnUpdate(UpdateArgs args)
{
if (args.Reason != UpdateReason.NewBar && args.Reason != UpdateReason.HistoricalBar)
{
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 = _htDcperiod.Update(input, args.IsNewBar());
_periodSeries.SetValue(result.Value, _htDcperiod.IsHot, ShowColdValues);
}
}
@@ -0,0 +1,49 @@
using System;
using QuanTAlib;
using Xunit;
namespace QuanTAlib.Tests.Cycles;
public class HtDcperiodTests
{
[Fact]
public void Constructor_SetsDefaults()
{
var ht = new HtDcperiod();
Assert.Equal("HtDcperiod", ht.Name);
Assert.Equal(32, ht.WarmupPeriod);
Assert.False(ht.IsHot);
}
[Fact]
public void Update_BecomesHotAfterWarmup()
{
var ht = new HtDcperiod();
var gbm = new GBM(seed: 42);
var bars = gbm.Fetch(80, 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 HtDcperiod();
var now = DateTime.UtcNow;
for (int i = 0; i < 40; 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);
}
}
@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using QuanTAlib;
using TALib;
using Xunit;
namespace QuanTAlib.Tests;
public sealed class HtDcperiodValidationTests : IDisposable
{
private readonly ValidationTestData _data;
private bool _disposed;
public HtDcperiodValidationTests()
{
_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 outPeriod = new double[input.Length];
var rc = TALib.Functions.HtDcPeriod(input, 0..^0, outPeriod, out var outRange);
Assert.Equal(Core.RetCode.Success, rc);
var q = new HtDcperiod();
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(outPeriod[talibIdx], qSeries.Values[i], ValidationHelper.TalibTolerance);
}
}
}
[Fact]
public void Validate_TaLib_Streaming()
{
var input = _data.RawData.Span;
var outPeriod = new double[input.Length];
var rc = TALib.Functions.HtDcPeriod(input, 0..^0, outPeriod, out var outRange);
Assert.Equal(Core.RetCode.Success, rc);
var streaming = new List<double>(_data.Data.Count);
var q = new HtDcperiod();
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(outPeriod[talibIdx], streaming[i], ValidationHelper.TalibTolerance);
}
}
}
[Fact]
public void Lookback_MatchesTaLib()
{
int talibLookback = TALib.Functions.HtDcPeriodLookback();
var q = new HtDcperiod();
Assert.Equal(talibLookback, q.WarmupPeriod);
}
}
+422
View File
@@ -0,0 +1,422 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// HT_DCPERIOD: Hilbert Transform Dominant Cycle Period - Estimates the period of the dominant cycle in the price data.
/// </summary>
/// <remarks>
/// The Hilbert Transform Dominant Cycle Period, developed by John Ehlers, uses the Hilbert Transform
/// to extract the dominant market cycle period. It measures the period of the phase change rate of the analytic signal.
///
/// Algorithm:
/// 1. Compute the Hilbert Transform of the detrended price to get InPhase (I) and Quadrature (Q) components.
/// 2. Determine the phase angle from I and Q.
/// 3. Measure the rate of change of the phase to derive the instantaneous period.
/// 4. Smooth the raw period using an Exponential Moving Average (EMA) and clamp values to a valid range.
///
/// Properties:
/// - Returns the period length (in bars) of the current dominant cycle.
/// - Adapts to changing market conditions.
/// - Used as a basis for other Ehlers indicators (Sinewave, Phasor).
/// </remarks>
[SkipLocalsInit]
public sealed class HtDcperiod : AbstractBase
{
private const int LOOKBACK = 32; // TA-Lib lookback for HT_DCPERIOD
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;
// 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 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, 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 HtDcperiod()
{
Name = "HtDcperiod";
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 HtDcperiod(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 = (2.0 * Math.PI) / angle;
}
}
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
{
_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);
}
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++];
s.PeriodWMASum -= s.PeriodWMASub;
}
}
_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 smoothedValue = s.PeriodWMASum * 0.1;
s.PeriodWMASum -= s.PeriodWMASub;
// Store smoothed value
_smoothPrice[s.SmoothPriceIdx] = smoothedValue;
s.SmoothPriceIdx = (s.SmoothPriceIdx + 1) % SMOOTH_PRICE_SIZE;
// 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(), smoothedValue, 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(), smoothedValue, 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);
// Write back state
_state = s;
return s.SmoothPeriod;
}
[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 HtDcperiod();
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 HtDcperiod();
return ht.Update(source);
}
}
+143
View File
@@ -0,0 +1,143 @@
# HT_DCPERIOD: Hilbert Transform - Dominant Cycle Period
> "Knowing the cycle period is the master key—it calibrates other indicators to the market's current rhythm."
HT_DCPERIOD estimates the period of the dominant market cycle using Ehlers' Hilbert Transform cascade. The indicator measures the instantaneous period based on the rate of change of the phase angle, providing a variable period length (typically 6-50 bars) that dynamically tunes other indicators.
## Historical Context
John Ehlers introduced the Hilbert Transform Dominant Cycle Period in *Rocket Science for Traders* (2001). The goal was to overcome the limitations of fixed-period indicators by measuring the actual cycle length present in the data.
TA-Lib implements HT_DCPERIOD using Ehlers' specific coefficients (A = 0.0962, B = 0.5769) and smoothing algorithms. QuanTAlib matches the TA-Lib implementation within floating-point tolerance.
## Architecture & Physics
The algorithm follows a complex pipeline to extract cycle period from phase information.
### 1. WMA Price Smoothing
$$
SmoothPrice_t = \frac{4P_t + 3P_{t-1} + 2P_{t-2} + P_{t-3}}{10}
$$
### 2. Hilbert Transform Components
The Hilbert Transform generates In-Phase (I) and Quadrature (Q) components:
- **Detrender**: Removes DC component and trend
- **Q1**: Quadrature component of detrender
- **I1**: In-Phase component (delayed detrender)
- **jI, jQ**: Hilbert transforms of I1 and Q1
### 3. Phasor Components
$$
I2_t = I1_t - jQ_t
$$
$$
Q2_t = Q1_t + jI_t
$$
Smoothed with EMA (α = 0.2).
### 4. Period Extraction
$$
Period_t = \frac{2\pi}{\arctan(Im_t / Re_t)}
$$
Clamped to [6, 50] and smoothed with EMA (α = 0.33).
## Performance Profile
### Operation Count (Streaming Mode, per Bar)
| Operation | Count | Cost (cycles) | Subtotal |
| :--- | :---: | :---: | :---: |
| MUL (Hilbert taps) | 28 | 3 | 84 |
| MUL (homodyne mix) | 4 | 3 | 12 |
| ADD/SUB | 40 | 1 | 40 |
| ATAN2 | 1 | 25 | 25 |
| DIV | 3 | 15 | 45 |
| **Total** | **76** | — | **~206 cycles** |
### Complexity Analysis
- **Streaming:** O(1) per bar—fixed Hilbert cascade
- **Memory:** ~1.2 KB per instance (circular buffers + state)
- **Warmup:** 32 bars (TA-Lib lookback)
## Validation
| Library | Status | Notes |
| :--- | :---: | :--- |
| TA-Lib | ✅ | Matches `TALib.Functions.HtDcPeriod()` |
| Skender | N/A | Not implemented |
| PineScript | ✅ | Matches `ht_dcperiod.pine` reference |
## Usage & Pitfalls
- **Output is period in bars** (6-50 range)—not an oscillator
- **32-bar warmup required**—ignore early values
- **Trending markets** cause period to drift to upper limit (50)
- **High noise** causes jitter—internal smoothing helps
- **Use for adaptive tuning**: `RSI(period: htDcperiod.Value / 2)`
- **Stable periods** indicate rhythmic market suitable for oscillators
## API
```mermaid
classDiagram
class HtDcperiod {
+double Value
+bool IsHot
+HtDcperiod()
+HtDcperiod(ITValuePublisher source)
+TValue Update(TValue input, bool isNew)
+void Reset()
}
```
### Class: `HtDcperiod`
| Parameter | Type | Default | Range | Description |
| :--- | :--- | :--- | :--- | :--- |
| (none) | — | — | — | No constructor parameters |
### Properties
- `Value` (`double`): Dominant cycle period in bars (6-50)
- `IsHot` (`bool`): Returns `true` when warmup (32 bars) is complete
### Methods
- `Update(TValue input, bool isNew)`: Updates the indicator with a new data point
## C# Example
```csharp
using QuanTAlib;
// Create HT_DCPERIOD
var htPeriod = new HtDcperiod();
// Update with streaming data
foreach (var bar in quotes)
{
var result = htPeriod.Update(new TValue(bar.Date, bar.Close));
if (htPeriod.IsHot)
{
double period = result.Value;
Console.WriteLine($"{bar.Date}: Dominant Cycle = {period:F2} bars");
// Use cycle to tune RSI adaptively
int adaptivePeriod = (int)(period / 2);
var adaptiveRsi = new Rsi(adaptivePeriod);
}
}
// Batch calculation
var output = HtDcperiod.Calculate(sourceSeries);
```