Add Vortex Indicator implementation and documentation

- Implemented Vortex Indicator in Vortex.cs, including calculation logic and event handling.
- Added detailed documentation for Vortex Indicator in Vortex.md, covering historical context, algorithm, outputs, and trading interpretation.
- Updated oscillators index to include TTM Wave indicator.
- Added TTM Wave documentation with algorithm and trading interpretation.
- Updated reversals index to include TTM Scalper Alert indicator.
- Added TTM Scalper Alert documentation with algorithm and trading strategy.
- Updated NDepend badges to reflect increased code metrics (classes, methods, lines of code, public types, comments, and complexity).
This commit is contained in:
Miha Kralj
2026-02-06 07:43:40 -08:00
parent 26280ce80b
commit 58f0812584
37 changed files with 4314 additions and 91 deletions
@@ -0,0 +1,79 @@
using TradingPlatform.BusinessLayer;
using QuanTAlib;
namespace QuanTAlib.Tests;
public class HtTrendmodeIndicatorTests
{
[Fact]
public void HtTrendmodeIndicator_Constructor_SetsDefaults()
{
var indicator = new HtTrendmodeIndicator();
Assert.Equal(SourceType.Close, indicator.SourceInput);
Assert.True(indicator.ShowColdValues);
Assert.Equal("HT_TRENDMODE - Hilbert Transform Trend Mode", indicator.Name);
Assert.True(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void HtTrendmodeIndicator_MinHistoryDepths_EqualsZero()
{
var indicator = new HtTrendmodeIndicator();
Assert.Equal(0, HtTrendmodeIndicator.MinHistoryDepths);
IWatchlistIndicator watchlistIndicator = indicator;
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
}
[Fact]
public void HtTrendmodeIndicator_Initialize_CreatesInternalIndicator()
{
var indicator = new HtTrendmodeIndicator();
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist (TrendMode)
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void HtTrendmodeIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new HtTrendmodeIndicator();
indicator.Initialize();
// Add historical data
var now = DateTime.UtcNow;
for (int i = 0; i < 50; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
// Process update for each bar to simulate history loading
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
}
// Line series should have a value
double trendMode = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(trendMode));
}
[Fact]
public void HtTrendmodeIndicator_ShortName_IsCorrect()
{
var indicator = new HtTrendmodeIndicator();
Assert.Equal("HT_TRENDMODE", indicator.ShortName);
}
[Fact]
public void HtTrendmodeIndicator_SourceCodeLink_IsValid()
{
var indicator = new HtTrendmodeIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.OrdinalIgnoreCase);
Assert.Contains("HtTrendmode.Quantower.cs", indicator.SourceCodeLink, StringComparison.OrdinalIgnoreCase);
}
}
@@ -0,0 +1,75 @@
using System.Drawing;
using System.Runtime.CompilerServices;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
[SkipLocalsInit]
public sealed class HtTrendmodeIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Data source", 10)]
public SourceType SourceInput { get; set; } = SourceType.Close;
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private HtTrendmode _indicator = null!;
private readonly LineSeries _trendModeSeries;
public static int MinHistoryDepths => 0;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => "HT_TRENDMODE";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/dynamics/ht_trendmode/HtTrendmode.Quantower.cs";
public HtTrendmodeIndicator()
{
OnBackGround = true;
SeparateWindow = true;
Name = "HT_TRENDMODE - Hilbert Transform Trend Mode";
Description = "Determines if market is trending (1) or cycling (0)";
_trendModeSeries = new LineSeries(name: "TrendMode", color: Color.Blue, width: 3, style: LineStyle.Solid);
AddLineSeries(_trendModeSeries);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnInit()
{
_indicator = new HtTrendmode();
base.OnInit();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnUpdate(UpdateArgs args)
{
double value = SourceInput switch
{
SourceType.Open => GetPrice(PriceType.Open),
SourceType.High => GetPrice(PriceType.High),
SourceType.Low => GetPrice(PriceType.Low),
SourceType.Close => GetPrice(PriceType.Close),
SourceType.HL2 => (GetPrice(PriceType.High) + GetPrice(PriceType.Low)) / 2,
SourceType.HLC3 => (GetPrice(PriceType.High) + GetPrice(PriceType.Low) + GetPrice(PriceType.Close)) / 3,
SourceType.OHLC4 => (GetPrice(PriceType.Open) + GetPrice(PriceType.High) + GetPrice(PriceType.Low) + GetPrice(PriceType.Close)) / 4,
SourceType.HLCC4 => (GetPrice(PriceType.High) + GetPrice(PriceType.Low) + 2 * GetPrice(PriceType.Close)) / 4,
_ => GetPrice(PriceType.Close)
};
bool isNew = args.IsNewBar();
var result = _indicator.Update(new TValue(Time(), value), isNew);
_trendModeSeries.SetValue(result.Value, _indicator.IsHot, ShowColdValues);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetPrice(PriceType priceType)
{
return HistoricalData[0, SeekOriginHistory.End][priceType];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private DateTime Time()
{
return HistoricalData[0, SeekOriginHistory.End].TimeLeft;
}
}
@@ -0,0 +1,337 @@
namespace QuanTAlib;
public class HtTrendmodeTests
{
[Fact]
public void HtTrendmode_BasicConstruction()
{
var indicator = new HtTrendmode();
Assert.Equal("HtTrendmode", indicator.Name);
Assert.Equal(63, indicator.WarmupPeriod); // TA-Lib lookback period
Assert.False(indicator.IsHot);
}
[Fact]
public void HtTrendmode_WarmupPeriod()
{
var indicator = new HtTrendmode();
// Feed warmup data - TA-Lib requires 63 bars for lookback
for (int i = 0; i < 70; i++)
{
_ = indicator.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + i));
if (i < 63)
{
Assert.False(indicator.IsHot, $"Should not be hot at bar {i}");
}
}
Assert.True(indicator.IsHot, "Should be hot after warmup period");
}
[Fact]
public void HtTrendmode_OutputsBinaryValues()
{
var indicator = new HtTrendmode();
// Use a mix of trending and cycling data
var rnd = new Random(42);
for (int i = 0; i < 100; i++)
{
double value = 100.0 + Math.Sin(i * 0.1) * 5 + rnd.NextDouble();
var result = indicator.Update(new TValue(DateTime.UtcNow.AddMinutes(i), value));
// After warmup, output should be 0 or 1
if (i >= 40)
{
Assert.True(result.Value == 0.0 || result.Value == 1.0,
$"TrendMode should be 0 or 1, got {result.Value} at bar {i}");
}
}
}
[Fact]
public void HtTrendmode_TrendModeProperty()
{
var indicator = new HtTrendmode();
// Feed data
for (int i = 0; i < 50; i++)
{
indicator.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + i * 0.5));
}
// TrendMode property should match output
int trendMode = indicator.TrendMode;
Assert.True(trendMode == 0 || trendMode == 1);
}
[Fact]
public void HtTrendmode_SmoothPeriodProperty()
{
var indicator = new HtTrendmode();
// Feed data
for (int i = 0; i < 50; i++)
{
indicator.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + Math.Sin(i * 0.2) * 10));
}
// SmoothPeriod should be in valid range
double smoothPeriod = indicator.SmoothPeriod;
Assert.True(smoothPeriod >= 6.0 && smoothPeriod <= 50.0,
$"SmoothPeriod {smoothPeriod} should be between 6 and 50");
}
[Fact]
public void HtTrendmode_InstPeriodProperty()
{
var indicator = new HtTrendmode();
// Feed data
for (int i = 0; i < 50; i++)
{
indicator.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + Math.Sin(i * 0.3) * 8));
}
// InstPeriod should be positive
double instPeriod = indicator.InstPeriod;
Assert.True(instPeriod > 0, $"InstPeriod {instPeriod} should be positive");
}
[Fact]
public void HtTrendmode_TrendingData_ShouldDetectTrend()
{
var indicator = new HtTrendmode();
// Strong trend: monotonically increasing
for (int i = 0; i < 100; i++)
{
indicator.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + i * 2.0));
}
// With strong trend, inst_period should be larger → trend mode likely
// (exact behavior depends on Hilbert Transform dynamics)
int trendMode = indicator.TrendMode;
Assert.True(trendMode == 0 || trendMode == 1, "Should output valid trend mode");
}
[Fact]
public void HtTrendmode_CyclicalData_ShouldDetectCycle()
{
var indicator = new HtTrendmode();
// Pure sinusoidal data (strong cycle)
for (int i = 0; i < 100; i++)
{
double value = 100.0 + Math.Sin(i * 0.4) * 10.0;
indicator.Update(new TValue(DateTime.UtcNow.AddMinutes(i), value));
}
// With cyclical data, smooth_period and inst_period should be closer
int trendMode = indicator.TrendMode;
Assert.True(trendMode == 0 || trendMode == 1, "Should output valid trend mode");
}
[Fact]
public void HtTrendmode_HandlesNaN()
{
var indicator = new HtTrendmode();
// Prime with valid data
for (int i = 0; i < 50; i++)
{
indicator.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + i));
}
// Feed NaN - should use last valid value
var resultNaN = indicator.Update(new TValue(DateTime.UtcNow.AddMinutes(50), double.NaN));
Assert.True(double.IsFinite(resultNaN.Value), "Should handle NaN gracefully");
}
[Fact]
public void HtTrendmode_HandlesInfinity()
{
var indicator = new HtTrendmode();
// Prime with valid data
for (int i = 0; i < 50; i++)
{
indicator.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + i));
}
// Feed Infinity - should use last valid value
var resultInf = indicator.Update(new TValue(DateTime.UtcNow.AddMinutes(50), double.PositiveInfinity));
Assert.True(double.IsFinite(resultInf.Value), "Should handle Infinity gracefully");
}
[Fact]
public void HtTrendmode_Reset()
{
var indicator = new HtTrendmode();
// Process enough data to be hot (warmup = 63)
for (int i = 0; i < 70; i++)
{
indicator.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + i));
}
Assert.True(indicator.IsHot, "Should be hot after warmup");
// Reset
indicator.Reset();
Assert.False(indicator.IsHot);
Assert.Equal(0, indicator.TrendMode);
}
[Fact]
public void HtTrendmode_BatchUpdate()
{
var indicator = new HtTrendmode();
var series = new TSeries();
for (int i = 0; i < 100; i++)
{
series.Add(DateTime.UtcNow.AddMinutes(i), 100.0 + Math.Sin(i * 0.2) * 10);
}
var result = indicator.Update(series);
Assert.Equal(100, result.Count);
// All values after warmup should be 0 or 1
for (int i = 40; i < result.Count; i++)
{
Assert.True(result.Values[i] == 0.0 || result.Values[i] == 1.0,
$"Batch result at {i} should be 0 or 1, got {result.Values[i]}");
}
}
[Fact]
public void HtTrendmode_StaticCalculate_SpanVersion()
{
double[] input = new double[100];
double[] output = new double[100];
for (int i = 0; i < input.Length; i++)
{
input[i] = 100.0 + Math.Sin(i * 0.15) * 8;
}
HtTrendmode.Calculate(input.AsSpan(), output.AsSpan());
// After warmup, all values should be 0 or 1
for (int i = 40; i < output.Length; i++)
{
Assert.True(output[i] == 0.0 || output[i] == 1.0,
$"Static Calculate at {i} should be 0 or 1, got {output[i]}");
}
}
[Fact]
public void HtTrendmode_StaticCalculate_TSeriesVersion()
{
var series = new TSeries();
for (int i = 0; i < 100; i++)
{
series.Add(DateTime.UtcNow.AddMinutes(i), 100.0 + Math.Sin(i * 0.25) * 12);
}
var result = HtTrendmode.Calculate(series);
Assert.Equal(100, result.Count);
}
[Fact]
public void HtTrendmode_BarCorrection_IsNewFalse()
{
var indicator = new HtTrendmode();
// Prime indicator
for (int i = 0; i < 50; i++)
{
indicator.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + i));
}
// Get baseline
_ = indicator.Update(new TValue(DateTime.UtcNow.AddMinutes(50), 150.0), isNew: true);
// Update same bar with different value
var corrected = indicator.Update(new TValue(DateTime.UtcNow.AddMinutes(50), 152.0), isNew: false);
// Should reflect the corrected value
Assert.True(corrected.Value == 0.0 || corrected.Value == 1.0);
}
[Fact]
public void HtTrendmode_StreamingVsBatch_Consistency()
{
var streamingIndicator = new HtTrendmode();
var batchIndicator = new HtTrendmode();
var series = new TSeries();
var streamingResults = new List<double>();
for (int i = 0; i < 100; i++)
{
double value = 100.0 + Math.Sin(i * 0.2) * 10 + Math.Cos(i * 0.3) * 5;
series.Add(DateTime.UtcNow.AddMinutes(i), value);
var result = streamingIndicator.Update(new TValue(DateTime.UtcNow.AddMinutes(i), value));
streamingResults.Add(result.Value);
}
var batchResult = batchIndicator.Update(series);
// Compare streaming vs batch
for (int i = 0; i < 100; i++)
{
Assert.Equal(streamingResults[i], batchResult.Values[i]);
}
}
[Fact]
public void HtTrendmode_Prime()
{
var indicator = new HtTrendmode();
// Prime with enough data to be hot (warmup = 63)
double[] primeData = new double[70];
for (int i = 0; i < primeData.Length; i++)
{
primeData[i] = 100.0 + i * 0.5;
}
indicator.Prime(primeData);
Assert.True(indicator.IsHot, "Should be hot after priming");
}
[Fact]
public void HtTrendmode_EmptySource()
{
var indicator = new HtTrendmode();
var emptySeries = new TSeries();
var result = indicator.Update(emptySeries);
Assert.Empty(result);
}
[Fact]
public void HtTrendmode_ConstantPrice_ShouldNotCrash()
{
var indicator = new HtTrendmode();
// Constant price (degenerate case)
for (int i = 0; i < 100; i++)
{
var result = indicator.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0));
Assert.True(double.IsFinite(result.Value), $"Result should be finite at bar {i}");
}
}
}
@@ -0,0 +1,200 @@
using TALib;
using QuanTAlib.Tests;
namespace QuanTAlib;
/// <summary>
/// Validation tests for HtTrendmode against TA-Lib reference implementation.
/// Note: TA-Lib's HT_TRENDMODE is the reference for this indicator.
/// </summary>
public sealed class HtTrendmodeValidationTests : IDisposable
{
private readonly ValidationTestData _data;
public HtTrendmodeValidationTests()
{
_data = new ValidationTestData();
}
public void Dispose()
{
_data.Dispose();
}
[Fact]
public void HtTrendmode_OutputsValidBinaryValues()
{
// Arrange
var indicator = new HtTrendmode();
var results = new List<double>();
var closeSpan = _data.GetCloseSpan();
var timestamps = _data.Timestamps.Span;
// Act - Process data
for (int i = 0; i < _data.Count; i++)
{
var result = indicator.Update(new TValue(timestamps[i], closeSpan[i]));
results.Add(result.Value);
}
// Assert - After warmup, all values should be 0 or 1
for (int i = 50; i < results.Count; i++)
{
double value = results[i];
Assert.True(value == 0.0 || value == 1.0,
$"TrendMode at index {i} should be 0 or 1, got {value}");
}
}
[Fact]
public void HtTrendmode_SmoothPeriod_InValidRange()
{
// Arrange
var indicator = new HtTrendmode();
// Act - Process with sinusoidal data
for (int i = 0; i < 200; i++)
{
double value = 100.0 + Math.Sin(i * 0.2) * 10.0 + Math.Sin(i * 0.05) * 5.0;
indicator.Update(new TValue(DateTime.UtcNow.AddMinutes(i), value));
}
// Assert - SmoothPeriod should be in valid range [6, 50]
double smoothPeriod = indicator.SmoothPeriod;
Assert.True(smoothPeriod >= 6.0 && smoothPeriod <= 50.0,
$"SmoothPeriod {smoothPeriod} should be between 6 and 50");
}
[Fact]
public void HtTrendmode_InstPeriod_Positive()
{
// Arrange
var indicator = new HtTrendmode();
// Act
for (int i = 0; i < 200; i++)
{
double value = 100.0 + Math.Sin(i * 0.15) * 8.0;
indicator.Update(new TValue(DateTime.UtcNow.AddMinutes(i), value));
}
// Assert
double instPeriod = indicator.InstPeriod;
Assert.True(instPeriod > 0, $"InstPeriod should be positive, got {instPeriod}");
}
[Fact]
public void HtTrendmode_StreamingVsBatch_Equal()
{
// Arrange
var streamingIndicator = new HtTrendmode();
var streamingResults = new List<double>();
var closeSpan = _data.GetCloseSpan();
var timestamps = _data.Timestamps.Span;
// Act - Streaming
var series = new TSeries();
for (int i = 0; i < _data.Count; i++)
{
series.Add(timestamps[i], closeSpan[i]);
var result = streamingIndicator.Update(new TValue(timestamps[i], closeSpan[i]));
streamingResults.Add(result.Value);
}
// Act - Batch
var batchResult = HtTrendmode.Calculate(series);
// Assert
Assert.Equal(streamingResults.Count, batchResult.Count);
for (int i = 0; i < streamingResults.Count; i++)
{
Assert.Equal(streamingResults[i], batchResult.Values[i]);
}
}
[Fact]
public void HtTrendmode_TrendModeLogic_TALibAlgorithm()
{
// Arrange - Our implementation now follows TA-Lib's Ehlers algorithm
var indicator = new HtTrendmode();
var closeSpan = _data.GetCloseSpan();
var timestamps = _data.Timestamps.Span;
// Act - Prime the indicator with enough data
for (int i = 0; i < 100; i++)
{
indicator.Update(new TValue(timestamps[i], closeSpan[i]));
}
// Assert - TA-Lib TrendMode: binary 0 or 1, using multi-criteria:
// 1. SineWave crossings reset daysInTrend
// 2. daysInTrend >= 0.5 * smoothPeriod → trending
// 3. Phase rate check (normal range → cycle mode)
// 4. Price-trendline deviation ≥1.5% → trend override
int trendMode = indicator.TrendMode;
Assert.True(trendMode == 0 || trendMode == 1, $"TrendMode should be 0 or 1, got {trendMode}");
// Verify DaysInTrend property works
Assert.True(indicator.DaysInTrend >= 0, "DaysInTrend should be non-negative");
}
/// <summary>
/// Tests TA-Lib validation. Our implementation now follows TA-Lib's Ehlers algorithm.
/// </summary>
[Fact]
public void MatchesTalib()
{
// Arrange
var indicator = new HtTrendmode();
var results = new List<double>();
var closeSpan = _data.GetCloseSpan();
var timestamps = _data.Timestamps.Span;
// Act - Process data
for (int i = 0; i < _data.Count; i++)
{
var result = indicator.Update(new TValue(timestamps[i], closeSpan[i]));
results.Add(result.Value);
}
// Get TA-Lib results
double[] inReal = closeSpan.ToArray();
int[] outInteger = new int[inReal.Length];
var retCode = Functions.HtTrendMode(inReal, 0..^0, outInteger, out var outRange);
Assert.Equal(Core.RetCode.Success, retCode);
// Compare after warmup
int lookback = Functions.HtTrendModeLookback();
double[] talibResults = outInteger.Select(x => (double)x).ToArray();
ValidationHelper.VerifyData(results, talibResults, outRange, lookback);
}
[Fact]
public void HtTrendmode_DeterministicOutput()
{
// Arrange
var indicator1 = new HtTrendmode();
var indicator2 = new HtTrendmode();
var closeSpan = _data.GetCloseSpan();
var timestamps = _data.Timestamps.Span;
// Act - Same data, same results
var results1 = new List<double>();
var results2 = new List<double>();
for (int i = 0; i < _data.Count; i++)
{
var r1 = indicator1.Update(new TValue(timestamps[i], closeSpan[i]));
var r2 = indicator2.Update(new TValue(timestamps[i], closeSpan[i]));
results1.Add(r1.Value);
results2.Add(r2.Value);
}
// Assert - Deterministic
for (int i = 0; i < results1.Count; i++)
{
Assert.Equal(results1[i], results2[i]);
}
}
}
+609
View File
@@ -0,0 +1,609 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// HT_TRENDMODE: Hilbert Transform Trend Mode - Determines if market is in trend or cycle mode.
/// </summary>
/// <remarks>
/// The Hilbert Transform Trend Mode, developed by John Ehlers and implemented following TA-Lib,
/// uses multiple criteria to determine whether the market is trending (1) or cycling (0).
///
/// Algorithm (from TA-Lib, based on Ehlers' original publication):
/// 1. Compute Hilbert Transform to get Sine/LeadSine indicators.
/// 2. Track days since last Sine/LeadSine crossing.
/// 3. If no crossing for half a dominant cycle period → trend mode.
/// 4. If phase change rate is "normal" (0.67× to 1.5× expected) → cycle mode.
/// 5. If price deviates ≥1.5% from trendline → trend mode override.
///
/// Properties:
/// - Returns binary output: 1 = trend mode, 0 = cycle mode.
/// - Trend mode indicates directional movement dominates.
/// - Cycle mode indicates mean-reverting/oscillating behavior dominates.
/// - Uses SineWave crossings as primary cycle timing.
///
/// Interpretation:
/// - Use trend-following strategies when TrendMode = 1.
/// - Use mean-reversion strategies when TrendMode = 0.
/// </remarks>
[SkipLocalsInit]
public sealed class HtTrendmode : AbstractBase
{
private const int LOOKBACK = 63; // TA-Lib lookback for HT_TRENDMODE
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 RAD2DEG = 180.0 / Math.PI;
private const double DEG2RAD = Math.PI / 180.0;
private const double CONST_DEG2RAD_BY_360 = 2.0 * Math.PI;
// 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 PrevDCPhase,
double I1ForOddPrev3, double I1ForEvenPrev3,
double I1ForOddPrev2, double I1ForEvenPrev2,
double PeriodWMASub, double PeriodWMASum, double TrailingWMAValue,
double Sine, double LeadSine, double PrevSine, double PrevLeadSine,
double Trendline, double ITrend1, double ITrend2, double ITrend3,
int TrailingWMAIdx, int HilbertIdx, int SmoothPriceIdx,
int DaysInTrend, double LastValidPrice, int Today, int TrendMode
)
{
public State() : this(
0, 0, 0, 0, 0.0, 0.0, 0, 0,
0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0, double.NaN, 0, 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;
/// <summary>
/// Gets the current trend mode: 1 = trending, 0 = cycling.
/// </summary>
public int TrendMode => _state.TrendMode;
/// <summary>
/// Gets the current smooth period from the Hilbert Transform.
/// </summary>
public double SmoothPeriod => _state.SmoothPeriod;
/// <summary>
/// Gets the current DC Phase.
/// </summary>
public double DCPhase => _state.DCPhase;
/// <summary>
/// Gets the current trendline value.
/// </summary>
public double Trendline => _state.Trendline;
/// <summary>
/// Gets days since last SineWave crossing.
/// </summary>
public int DaysInTrend => _state.DaysInTrend;
/// <summary>
/// Gets the instantaneous period (unsmoothed dominant cycle period).
/// </summary>
public double InstPeriod => _state.Period;
public override bool IsHot => _state.Today > LOOKBACK;
public HtTrendmode()
{
Name = "HtTrendmode";
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 HtTrendmode(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 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 initVal = _priceHistory[0];
s.PeriodWMASub = initVal;
s.PeriodWMASum = initVal;
initVal = _priceHistory[1];
s.PeriodWMASub += initVal;
s.PeriodWMASum += initVal * 2.0;
initVal = _priceHistory[2];
s.PeriodWMASub += initVal;
s.PeriodWMASum += initVal * 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;
// 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 Hilbert state
s.HilbertIdx = hilbertIdx;
s.I1ForOddPrev2 = i1ForOddPrev2;
s.I1ForEvenPrev2 = i1ForEvenPrev2;
// Calculate period from Re/Im
re = Math.FusedMultiplyAdd(0.2, (i2 * prevI2) + (q2 * prevQ2), 0.8 * re);
im = Math.FusedMultiplyAdd(0.2, (i2 * prevQ2) - (q2 * prevI2), 0.8 * im);
s.PrevQ2 = q2;
s.PrevI2 = i2;
s.Re = re;
s.Im = im;
double tempReal = period;
if (Math.Abs(im) > 1e-10 && Math.Abs(re) > 1e-10)
{
period = 360.0 / (Math.Atan(im / re) * RAD2DEG);
}
double tempReal2 = 1.5 * tempReal;
if (period > tempReal2)
{
period = tempReal2;
}
tempReal2 = 0.67 * tempReal;
if (period < tempReal2)
{
period = tempReal2;
}
if (period < 6)
{
period = 6;
}
else if (period > 50)
{
period = 50;
}
period = (0.2 * period) + (0.8 * tempReal);
s.Period = period;
s.SmoothPeriod = Math.FusedMultiplyAdd(0.33, period, 0.67 * s.SmoothPeriod);
// ==========================================
// Compute Dominant Cycle Phase (DCPhase)
// ==========================================
s.PrevDCPhase = s.DCPhase;
double dcPeriod = s.SmoothPeriod + 0.5;
int dcPeriodInt = (int)dcPeriod;
double realPart = 0.0;
double imagPart = 0.0;
// Sum over smoothPrice circular buffer
int idx = s.SmoothPriceIdx;
for (int i = 0; i < dcPeriodInt && i < SMOOTH_PRICE_SIZE; i++)
{
double angle = ((double)i * CONST_DEG2RAD_BY_360) / (double)dcPeriodInt;
double spVal = _smoothPrice[idx];
realPart += Math.Sin(angle) * spVal;
imagPart += Math.Cos(angle) * spVal;
if (idx == 0)
{
idx = SMOOTH_PRICE_SIZE - 1;
}
else
{
idx--;
}
}
double dcPhase;
double absImagPart = Math.Abs(imagPart);
if (absImagPart > 0.0)
{
dcPhase = Math.Atan(realPart / imagPart) * RAD2DEG;
}
else if (absImagPart <= 0.01)
{
dcPhase = s.DCPhase; // Keep previous
if (realPart < 0.0)
{
dcPhase -= 90.0;
}
else if (realPart > 0.0)
{
dcPhase += 90.0;
}
}
else
{
dcPhase = s.DCPhase;
}
dcPhase += 90.0;
// Compensate for one bar lag of the WMA
dcPhase += 360.0 / s.SmoothPeriod;
if (imagPart < 0.0)
{
dcPhase += 180.0;
}
if (dcPhase > 315.0)
{
dcPhase -= 360.0;
}
s.DCPhase = dcPhase;
// ==========================================
// Compute Sine and LeadSine
// ==========================================
s.PrevSine = s.Sine;
s.PrevLeadSine = s.LeadSine;
s.Sine = Math.Sin(dcPhase * DEG2RAD);
s.LeadSine = Math.Sin((dcPhase + 45) * DEG2RAD);
// ==========================================
// Compute Trendline (SMA over dominant cycle smoothed by WMA)
// ==========================================
dcPeriod = s.SmoothPeriod + 0.5;
dcPeriodInt = (int)dcPeriod;
// Sum price over dcPeriodInt bars
double sumPrice = 0.0;
int priceIdx2 = today;
for (int i = 0; i < dcPeriodInt && i < PRICE_HISTORY_SIZE && priceIdx2 >= 0; i++)
{
sumPrice += _priceHistory[priceIdx2 % PRICE_HISTORY_SIZE];
priceIdx2--;
}
double smaValue = (dcPeriodInt > 0) ? sumPrice / (double)dcPeriodInt : price;
// WMA smoothing of SMA: (4*current + 3*prev1 + 2*prev2 + prev3) / 10
double trendline = (4.0 * smaValue + 3.0 * s.ITrend1 + 2.0 * s.ITrend2 + s.ITrend3) / 10.0;
s.ITrend3 = s.ITrend2;
s.ITrend2 = s.ITrend1;
s.ITrend1 = smaValue;
s.Trendline = trendline;
// ==========================================
// Compute Trend Mode (TA-Lib algorithm)
// ==========================================
int trend = 1; // Assume trend by default
// Condition 1: Check for SineWave crossings
// If sine crosses leadsine, reset daysInTrend and set to cycle mode
if (((s.Sine > s.LeadSine) && (s.PrevSine <= s.PrevLeadSine)) ||
((s.Sine < s.LeadSine) && (s.PrevSine >= s.PrevLeadSine)))
{
s.DaysInTrend = 0;
trend = 0;
}
s.DaysInTrend++;
// Condition 2: Must be in trend for at least half the smooth period
if (s.DaysInTrend < (0.5 * s.SmoothPeriod))
{
trend = 0;
}
// Condition 3: Phase change rate check
// If phase change is "normal" (between 0.67× and 1.5× expected rate), it's cycle mode
double phaseChange = s.DCPhase - s.PrevDCPhase;
if (s.SmoothPeriod > 0.0)
{
double expectedPhaseChange = 360.0 / s.SmoothPeriod;
if ((phaseChange > (0.67 * expectedPhaseChange)) && (phaseChange < (1.5 * expectedPhaseChange)))
{
trend = 0;
}
}
// Condition 4: Price deviation from trendline
// If price deviates ≥1.5% from trendline, it's definitely trending
double smoothPriceNow = _smoothPrice[s.SmoothPriceIdx];
if (Math.Abs(trendline) > 1e-10 && Math.Abs((smoothPriceNow - trendline) / trendline) >= 0.015)
{
trend = 1;
}
s.TrendMode = trend;
// Advance smooth price index
s.SmoothPriceIdx = (s.SmoothPriceIdx + 1) % SMOOTH_PRICE_SIZE;
// Write back state
_state = s;
return s.TrendMode;
}
[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 HtTrendmode();
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 HtTrendmode();
return ht.Update(source);
}
}
+230
View File
@@ -0,0 +1,230 @@
# HT_TRENDMODE: Hilbert Transform Trend Mode
## Historical Context
The Hilbert Transform Trend Mode indicator was developed by **John Ehlers** as part of his cycle analysis toolkit. It uses the Hilbert Transform—a signal processing technique—to determine whether price action is dominated by **trending behavior** or **cyclical/mean-reverting behavior**.
This implementation follows **TA-Lib's Ehlers-faithful algorithm** from his February 2002 publication "The Instantaneous Trendline." The key insight: trend mode is detected via multiple criteria including SineWave crossings, phase rate analysis, and price-trendline deviation.
## Architecture & Physics
### The Trend/Cycle Duality
Markets alternate between two fundamental states:
| State | Characteristic | Strategy |
|-------|---------------|----------|
| **Trend Mode (1)** | Directional momentum | Trend-following |
| **Cycle Mode (0)** | Mean-reverting oscillation | Range-trading |
The TA-Lib algorithm uses **four criteria** to determine trend mode:
1. **SineWave Crossings**: Reset trend counter when Sine crosses LeadSine
2. **Days in Trend**: Must exceed half the smooth period
3. **Phase Rate Check**: Normal phase change rate indicates cycle mode
4. **Price-Trendline Deviation**: ≥1.5% deviation forces trend mode
## Mathematical Foundation
### 1. Hilbert Transform Components
The indicator uses the same Hilbert Transform core as HT_DCPERIOD:
```
smooth_price = (4×P₀ + 3×P₁ + 2×P₂ + P₃) / 10
detrender = FIR(smooth_price) × bandwidth
Q1 = FIR(detrender) × bandwidth
I1 = detrender[3]
// Phasor rotation
I2 = I1 - jQ
Q2 = Q1 + jI
```
### 2. Period and DC Phase
```
Re = 0.2×(I2×I2[1] + Q2×Q2[1]) + 0.8×Re[1]
Im = 0.2×(I2×Q2[1] - Q2×I2[1]) + 0.8×Im[1]
period = 360 / (atan(Im/Re) × RAD2DEG)
smooth_period = 0.33×period + 0.67×smooth_period[1]
// DC Phase calculation
realPart = Σ sin(i × 360/dcPeriod) × smoothPrice[i]
imagPart = Σ cos(i × 360/dcPeriod) × smoothPrice[i]
dcPhase = atan(realPart/imagPart) × RAD2DEG + 90 + lag_compensation
```
### 3. SineWave Indicators
```
sine = sin(dcPhase × DEG2RAD)
leadSine = sin((dcPhase + 45) × DEG2RAD)
```
### 4. Trendline Calculation
```
// SMA over dominant cycle period
sma = average(price, dcPeriodInt)
// WMA smoothing
trendline = (4×sma₀ + 3×sma₁ + 2×sma₂ + sma₃) / 10
```
### 5. Trend Mode Decision (TA-Lib Algorithm)
```
trend = 1 // Assume trend by default
// Criterion 1: SineWave crossing resets counter
if (sine crosses leadSine):
daysInTrend = 0
trend = 0
daysInTrend++
// Criterion 2: Must be trending for half a cycle
if (daysInTrend < 0.5 × smoothPeriod):
trend = 0
// Criterion 3: Normal phase rate → cycle mode
phaseChange = dcPhase - prevDcPhase
expectedChange = 360 / smoothPeriod
if (phaseChange > 0.67×expectedChange AND phaseChange < 1.5×expectedChange):
trend = 0
// Criterion 4: Price deviation override
if (abs((smoothPrice - trendline) / trendline) >= 0.015):
trend = 1
```
## Performance Profile
- **Complexity**: O(1) per update
- **Memory**: ~450 bytes state + circular buffers
- **Lookback**: 63 bars (TA-Lib compatible)
### Zero-Allocation Design
```csharp
[SkipLocalsInit]
public sealed class HtTrendmode : AbstractBase
{
// All state in value types
private State _state;
private State _p_state;
// Pre-allocated buffers for Hilbert Transform
private readonly double[] _circBuffer;
private readonly double[] _smoothPrice;
private readonly double[] _priceHistory;
}
```
### Bar Correction Pattern
Supports streaming updates with correction:
```csharp
// New bar
var result = indicator.Update(price, isNew: true);
// Same bar, corrected price
var corrected = indicator.Update(newPrice, isNew: false);
```
## Usage
### Streaming
```csharp
var indicator = new HtTrendmode();
foreach (var bar in bars)
{
var result = indicator.Update(bar.Close, isNew: true);
if (indicator.TrendMode == 1)
{
// Use trend-following strategy
ApplyMomentumStrategy();
}
else
{
// Use mean-reversion strategy
ApplyRangeStrategy();
}
}
```
### Batch
```csharp
var result = HtTrendmode.Calculate(closePrices);
```
### Properties
| Property | Type | Description |
|----------|------|-------------|
| `TrendMode` | int | Current mode: 1=trend, 0=cycle |
| `SmoothPeriod` | double | Smoothed dominant cycle period [6-50] |
| `InstPeriod` | double | Instantaneous (unsmoothed) period |
| `DCPhase` | double | Dominant cycle phase in degrees |
| `Trendline` | double | WMA-smoothed SMA over cycle period |
| `DaysInTrend` | int | Days since last SineWave crossing |
## Interpretation
### Signal Interpretation
| Value | Mode | Interpretation |
|-------|------|----------------|
| **1** | Trend | Price is trending; momentum strategies preferred |
| **0** | Cycle | Price is oscillating; mean-reversion preferred |
### Common Patterns
1. **Trend Confirmation**: When TrendMode flips from 0→1 after a breakout
2. **Cycle Entry**: When TrendMode flips from 1→0 at potential reversal zones
3. **Mode Persistence**: Long runs of 1s indicate strong trends
4. **Mode Oscillation**: Rapid flipping indicates choppy markets
### Using Auxiliary Properties
```csharp
// Access the trendline for support/resistance
double trend = indicator.Trendline;
// Check how long in current trend
int duration = indicator.DaysInTrend;
// Use phase for timing entries
double phase = indicator.DCPhase;
```
## Validation
### Cross-Library Comparison
| Library | Function | Notes |
|---------|----------|-------|
| TA-Lib | `HT_TRENDMODE` | Reference implementation (matched) |
| TradingView | Built-in | PineScript version (differs) |
### Common Pitfalls
1. **Lag**: Hilbert Transform has inherent lag (~32-63 bars for reliable signal)
2. **Whipsaws**: Mode can flip rapidly in transitional markets
3. **Warmup**: Requires 63+ bars before valid output
4. **Division Safety**: Use epsilon checks to avoid division by zero
## References
- Ehlers, J.F. "The Instantaneous Trendline" (February 2002)
- Ehlers, J.F. "MESA and Trading Market Cycles" (2002)
- Ehlers, J.F. "Rocket Science for Traders" (2001)
- [TA-Lib HT_TRENDMODE Source](https://github.com/TA-Lib/ta-lib/blob/main/src/ta_func/ta_HT_TRENDMODE.c)
+119 -40
View File
@@ -1,33 +1,20 @@
// The MIT License (MIT)
// © mihakralj
//@version=6
indicator("HT_TRENDMODE: Hilbert Transform Trend Mode", "HT_TRENDMODE", overlay=false)
indicator("HT_TRENDMODE: Hilbert Transform Trend Mode (TA-Lib)", "HT_TRENDMODE", overlay=false)
//@function Numerically stable atan2 implementation for quadrant-aware angle calculation
//@param y Y-coordinate (imaginary/quadrature component)
//@param x X-coordinate (real/in-phase component)
//@returns Angle in radians from -π to π
atan2(series float y, series float x) =>
if y == 0.0 and x == 0.0
runtime.error("atan2: Both y and x cannot be zero")
ay = math.abs(y)
ax = math.abs(x)
angle = 0.0
if ax > ay
angle := math.atan(ay / ax)
else
angle := (math.pi / 2.0) - math.atan(ax / ay)
if x < 0.0
angle := math.pi - angle
if y < 0.0
angle := -angle
angle
//@function Determines if market is in trend mode (1) or cycle mode (0)
//@function Determines if market is in trend mode (1) or cycle mode (0) using TA-Lib's Ehlers algorithm
//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/dynamics/ht_trendmode.md
//@param source Series to analyze for trend/cycle state
//@returns 1 for trend mode, 0 for cycle mode
ht_trendmode(series float source) =>
// Constants
var float A_CONST = 0.0962
var float B_CONST = 0.5769
var float RAD2DEG = 180.0 / math.pi
var float DEG2RAD = math.pi / 180.0
// State variables
var float smooth_price = 0.0
var float detrender = 0.0
var float i1 = 0.0
@@ -41,39 +28,131 @@ ht_trendmode(series float source) =>
var float period = 15.0
var float smooth_period = 15.0
var float dc_phase = 0.0
var float inst_period = 15.0
var float prev_dc_phase = 0.0
var float sine = 0.0
var float lead_sine = 0.0
var float prev_sine = 0.0
var float prev_lead_sine = 0.0
var float trendline = 0.0
var float i_trend_1 = 0.0
var float i_trend_2 = 0.0
var float i_trend_3 = 0.0
var int days_in_trend = 0
var int trend_mode = 0
float price = nz(source)
float bandwidth = 0.075 * smooth_period + 0.54
// Smoothed price (WMA-like)
smooth_price := (4.0 * price + 3.0 * nz(price[1]) + 2.0 * nz(price[2]) + nz(price[3])) / 10.0
detrender := (0.0962 * smooth_price + 0.5769 * nz(smooth_price[2]) - 0.5769 * nz(smooth_price[4]) - 0.0962 * nz(smooth_price[6])) * bandwidth
q1 := (0.0962 * detrender + 0.5769 * nz(detrender[2]) - 0.5769 * nz(detrender[4]) - 0.0962 * nz(detrender[6])) * bandwidth
// Hilbert Transform
detrender := (A_CONST * smooth_price + B_CONST * nz(smooth_price[2]) - B_CONST * nz(smooth_price[4]) - A_CONST * nz(smooth_price[6])) * bandwidth
q1 := (A_CONST * detrender + B_CONST * nz(detrender[2]) - B_CONST * nz(detrender[4]) - A_CONST * nz(detrender[6])) * bandwidth
i1 := nz(detrender[3])
ji := (0.0962 * i1 + 0.5769 * nz(i1[2]) - 0.5769 * nz(i1[4]) - 0.0962 * nz(i1[6])) * bandwidth
jq := (0.0962 * q1 + 0.5769 * nz(q1[2]) - 0.5769 * nz(q1[4]) - 0.0962 * nz(q1[6])) * bandwidth
ji := (A_CONST * i1 + B_CONST * nz(i1[2]) - B_CONST * nz(i1[4]) - A_CONST * nz(i1[6])) * bandwidth
jq := (A_CONST * q1 + B_CONST * nz(q1[2]) - B_CONST * nz(q1[4]) - A_CONST * nz(q1[6])) * bandwidth
// Phasor rotation
i2 := i1 - jq
q2 := q1 + ji
i2 := 0.2 * i2 + 0.8 * nz(i2[1])
q2 := 0.2 * q2 + 0.8 * nz(q2[1])
// Re/Im calculation
re := i2 * nz(i2[1]) + q2 * nz(q2[1])
im := i2 * nz(q2[1]) - q2 * nz(i2[1])
re := 0.2 * re + 0.8 * nz(re[1])
im := 0.2 * im + 0.8 * nz(im[1])
if im != 0.0 or re != 0.0
float angle = atan2(im, re)
if angle != 0.0
period := 2.0 * math.pi / angle
// Period calculation
float temp_period = period
if math.abs(im) > 1e-10 and math.abs(re) > 1e-10
period := 360.0 / (math.atan(im / re) * RAD2DEG)
// Clamp period to 1.5x and 0.67x of previous
if period > 1.5 * temp_period
period := 1.5 * temp_period
if period < 0.67 * temp_period
period := 0.67 * temp_period
period := math.max(6.0, math.min(50.0, period))
period := 0.2 * period + 0.8 * temp_period
smooth_period := 0.33 * period + 0.67 * smooth_period
if im != 0.0 or re != 0.0
dc_phase := atan2(im, re)
float delta_phase = dc_phase - nz(dc_phase[1])
if math.abs(delta_phase) < 0.1
delta_phase := nz(delta_phase[1])
if delta_phase != 0.0
float temp_period = 2.0 * math.pi / delta_phase
inst_period := 0.33 * temp_period + 0.67 * nz(inst_period[1])
trend_mode := inst_period > (1.5 * smooth_period) ? 1 : 0
// DC Phase calculation
prev_dc_phase := dc_phase
int dc_period_int = int(smooth_period + 0.5)
float real_part = 0.0
float imag_part = 0.0
for i = 0 to dc_period_int - 1
float angle = (float(i) * 360.0 / float(dc_period_int)) * DEG2RAD
real_part += math.sin(angle) * nz(smooth_price[i])
imag_part += math.cos(angle) * nz(smooth_price[i])
if math.abs(imag_part) > 0.0
dc_phase := math.atan(real_part / imag_part) * RAD2DEG
else if math.abs(imag_part) <= 0.01
if real_part < 0.0
dc_phase := dc_phase - 90.0
else if real_part > 0.0
dc_phase := dc_phase + 90.0
dc_phase += 90.0
dc_phase += 360.0 / smooth_period // Lag compensation
if imag_part < 0.0
dc_phase += 180.0
if dc_phase > 315.0
dc_phase -= 360.0
// Sine and LeadSine
prev_sine := sine
prev_lead_sine := lead_sine
sine := math.sin(dc_phase * DEG2RAD)
lead_sine := math.sin((dc_phase + 45.0) * DEG2RAD)
// Trendline calculation (SMA over cycle, then WMA smoothing)
float sum_price = 0.0
for i = 0 to dc_period_int - 1
sum_price += nz(source[i])
float sma_value = dc_period_int > 0 ? sum_price / float(dc_period_int) : price
trendline := (4.0 * sma_value + 3.0 * i_trend_1 + 2.0 * i_trend_2 + i_trend_3) / 10.0
i_trend_3 := i_trend_2
i_trend_2 := i_trend_1
i_trend_1 := sma_value
// ==========================================
// Trend Mode Decision (TA-Lib Algorithm)
// ==========================================
int trend = 1 // Assume trend by default
// Criterion 1: SineWave crossing resets counter
bool sine_crosses = ((sine > lead_sine) and (prev_sine <= prev_lead_sine)) or
((sine < lead_sine) and (prev_sine >= prev_lead_sine))
if sine_crosses
days_in_trend := 0
trend := 0
days_in_trend += 1
// Criterion 2: Must be trending for at least half the smooth period
if days_in_trend < int(0.5 * smooth_period)
trend := 0
// Criterion 3: Phase rate check (normal rate → cycle mode)
float phase_change = dc_phase - prev_dc_phase
if smooth_period > 0.0
float expected_change = 360.0 / smooth_period
if (phase_change > 0.67 * expected_change) and (phase_change < 1.5 * expected_change)
trend := 0
// Criterion 4: Price-trendline deviation override (≥1.5% → trend)
if math.abs(trendline) > 1e-10
if math.abs((smooth_price - trendline) / trendline) >= 0.015
trend := 1
trend_mode := trend
trend_mode
// ---------- Main loop ----------