mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-17 10:08:05 +00:00
- Updated the Prime method signature in multiple indicators (Jma, Kama, Lsma, Mama, Mgdi, Pwma, Rma, Sma, Ssf, Super, T3, Tema, Trima, Usf, Vidya, Wma, Atr) to accept an optional TimeSpan parameter for improved flexibility. - Added unit tests for Lsma to verify Dispose functionality, ensuring proper unsubscription from the source and thread safety. - Enhanced Mama and Wma classes to handle non-finite inputs gracefully and added checks for valid parameters in constructors. - Introduced additional tests for T3 to validate constructor behavior with invalid volume factors. - Ensured all indicators maintain consistent behavior when handling edge cases, such as empty buffers and non-finite values.
461 lines
16 KiB
C#
461 lines
16 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace QuanTAlib;
|
|
|
|
/// <summary>
|
|
/// HTIT: Ehlers Hilbert Transform Instantaneous Trend
|
|
/// A trend-following indicator that uses the Hilbert Transform to measure the dominant cycle period
|
|
/// and compute an instantaneous trendline. It adapts to market cycles to reduce lag while maintaining smoothness.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Sources:
|
|
/// https://github.com/mihakralj/pinescript/blob/main/indicators/trends_IIR/htit.md
|
|
/// https://dotnet.stockindicators.dev/indicators/HtTrendline/
|
|
/// </remarks>
|
|
[SkipLocalsInit]
|
|
public sealed class Htit : AbstractBase
|
|
{
|
|
public override bool IsHot => _state.Index >= WarmupPeriod;
|
|
|
|
[StructLayout(LayoutKind.Auto)]
|
|
private record struct State(
|
|
double I2, double Q2, double Re, double Im,
|
|
double Period, double SmoothPeriod,
|
|
double LastValidPrice, int Index
|
|
)
|
|
{
|
|
// Initialize LastValidPrice to NaN to detect first valid price
|
|
public State() : this(0, 0, 0, 0, 0, 0, double.NaN, 0) { }
|
|
}
|
|
private State _state;
|
|
private State _p_state;
|
|
|
|
private readonly RingBuffer _priceBuffer;
|
|
private readonly RingBuffer _smoothBuffer;
|
|
private readonly RingBuffer _detrenderBuffer;
|
|
private readonly RingBuffer _i1Buffer;
|
|
private readonly RingBuffer _q1Buffer;
|
|
private readonly RingBuffer _itBuffer;
|
|
private readonly TValuePublishedHandler _handler;
|
|
|
|
// High-precision constants
|
|
private const double c1 = 5.0 / 52.0; // ~0.09615385
|
|
private const double c2 = 15.0 / 26.0; // ~0.57692308
|
|
private const double adjSlope = 3.0 / 40.0; // 0.075
|
|
private const double adjIntercept = 27.0 / 50.0; // 0.54
|
|
private const double TwoPi = 2.0 * Math.PI;
|
|
private const double MinDeltaRadians = Math.PI / 180.0; // 1 degree in radians
|
|
|
|
public Htit()
|
|
{
|
|
Name = "Htit";
|
|
WarmupPeriod = 12;
|
|
_handler = Handle;
|
|
|
|
// Initialize buffers with size 8 (power of 2) for consistency with Calculate optimization
|
|
// except priceBuffer which needs to be larger for IT calculation
|
|
_priceBuffer = new RingBuffer(64); // Needs to hold enough history for IT calculation (up to 50 bars)
|
|
_smoothBuffer = new RingBuffer(8);
|
|
_detrenderBuffer = new RingBuffer(8);
|
|
_i1Buffer = new RingBuffer(8);
|
|
_q1Buffer = new RingBuffer(8);
|
|
_itBuffer = new RingBuffer(8);
|
|
|
|
Init();
|
|
}
|
|
|
|
public Htit(ITValuePublisher source) : this()
|
|
{
|
|
source.Pub += _handler;
|
|
}
|
|
|
|
private void Init()
|
|
{
|
|
Reset();
|
|
}
|
|
|
|
public override void Reset()
|
|
{
|
|
_state = default;
|
|
_p_state = default;
|
|
|
|
_priceBuffer.Clear();
|
|
_smoothBuffer.Clear();
|
|
_detrenderBuffer.Clear();
|
|
_i1Buffer.Clear();
|
|
_q1Buffer.Clear();
|
|
_itBuffer.Clear();
|
|
|
|
Last = new TValue(DateTime.MinValue, double.NaN);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private double Step(double price, bool isNew)
|
|
{
|
|
if (isNew)
|
|
{
|
|
_p_state = _state;
|
|
_state.Index++;
|
|
}
|
|
else
|
|
{
|
|
_state = _p_state;
|
|
}
|
|
|
|
// Handle non-finite input: skip processing if no valid price seen yet
|
|
if (!double.IsFinite(price))
|
|
{
|
|
// If we haven't seen a valid price yet, return NaN (early exit)
|
|
if (double.IsNaN(_state.LastValidPrice))
|
|
{
|
|
return double.NaN;
|
|
}
|
|
// Otherwise, use the last valid price
|
|
price = _state.LastValidPrice;
|
|
}
|
|
else
|
|
{
|
|
_state.LastValidPrice = price;
|
|
}
|
|
|
|
_priceBuffer.Add(price, isNew);
|
|
|
|
// Need enough data for smooth calculation (4 bars) + detrender (7 bars total lag)
|
|
if (_state.Index < 7)
|
|
{
|
|
// During warmup, propagate NaN if input is NaN
|
|
_smoothBuffer.Add(price, isNew);
|
|
_detrenderBuffer.Add(0, isNew);
|
|
_i1Buffer.Add(0, isNew);
|
|
_q1Buffer.Add(0, isNew);
|
|
_itBuffer.Add(price, isNew);
|
|
return price; // May be NaN if no valid input yet
|
|
}
|
|
|
|
// 1. Smooth Price
|
|
// smooth = (4*Price + 3*Price[1] + 2*Price[2] + Price[3]) / 10
|
|
double smooth = (4.0 * _priceBuffer[^1] + 3.0 * _priceBuffer[^2] + 2.0 * _priceBuffer[^3] + _priceBuffer[^4]) * 0.1;
|
|
_smoothBuffer.Add(smooth, isNew);
|
|
|
|
// 2. Detrender
|
|
// In streaming, we use previous period from state
|
|
double prevPeriod = _p_state.Period;
|
|
double adj = (adjSlope * prevPeriod) + adjIntercept;
|
|
|
|
double detrender = (c1 * _smoothBuffer[^1] + c2 * _smoothBuffer[^3] - c2 * _smoothBuffer[^5] - c1 * _smoothBuffer[^7]) * adj;
|
|
_detrenderBuffer.Add(detrender, isNew);
|
|
|
|
// 3. In-Phase and Quadrature
|
|
double q1 = (c1 * _detrenderBuffer[^1] + c2 * _detrenderBuffer[^3] - c2 * _detrenderBuffer[^5] - c1 * _detrenderBuffer[^7]) * adj;
|
|
double i1 = _detrenderBuffer[^4];
|
|
|
|
_q1Buffer.Add(q1, isNew);
|
|
_i1Buffer.Add(i1, isNew);
|
|
|
|
// 4. Advance phases by 90 degrees
|
|
double jI = (c1 * _i1Buffer[^1] + c2 * _i1Buffer[^3] - c2 * _i1Buffer[^5] - c1 * _i1Buffer[^7]) * adj;
|
|
double jQ = (c1 * _q1Buffer[^1] + c2 * _q1Buffer[^3] - c2 * _q1Buffer[^5] - c1 * _q1Buffer[^7]) * adj;
|
|
|
|
// 5. Phasor addition
|
|
double i2_val = i1 - jQ;
|
|
double q2_val = q1 + jI;
|
|
|
|
// Smooth i2, q2
|
|
_state.I2 = 0.2 * i2_val + 0.8 * _p_state.I2;
|
|
_state.Q2 = 0.2 * q2_val + 0.8 * _p_state.Q2;
|
|
|
|
// 6. Homodyne Discriminator
|
|
double re_val = (_state.I2 * _p_state.I2) + (_state.Q2 * _p_state.Q2);
|
|
double im_val = (_state.I2 * _p_state.Q2) - (_state.Q2 * _p_state.I2);
|
|
|
|
// Smooth re, im
|
|
_state.Re = 0.2 * re_val + 0.8 * _p_state.Re;
|
|
_state.Im = 0.2 * im_val + 0.8 * _p_state.Im;
|
|
|
|
// 7. Calculate Period
|
|
double angle = Math.Atan2(_state.Im, _state.Re);
|
|
double period = Math.Abs(angle) > MinDeltaRadians
|
|
? TwoPi / Math.Abs(angle)
|
|
: _p_state.Period;
|
|
|
|
// Adjust period to thresholds
|
|
if (prevPeriod > 0)
|
|
{
|
|
double cap = 1.5 * prevPeriod;
|
|
double floor = 0.67 * prevPeriod;
|
|
if (period > cap) period = cap;
|
|
if (period < floor) period = floor;
|
|
}
|
|
if (period < 6) period = 6;
|
|
if (period > 50) period = 50;
|
|
|
|
// Smooth the period
|
|
_state.Period = 0.2 * period + 0.8 * prevPeriod;
|
|
_state.SmoothPeriod = 0.33 * _state.Period + 0.67 * _p_state.SmoothPeriod;
|
|
|
|
// 8. Instantaneous Trend
|
|
int dcPeriods = (int)(double.IsNaN(_state.SmoothPeriod) ? 0 : _state.SmoothPeriod + 0.5);
|
|
double sumPr = 0;
|
|
int count = 0;
|
|
|
|
// Sum price over dcPeriods
|
|
for (int d = 0; d < dcPeriods; d++)
|
|
{
|
|
// Check if we have enough history
|
|
if (d < _priceBuffer.Count)
|
|
{
|
|
sumPr += _priceBuffer[^(d + 1)];
|
|
count++;
|
|
}
|
|
}
|
|
|
|
double it = count > 0 ? sumPr / count : price;
|
|
_itBuffer.Add(it, isNew);
|
|
|
|
// 9. Final Trendline
|
|
// Need at least 12 bars total (Index > 11) to have valid IT history for smoothing
|
|
if (_state.Index >= 12)
|
|
{
|
|
// NaN will propagate if IT buffer contains NaN
|
|
return (4.0 * _itBuffer[^1] + 3.0 * _itBuffer[^2] + 2.0 * _itBuffer[^3] + _itBuffer[^4]) * 0.1;
|
|
}
|
|
|
|
return price; // May be NaN if no valid input yet
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public override TValue Update(TValue input, bool isNew = true)
|
|
{
|
|
double val = Step(input.Value, isNew);
|
|
Last = new TValue(input.Time, val);
|
|
PubEvent(Last, isNew);
|
|
return Last;
|
|
}
|
|
|
|
public override TSeries Update(TSeries source)
|
|
{
|
|
if (source.Count == 0) return new TSeries([], []);
|
|
|
|
int len = source.Count;
|
|
var v = new List<double>(len);
|
|
var t = new List<long>(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);
|
|
}
|
|
|
|
private void Handle(object? sender, TValueEventArgs args)
|
|
{
|
|
Update(args.Value, args.IsNew);
|
|
}
|
|
|
|
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
|
{
|
|
foreach (var value in source)
|
|
{
|
|
Step(value, true);
|
|
}
|
|
}
|
|
|
|
public static TSeries Batch(TSeries source)
|
|
{
|
|
var htit = new Htit();
|
|
return htit.Update(source);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static void Calculate(ReadOnlySpan<double> source, Span<double> output)
|
|
{
|
|
if (source.Length != output.Length)
|
|
throw new ArgumentException("Source and output must have the same length", nameof(output));
|
|
|
|
if (source.Length == 0) return;
|
|
|
|
// Stack allocate buffers
|
|
// priceBuffer needs to be larger for IT calculation (up to 50 bars)
|
|
// Using 64 (power of 2) for efficient masking
|
|
Span<double> priceBuffer = stackalloc double[64];
|
|
Span<double> smoothBuffer = stackalloc double[8];
|
|
Span<double> detrenderBuffer = stackalloc double[8];
|
|
Span<double> i1Buffer = stackalloc double[8];
|
|
Span<double> q1Buffer = stackalloc double[8];
|
|
Span<double> itBuffer = stackalloc double[8];
|
|
|
|
int pIdx = 0; // Index for priceBuffer (mask 63)
|
|
int sIdx = 0; // Index for other buffers (mask 7)
|
|
int count = 0;
|
|
|
|
// State variables
|
|
double i2 = 0, q2 = 0, re = 0, im = 0;
|
|
double period = 0, smoothPeriod = 0;
|
|
// Initialize to NaN to detect first valid price
|
|
double lastValidPrice = double.NaN;
|
|
|
|
// Previous state variables
|
|
double p_i2 = 0, p_q2 = 0, p_re = 0, p_im = 0;
|
|
double p_period = 0, p_smoothPeriod = 0;
|
|
|
|
const int Mask63 = 63;
|
|
const int Mask7 = 7;
|
|
|
|
for (int i = 0; i < source.Length; i++)
|
|
{
|
|
double price = source[i];
|
|
|
|
// Handle non-finite input: skip processing if no valid price seen yet
|
|
if (!double.IsFinite(price))
|
|
{
|
|
// If we haven't seen a valid price yet, output NaN
|
|
if (double.IsNaN(lastValidPrice))
|
|
{
|
|
output[i] = double.NaN;
|
|
continue;
|
|
}
|
|
// Otherwise, use the last valid price
|
|
price = lastValidPrice;
|
|
}
|
|
else
|
|
{
|
|
lastValidPrice = price;
|
|
}
|
|
|
|
// Update circular buffer indices
|
|
pIdx = (pIdx + 1) & Mask63;
|
|
sIdx = (sIdx + 1) & Mask7;
|
|
count++;
|
|
|
|
priceBuffer[pIdx] = price;
|
|
|
|
if (count > 6)
|
|
{
|
|
// 1. Smooth Price
|
|
double smooth = (4.0 * priceBuffer[pIdx] +
|
|
3.0 * priceBuffer[(pIdx - 1) & Mask63] +
|
|
2.0 * priceBuffer[(pIdx - 2) & Mask63] +
|
|
priceBuffer[(pIdx - 3) & Mask63]) * 0.1;
|
|
smoothBuffer[sIdx] = smooth;
|
|
|
|
// 2. Detrender
|
|
double adj = (adjSlope * p_period) + adjIntercept;
|
|
|
|
double detrender = (c1 * smoothBuffer[sIdx] +
|
|
c2 * smoothBuffer[(sIdx - 2) & Mask7] -
|
|
c2 * smoothBuffer[(sIdx - 4) & Mask7] -
|
|
c1 * smoothBuffer[(sIdx - 6) & Mask7]) * adj;
|
|
detrenderBuffer[sIdx] = detrender;
|
|
|
|
// 3. In-Phase and Quadrature
|
|
double q1 = (c1 * detrender +
|
|
c2 * detrenderBuffer[(sIdx - 2) & Mask7] -
|
|
c2 * detrenderBuffer[(sIdx - 4) & Mask7] -
|
|
c1 * detrenderBuffer[(sIdx - 6) & Mask7]) * adj;
|
|
q1Buffer[sIdx] = q1;
|
|
|
|
double i1 = detrenderBuffer[(sIdx - 3) & Mask7];
|
|
i1Buffer[sIdx] = i1;
|
|
|
|
// 4. Advance phases
|
|
double jI = (c1 * i1 +
|
|
c2 * i1Buffer[(sIdx - 2) & Mask7] -
|
|
c2 * i1Buffer[(sIdx - 4) & Mask7] -
|
|
c1 * i1Buffer[(sIdx - 6) & Mask7]) * adj;
|
|
|
|
double jQ = (c1 * q1 +
|
|
c2 * q1Buffer[(sIdx - 2) & Mask7] -
|
|
c2 * q1Buffer[(sIdx - 4) & Mask7] -
|
|
c1 * q1Buffer[(sIdx - 6) & Mask7]) * adj;
|
|
|
|
// 5. Phasor addition
|
|
double i2_val = i1 - jQ;
|
|
double q2_val = q1 + jI;
|
|
|
|
i2 = 0.2 * i2_val + 0.8 * p_i2;
|
|
q2 = 0.2 * q2_val + 0.8 * p_q2;
|
|
|
|
// 6. Homodyne Discriminator
|
|
double re_val = (i2 * p_i2) + (q2 * p_q2);
|
|
double im_val = (i2 * p_q2) - (q2 * p_i2);
|
|
|
|
re = 0.2 * re_val + 0.8 * p_re;
|
|
im = 0.2 * im_val + 0.8 * p_im;
|
|
|
|
// 7. Calculate Period
|
|
double angle = Math.Atan2(im, re);
|
|
double newPeriod = Math.Abs(angle) > MinDeltaRadians
|
|
? TwoPi / Math.Abs(angle)
|
|
: p_period;
|
|
|
|
if (p_period > 0)
|
|
{
|
|
double cap = 1.5 * p_period;
|
|
double floor = 0.67 * p_period;
|
|
if (newPeriod > cap) newPeriod = cap;
|
|
if (newPeriod < floor) newPeriod = floor;
|
|
}
|
|
if (newPeriod < 6) newPeriod = 6;
|
|
if (newPeriod > 50) newPeriod = 50;
|
|
|
|
period = 0.2 * newPeriod + 0.8 * p_period;
|
|
smoothPeriod = 0.33 * period + 0.67 * p_smoothPeriod;
|
|
|
|
// 8. Instantaneous Trend
|
|
double safeSmooth = double.IsNaN(smoothPeriod) ? 0 : smoothPeriod;
|
|
int dcPeriods = (int)(safeSmooth + 0.5);
|
|
double sumPr = 0;
|
|
int prCount = 0;
|
|
|
|
for (int d = 0; d < dcPeriods; d++)
|
|
{
|
|
if (d < count)
|
|
{
|
|
sumPr += priceBuffer[(pIdx - d) & Mask63];
|
|
prCount++;
|
|
}
|
|
}
|
|
|
|
double it = prCount > 0 ? sumPr / prCount : price;
|
|
itBuffer[sIdx] = it;
|
|
|
|
// 9. Final Trendline
|
|
output[i] = count >= 12
|
|
? (4.0 * itBuffer[sIdx] +
|
|
3.0 * itBuffer[(sIdx - 1) & Mask7] +
|
|
2.0 * itBuffer[(sIdx - 2) & Mask7] +
|
|
itBuffer[(sIdx - 3) & Mask7]) * 0.1
|
|
: price;
|
|
|
|
// Update previous state
|
|
p_i2 = i2;
|
|
p_q2 = q2;
|
|
p_re = re;
|
|
p_im = im;
|
|
p_period = period;
|
|
p_smoothPeriod = smoothPeriod;
|
|
}
|
|
else
|
|
{
|
|
// Initialization - propagate NaN if no valid price yet
|
|
smoothBuffer[sIdx] = price;
|
|
detrenderBuffer[sIdx] = 0;
|
|
i1Buffer[sIdx] = 0;
|
|
q1Buffer[sIdx] = 0;
|
|
itBuffer[sIdx] = price;
|
|
output[i] = price; // May be NaN if no valid input yet
|
|
|
|
// Reset state variables
|
|
p_i2 = 0; p_q2 = 0; p_re = 0; p_im = 0;
|
|
p_period = 0; p_smoothPeriod = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|