mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-06 21:17:44 +00:00
a7b7207801
- Updated mathematical foundations and performance profiles where necessary to maintain clarity and coherence.
456 lines
16 KiB
C#
456 lines
16 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
using QuanTAlib;
|
|
|
|
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
|
|
{
|
|
private readonly RingBuffer _priceBuffer;
|
|
private readonly RingBuffer _smoothBuffer;
|
|
private readonly RingBuffer _detrenderBuffer;
|
|
private readonly RingBuffer _i1Buffer;
|
|
private readonly RingBuffer _q1Buffer;
|
|
private readonly RingBuffer _periodBuffer;
|
|
private readonly RingBuffer _smoothPeriodBuffer;
|
|
private readonly RingBuffer _itBuffer;
|
|
|
|
// 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 record struct State(double I2, double Q2, double Re, double Im, double LastValidValue);
|
|
private State _state;
|
|
private State _p_state;
|
|
|
|
public override bool IsHot => _priceBuffer.Count >= WarmupPeriod;
|
|
|
|
public Htit()
|
|
{
|
|
Name = "Htit";
|
|
WarmupPeriod = 12; // Based on logic: _priceBuffer.Count >= 12
|
|
_priceBuffer = new RingBuffer(50);
|
|
_smoothBuffer = new RingBuffer(7);
|
|
_detrenderBuffer = new RingBuffer(7);
|
|
_i1Buffer = new RingBuffer(7);
|
|
_q1Buffer = new RingBuffer(7);
|
|
_periodBuffer = new RingBuffer(2);
|
|
_smoothPeriodBuffer = new RingBuffer(2);
|
|
_itBuffer = new RingBuffer(4);
|
|
Init();
|
|
}
|
|
|
|
public Htit(ITValuePublisher source) : this()
|
|
{
|
|
source.Pub += (item) => Update(item);
|
|
}
|
|
|
|
private void Init()
|
|
{
|
|
_priceBuffer.Clear();
|
|
_smoothBuffer.Clear();
|
|
_detrenderBuffer.Clear();
|
|
_i1Buffer.Clear();
|
|
_q1Buffer.Clear();
|
|
_periodBuffer.Clear();
|
|
_smoothPeriodBuffer.Clear();
|
|
_itBuffer.Clear();
|
|
_state = default;
|
|
_p_state = default;
|
|
Last = default;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public override TValue Update(TValue input, bool isNew = true)
|
|
{
|
|
ManageState(isNew);
|
|
double price = ValidateInput(input.Value);
|
|
UpdateBuffer(_priceBuffer, price, isNew);
|
|
|
|
if (_priceBuffer.Count < 7)
|
|
return ProcessWarmup(input, price, isNew);
|
|
|
|
// 1. Smooth Price
|
|
double smooth = (4 * _priceBuffer[^1] + 3 * _priceBuffer[^2] + 2 * _priceBuffer[^3] + _priceBuffer[^4]) / 10.0;
|
|
UpdateBuffer(_smoothBuffer, smooth, isNew);
|
|
|
|
// 2. Detrender
|
|
double prevPeriod = _periodBuffer[isNew ? ^1 : ^2];
|
|
double adj = (adjSlope * prevPeriod) + adjIntercept;
|
|
double detrender = (c1 * _smoothBuffer[^1] + c2 * _smoothBuffer[^3] - c2 * _smoothBuffer[^5] - c1 * _smoothBuffer[^7]) * adj;
|
|
UpdateBuffer(_detrenderBuffer, 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];
|
|
UpdateBuffer(_q1Buffer, q1, isNew);
|
|
UpdateBuffer(_i1Buffer, 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 & 6. Homodyne Discriminator
|
|
ProcessPhasorAndHomodyne(i1, q1, jI, jQ);
|
|
|
|
// 7. Calculate Period
|
|
double period = CalculatePeriod(prevPeriod);
|
|
UpdateBuffer(_periodBuffer, period, isNew);
|
|
|
|
// Smooth dominant cycle period
|
|
double prevSmoothPeriod = _smoothPeriodBuffer[isNew ? ^1 : ^2];
|
|
double smoothPeriod = (0.33 * period) + (0.67 * prevSmoothPeriod);
|
|
UpdateBuffer(_smoothPeriodBuffer, smoothPeriod, isNew);
|
|
|
|
// 8. Instantaneous Trend
|
|
double it = CalculateInstantaneousTrend(smoothPeriod, price);
|
|
UpdateBuffer(_itBuffer, it, isNew);
|
|
|
|
// 9. Final Trendline
|
|
double trendline = _priceBuffer.Count >= 12
|
|
? (4 * _itBuffer[^1] + 3 * _itBuffer[^2] + 2 * _itBuffer[^3] + _itBuffer[^4]) / 10.0
|
|
: price;
|
|
|
|
Last = new TValue(input.Time, trendline);
|
|
PubEvent(Last);
|
|
return Last;
|
|
}
|
|
|
|
public override TSeries Update(TSeries source)
|
|
{
|
|
if (source.Count == 0) return [];
|
|
|
|
int len = source.Count;
|
|
var t = new List<long>(len);
|
|
var v = new List<double>(len);
|
|
CollectionsMarshal.SetCount(t, len);
|
|
CollectionsMarshal.SetCount(v, len);
|
|
|
|
var tSpan = CollectionsMarshal.AsSpan(t);
|
|
var vSpan = CollectionsMarshal.AsSpan(v);
|
|
|
|
Calculate(source.Values, vSpan);
|
|
source.Times.CopyTo(tSpan);
|
|
|
|
// Restore state by replaying last 50 bars
|
|
Init();
|
|
int startIndex = Math.Max(0, len - 50);
|
|
for (int i = startIndex; i < len; i++)
|
|
{
|
|
Update(new TValue(source.Times[i], source.Values[i]));
|
|
}
|
|
|
|
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
|
|
return new TSeries(t, v);
|
|
}
|
|
|
|
public override void Prime(ReadOnlySpan<double> source)
|
|
{
|
|
foreach (var value in source)
|
|
{
|
|
Update(new TValue(DateTime.MinValue, value));
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void ManageState(bool isNew)
|
|
{
|
|
if (isNew) _p_state = _state;
|
|
else _state = _p_state;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private double ValidateInput(double value)
|
|
{
|
|
double price = double.IsFinite(value) ? value : _state.LastValidValue;
|
|
_state.LastValidValue = price;
|
|
return price;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void UpdateBuffer(RingBuffer buffer, double val, bool isNew)
|
|
{
|
|
if (isNew) buffer.Add(val);
|
|
else buffer.UpdateNewest(val);
|
|
}
|
|
|
|
private TValue ProcessWarmup(TValue input, double price, bool isNew)
|
|
{
|
|
UpdateBuffer(_smoothBuffer, price, isNew);
|
|
UpdateBuffer(_detrenderBuffer, 0, isNew);
|
|
UpdateBuffer(_i1Buffer, 0, isNew);
|
|
UpdateBuffer(_q1Buffer, 0, isNew);
|
|
UpdateBuffer(_periodBuffer, 0, isNew);
|
|
UpdateBuffer(_smoothPeriodBuffer, 0, isNew);
|
|
UpdateBuffer(_itBuffer, price, isNew);
|
|
|
|
Last = new TValue(input.Time, price);
|
|
PubEvent(Last);
|
|
return Last;
|
|
}
|
|
|
|
private void ProcessPhasorAndHomodyne(double i1, double q1, double jI, double jQ)
|
|
{
|
|
// 5. Phasor addition
|
|
double i2_raw = i1 - jQ;
|
|
double q2_raw = q1 + jI;
|
|
|
|
// Smoothing
|
|
_state.I2 = (0.2 * i2_raw) + (0.8 * _p_state.I2);
|
|
_state.Q2 = (0.2 * q2_raw) + (0.8 * _p_state.Q2);
|
|
|
|
// 6. Homodyne Discriminator
|
|
double re_raw = (_state.I2 * _p_state.I2) + (_state.Q2 * _p_state.Q2);
|
|
double im_raw = (_state.I2 * _p_state.Q2) - (_state.Q2 * _p_state.I2);
|
|
|
|
// Smoothing
|
|
_state.Re = (0.2 * re_raw) + (0.8 * _p_state.Re);
|
|
_state.Im = (0.2 * im_raw) + (0.8 * _p_state.Im);
|
|
}
|
|
|
|
private double CalculatePeriod(double prevPeriod)
|
|
{
|
|
double period = 0;
|
|
if (Math.Abs(_state.Im) > 1e-9 && Math.Abs(_state.Re) > 1e-9)
|
|
{
|
|
period = 2 * Math.PI / Math.Atan(_state.Im / _state.Re);
|
|
}
|
|
|
|
// Adjust period to thresholds
|
|
if (prevPeriod > 0)
|
|
{
|
|
if (period > 1.5 * prevPeriod) period = 1.5 * prevPeriod;
|
|
if (period < 0.67 * prevPeriod) period = 0.67 * prevPeriod;
|
|
}
|
|
if (period < 6) period = 6;
|
|
if (period > 50) period = 50;
|
|
|
|
// Smooth the period
|
|
return (0.2 * period) + (0.8 * prevPeriod);
|
|
}
|
|
|
|
private double CalculateInstantaneousTrend(double smoothPeriod, double price)
|
|
{
|
|
int dcPeriods = (int)(double.IsNaN(smoothPeriod) ? 0 : smoothPeriod + 0.5);
|
|
double sumPr = 0;
|
|
int count = 0;
|
|
|
|
// Sum price over dcPeriods
|
|
for (int d = 0; d < dcPeriods; d++)
|
|
{
|
|
if (d < _priceBuffer.Count)
|
|
{
|
|
sumPr += _priceBuffer[^(d + 1)];
|
|
count++;
|
|
}
|
|
}
|
|
|
|
return count > 0 ? sumPr / count : price;
|
|
}
|
|
|
|
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");
|
|
|
|
int len = source.Length;
|
|
if (len == 0) return;
|
|
|
|
// Buffers
|
|
Span<double> priceBuffer = stackalloc double[50];
|
|
Span<double> smoothBuffer = stackalloc double[7];
|
|
Span<double> detrenderBuffer = stackalloc double[7];
|
|
Span<double> i1Buffer = stackalloc double[7];
|
|
Span<double> q1Buffer = stackalloc double[7];
|
|
Span<double> periodBuffer = stackalloc double[2];
|
|
Span<double> smoothPeriodBuffer = stackalloc double[2];
|
|
Span<double> itBuffer = stackalloc double[4];
|
|
|
|
int pIdx = 0, sIdx = 0, dIdx = 0, i1Idx = 0, q1Idx = 0, pdIdx = 0, sdIdx = 0, itIdx = 0;
|
|
int pCount = 0;
|
|
|
|
double i2 = 0, q2 = 0, re = 0, im = 0;
|
|
double p_i2 = 0, p_q2 = 0, p_re = 0, p_im = 0;
|
|
double lastValid = 0;
|
|
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
double price = source[i];
|
|
if (double.IsFinite(price)) lastValid = price; else price = lastValid;
|
|
|
|
// Add to price buffer
|
|
priceBuffer[pIdx] = price;
|
|
pCount++;
|
|
|
|
if (pCount < 7)
|
|
{
|
|
smoothBuffer[sIdx] = price;
|
|
detrenderBuffer[dIdx] = 0;
|
|
i1Buffer[i1Idx] = 0;
|
|
q1Buffer[q1Idx] = 0;
|
|
periodBuffer[pdIdx] = 0;
|
|
smoothPeriodBuffer[sdIdx] = 0;
|
|
itBuffer[itIdx] = price;
|
|
output[i] = price;
|
|
}
|
|
else
|
|
{
|
|
// 1. Smooth Price
|
|
double p0 = priceBuffer[pIdx];
|
|
double p1 = priceBuffer[(pIdx - 1 + 50) % 50];
|
|
double p2 = priceBuffer[(pIdx - 2 + 50) % 50];
|
|
double p3 = priceBuffer[(pIdx - 3 + 50) % 50];
|
|
double smooth = (4 * p0 + 3 * p1 + 2 * p2 + p3) / 10.0;
|
|
smoothBuffer[sIdx] = smooth;
|
|
|
|
// 2. Detrender
|
|
double prevPeriod = periodBuffer[(pdIdx - 1 + 2) % 2];
|
|
double adj = (adjSlope * prevPeriod) + adjIntercept;
|
|
|
|
double s0 = smoothBuffer[sIdx];
|
|
double s2 = smoothBuffer[(sIdx - 2 + 7) % 7];
|
|
double s4 = smoothBuffer[(sIdx - 4 + 7) % 7];
|
|
double s6 = smoothBuffer[(sIdx - 6 + 7) % 7];
|
|
|
|
double detrender = (c1 * s0 + c2 * s2 - c2 * s4 - c1 * s6) * adj;
|
|
detrenderBuffer[dIdx] = detrender;
|
|
|
|
// 3. In-Phase and Quadrature
|
|
double d0 = detrenderBuffer[dIdx];
|
|
double d2 = detrenderBuffer[(dIdx - 2 + 7) % 7];
|
|
double d4 = detrenderBuffer[(dIdx - 4 + 7) % 7];
|
|
double d6 = detrenderBuffer[(dIdx - 6 + 7) % 7];
|
|
|
|
double q1 = (c1 * d0 + c2 * d2 - c2 * d4 - c1 * d6) * adj;
|
|
double i1 = detrenderBuffer[(dIdx - 3 + 7) % 7];
|
|
|
|
q1Buffer[q1Idx] = q1;
|
|
i1Buffer[i1Idx] = i1;
|
|
|
|
// 4. Advance phases
|
|
double i1_0 = i1Buffer[i1Idx];
|
|
double i1_2 = i1Buffer[(i1Idx - 2 + 7) % 7];
|
|
double i1_4 = i1Buffer[(i1Idx - 4 + 7) % 7];
|
|
double i1_6 = i1Buffer[(i1Idx - 6 + 7) % 7];
|
|
double jI = (c1 * i1_0 + c2 * i1_2 - c2 * i1_4 - c1 * i1_6) * adj;
|
|
|
|
double q1_0 = q1Buffer[q1Idx];
|
|
double q1_2 = q1Buffer[(q1Idx - 2 + 7) % 7];
|
|
double q1_4 = q1Buffer[(q1Idx - 4 + 7) % 7];
|
|
double q1_6 = q1Buffer[(q1Idx - 6 + 7) % 7];
|
|
double jQ = (c1 * q1_0 + c2 * q1_2 - c2 * q1_4 - c1 * q1_6) * adj;
|
|
|
|
// 5. Phasor addition
|
|
double i2_raw = i1 - jQ;
|
|
double q2_raw = q1 + jI;
|
|
|
|
i2 = (0.2 * i2_raw) + (0.8 * p_i2);
|
|
q2 = (0.2 * q2_raw) + (0.8 * p_q2);
|
|
|
|
// 6. Homodyne Discriminator
|
|
double re_raw = (i2 * p_i2) + (q2 * p_q2);
|
|
double im_raw = (i2 * p_q2) - (q2 * p_i2);
|
|
|
|
re = (0.2 * re_raw) + (0.8 * p_re);
|
|
im = (0.2 * im_raw) + (0.8 * p_im);
|
|
|
|
// 7. Calculate Period
|
|
double period = 0;
|
|
if (Math.Abs(im) > 1e-9 && Math.Abs(re) > 1e-9)
|
|
{
|
|
period = 2 * Math.PI / Math.Atan(im / re);
|
|
}
|
|
|
|
if (prevPeriod > 0)
|
|
{
|
|
if (period > 1.5 * prevPeriod) period = 1.5 * prevPeriod;
|
|
if (period < 0.67 * prevPeriod) period = 0.67 * prevPeriod;
|
|
}
|
|
if (period < 6) period = 6;
|
|
if (period > 50) period = 50;
|
|
|
|
period = (0.2 * period) + (0.8 * prevPeriod);
|
|
periodBuffer[pdIdx] = period;
|
|
|
|
double prevSmoothPeriod = smoothPeriodBuffer[(sdIdx - 1 + 2) % 2];
|
|
double smoothPeriod = (0.33 * period) + (0.67 * prevSmoothPeriod);
|
|
smoothPeriodBuffer[sdIdx] = smoothPeriod;
|
|
|
|
// 8. Instantaneous Trend
|
|
int dcPeriods = (int)(double.IsNaN(smoothPeriod) ? 0 : smoothPeriod + 0.5);
|
|
double sumPr = 0;
|
|
int count = 0;
|
|
|
|
for (int d = 0; d < dcPeriods; d++)
|
|
{
|
|
if (d < pCount)
|
|
{
|
|
sumPr += priceBuffer[(pIdx - d + 50) % 50];
|
|
count++;
|
|
}
|
|
}
|
|
|
|
double it = count > 0 ? sumPr / count : price;
|
|
itBuffer[itIdx] = it;
|
|
|
|
// 9. Final Trendline
|
|
if (pCount >= 12)
|
|
{
|
|
double it0 = itBuffer[itIdx];
|
|
double it1 = itBuffer[(itIdx - 1 + 4) % 4];
|
|
double it2 = itBuffer[(itIdx - 2 + 4) % 4];
|
|
double it3 = itBuffer[(itIdx - 3 + 4) % 4];
|
|
output[i] = (4 * it0 + 3 * it1 + 2 * it2 + it3) / 10.0;
|
|
}
|
|
else
|
|
{
|
|
output[i] = price;
|
|
}
|
|
|
|
// Update state
|
|
p_i2 = i2;
|
|
p_q2 = q2;
|
|
p_re = re;
|
|
p_im = im;
|
|
}
|
|
|
|
// Advance indices
|
|
pIdx = (pIdx + 1) % 50;
|
|
sIdx = (sIdx + 1) % 7;
|
|
dIdx = (dIdx + 1) % 7;
|
|
i1Idx = (i1Idx + 1) % 7;
|
|
q1Idx = (q1Idx + 1) % 7;
|
|
pdIdx = (pdIdx + 1) % 2;
|
|
sdIdx = (sdIdx + 1) % 2;
|
|
itIdx = (itIdx + 1) % 4;
|
|
}
|
|
}
|
|
|
|
public override void Reset()
|
|
{
|
|
Init();
|
|
}
|
|
}
|