mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-07-27 17:27:43 +00:00
229 lines
7.4 KiB
C#
229 lines
7.4 KiB
C#
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.Batch(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(TALib.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]);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void HtTrendmode_Correction_Recomputes()
|
|
{
|
|
var ind = new HtTrendmode();
|
|
var t0 = new DateTime(946_684_800_000_000_0L, DateTimeKind.Utc);
|
|
|
|
// Build state well past warmup
|
|
for (int i = 0; i < 100; i++)
|
|
{
|
|
ind.Update(new TValue(t0.AddMinutes(i),
|
|
100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 20.0)), isNew: true);
|
|
}
|
|
|
|
// Anchor bar
|
|
var anchorTime = t0.AddMinutes(100);
|
|
const double anchorPrice = 105.5;
|
|
ind.Update(new TValue(anchorTime, anchorPrice), isNew: true);
|
|
double anchorSmooth = ind.SmoothPeriod;
|
|
|
|
// Correction with a dramatically different price — SmoothPeriod must change
|
|
ind.Update(new TValue(anchorTime, anchorPrice * 10.0), isNew: false);
|
|
Assert.NotEqual(anchorSmooth, ind.SmoothPeriod);
|
|
|
|
// Correction back to original price — must exactly restore original SmoothPeriod
|
|
ind.Update(new TValue(anchorTime, anchorPrice), isNew: false);
|
|
Assert.Equal(anchorSmooth, ind.SmoothPeriod, 1e-9);
|
|
}
|
|
}
|