mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-22 04:28:04 +00:00
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:
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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 ----------
|
||||
|
||||
Reference in New Issue
Block a user