mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-22 04:28: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,296 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class TrIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void TrIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new TrIndicator();
|
||||
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("TR - True Range", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrIndicator_ShortName_IsTr()
|
||||
{
|
||||
var indicator = new TrIndicator();
|
||||
Assert.Equal("TR", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrIndicator_MinHistoryDepths_EqualsOne()
|
||||
{
|
||||
var indicator = new TrIndicator();
|
||||
|
||||
Assert.Equal(1, TrIndicator.MinHistoryDepths);
|
||||
Assert.Equal(1, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrIndicator_Initialize_CreatesInternalTr()
|
||||
{
|
||||
var indicator = new TrIndicator();
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new TrIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data with varying ranges
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
double range = 2 + (i % 5);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + range, basePrice - range, basePrice + 1, 1000);
|
||||
|
||||
// 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 val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
Assert.True(val >= 0, "True Range should be non-negative");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new TrIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 2, 1000);
|
||||
}
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Add new bar with gap up
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(20), 130, 135, 125, 133, 1500);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrIndicator_ShowColdValues_CanBeToggled()
|
||||
{
|
||||
var indicator = new TrIndicator();
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = true;
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new TrIndicator();
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Tr.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrIndicator_FirstBar_UsesHighMinusLow()
|
||||
{
|
||||
var indicator = new TrIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// First bar: High=110, Low=90, so TR should be 20
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(20.0, val, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrIndicator_GapUp_CapturesGap()
|
||||
{
|
||||
var indicator = new TrIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// First bar: close at 100
|
||||
indicator.HistoricalData.AddBar(now, 98, 102, 98, 100, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Second bar: gap up to 110-115, so TR = max(5, 15, 10) = 15
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 112, 115, 110, 113, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(15.0, val, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrIndicator_GapDown_CapturesGap()
|
||||
{
|
||||
var indicator = new TrIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// First bar: close at 100
|
||||
indicator.HistoricalData.AddBar(now, 98, 102, 98, 100, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Second bar: gap down to 85-90, so TR = max(5, 10, 15) = 15
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 88, 90, 85, 87, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(15.0, val, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrIndicator_NoGap_EqualsHighMinusLow()
|
||||
{
|
||||
var indicator = new TrIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// First bar: close at 100
|
||||
indicator.HistoricalData.AddBar(now, 98, 102, 98, 100, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Second bar: no gap, H=108, L=92, pC=100, so TR = max(16, 8, 8) = 16
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 99, 108, 92, 105, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(16.0, val, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrIndicator_HigherVolatility_ProducesHigherTr()
|
||||
{
|
||||
var indicator1 = new TrIndicator();
|
||||
var indicator2 = new TrIndicator();
|
||||
indicator1.Initialize();
|
||||
indicator2.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Indicator 1: low volatility (narrow range)
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double basePrice = 100;
|
||||
indicator1.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 1, basePrice - 1, basePrice + 0.5, 1000);
|
||||
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// Indicator 2: high volatility (wide range)
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double basePrice = 100;
|
||||
indicator2.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 10, basePrice - 10, basePrice + 2, 1000);
|
||||
indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double lowVol = indicator1.LinesSeries[0].GetValue(0);
|
||||
double highVol = indicator2.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(lowVol));
|
||||
Assert.True(double.IsFinite(highVol));
|
||||
Assert.True(highVol > lowVol, "Higher volatility bars should produce higher TR value");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrIndicator_FlatBar_ProducesZero()
|
||||
{
|
||||
var indicator = new TrIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// Flat bar: H=L=O=C
|
||||
indicator.HistoricalData.AddBar(now, 100, 100, 100, 100, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(0.0, val, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrIndicator_FlatBarWithGap_CapturesGap()
|
||||
{
|
||||
var indicator = new TrIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// First bar: close at 100
|
||||
indicator.HistoricalData.AddBar(now, 100, 100, 100, 100, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Second bar: flat but at 105 (gap of 5)
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 105, 105, 105, 105, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(5.0, val, 10); // Gap = |105-100| = 5
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrIndicator_IsHotImmediately()
|
||||
{
|
||||
var indicator = new TrIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// TR has warmup of 1, so should be hot after first bar
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Value should be valid (not cold)
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
Assert.True(val >= 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrIndicator_UsesAllOhlcComponents()
|
||||
{
|
||||
// TR uses H, L, and previous Close - verify it captures gaps properly
|
||||
var indicator = new TrIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// First bar: standard range
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 100, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
double firstTr = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(10.0, firstTr, 10); // H-L = 105-95 = 10
|
||||
|
||||
// Second bar: big gap up (prevClose=100, current range 150-160)
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 155, 160, 150, 158, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
double secondTr = indicator.LinesSeries[0].GetValue(0);
|
||||
// TR = max(10, 60, 50) = 60
|
||||
Assert.Equal(60.0, secondTr, 10);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,533 @@
|
||||
// TR Unit Tests
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class TrTests
|
||||
{
|
||||
private readonly GBM _gbm;
|
||||
private const double Tolerance = 1e-10;
|
||||
|
||||
public TrTests()
|
||||
{
|
||||
_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 tr = new Tr();
|
||||
Assert.Equal("Tr", tr.Name);
|
||||
Assert.Equal(1, tr.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithSource_SubscribesToEvents()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var tr = new Tr(source);
|
||||
source.Add(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.NotEqual(default, tr.Last);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Basic Calculation Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_FirstBar_ReturnsHighMinusLow()
|
||||
{
|
||||
var tr = new Tr();
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 105, 98, 102, 1000);
|
||||
var result = tr.Update(bar);
|
||||
// First bar: TR = High - Low = 105 - 98 = 7
|
||||
Assert.Equal(7.0, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_SecondBar_CalculatesTrueRange()
|
||||
{
|
||||
var tr = new Tr();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// First bar: Close = 100
|
||||
tr.Update(new TBar(time.AddSeconds(-1), 99, 101, 97, 100, 1000));
|
||||
|
||||
// Second bar: H=105, L=98, prevClose=100
|
||||
// TR1 = 105 - 98 = 7
|
||||
// TR2 = |105 - 100| = 5
|
||||
// TR3 = |98 - 100| = 2
|
||||
// TR = max(7, 5, 2) = 7
|
||||
var result = tr.Update(new TBar(time, 100, 105, 98, 103, 1000));
|
||||
Assert.Equal(7.0, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_GapUp_UsesPrevClose()
|
||||
{
|
||||
var tr = new Tr();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// First bar: Close = 100
|
||||
tr.Update(new TBar(time.AddSeconds(-1), 99, 101, 97, 100, 1000));
|
||||
|
||||
// Gap up bar: H=115, L=110, prevClose=100
|
||||
// TR1 = 115 - 110 = 5
|
||||
// TR2 = |115 - 100| = 15
|
||||
// TR3 = |110 - 100| = 10
|
||||
// TR = max(5, 15, 10) = 15
|
||||
var result = tr.Update(new TBar(time, 112, 115, 110, 113, 1000));
|
||||
Assert.Equal(15.0, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_GapDown_UsesPrevClose()
|
||||
{
|
||||
var tr = new Tr();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// First bar: Close = 100
|
||||
tr.Update(new TBar(time.AddSeconds(-1), 99, 101, 97, 100, 1000));
|
||||
|
||||
// Gap down bar: H=90, L=85, prevClose=100
|
||||
// TR1 = 90 - 85 = 5
|
||||
// TR2 = |90 - 100| = 10
|
||||
// TR3 = |85 - 100| = 15
|
||||
// TR = max(5, 10, 15) = 15
|
||||
var result = tr.Update(new TBar(time, 88, 90, 85, 87, 1000));
|
||||
Assert.Equal(15.0, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsNonNegative()
|
||||
{
|
||||
var tr = new Tr();
|
||||
var bars = GenerateBars(100);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = tr.Update(bars[i]);
|
||||
Assert.True(result.Value >= 0, $"TR should be non-negative, got {result.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithTValue_ReturnsZeroRange()
|
||||
{
|
||||
var tr = new Tr();
|
||||
// When using TValue, H=L=C, so range is always 0 for first bar
|
||||
var result = tr.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.Equal(0.0, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IsHot and WarmupPeriod Tests
|
||||
|
||||
[Fact]
|
||||
public void IsHot_AfterFirstBar_ReturnsTrue()
|
||||
{
|
||||
var tr = new Tr();
|
||||
Assert.False(tr.IsHot);
|
||||
|
||||
tr.Update(new TBar(DateTime.UtcNow, 99, 101, 97, 100, 1000));
|
||||
Assert.True(tr.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_EqualsOne()
|
||||
{
|
||||
var tr = new Tr();
|
||||
Assert.Equal(1, tr.WarmupPeriod);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region State and Bar Correction Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewTrue_AdvancesState()
|
||||
{
|
||||
var tr = new Tr();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
tr.Update(new TBar(time.AddSeconds(-2), 99, 101, 97, 100, 1000), isNew: true);
|
||||
var val1 = tr.Update(new TBar(time.AddSeconds(-1), 100, 105, 98, 103, 1000), isNew: true);
|
||||
|
||||
// New sequence with different previous close
|
||||
var tr2 = new Tr();
|
||||
tr2.Update(new TBar(time.AddSeconds(-2), 99, 101, 97, 95, 1000), isNew: true);
|
||||
var val2 = tr2.Update(new TBar(time.AddSeconds(-1), 100, 105, 98, 103, 1000), isNew: true);
|
||||
|
||||
// Different previous close should produce different TR
|
||||
Assert.NotEqual(val1.Value, val2.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewFalse_RollsBackState()
|
||||
{
|
||||
var tr = new Tr();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Build up history
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var bar = GenerateBars(1)[0];
|
||||
tr.Update(bar, isNew: true);
|
||||
}
|
||||
|
||||
var lastBar = GenerateBars(1)[0];
|
||||
|
||||
// New bar
|
||||
var result1 = tr.Update(new TBar(time, lastBar.Open, 110, 90, 100, 1000), isNew: true);
|
||||
|
||||
// Update same bar with different values - should rollback
|
||||
var result2 = tr.Update(new TBar(time, lastBar.Open, 120, 80, 100, 1000), isNew: false);
|
||||
|
||||
// Different range should produce different result
|
||||
Assert.NotEqual(result1.Value, result2.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IterativeCorrections_RestoreState()
|
||||
{
|
||||
var tr = new Tr();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Build history
|
||||
tr.Update(new TBar(time.AddSeconds(-1), 99, 101, 97, 100, 1000), isNew: true);
|
||||
|
||||
// Start a new bar
|
||||
var newBarResult = tr.Update(new TBar(time, 100, 110, 95, 105, 1000), isNew: true);
|
||||
|
||||
// Multiple corrections
|
||||
_ = tr.Update(new TBar(time, 100, 115, 90, 105, 1000), isNew: false);
|
||||
_ = tr.Update(new TBar(time, 100, 120, 85, 105, 1000), isNew: false);
|
||||
var correction3 = tr.Update(new TBar(time, 100, 110, 95, 105, 1000), isNew: false);
|
||||
|
||||
// Going back to original values should restore original result
|
||||
Assert.Equal(newBarResult.Value, correction3.Value, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reset Tests
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var tr = new Tr();
|
||||
|
||||
var bars = GenerateBars(10);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
tr.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.True(tr.IsHot);
|
||||
|
||||
tr.Reset();
|
||||
|
||||
Assert.False(tr.IsHot);
|
||||
Assert.Equal(default, tr.Last);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_AllowsReuseOfIndicator()
|
||||
{
|
||||
var tr = new Tr();
|
||||
var bars = GenerateBars(10);
|
||||
|
||||
// First run
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
tr.Update(bars[i]);
|
||||
}
|
||||
var firstResult = tr.Last;
|
||||
|
||||
tr.Reset();
|
||||
|
||||
// Second run with same data
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
tr.Update(bars[i]);
|
||||
}
|
||||
var secondResult = tr.Last;
|
||||
|
||||
Assert.Equal(firstResult.Value, secondResult.Value, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region NaN and Infinity Handling Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_NaNHigh_UsesLastValidValue()
|
||||
{
|
||||
var tr = new Tr();
|
||||
|
||||
tr.Update(new TBar(DateTime.UtcNow.AddSeconds(-1), 99, 101, 97, 100, 1000));
|
||||
_ = tr.Update(new TBar(DateTime.UtcNow, 100, 110, 95, 105, 1000));
|
||||
|
||||
var nanResult = tr.Update(new TBar(DateTime.UtcNow.AddSeconds(1), 100, double.NaN, 90, 95, 1000));
|
||||
|
||||
Assert.True(double.IsFinite(nanResult.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_NaNLow_UsesLastValidValue()
|
||||
{
|
||||
var tr = new Tr();
|
||||
|
||||
tr.Update(new TBar(DateTime.UtcNow.AddSeconds(-1), 99, 101, 97, 100, 1000));
|
||||
tr.Update(new TBar(DateTime.UtcNow, 100, 110, 95, 105, 1000));
|
||||
|
||||
var nanResult = tr.Update(new TBar(DateTime.UtcNow.AddSeconds(1), 100, 115, double.NaN, 112, 1000));
|
||||
|
||||
Assert.True(double.IsFinite(nanResult.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_InfinityInput_UsesLastValidValue()
|
||||
{
|
||||
var tr = new Tr();
|
||||
|
||||
tr.Update(new TBar(DateTime.UtcNow.AddSeconds(-1), 99, 101, 97, 100, 1000));
|
||||
tr.Update(new TBar(DateTime.UtcNow, 100, 110, 95, 105, 1000));
|
||||
|
||||
var infResult = tr.Update(new TBar(DateTime.UtcNow.AddSeconds(1), 100, double.PositiveInfinity, 90, 95, 1000));
|
||||
|
||||
Assert.True(double.IsFinite(infResult.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_WithNaN_ProducesSafeOutput()
|
||||
{
|
||||
double[] highs = [101, 110, double.NaN, 108, 115];
|
||||
double[] lows = [97, 95, 92, 90, 100];
|
||||
double[] closes = [100, 105, 95, 102, 110];
|
||||
double[] output = new double[5];
|
||||
|
||||
Tr.Batch(highs, lows, closes, output);
|
||||
|
||||
foreach (var val in output)
|
||||
{
|
||||
Assert.True(double.IsFinite(val));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Mode Consistency Tests
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceConsistentResults()
|
||||
{
|
||||
const int dataLen = 100;
|
||||
var bars = GenerateBars(dataLen);
|
||||
|
||||
// Mode 1: Streaming
|
||||
var tr1 = new Tr();
|
||||
for (int i = 0; i < dataLen; i++)
|
||||
{
|
||||
tr1.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
// Mode 2: Batch via TBarSeries
|
||||
var batchResult = Tr.Batch(bars);
|
||||
|
||||
// Mode 3: Span-based
|
||||
double[] highs = new double[dataLen];
|
||||
double[] lows = new double[dataLen];
|
||||
double[] closes = new double[dataLen];
|
||||
double[] spanOutput = new double[dataLen];
|
||||
|
||||
for (int i = 0; i < dataLen; i++)
|
||||
{
|
||||
highs[i] = bars[i].High;
|
||||
lows[i] = bars[i].Low;
|
||||
closes[i] = bars[i].Close;
|
||||
}
|
||||
|
||||
Tr.Batch(highs, lows, closes, spanOutput);
|
||||
|
||||
// Compare last 50 values
|
||||
int compareStart = dataLen - 50;
|
||||
for (int i = compareStart; i < dataLen; i++)
|
||||
{
|
||||
double batch = batchResult[i].Value;
|
||||
double span = spanOutput[i];
|
||||
|
||||
// Batch and Span should match exactly
|
||||
Assert.Equal(batch, span, Tolerance);
|
||||
}
|
||||
|
||||
// Final values should match
|
||||
Assert.Equal(tr1.Last.Value, batchResult[dataLen - 1].Value, 1e-8);
|
||||
Assert.Equal(tr1.Last.Value, spanOutput[dataLen - 1], 1e-8);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Span API Tests
|
||||
|
||||
[Fact]
|
||||
public void Batch_ValidatesOutputLength()
|
||||
{
|
||||
double[] highs = [101, 102, 103];
|
||||
double[] lows = [99, 98, 97];
|
||||
double[] closes = [100, 101, 102];
|
||||
double[] output = new double[2]; // Too short
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Tr.Batch(highs, lows, closes, output));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_ValidatesInputLengths()
|
||||
{
|
||||
double[] highs = [101, 102, 103];
|
||||
double[] lows = [99, 98]; // Wrong length
|
||||
double[] closes = [100, 101, 102];
|
||||
double[] output = new double[3];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Tr.Batch(highs, lows, closes, output));
|
||||
Assert.Equal("low", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_EmptyInput_ProducesNoOutput()
|
||||
{
|
||||
double[] highs = [];
|
||||
double[] lows = [];
|
||||
double[] closes = [];
|
||||
double[] output = [];
|
||||
|
||||
Tr.Batch(highs, lows, closes, output);
|
||||
// Should not throw
|
||||
Assert.Empty(output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_MatchesStreamingMode()
|
||||
{
|
||||
const int dataLen = 50;
|
||||
var bars = GenerateBars(dataLen);
|
||||
|
||||
double[] highs = new double[dataLen];
|
||||
double[] lows = new double[dataLen];
|
||||
double[] closes = new double[dataLen];
|
||||
|
||||
for (int i = 0; i < dataLen; i++)
|
||||
{
|
||||
highs[i] = bars[i].High;
|
||||
lows[i] = bars[i].Low;
|
||||
closes[i] = bars[i].Close;
|
||||
}
|
||||
|
||||
// Streaming
|
||||
var tr = new Tr();
|
||||
for (int i = 0; i < dataLen; i++)
|
||||
{
|
||||
tr.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Batch
|
||||
double[] batchOutput = new double[dataLen];
|
||||
Tr.Batch(highs, lows, closes, batchOutput);
|
||||
|
||||
// Compare final value
|
||||
Assert.Equal(tr.Last.Value, batchOutput[dataLen - 1], 1e-8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_LargeDataset_NoStackOverflow()
|
||||
{
|
||||
const int dataLen = 10000;
|
||||
var bars = new GBM(seed: 42).Fetch(dataLen, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
double[] highs = bars.HighValues.ToArray();
|
||||
double[] lows = bars.LowValues.ToArray();
|
||||
double[] closes = bars.CloseValues.ToArray();
|
||||
double[] output = new double[dataLen];
|
||||
|
||||
Tr.Batch(highs, lows, closes, output);
|
||||
|
||||
// Verify all outputs are valid
|
||||
for (int i = 0; i < dataLen; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]));
|
||||
Assert.True(output[i] >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Chainability Tests
|
||||
|
||||
[Fact]
|
||||
public void Pub_FiresOnUpdate()
|
||||
{
|
||||
var tr = new Tr();
|
||||
int eventCount = 0;
|
||||
|
||||
tr.Pub += (object? sender, in TValueEventArgs args) => eventCount++;
|
||||
|
||||
tr.Update(new TBar(DateTime.UtcNow.AddSeconds(0), 99, 101, 97, 100, 1000));
|
||||
tr.Update(new TBar(DateTime.UtcNow.AddSeconds(1), 100, 105, 98, 103, 1000));
|
||||
tr.Update(new TBar(DateTime.UtcNow.AddSeconds(2), 102, 108, 100, 106, 1000));
|
||||
|
||||
Assert.Equal(3, eventCount);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TBarSeries Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_TBarSeries_ReturnsCorrectLength()
|
||||
{
|
||||
var tr = new Tr();
|
||||
var bars = GenerateBars(50);
|
||||
|
||||
var result = tr.Update(bars);
|
||||
|
||||
Assert.Equal(50, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Static_TBarSeries_Works()
|
||||
{
|
||||
var bars = GenerateBars(50);
|
||||
|
||||
var result = Tr.Batch(bars);
|
||||
|
||||
Assert.Equal(50, result.Count);
|
||||
Assert.All(result.Values.ToArray(), v => Assert.True(v >= 0));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Prime Tests
|
||||
|
||||
[Fact]
|
||||
public void Prime_SetsInitialState()
|
||||
{
|
||||
var tr = new Tr();
|
||||
double[] warmupData = [100, 101, 102, 103, 104];
|
||||
|
||||
tr.Prime(warmupData);
|
||||
|
||||
Assert.True(tr.IsHot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,763 @@
|
||||
using Skender.Stock.Indicators;
|
||||
using TALib;
|
||||
|
||||
namespace QuanTAlib.Test;
|
||||
|
||||
using QuanTAlib.Tests;
|
||||
using Xunit;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for TR (True Range).
|
||||
/// TR = max(High - Low, |High - prevClose|, |Low - prevClose|)
|
||||
/// First bar uses High - Low only.
|
||||
/// </summary>
|
||||
public class TrValidationTests
|
||||
{
|
||||
private static TBarSeries GenerateTestData(int count = 100)
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
// === Mathematical Validation ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates the TR formula: max(H-L, |H-pC|, |L-pC|)
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_Formula_IsCorrect()
|
||||
{
|
||||
double high = 105.0;
|
||||
double low = 95.0;
|
||||
double prevClose = 100.0;
|
||||
|
||||
double tr1 = high - low; // 10
|
||||
double tr2 = Math.Abs(high - prevClose); // 5
|
||||
double tr3 = Math.Abs(low - prevClose); // 5
|
||||
|
||||
double expected = Math.Max(tr1, Math.Max(tr2, tr3)); // 10
|
||||
|
||||
Assert.Equal(10.0, expected, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates TR with gap up scenario.
|
||||
/// Gap up: prevClose below current Low, so |H-pC| > H-L
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_GapUp_CapturesGap()
|
||||
{
|
||||
double high = 115.0;
|
||||
double low = 110.0;
|
||||
double prevClose = 100.0; // Gap up from 100 to 110-115
|
||||
|
||||
double tr1 = high - low; // 5
|
||||
double tr2 = Math.Abs(high - prevClose); // 15
|
||||
double tr3 = Math.Abs(low - prevClose); // 10
|
||||
|
||||
double expected = Math.Max(tr1, Math.Max(tr2, tr3)); // 15
|
||||
|
||||
Assert.Equal(15.0, expected, 10);
|
||||
Assert.True(expected > tr1, "TR should capture the gap, exceeding H-L range");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates TR with gap down scenario.
|
||||
/// Gap down: prevClose above current High, so |L-pC| > H-L
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_GapDown_CapturesGap()
|
||||
{
|
||||
double high = 95.0;
|
||||
double low = 90.0;
|
||||
double prevClose = 110.0; // Gap down from 110 to 90-95
|
||||
|
||||
double tr1 = high - low; // 5
|
||||
double tr2 = Math.Abs(high - prevClose); // 15
|
||||
double tr3 = Math.Abs(low - prevClose); // 20
|
||||
|
||||
double expected = Math.Max(tr1, Math.Max(tr2, tr3)); // 20
|
||||
|
||||
Assert.Equal(20.0, expected, 10);
|
||||
Assert.True(expected > tr1, "TR should capture the gap, exceeding H-L range");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates TR when prevClose is within H-L range (no gap).
|
||||
/// In this case TR = H - L
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_NoGap_EqualsHighMinusLow()
|
||||
{
|
||||
double high = 105.0;
|
||||
double low = 95.0;
|
||||
double prevClose = 100.0; // Within range
|
||||
|
||||
double tr1 = high - low; // 10
|
||||
double tr2 = Math.Abs(high - prevClose); // 5
|
||||
double tr3 = Math.Abs(low - prevClose); // 5
|
||||
|
||||
double expected = Math.Max(tr1, Math.Max(tr2, tr3)); // 10
|
||||
|
||||
Assert.Equal(tr1, expected, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates first bar uses H - L only.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_FirstBar_UsesHighMinusLow()
|
||||
{
|
||||
var tr = new Tr();
|
||||
var bar = new TBar(DateTime.UtcNow.Ticks, 100, 110, 90, 105, 1000);
|
||||
|
||||
var result = tr.Update(bar);
|
||||
|
||||
Assert.Equal(20.0, result.Value, 10); // 110 - 90 = 20
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates second bar uses full TR formula.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_SecondBar_UsesFullFormula()
|
||||
{
|
||||
var tr = new Tr();
|
||||
|
||||
// First bar: close at 100
|
||||
var bar1 = new TBar(DateTime.UtcNow.Ticks, 98, 102, 98, 100, 1000);
|
||||
tr.Update(bar1);
|
||||
|
||||
// Second bar: gap up, H=115, L=110, pC=100
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1).Ticks, 110, 115, 110, 113, 1000);
|
||||
var result = tr.Update(bar2);
|
||||
|
||||
// TR = max(5, 15, 10) = 15
|
||||
Assert.Equal(15.0, result.Value, 10);
|
||||
}
|
||||
|
||||
// === Streaming Validation ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates streaming calculation matches manual calculation.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_StreamingMatchesManual()
|
||||
{
|
||||
var tr = new Tr();
|
||||
var bars = GenerateTestData(50);
|
||||
|
||||
double? prevClose = null;
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var bar = bars[i];
|
||||
var result = tr.Update(bar);
|
||||
|
||||
double expected;
|
||||
if (prevClose == null)
|
||||
{
|
||||
expected = bar.High - bar.Low;
|
||||
}
|
||||
else
|
||||
{
|
||||
double tr1 = bar.High - bar.Low;
|
||||
double tr2 = Math.Abs(bar.High - prevClose.Value);
|
||||
double tr3 = Math.Abs(bar.Low - prevClose.Value);
|
||||
expected = Math.Max(tr1, Math.Max(tr2, tr3));
|
||||
}
|
||||
|
||||
Assert.Equal(expected, result.Value, 10);
|
||||
prevClose = bar.Close;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates batch calculation matches streaming.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_BatchMatchesStreaming()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
|
||||
// Streaming
|
||||
var streamingTr = new Tr();
|
||||
var streamingResults = new double[bars.Count];
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingResults[i] = streamingTr.Update(bars[i]).Value;
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchOutput = new double[bars.Count];
|
||||
Tr.Batch(bars, batchOutput);
|
||||
|
||||
// Compare all values
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i], batchOutput[i], 10);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates TBarSeries batch matches streaming.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_TBarSeriesBatchMatchesStreaming()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
|
||||
// Streaming
|
||||
var streamingTr = new Tr();
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingTr.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Batch via TBarSeries
|
||||
var batchResult = Tr.Batch(bars);
|
||||
|
||||
Assert.Equal(streamingTr.Last.Value, batchResult.Last.Value, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates span-based batch matches streaming.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_SpanBatchMatchesStreaming()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
|
||||
// Streaming
|
||||
var streamingTr = new Tr();
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingTr.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Extract OHLC
|
||||
var highs = new double[bars.Count];
|
||||
var lows = new double[bars.Count];
|
||||
var closes = new double[bars.Count];
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
highs[i] = bars[i].High;
|
||||
lows[i] = bars[i].Low;
|
||||
closes[i] = bars[i].Close;
|
||||
}
|
||||
|
||||
// Span batch
|
||||
var output = new double[bars.Count];
|
||||
Tr.Batch(highs, lows, closes, output);
|
||||
|
||||
Assert.Equal(streamingTr.Last.Value, output[^1], 10);
|
||||
}
|
||||
|
||||
// === Property Validation ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates TR is always non-negative.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_Output_IsNonNegative()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
var tr = new Tr();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = tr.Update(bars[i]);
|
||||
Assert.True(result.Value >= 0, $"TR should be non-negative at bar {i}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates TR >= High - Low for all bars (since it's the max of three components).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_GreaterOrEqualToHighMinusLow()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
var tr = new Tr();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var bar = bars[i];
|
||||
var result = tr.Update(bar);
|
||||
double hlRange = bar.High - bar.Low;
|
||||
|
||||
Assert.True(result.Value >= hlRange - 1e-10,
|
||||
$"TR should be >= H-L at bar {i}. TR={result.Value}, H-L={hlRange}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates TR output is always finite.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_Output_IsFinite()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
var tr = new Tr();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = tr.Update(bars[i]);
|
||||
Assert.True(double.IsFinite(result.Value), $"TR should be finite at bar {i}");
|
||||
}
|
||||
}
|
||||
|
||||
// === Edge Cases ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates handling of flat bars (H = L).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_FlatBars_HandledCorrectly()
|
||||
{
|
||||
var tr = new Tr();
|
||||
|
||||
// First bar: flat
|
||||
var bar1 = new TBar(DateTime.UtcNow.Ticks, 100, 100, 100, 100, 1000);
|
||||
var result1 = tr.Update(bar1);
|
||||
Assert.Equal(0.0, result1.Value, 10);
|
||||
|
||||
// Second bar: flat but different price (gap)
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1).Ticks, 105, 105, 105, 105, 1000);
|
||||
var result2 = tr.Update(bar2);
|
||||
Assert.Equal(5.0, result2.Value, 10); // |105-100| = 5
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates handling of very large gaps.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_LargeGaps_HandledCorrectly()
|
||||
{
|
||||
var tr = new Tr();
|
||||
|
||||
// First bar at 100
|
||||
var bar1 = new TBar(DateTime.UtcNow.Ticks, 100, 101, 99, 100, 1000);
|
||||
tr.Update(bar1);
|
||||
|
||||
// Second bar with huge gap up
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1).Ticks, 200, 202, 198, 200, 1000);
|
||||
var result = tr.Update(bar2);
|
||||
|
||||
// TR = max(4, 102, 98) = 102
|
||||
Assert.Equal(102.0, result.Value, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates handling of very small ranges.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_SmallRanges_HandledCorrectly()
|
||||
{
|
||||
var tr = new Tr();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bar = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i).Ticks,
|
||||
100.0, 100.001, 99.999, 100.0, 1000
|
||||
);
|
||||
var result = tr.Update(bar);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.True(result.Value >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates bar correction works correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_BarCorrection_WorksCorrectly()
|
||||
{
|
||||
var tr = new Tr();
|
||||
var bars = GenerateTestData(20);
|
||||
|
||||
// Feed initial bars
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
tr.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
// Add new bar
|
||||
tr.Update(bars[15], isNew: true);
|
||||
double afterNew = tr.Last.Value;
|
||||
|
||||
// Correct with different bar (much larger range)
|
||||
var correctedBar = new TBar(
|
||||
bars[15].Time,
|
||||
100, 200, 50, 150, 1000
|
||||
);
|
||||
tr.Update(correctedBar, isNew: false);
|
||||
double afterCorrection = tr.Last.Value;
|
||||
|
||||
// Restore original
|
||||
tr.Update(bars[15], isNew: false);
|
||||
double afterRestore = tr.Last.Value;
|
||||
|
||||
Assert.NotEqual(afterNew, afterCorrection);
|
||||
Assert.Equal(afterNew, afterRestore, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates iterative corrections converge.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_IterativeCorrections_Converge()
|
||||
{
|
||||
var tr = new Tr();
|
||||
var bars = GenerateTestData(20);
|
||||
|
||||
// Feed bars
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
tr.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
// Multiple corrections on same bar
|
||||
for (int j = 0; j < 5; j++)
|
||||
{
|
||||
var tempBar = new TBar(
|
||||
bars[14].Time,
|
||||
100 + j, 110 + j, 90 + j, 105 + j, 1000
|
||||
);
|
||||
tr.Update(tempBar, isNew: false);
|
||||
}
|
||||
|
||||
// Final correction back to original
|
||||
tr.Update(bars[14], isNew: false);
|
||||
double afterCorrections = tr.Last.Value;
|
||||
|
||||
// Fresh calculation
|
||||
var trFresh = new Tr();
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
trFresh.Update(bars[i], isNew: true);
|
||||
}
|
||||
double freshValue = trFresh.Last.Value;
|
||||
|
||||
Assert.Equal(freshValue, afterCorrections, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates Reset clears state completely.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_Reset_ClearsState()
|
||||
{
|
||||
var tr = new Tr();
|
||||
var bars = GenerateTestData(30);
|
||||
|
||||
// Feed bars
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
tr.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Reset
|
||||
tr.Reset();
|
||||
|
||||
// State should be cleared
|
||||
Assert.False(tr.IsHot);
|
||||
Assert.Equal(default, tr.Last);
|
||||
|
||||
// Feed bars again
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
tr.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Fresh indicator
|
||||
var trFresh = new Tr();
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
trFresh.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.Equal(trFresh.Last.Value, tr.Last.Value, 10);
|
||||
}
|
||||
|
||||
// === Consistency Tests ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates stability over repeated runs with same seed.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_Stability_ConsistentOverRepeatedRuns()
|
||||
{
|
||||
var results = new List<double>();
|
||||
|
||||
for (int run = 0; run < 3; run++)
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var tr = new Tr();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
tr.Update(bars[i]);
|
||||
}
|
||||
results.Add(tr.Last.Value);
|
||||
}
|
||||
|
||||
Assert.Equal(results[0], results[1], 15);
|
||||
Assert.Equal(results[1], results[2], 15);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates TR responds to volatility regime changes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_RespondsToVolatilityChange()
|
||||
{
|
||||
var tr = new Tr();
|
||||
var lowVolResults = new List<double>();
|
||||
var highVolResults = new List<double>();
|
||||
|
||||
// Low volatility regime
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var bar = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i).Ticks,
|
||||
100.0, 101.0, 99.0, 100.0, 1000
|
||||
);
|
||||
lowVolResults.Add(tr.Update(bar).Value);
|
||||
}
|
||||
|
||||
// High volatility regime
|
||||
for (int i = 20; i < 40; i++)
|
||||
{
|
||||
var bar = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i).Ticks,
|
||||
100.0, 110.0, 90.0, 100.0, 1000
|
||||
);
|
||||
highVolResults.Add(tr.Update(bar).Value);
|
||||
}
|
||||
|
||||
double avgLowVol = lowVolResults.Skip(1).Average(); // Skip first (no gap reference)
|
||||
double avgHighVol = highVolResults.Average();
|
||||
|
||||
Assert.True(avgHighVol > avgLowVol * 5,
|
||||
$"High vol TR ({avgHighVol:F2}) should be much larger than low vol ({avgLowVol:F2})");
|
||||
}
|
||||
|
||||
// === WarmupPeriod Validation ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates WarmupPeriod is 1 (TR is hot immediately).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_WarmupPeriod_IsOne()
|
||||
{
|
||||
var tr = new Tr();
|
||||
Assert.Equal(1, tr.WarmupPeriod);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates IsHot is true after first bar.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_IsHot_AfterFirstBar()
|
||||
{
|
||||
var tr = new Tr();
|
||||
Assert.False(tr.IsHot);
|
||||
|
||||
var bar = new TBar(DateTime.UtcNow.Ticks, 100, 105, 95, 102, 1000);
|
||||
tr.Update(bar);
|
||||
|
||||
Assert.True(tr.IsHot);
|
||||
}
|
||||
|
||||
// === NaN/Infinity Handling ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates NaN high uses last valid value.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_NaNHigh_UsesLastValid()
|
||||
{
|
||||
var tr = new Tr();
|
||||
|
||||
var bar1 = new TBar(DateTime.UtcNow.Ticks, 100, 105, 95, 100, 1000);
|
||||
tr.Update(bar1);
|
||||
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1).Ticks, 100, double.NaN, 95, 100, 1000);
|
||||
var result = tr.Update(bar2);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates NaN low uses last valid value.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_NaNLow_UsesLastValid()
|
||||
{
|
||||
var tr = new Tr();
|
||||
|
||||
var bar1 = new TBar(DateTime.UtcNow.Ticks, 100, 105, 95, 100, 1000);
|
||||
tr.Update(bar1);
|
||||
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1).Ticks, 100, 105, double.NaN, 100, 1000);
|
||||
var result = tr.Update(bar2);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates NaN close uses last valid value.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_NaNClose_UsesLastValid()
|
||||
{
|
||||
var tr = new Tr();
|
||||
|
||||
var bar1 = new TBar(DateTime.UtcNow.Ticks, 100, 105, 95, 100, 1000);
|
||||
tr.Update(bar1);
|
||||
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1).Ticks, 100, 105, 95, double.NaN, 1000);
|
||||
var result = tr.Update(bar2);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates Infinity values are handled.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_Infinity_UsesLastValid()
|
||||
{
|
||||
var tr = new Tr();
|
||||
|
||||
var bar1 = new TBar(DateTime.UtcNow.Ticks, 100, 105, 95, 100, 1000);
|
||||
tr.Update(bar1);
|
||||
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1).Ticks, 100, double.PositiveInfinity, 95, 100, 1000);
|
||||
var result = tr.Update(bar2);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates batch handles NaN values.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_BatchNaN_HandledCorrectly()
|
||||
{
|
||||
var highs = new double[] { 105, 106, double.NaN, 108, 109 };
|
||||
var lows = new double[] { 95, 96, 97, double.NaN, 99 };
|
||||
var closes = new double[] { 100, 101, 102, 103, double.NaN };
|
||||
var output = new double[5];
|
||||
|
||||
Tr.Batch(highs, lows, closes, output);
|
||||
|
||||
for (int i = 0; i < output.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]), $"Output at index {i} should be finite");
|
||||
Assert.True(output[i] >= 0, $"Output at index {i} should be non-negative");
|
||||
}
|
||||
}
|
||||
|
||||
// === External Library Validation ===
|
||||
|
||||
[Fact]
|
||||
public void Validate_Talib_TrueRange()
|
||||
{
|
||||
var bars = GenerateTestData(500);
|
||||
double[] high = bars.Select(b => b.High).ToArray();
|
||||
double[] low = bars.Select(b => b.Low).ToArray();
|
||||
double[] close = bars.Select(b => b.Close).ToArray();
|
||||
double[] output = new double[high.Length];
|
||||
|
||||
var retCode = Functions.TRange<double>(high, low, close, 0..^0, output, out var outRange);
|
||||
Assert.Equal(TALib.Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = Functions.TRangeLookback();
|
||||
|
||||
// Batch comparison
|
||||
double[] qOutput = new double[high.Length];
|
||||
Tr.Batch(high, low, close, qOutput);
|
||||
|
||||
// Use ValidationHelper for correct TALib index mapping
|
||||
ValidationHelper.VerifyData(qOutput, output, outRange, lookback);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Tulip_TrueRange()
|
||||
{
|
||||
var bars = GenerateTestData(500);
|
||||
double[] high = bars.Select(b => b.High).ToArray();
|
||||
double[] low = bars.Select(b => b.Low).ToArray();
|
||||
double[] close = bars.Select(b => b.Close).ToArray();
|
||||
|
||||
var trIndicator = Tulip.Indicators.tr;
|
||||
double[][] inputs = { high, low, close };
|
||||
double[] options = Array.Empty<double>();
|
||||
int lookback = trIndicator.Start(options);
|
||||
double[][] outputs = { new double[high.Length - lookback] };
|
||||
trIndicator.Run(inputs, options, outputs);
|
||||
|
||||
double[] qOutput = new double[high.Length];
|
||||
Tr.Batch(high, low, close, qOutput);
|
||||
|
||||
int tulipLen = outputs[0].Length;
|
||||
int count = Math.Min(tulipLen, 100);
|
||||
int start = tulipLen - count;
|
||||
for (int i = start; i < tulipLen; i++)
|
||||
{
|
||||
int qIdx = lookback + i;
|
||||
Assert.True(
|
||||
Math.Abs(qOutput[qIdx] - outputs[0][i]) <= 1e-7,
|
||||
$"TR mismatch at {qIdx}: QuanTAlib={qOutput[qIdx]:G17}, Tulip={outputs[0][i]:G17}");
|
||||
}
|
||||
}
|
||||
|
||||
// === Skender Validation ===
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_Batch()
|
||||
{
|
||||
var data = new ValidationTestData();
|
||||
var tr = new global::QuanTAlib.Tr();
|
||||
var qResult = tr.Update(data.Bars);
|
||||
|
||||
var sResult = data.SkenderQuotes.GetTr().ToList();
|
||||
|
||||
ValidationHelper.VerifyData(qResult, sResult, s => s.Tr, tolerance: ValidationHelper.SkenderTolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_Streaming()
|
||||
{
|
||||
var data = new ValidationTestData();
|
||||
var tr = new global::QuanTAlib.Tr();
|
||||
var qResults = new List<double>();
|
||||
foreach (var bar in data.Bars)
|
||||
{
|
||||
qResults.Add(tr.Update(bar).Value);
|
||||
}
|
||||
|
||||
var sResult = data.SkenderQuotes.GetTr().ToList();
|
||||
|
||||
ValidationHelper.VerifyData(qResults, sResult, s => s.Tr, tolerance: ValidationHelper.SkenderTolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_Span()
|
||||
{
|
||||
var data = new ValidationTestData();
|
||||
double[] high = data.HighPrices.ToArray();
|
||||
double[] low = data.LowPrices.ToArray();
|
||||
double[] close = data.ClosePrices.ToArray();
|
||||
var output = new double[high.Length];
|
||||
global::QuanTAlib.Tr.Batch(high, low, close, output);
|
||||
|
||||
var sResult = data.SkenderQuotes.GetTr().ToList();
|
||||
|
||||
ValidationHelper.VerifyData(output, sResult, s => s.Tr, tolerance: ValidationHelper.SkenderTolerance);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user