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:
Miha Kralj
2026-03-12 12:34:16 -07:00
parent 8937b0c0fa
commit 060649192f
1149 changed files with 1780 additions and 3316 deletions
@@ -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);
}
}
+533
View File
@@ -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);
}
}