mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-07-30 10:37:44 +00:00
060649192f
- Remove 'C# Implementation Considerations' sections from 34 indicator .md files - Delete 29 temp PowerShell scripts (_fix_mojibake.ps1, _hex_scan.ps1, etc.) - Move test files into tests/ subdirectories for consistent project structure - Add trader-focused bullet points to indicator documentation
458 lines
14 KiB
C#
458 lines
14 KiB
C#
// Ha Unit Tests
|
|
|
|
using Xunit;
|
|
|
|
namespace QuanTAlib.Tests;
|
|
|
|
public class HaTests
|
|
{
|
|
private readonly GBM _gbm;
|
|
private const double Tolerance = 1e-10;
|
|
|
|
public HaTests()
|
|
{
|
|
_gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
|
|
}
|
|
|
|
private TBarSeries GenerateBars(int count)
|
|
{
|
|
_gbm.Reset(DateTime.UtcNow.Ticks);
|
|
return _gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
|
}
|
|
|
|
#region Constructor Tests
|
|
|
|
[Fact]
|
|
public void Constructor_DefaultParameters_SetsCorrectValues()
|
|
{
|
|
var indicator = new Ha();
|
|
Assert.Equal("Ha", indicator.Name);
|
|
Assert.Equal(1, indicator.WarmupPeriod);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_WithSource_SubscribesToEvents()
|
|
{
|
|
var source = new TSeries();
|
|
var indicator = new Ha(source);
|
|
source.Add(new TValue(DateTime.UtcNow, 100.0));
|
|
Assert.NotEqual(default, indicator.Last);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Basic Calculation Tests
|
|
|
|
[Fact]
|
|
public void Update_FirstBar_HaCloseIsOHLC4()
|
|
{
|
|
var indicator = new Ha();
|
|
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000);
|
|
var result = indicator.UpdateBar(bar);
|
|
// HA Close = (100 + 110 + 90 + 105) / 4 = 101.25
|
|
Assert.Equal(101.25, result.Close, Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_FirstBar_HaOpenIsMidpointOC()
|
|
{
|
|
var indicator = new Ha();
|
|
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000);
|
|
var result = indicator.UpdateBar(bar);
|
|
// HA Open on first bar = (O + C) / 2 = (100 + 105) / 2 = 102.5
|
|
Assert.Equal(102.5, result.Open, Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_FirstBar_HaHighIsMaxOfHOC()
|
|
{
|
|
var indicator = new Ha();
|
|
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000);
|
|
var result = indicator.UpdateBar(bar);
|
|
// HA High = max(110, 102.5, 101.25) = 110
|
|
Assert.Equal(110, result.High, Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_FirstBar_HaLowIsMinOfLOC()
|
|
{
|
|
var indicator = new Ha();
|
|
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000);
|
|
var result = indicator.UpdateBar(bar);
|
|
// HA Low = min(90, 102.5, 101.25) = 90
|
|
Assert.Equal(90, result.Low, Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_SecondBar_HaOpenIsRecursive()
|
|
{
|
|
var indicator = new Ha();
|
|
var time = DateTime.UtcNow;
|
|
|
|
// First bar: O=100, H=110, L=90, C=105
|
|
// HA_Open1 = (100+105)/2 = 102.5, HA_Close1 = 101.25
|
|
indicator.UpdateBar(new TBar(time, 100, 110, 90, 105, 1000));
|
|
|
|
// Second bar: O=105, H=115, L=95, C=110
|
|
// HA_Open2 = (prevHaOpen + prevHaClose) / 2 = (102.5 + 101.25) / 2 = 101.875
|
|
var result = indicator.UpdateBar(new TBar(time.AddMinutes(1), 105, 115, 95, 110, 1000));
|
|
Assert.Equal(101.875, result.Open, Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_SecondBar_HaCloseIsOHLC4()
|
|
{
|
|
var indicator = new Ha();
|
|
var time = DateTime.UtcNow;
|
|
|
|
indicator.UpdateBar(new TBar(time, 100, 110, 90, 105, 1000));
|
|
|
|
var result = indicator.UpdateBar(new TBar(time.AddMinutes(1), 105, 115, 95, 110, 1000));
|
|
// HA Close = (105 + 115 + 95 + 110) / 4 = 106.25
|
|
Assert.Equal(106.25, result.Close, Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_VolumePassthrough()
|
|
{
|
|
var indicator = new Ha();
|
|
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1234.5);
|
|
var result = indicator.UpdateBar(bar);
|
|
Assert.Equal(1234.5, result.Volume, Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_TimePassthrough()
|
|
{
|
|
var indicator = new Ha();
|
|
var time = DateTime.UtcNow;
|
|
var bar = new TBar(time, 100, 110, 90, 105, 1000);
|
|
var result = indicator.UpdateBar(bar);
|
|
Assert.Equal(time.Ticks, result.Time);
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_HaHighAlwaysGEHaOpenAndHaClose()
|
|
{
|
|
var indicator = new Ha();
|
|
var bars = GenerateBars(100);
|
|
|
|
for (int i = 0; i < bars.Count; i++)
|
|
{
|
|
var ha = indicator.UpdateBar(bars[i], isNew: true);
|
|
Assert.True(ha.High >= ha.Open, $"Bar {i}: High {ha.High} < Open {ha.Open}");
|
|
Assert.True(ha.High >= ha.Close, $"Bar {i}: High {ha.High} < Close {ha.Close}");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_HaLowAlwaysLEHaOpenAndHaClose()
|
|
{
|
|
var indicator = new Ha();
|
|
var bars = GenerateBars(100);
|
|
|
|
for (int i = 0; i < bars.Count; i++)
|
|
{
|
|
var ha = indicator.UpdateBar(bars[i], isNew: true);
|
|
Assert.True(ha.Low <= ha.Open, $"Bar {i}: Low {ha.Low} > Open {ha.Open}");
|
|
Assert.True(ha.Low <= ha.Close, $"Bar {i}: Low {ha.Low} > Close {ha.Close}");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_LastProperty_ReturnsHaClose()
|
|
{
|
|
var indicator = new Ha();
|
|
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000);
|
|
indicator.UpdateBar(bar);
|
|
// Last.Value should equal HA Close
|
|
Assert.Equal(101.25, indicator.Last.Value, Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_LastBarProperty_ReturnsFullHaBar()
|
|
{
|
|
var indicator = new Ha();
|
|
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000);
|
|
var result = indicator.UpdateBar(bar);
|
|
Assert.Equal(result, indicator.LastBar);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region State and Bar Correction Tests
|
|
|
|
[Fact]
|
|
public void IsHot_AfterFirstBar_ReturnsTrue()
|
|
{
|
|
var indicator = new Ha();
|
|
Assert.False(indicator.IsHot);
|
|
indicator.UpdateBar(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
|
|
Assert.True(indicator.IsHot);
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_IsNewFalse_RestoresPreviousState()
|
|
{
|
|
var indicator = new Ha();
|
|
var time = DateTime.UtcNow;
|
|
|
|
// First bar
|
|
indicator.UpdateBar(new TBar(time, 100, 110, 90, 105, 1000), isNew: true);
|
|
|
|
// Second bar (new)
|
|
indicator.UpdateBar(new TBar(time.AddMinutes(1), 105, 115, 95, 110, 1000), isNew: true);
|
|
|
|
// Correction on second bar
|
|
var corrected = indicator.UpdateBar(new TBar(time.AddMinutes(1), 106, 116, 96, 111, 1000), isNew: false);
|
|
|
|
// Verify the HA Open is computed from first bar's HA values, not second bar's
|
|
// After first bar: prevHaOpen=102.5, prevHaClose=101.25
|
|
// Corrected HA_Open = (102.5 + 101.25)/2 = 101.875
|
|
Assert.Equal(101.875, corrected.Open, Tolerance);
|
|
// Corrected HA_Close = (106+116+96+111)/4 = 107.25
|
|
Assert.Equal(107.25, corrected.Close, Tolerance);
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_MultipleIsNewFalse_ProducesIdempotentResults()
|
|
{
|
|
var indicator = new Ha();
|
|
var time = DateTime.UtcNow;
|
|
|
|
indicator.UpdateBar(new TBar(time, 100, 110, 90, 105, 1000), isNew: true);
|
|
|
|
var bar = new TBar(time.AddMinutes(1), 105, 115, 95, 110, 1000);
|
|
var result1 = indicator.UpdateBar(bar, isNew: false);
|
|
var result2 = indicator.UpdateBar(bar, isNew: false);
|
|
var result3 = indicator.UpdateBar(bar, isNew: false);
|
|
|
|
Assert.Equal(result1.Open, result2.Open, Tolerance);
|
|
Assert.Equal(result1.Close, result2.Close, Tolerance);
|
|
Assert.Equal(result1.High, result2.High, Tolerance);
|
|
Assert.Equal(result1.Low, result2.Low, Tolerance);
|
|
Assert.Equal(result2, result3);
|
|
}
|
|
|
|
[Fact]
|
|
public void Reset_ClearsState()
|
|
{
|
|
var indicator = new Ha();
|
|
indicator.UpdateBar(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
|
|
Assert.True(indicator.IsHot);
|
|
|
|
indicator.Reset();
|
|
Assert.False(indicator.IsHot);
|
|
Assert.Equal(default, indicator.Last);
|
|
Assert.Equal(default, indicator.LastBar);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region NaN/Infinity Robustness Tests
|
|
|
|
[Fact]
|
|
public void Update_NaN_UsesLastValidValue()
|
|
{
|
|
var indicator = new Ha();
|
|
var time = DateTime.UtcNow;
|
|
|
|
// Valid bar first
|
|
indicator.UpdateBar(new TBar(time, 100, 110, 90, 105, 1000), isNew: true);
|
|
_ = indicator.LastBar;
|
|
|
|
// NaN bar — should substitute last valid values
|
|
var nanBar = new TBar(time.AddMinutes(1), double.NaN, double.NaN, double.NaN, double.NaN, 1000);
|
|
var result = indicator.UpdateBar(nanBar, isNew: true);
|
|
Assert.True(double.IsFinite(result.Open));
|
|
Assert.True(double.IsFinite(result.High));
|
|
Assert.True(double.IsFinite(result.Low));
|
|
Assert.True(double.IsFinite(result.Close));
|
|
}
|
|
|
|
[Fact]
|
|
public void Update_Infinity_UsesLastValidValue()
|
|
{
|
|
var indicator = new Ha();
|
|
var time = DateTime.UtcNow;
|
|
|
|
indicator.UpdateBar(new TBar(time, 100, 110, 90, 105, 1000), isNew: true);
|
|
|
|
var infBar = new TBar(time.AddMinutes(1), double.PositiveInfinity, double.NegativeInfinity, double.NaN, double.PositiveInfinity, 1000);
|
|
var result = indicator.UpdateBar(infBar, isNew: true);
|
|
Assert.True(double.IsFinite(result.Open));
|
|
Assert.True(double.IsFinite(result.High));
|
|
Assert.True(double.IsFinite(result.Low));
|
|
Assert.True(double.IsFinite(result.Close));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Consistency Tests (All Modes)
|
|
|
|
[Fact]
|
|
public void AllModes_StreamingAndBatch_ProduceConsistentResults()
|
|
{
|
|
var bars = GenerateBars(100);
|
|
|
|
// Mode 1: Streaming
|
|
var streaming = new Ha();
|
|
TBar[] streamingResults = new TBar[bars.Count];
|
|
for (int i = 0; i < bars.Count; i++)
|
|
{
|
|
streamingResults[i] = streaming.UpdateBar(bars[i], isNew: true);
|
|
}
|
|
|
|
// Mode 2: Batch (TBarSeries)
|
|
var batchResult = Ha.Batch(bars);
|
|
|
|
// Mode 3: Span batch
|
|
double[] haOpenOut = new double[bars.Count];
|
|
double[] haHighOut = new double[bars.Count];
|
|
double[] haLowOut = new double[bars.Count];
|
|
double[] haCloseOut = new double[bars.Count];
|
|
Ha.Batch(bars.OpenValues, bars.HighValues, bars.LowValues, bars.CloseValues,
|
|
haOpenOut, haHighOut, haLowOut, haCloseOut);
|
|
|
|
for (int i = 0; i < bars.Count; i++)
|
|
{
|
|
Assert.Equal(streamingResults[i].Open, batchResult[i].Open, Tolerance);
|
|
Assert.Equal(streamingResults[i].High, batchResult[i].High, Tolerance);
|
|
Assert.Equal(streamingResults[i].Low, batchResult[i].Low, Tolerance);
|
|
Assert.Equal(streamingResults[i].Close, batchResult[i].Close, Tolerance);
|
|
|
|
Assert.Equal(streamingResults[i].Open, haOpenOut[i], Tolerance);
|
|
Assert.Equal(streamingResults[i].High, haHighOut[i], Tolerance);
|
|
Assert.Equal(streamingResults[i].Low, haLowOut[i], Tolerance);
|
|
Assert.Equal(streamingResults[i].Close, haCloseOut[i], Tolerance);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void AllBars_HaCloseMatchesOHLC4()
|
|
{
|
|
var bars = GenerateBars(50);
|
|
var indicator = new Ha();
|
|
|
|
for (int i = 0; i < bars.Count; i++)
|
|
{
|
|
var result = indicator.UpdateBar(bars[i], isNew: true);
|
|
Assert.Equal(bars[i].OHLC4, result.Close, Tolerance);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Batch Validation Tests
|
|
|
|
[Fact]
|
|
public void Batch_MismatchedLengths_ThrowsArgumentException()
|
|
{
|
|
double[] open = new double[10];
|
|
double[] high = new double[10];
|
|
double[] low = new double[5]; // mismatched
|
|
double[] close = new double[10];
|
|
double[] ho = new double[10], hh = new double[10], hl = new double[10], hc = new double[10];
|
|
|
|
var ex = Assert.Throws<ArgumentException>(() => Ha.Batch(open, high, low, close, ho, hh, hl, hc));
|
|
Assert.Equal("high", ex.ParamName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Batch_OutputTooShort_ThrowsArgumentException()
|
|
{
|
|
double[] open = new double[10];
|
|
double[] high = new double[10];
|
|
double[] low = new double[10];
|
|
double[] close = new double[10];
|
|
double[] ho = new double[5]; // too short
|
|
double[] hh = new double[10], hl = new double[10], hc = new double[10];
|
|
|
|
var ex = Assert.Throws<ArgumentException>(() => Ha.Batch(open, high, low, close, ho, hh, hl, hc));
|
|
Assert.Equal("haOpenOut", ex.ParamName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Batch_EmptyInput_NoOutput()
|
|
{
|
|
var bars = new TBarSeries();
|
|
var result = Ha.Batch(bars);
|
|
Assert.Empty(result);
|
|
}
|
|
|
|
[Fact]
|
|
public void Batch_LargeDataset_NoStackOverflow()
|
|
{
|
|
var bars = GenerateBars(10_000);
|
|
var result = Ha.Batch(bars);
|
|
Assert.Equal(bars.Count, result.Count);
|
|
Assert.True(double.IsFinite(result[^1].Close));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region HA-Specific Property Tests
|
|
|
|
[Fact]
|
|
public void ConstantInput_ConvergesToConstant()
|
|
{
|
|
var indicator = new Ha();
|
|
var time = DateTime.UtcNow;
|
|
|
|
// Feed constant bars: O=100, H=100, L=100, C=100
|
|
for (int i = 0; i < 20; i++)
|
|
{
|
|
_ = indicator.UpdateBar(new TBar(time.AddMinutes(i), 100, 100, 100, 100, 1000), isNew: true);
|
|
}
|
|
|
|
var last = indicator.LastBar;
|
|
// After many constant bars, all HA values should converge to 100
|
|
Assert.Equal(100.0, last.Open, 1e-6);
|
|
Assert.Equal(100.0, last.High, 1e-6);
|
|
Assert.Equal(100.0, last.Low, 1e-6);
|
|
Assert.Equal(100.0, last.Close, 1e-6);
|
|
}
|
|
|
|
[Fact]
|
|
public void HaHighGERealHigh_WhenBodyExceedsHigh()
|
|
{
|
|
// This tests the clamping: HA High is at least as large as HA Open and HA Close
|
|
var indicator = new Ha();
|
|
var bars = GenerateBars(100);
|
|
|
|
for (int i = 0; i < bars.Count; i++)
|
|
{
|
|
var ha = indicator.UpdateBar(bars[i], isNew: true);
|
|
// HA High should be >= real High OR >= haOpen/haClose
|
|
Assert.True(ha.High >= ha.Open);
|
|
Assert.True(ha.High >= ha.Close);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Event Chaining Tests
|
|
|
|
[Fact]
|
|
public void Pub_EventFires_OnUpdate()
|
|
{
|
|
var indicator = new Ha();
|
|
bool fired = false;
|
|
indicator.Pub += (object? sender, in TValueEventArgs args) => fired = true;
|
|
|
|
indicator.UpdateBar(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
|
|
Assert.True(fired);
|
|
}
|
|
|
|
[Fact]
|
|
public void Calculate_Static_ReturnsResultsAndIndicator()
|
|
{
|
|
var bars = GenerateBars(50);
|
|
var (results, ind) = Ha.Calculate(bars);
|
|
Assert.Equal(bars.Count, results.Count);
|
|
Assert.True(ind.IsHot);
|
|
}
|
|
|
|
#endregion
|
|
}
|