mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-23 13:08:04 +00:00
docs: remove C# Implementation Considerations sections, clean up temp scripts, reorganize test files
- 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
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class HaIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void HaIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new HaIndicator();
|
||||
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("HA - Heikin-Ashi", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HaIndicator_ShortName_IsHa()
|
||||
{
|
||||
var indicator = new HaIndicator();
|
||||
Assert.Equal("HA", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HaIndicator_MinHistoryDepths_EqualsOne()
|
||||
{
|
||||
var indicator = new HaIndicator();
|
||||
|
||||
Assert.Equal(1, HaIndicator.MinHistoryDepths);
|
||||
Assert.Equal(1, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HaIndicator_Initialize_CreatesFourLineSeries()
|
||||
{
|
||||
var indicator = new HaIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Equal(4, indicator.LinesSeries.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HaIndicator_ProcessUpdate_HistoricalBar_ComputesValues()
|
||||
{
|
||||
var indicator = new HaIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 1, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// All 4 series should have finite values
|
||||
for (int s = 0; s < 4; s++)
|
||||
{
|
||||
double val = indicator.LinesSeries[s].GetValue(0);
|
||||
Assert.True(double.IsFinite(val), $"LineSeries[{s}] should be finite");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HaIndicator_ProcessUpdate_NewBar_ComputesValues()
|
||||
{
|
||||
var indicator = new HaIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i, 1000);
|
||||
}
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(10), 110, 115, 105, 112, 1500);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
Assert.Equal(2, indicator.LinesSeries[1].Count);
|
||||
Assert.Equal(2, indicator.LinesSeries[2].Count);
|
||||
Assert.Equal(2, indicator.LinesSeries[3].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HaIndicator_ShowColdValues_CanBeToggled()
|
||||
{
|
||||
var indicator = new HaIndicator();
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = true;
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HaIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new HaIndicator();
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Ha.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HaIndicator_ComputesCorrectValues()
|
||||
{
|
||||
var indicator = new HaIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// First bar: O=100, H=110, L=90, C=105
|
||||
// HA_Close = (100+110+90+105)/4 = 101.25
|
||||
// HA_Open = (100+105)/2 = 102.5 (seed)
|
||||
// HA_High = max(110, 102.5, 101.25) = 110
|
||||
// HA_Low = min(90, 102.5, 101.25) = 90
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
double haOpen = indicator.LinesSeries[0].GetValue(0);
|
||||
double haHigh = indicator.LinesSeries[1].GetValue(0);
|
||||
double haLow = indicator.LinesSeries[2].GetValue(0);
|
||||
double haClose = indicator.LinesSeries[3].GetValue(0);
|
||||
|
||||
Assert.Equal(102.5, haOpen, 10);
|
||||
Assert.Equal(110.0, haHigh, 10);
|
||||
Assert.Equal(90.0, haLow, 10);
|
||||
Assert.Equal(101.25, haClose, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HaIndicator_IsHotImmediately()
|
||||
{
|
||||
var indicator = new HaIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// All 4 series should have finite values (IsHot after first bar)
|
||||
for (int s = 0; s < 4; s++)
|
||||
{
|
||||
double val = indicator.LinesSeries[s].GetValue(0);
|
||||
Assert.True(double.IsFinite(val), $"LineSeries[{s}] should be finite after one bar");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HaIndicator_HighAlwaysAboveOrEqualLow()
|
||||
{
|
||||
var indicator = new HaIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double basePrice = 100 + (i * 2);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 1, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double haHigh = indicator.LinesSeries[1].GetValue(0);
|
||||
double haLow = indicator.LinesSeries[2].GetValue(0);
|
||||
|
||||
Assert.True(haHigh >= haLow, "HA High must be >= HA Low");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,457 @@
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
// Ha Validation Tests
|
||||
// Cross-validates Heikin-Ashi against Skender.Stock.Indicators GetHeikinAshi()
|
||||
// plus self-consistency tests for batch/streaming/span equivalence.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using Skender.Stock.Indicators;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation for Ha (Heikin-Ashi) indicator.
|
||||
/// Cross-validates all 4 OHLC channels against Skender GetHeikinAshi(),
|
||||
/// plus self-consistency (batch == streaming == span), constant convergence,
|
||||
/// formula verification, and bar correction.
|
||||
/// </summary>
|
||||
public sealed class HaValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _data = new();
|
||||
private readonly ITestOutputHelper _output;
|
||||
private readonly GBM _gbm;
|
||||
private bool _disposed;
|
||||
|
||||
private const double SelfTolerance = 1e-10;
|
||||
private const int DataSize = 5000;
|
||||
|
||||
public HaValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
_gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.5, seed: 42);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed && disposing)
|
||||
{
|
||||
_data.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private TBarSeries GenerateBars(int count)
|
||||
{
|
||||
_gbm.Reset(DateTime.UtcNow.Ticks);
|
||||
return _gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// Skender Cross-Validation Tests
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// ── A) Skender GetHeikinAshi batch validation (all 4 OHLC channels) ──
|
||||
[Fact]
|
||||
public void Validate_Against_Skender_HeikinAshi_Batch()
|
||||
{
|
||||
var skenderResults = _data.SkenderQuotes
|
||||
.GetHeikinAshi()
|
||||
.ToList();
|
||||
|
||||
var qlResult = Ha.Batch(_data.Bars);
|
||||
|
||||
Assert.Equal(qlResult.Count, skenderResults.Count);
|
||||
|
||||
int count = qlResult.Count;
|
||||
int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount);
|
||||
|
||||
int failures = 0;
|
||||
for (int i = start; i < count; i++)
|
||||
{
|
||||
double qlOpen = qlResult[i].Open;
|
||||
double qlHigh = qlResult[i].High;
|
||||
double qlLow = qlResult[i].Low;
|
||||
double qlClose = qlResult[i].Close;
|
||||
|
||||
double skOpen = (double)skenderResults[i].Open;
|
||||
double skHigh = (double)skenderResults[i].High;
|
||||
double skLow = (double)skenderResults[i].Low;
|
||||
double skClose = (double)skenderResults[i].Close;
|
||||
|
||||
if (Math.Abs(qlOpen - skOpen) > ValidationHelper.SkenderTolerance)
|
||||
{
|
||||
_output.WriteLine($"Open mismatch at {i}: QL={qlOpen:G17}, SK={skOpen:G17}, Δ={Math.Abs(qlOpen - skOpen):G17}");
|
||||
failures++;
|
||||
}
|
||||
if (Math.Abs(qlHigh - skHigh) > ValidationHelper.SkenderTolerance)
|
||||
{
|
||||
_output.WriteLine($"High mismatch at {i}: QL={qlHigh:G17}, SK={skHigh:G17}, Δ={Math.Abs(qlHigh - skHigh):G17}");
|
||||
failures++;
|
||||
}
|
||||
if (Math.Abs(qlLow - skLow) > ValidationHelper.SkenderTolerance)
|
||||
{
|
||||
_output.WriteLine($"Low mismatch at {i}: QL={qlLow:G17}, SK={skLow:G17}, Δ={Math.Abs(qlLow - skLow):G17}");
|
||||
failures++;
|
||||
}
|
||||
if (Math.Abs(qlClose - skClose) > ValidationHelper.SkenderTolerance)
|
||||
{
|
||||
_output.WriteLine($"Close mismatch at {i}: QL={qlClose:G17}, SK={skClose:G17}, Δ={Math.Abs(qlClose - skClose):G17}");
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(failures == 0, $"Skender batch validation: {failures} OHLC channel mismatches in bars {start}..{count - 1}");
|
||||
_output.WriteLine($"HA vs Skender GetHeikinAshi batch: {count} bars, last {count - start} verified (4 channels) within {ValidationHelper.SkenderTolerance}: PASSED");
|
||||
}
|
||||
|
||||
// ── B) Skender GetHeikinAshi streaming validation ─────────────────────
|
||||
[Fact]
|
||||
public void Validate_Against_Skender_HeikinAshi_Streaming()
|
||||
{
|
||||
var skenderResults = _data.SkenderQuotes
|
||||
.GetHeikinAshi()
|
||||
.ToList();
|
||||
|
||||
var ind = new Ha();
|
||||
int count = _data.Bars.Count;
|
||||
double[] sOpen = new double[count];
|
||||
double[] sHigh = new double[count];
|
||||
double[] sLow = new double[count];
|
||||
double[] sClose = new double[count];
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var ha = ind.UpdateBar(_data.Bars[i], isNew: true);
|
||||
sOpen[i] = ha.Open;
|
||||
sHigh[i] = ha.High;
|
||||
sLow[i] = ha.Low;
|
||||
sClose[i] = ha.Close;
|
||||
}
|
||||
|
||||
int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount);
|
||||
int failures = 0;
|
||||
|
||||
for (int i = start; i < count; i++)
|
||||
{
|
||||
double skOpen = (double)skenderResults[i].Open;
|
||||
double skHigh = (double)skenderResults[i].High;
|
||||
double skLow = (double)skenderResults[i].Low;
|
||||
double skClose = (double)skenderResults[i].Close;
|
||||
|
||||
if (Math.Abs(sOpen[i] - skOpen) > ValidationHelper.SkenderTolerance)
|
||||
{
|
||||
_output.WriteLine($"Stream Open mismatch at {i}: QL={sOpen[i]:G17}, SK={skOpen:G17}");
|
||||
failures++;
|
||||
}
|
||||
if (Math.Abs(sHigh[i] - skHigh) > ValidationHelper.SkenderTolerance)
|
||||
{
|
||||
_output.WriteLine($"Stream High mismatch at {i}: QL={sHigh[i]:G17}, SK={skHigh:G17}");
|
||||
failures++;
|
||||
}
|
||||
if (Math.Abs(sLow[i] - skLow) > ValidationHelper.SkenderTolerance)
|
||||
{
|
||||
_output.WriteLine($"Stream Low mismatch at {i}: QL={sLow[i]:G17}, SK={skLow:G17}");
|
||||
failures++;
|
||||
}
|
||||
if (Math.Abs(sClose[i] - skClose) > ValidationHelper.SkenderTolerance)
|
||||
{
|
||||
_output.WriteLine($"Stream Close mismatch at {i}: QL={sClose[i]:G17}, SK={skClose:G17}");
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(failures == 0, $"Skender streaming validation: {failures} OHLC channel mismatches");
|
||||
_output.WriteLine($"HA streaming vs Skender GetHeikinAshi: {count} bars, last {count - start} verified: PASSED");
|
||||
}
|
||||
|
||||
// ── C) Skender GetHeikinAshi span validation ─────────────────────────
|
||||
[Fact]
|
||||
[SkipLocalsInit]
|
||||
public void Validate_Against_Skender_HeikinAshi_Span()
|
||||
{
|
||||
var skenderResults = _data.SkenderQuotes
|
||||
.GetHeikinAshi()
|
||||
.ToList();
|
||||
|
||||
int count = _data.Bars.Count;
|
||||
double[] o = new double[count];
|
||||
double[] h = new double[count];
|
||||
double[] l = new double[count];
|
||||
double[] c = new double[count];
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
o[i] = _data.Bars[i].Open;
|
||||
h[i] = _data.Bars[i].High;
|
||||
l[i] = _data.Bars[i].Low;
|
||||
c[i] = _data.Bars[i].Close;
|
||||
}
|
||||
|
||||
double[] haO = new double[count];
|
||||
double[] haH = new double[count];
|
||||
double[] haL = new double[count];
|
||||
double[] haC = new double[count];
|
||||
Ha.Batch(o.AsSpan(), h.AsSpan(), l.AsSpan(), c.AsSpan(),
|
||||
haO.AsSpan(), haH.AsSpan(), haL.AsSpan(), haC.AsSpan());
|
||||
|
||||
int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount);
|
||||
int failures = 0;
|
||||
|
||||
for (int i = start; i < count; i++)
|
||||
{
|
||||
double skOpen = (double)skenderResults[i].Open;
|
||||
double skHigh = (double)skenderResults[i].High;
|
||||
double skLow = (double)skenderResults[i].Low;
|
||||
double skClose = (double)skenderResults[i].Close;
|
||||
|
||||
if (Math.Abs(haO[i] - skOpen) > ValidationHelper.SkenderTolerance)
|
||||
{
|
||||
_output.WriteLine($"Span Open mismatch at {i}: QL={haO[i]:G17}, SK={skOpen:G17}");
|
||||
failures++;
|
||||
}
|
||||
if (Math.Abs(haH[i] - skHigh) > ValidationHelper.SkenderTolerance)
|
||||
{
|
||||
_output.WriteLine($"Span High mismatch at {i}: QL={haH[i]:G17}, SK={skHigh:G17}");
|
||||
failures++;
|
||||
}
|
||||
if (Math.Abs(haL[i] - skLow) > ValidationHelper.SkenderTolerance)
|
||||
{
|
||||
_output.WriteLine($"Span Low mismatch at {i}: QL={haL[i]:G17}, SK={skLow:G17}");
|
||||
failures++;
|
||||
}
|
||||
if (Math.Abs(haC[i] - skClose) > ValidationHelper.SkenderTolerance)
|
||||
{
|
||||
_output.WriteLine($"Span Close mismatch at {i}: QL={haC[i]:G17}, SK={skClose:G17}");
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(failures == 0, $"Skender span validation: {failures} OHLC channel mismatches");
|
||||
_output.WriteLine($"HA span vs Skender GetHeikinAshi: {count} bars, last {count - start} verified: PASSED");
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// Self-Consistency Tests
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// ── D) Batch == Streaming ─────────────────────────────────────────────
|
||||
[Fact]
|
||||
public void BatchAndStreaming_Match()
|
||||
{
|
||||
var bars = GenerateBars(DataSize);
|
||||
|
||||
// Streaming
|
||||
var streaming = new Ha();
|
||||
var streamingBars = new List<TBar>(DataSize);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingBars.Add(streaming.UpdateBar(bars[i], isNew: true));
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchResult = Ha.Batch(bars);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamingBars[i].Open, batchResult[i].Open, SelfTolerance);
|
||||
Assert.Equal(streamingBars[i].High, batchResult[i].High, SelfTolerance);
|
||||
Assert.Equal(streamingBars[i].Low, batchResult[i].Low, SelfTolerance);
|
||||
Assert.Equal(streamingBars[i].Close, batchResult[i].Close, SelfTolerance);
|
||||
}
|
||||
|
||||
_output.WriteLine($"Batch == Streaming: {bars.Count} bars, all 4 OHLC channels matched within {SelfTolerance}: PASSED");
|
||||
}
|
||||
|
||||
// ── E) Span == Streaming ──────────────────────────────────────────────
|
||||
[Fact]
|
||||
public void SpanAndStreaming_Match()
|
||||
{
|
||||
var bars = GenerateBars(DataSize);
|
||||
|
||||
// Streaming
|
||||
var streaming = new Ha();
|
||||
double[] sOpen = new double[bars.Count];
|
||||
double[] sHigh = new double[bars.Count];
|
||||
double[] sLow = new double[bars.Count];
|
||||
double[] sClose = new double[bars.Count];
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var ha = streaming.UpdateBar(bars[i], isNew: true);
|
||||
sOpen[i] = ha.Open;
|
||||
sHigh[i] = ha.High;
|
||||
sLow[i] = ha.Low;
|
||||
sClose[i] = ha.Close;
|
||||
}
|
||||
|
||||
// Span batch
|
||||
double[] haO = new double[bars.Count];
|
||||
double[] haH = new double[bars.Count];
|
||||
double[] haL = new double[bars.Count];
|
||||
double[] haC = new double[bars.Count];
|
||||
Ha.Batch(bars.OpenValues, bars.HighValues, bars.LowValues, bars.CloseValues,
|
||||
haO, haH, haL, haC);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(sOpen[i], haO[i], SelfTolerance);
|
||||
Assert.Equal(sHigh[i], haH[i], SelfTolerance);
|
||||
Assert.Equal(sLow[i], haL[i], SelfTolerance);
|
||||
Assert.Equal(sClose[i], haC[i], SelfTolerance);
|
||||
}
|
||||
|
||||
_output.WriteLine($"Span == Streaming: {bars.Count} bars matched within {SelfTolerance}: PASSED");
|
||||
}
|
||||
|
||||
// ── F) Constant bars converge ─────────────────────────────────────────
|
||||
[Fact]
|
||||
public void ConstantBars_ConvergeToConstant()
|
||||
{
|
||||
var indicator = new Ha();
|
||||
var time = DateTime.UtcNow;
|
||||
double price = 50.0;
|
||||
|
||||
TBar last = default;
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
last = indicator.UpdateBar(new TBar(time.AddMinutes(i), price, price, price, price, 1000), isNew: true);
|
||||
}
|
||||
|
||||
Assert.Equal(price, last.Open, 1e-6);
|
||||
Assert.Equal(price, last.High, 1e-6);
|
||||
Assert.Equal(price, last.Low, 1e-6);
|
||||
Assert.Equal(price, last.Close, 1e-6);
|
||||
|
||||
_output.WriteLine($"Constant convergence: price={price}, all OHLC matched: PASSED");
|
||||
}
|
||||
|
||||
// ── G) HA Close always equals OHLC4 of source bar ─────────────────────
|
||||
[Fact]
|
||||
public void HaClose_AlwaysEqualsOHLC4()
|
||||
{
|
||||
var bars = GenerateBars(DataSize);
|
||||
var indicator = new Ha();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var ha = indicator.UpdateBar(bars[i], isNew: true);
|
||||
double expected = bars[i].OHLC4;
|
||||
Assert.Equal(expected, ha.Close, SelfTolerance);
|
||||
}
|
||||
|
||||
_output.WriteLine($"HA Close == source OHLC4: {bars.Count} bars verified: PASSED");
|
||||
}
|
||||
|
||||
// ── H) HA High/Low always contain body ────────────────────────────────
|
||||
[Fact]
|
||||
public void HaHighLow_AlwaysContainBody()
|
||||
{
|
||||
var bars = GenerateBars(DataSize);
|
||||
var indicator = new Ha();
|
||||
|
||||
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}");
|
||||
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}");
|
||||
}
|
||||
|
||||
_output.WriteLine($"HA High/Low contain body: {bars.Count} bars verified: PASSED");
|
||||
}
|
||||
|
||||
// ── I) Bar correction consistency ─────────────────────────────────────
|
||||
[Fact]
|
||||
public void BarCorrection_Consistency()
|
||||
{
|
||||
var bars = GenerateBars(100);
|
||||
var indicator1 = new Ha();
|
||||
var indicator2 = new Ha();
|
||||
|
||||
// Run indicator1 normally
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
indicator1.UpdateBar(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
// Run indicator2 with corrections
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
indicator2.UpdateBar(bars[i], isNew: true);
|
||||
// Simulate correction
|
||||
if (i > 0 && i % 5 == 0)
|
||||
{
|
||||
indicator2.UpdateBar(bars[i], isNew: false);
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Equal(indicator1.LastBar.Open, indicator2.LastBar.Open, SelfTolerance);
|
||||
Assert.Equal(indicator1.LastBar.Close, indicator2.LastBar.Close, SelfTolerance);
|
||||
|
||||
_output.WriteLine("Bar correction consistency: PASSED");
|
||||
}
|
||||
|
||||
// ── J) Calculate returns hot indicator ─────────────────────────────────
|
||||
[Fact]
|
||||
public void Calculate_ReturnsHotIndicator()
|
||||
{
|
||||
var bars = GenerateBars(50);
|
||||
var (results, indicator) = Ha.Calculate(bars);
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.Equal(bars.Count, results.Count);
|
||||
|
||||
_output.WriteLine($"Calculate returns hot indicator: {results.Count} bars, IsHot=true: PASSED");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user