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,158 @@
using TradingPlatform.BusinessLayer;
namespace QuanTAlib.Tests;
public class NatrIndicatorTests
{
[Fact]
public void NatrIndicator_Constructor_SetsDefaults()
{
var indicator = new NatrIndicator();
Assert.Equal(14, indicator.Period);
Assert.True(indicator.ShowColdValues);
Assert.Equal("NATR - Normalized Average True Range", indicator.Name);
Assert.True(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void NatrIndicator_ShortName_IncludesParameters()
{
var indicator = new NatrIndicator { Period = 20 };
Assert.Equal("NATR 20", indicator.ShortName);
}
[Fact]
public void NatrIndicator_MinHistoryDepths_EqualsZero()
{
var indicator = new NatrIndicator();
Assert.Equal(0, NatrIndicator.MinHistoryDepths);
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
public void NatrIndicator_Initialize_CreatesInternalNatr()
{
var indicator = new NatrIndicator();
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void NatrIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new NatrIndicator { Period = 5 };
indicator.Initialize();
// Add historical data with volatility
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);
// 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); // NATR should be positive with volatility
Assert.True(val < 100); // NATR as percentage should be reasonable
}
[Fact]
public void NatrIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new NatrIndicator { Period = 5 };
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
indicator.HistoricalData.AddBar(now.AddMinutes(20), 120, 128, 115, 125, 1500);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
[Fact]
public void NatrIndicator_DifferentPeriods_Work()
{
int[] periods = { 5, 10, 14, 20, 50 };
foreach (var period in periods)
{
var indicator = new NatrIndicator { Period = period };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 60; 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));
}
double val = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(val), $"Period {period} should produce finite value");
Assert.True(val > 0, $"Period {period} should produce positive NATR");
}
}
[Fact]
public void NatrIndicator_Period_CanBeChanged()
{
var indicator = new NatrIndicator();
Assert.Equal(14, indicator.Period);
indicator.Period = 20;
Assert.Equal(20, indicator.Period);
indicator.Period = 5;
Assert.Equal(5, indicator.Period);
}
[Fact]
public void NatrIndicator_ShowColdValues_CanBeToggled()
{
var indicator = new NatrIndicator();
Assert.True(indicator.ShowColdValues);
indicator.ShowColdValues = false;
Assert.False(indicator.ShowColdValues);
indicator.ShowColdValues = true;
Assert.True(indicator.ShowColdValues);
}
[Fact]
public void NatrIndicator_SourceCodeLink_IsValid()
{
var indicator = new NatrIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
Assert.Contains("Natr.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
}
[Fact]
public void NatrIndicator_Description_IsSet()
{
var indicator = new NatrIndicator();
Assert.Contains("percentage", indicator.Description, StringComparison.OrdinalIgnoreCase);
}
}
+601
View File
@@ -0,0 +1,601 @@
namespace QuanTAlib.Tests;
public class NatrTests
{
private const double Tolerance = 1e-9;
private static TBarSeries GenerateTestBars(int count = 100)
{
var gbm = new GBM(seed: 42);
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
}
// ============== Constructor & Parameter Validation ==============
[Fact]
public void Constructor_ValidatesInput()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new Natr(0));
Assert.Throws<ArgumentOutOfRangeException>(() => new Natr(-1));
var natr = new Natr(14);
Assert.NotNull(natr);
}
[Fact]
public void Constructor_SetsCorrectName()
{
var natr = new Natr(14);
Assert.Equal("Natr(14)", natr.Name);
Assert.True(natr.WarmupPeriod > 0);
var natr2 = new Natr(5);
Assert.Equal("Natr(5)", natr2.Name);
}
[Fact]
public void Constructor_SetsCorrectWarmup()
{
var natr = new Natr(14);
// Warmup based on RMA convergence: ln(0.05) / ln(1 - 1/14) ≈ 41
Assert.True(natr.WarmupPeriod > 0);
}
[Fact]
public void Constructor_DefaultPeriod()
{
var natr = new Natr();
Assert.Equal("Natr(14)", natr.Name);
}
// ============== Basic Functionality ==============
[Fact]
public void BasicCalculation_DoesNotCrash()
{
var natr = new Natr(14);
var bars = GenerateTestBars(100);
foreach (var bar in bars)
{
natr.Update(bar);
}
Assert.True(double.IsFinite(natr.Last.Value));
}
[Fact]
public void Calc_ReturnsValidValue()
{
var natr = new Natr(14);
var bars = GenerateTestBars(50);
foreach (var bar in bars)
{
var result = natr.Update(bar);
Assert.True(double.IsFinite(result.Value) || double.IsNaN(result.Value));
}
}
[Fact]
public void Properties_Accessible()
{
var natr = new Natr(14);
Assert.False(natr.IsHot);
Assert.Contains("Natr", natr.Name, StringComparison.Ordinal);
var bars = GenerateTestBars(60);
foreach (var bar in bars)
{
natr.Update(bar);
}
// After warmup, properties should be valid
Assert.True(double.IsFinite(natr.Atr));
Assert.True(natr.Atr >= 0);
}
[Fact]
public void AtrProperty_IsPositive()
{
var natr = new Natr(14);
var bars = GenerateTestBars(50);
foreach (var bar in bars)
{
natr.Update(bar);
}
// ATR should be positive
Assert.True(natr.Atr >= 0);
}
[Fact]
public void Natr_IsPercentage()
{
var natr = new Natr(14);
var bars = GenerateTestBars(100);
foreach (var bar in bars)
{
natr.Update(bar);
}
// NATR is a percentage - typically 0-10% for stocks
Assert.True(natr.Last.Value >= 0, $"NATR {natr.Last.Value} should be >= 0");
Assert.True(natr.Last.Value < 100, $"NATR {natr.Last.Value} should be < 100%");
}
// ============== State Management & Bar Correction ==============
[Fact]
public void Calc_IsNew_AcceptsParameter()
{
var natr = new Natr(14);
var bars = GenerateTestBars(50);
for (int i = 0; i < 49; i++)
{
natr.Update(bars[i], isNew: true);
}
double valueBefore = natr.Last.Value;
natr.Update(bars[49], isNew: true);
double valueAfter = natr.Last.Value;
Assert.True(double.IsFinite(valueBefore));
Assert.True(double.IsFinite(valueAfter));
}
[Fact]
public void Calc_IsNew_False_UpdatesValue()
{
var natr = new Natr(14);
var bars = GenerateTestBars(50);
for (int i = 0; i < 49; i++)
{
natr.Update(bars[i], isNew: true);
}
natr.Update(bars[49], isNew: true);
double beforeUpdate = natr.Last.Value;
// Update same bar with different value (isNew=false)
var modifiedBar = new TBar(bars[49].Time, bars[49].Open, bars[49].High + 5,
bars[49].Low - 5, bars[49].Close, bars[49].Volume);
natr.Update(modifiedBar, isNew: false);
double afterUpdate = natr.Last.Value;
// Values should be different after the correction (wider range)
Assert.True(Math.Abs(beforeUpdate - afterUpdate) > Tolerance);
}
[Fact]
public void IsNew_Consistency()
{
var natr = new Natr(14);
var bars = GenerateTestBars(100);
for (int i = 0; i < 99; i++)
{
natr.Update(bars[i]);
}
natr.Update(bars[99], true);
var modifiedBar = new TBar(bars[99].Time, bars[99].Open, bars[99].High + 5,
bars[99].Low - 5, bars[99].Close, bars[99].Volume);
double val2 = natr.Update(modifiedBar, false).Value;
// Create new instance and feed up to modified
var natr2 = new Natr(14);
for (int i = 0; i < 99; i++)
{
natr2.Update(bars[i]);
}
double val3 = natr2.Update(modifiedBar, true).Value;
Assert.Equal(val3, val2, Tolerance);
}
[Fact]
public void IterativeCorrections_RestoreToOriginalState()
{
var natr = new Natr(5);
var bars = GenerateTestBars(20);
TBar tenthBar = default;
for (int i = 0; i < 10; i++)
{
tenthBar = bars[i];
natr.Update(tenthBar, isNew: true);
}
double stateAfterTen = natr.Last.Value;
for (int i = 10; i < 19; i++)
{
natr.Update(bars[i], isNew: false);
}
TValue finalResult = natr.Update(tenthBar, isNew: false);
Assert.Equal(stateAfterTen, finalResult.Value, Tolerance);
}
[Fact]
public void Reset_Works()
{
var natr = new Natr(14);
var bars = GenerateTestBars(50);
foreach (var bar in bars)
{
natr.Update(bar);
}
natr.Reset();
Assert.False(natr.IsHot);
Assert.Equal(0.0, natr.Atr, Tolerance);
}
// ============== Warmup & Convergence ==============
[Fact]
public void IsHot_BecomesTrueAfterWarmup()
{
var natr = new Natr(14);
Assert.False(natr.IsHot);
var bars = GenerateTestBars(100);
int steps = 0;
while (!natr.IsHot && steps < bars.Count)
{
natr.Update(bars[steps]);
steps++;
}
Assert.True(natr.IsHot);
Assert.True(steps <= natr.WarmupPeriod + 5); // Allow some buffer
}
[Fact]
public void WarmupPeriod_IsPositive()
{
var natr = new Natr(14);
Assert.True(natr.WarmupPeriod > 0);
var natr2 = new Natr(5);
Assert.True(natr2.WarmupPeriod > 0);
}
// ============== NaN/Infinity Handling ==============
[Fact]
public void NaN_Input_UsesLastValidValue()
{
var natr = new Natr(5);
var bars = GenerateTestBars(20);
for (int i = 0; i < 15; i++)
{
natr.Update(bars[i]);
}
var inputWithNaN = new TBar(DateTime.UtcNow.AddMinutes(20).Ticks,
double.NaN, double.NaN, double.NaN, double.NaN, 0);
var resultAfterNaN = natr.Update(inputWithNaN);
Assert.True(double.IsFinite(resultAfterNaN.Value));
}
[Fact]
public void Infinity_Input_UsesLastValidValue()
{
var natr = new Natr(5);
var bars = GenerateTestBars(20);
for (int i = 0; i < 15; i++)
{
natr.Update(bars[i]);
}
var inputWithInf = new TBar(DateTime.UtcNow.AddMinutes(20).Ticks,
double.PositiveInfinity, double.PositiveInfinity,
double.NegativeInfinity, double.PositiveInfinity, 0);
var resultAfterInf = natr.Update(inputWithInf);
Assert.True(double.IsFinite(resultAfterInf.Value));
}
[Fact]
public void BatchNaN_Safe()
{
var natr = new Natr(5);
var bars = GenerateTestBars(20);
for (int i = 0; i < 15; i++)
{
natr.Update(bars[i]);
}
for (int i = 0; i < 5; i++)
{
var nanInput = new TBar(DateTime.UtcNow.AddMinutes(15 + i).Ticks,
double.NaN, double.NaN, double.NaN, double.NaN, 0);
var result = natr.Update(nanInput);
Assert.True(double.IsFinite(result.Value));
}
}
// ============== Consistency Tests ==============
[Fact]
public void TBarSeries_MatchesIterativeCalc()
{
var natrIterative = new Natr(14);
var bars = GenerateTestBars(100);
foreach (var bar in bars)
{
natrIterative.Update(bar);
}
var natrBatch = new Natr(14);
_ = natrBatch.Update(bars);
Assert.Equal(natrIterative.Last.Value, natrBatch.Last.Value, Tolerance);
}
[Fact]
public void Chainability_Works()
{
var natr = new Natr(14);
var bars = GenerateTestBars(50);
var result = natr.Update(bars);
Assert.Equal(50, result.Count);
Assert.Equal(natr.Last.Value, result.Last.Value);
}
// ============== TValue Update Not Supported ==============
[Fact]
public void TValueUpdate_ThrowsNotSupported()
{
var natr = new Natr(14);
var input = new TValue(DateTime.UtcNow.Ticks, 1.5);
Assert.Throws<NotSupportedException>(() => natr.Update(input));
}
[Fact]
public void TSeriesUpdate_ThrowsNotSupported()
{
var natr = new Natr(14);
var series = new TSeries();
series.Add(new TValue(DateTime.UtcNow.Ticks, 1.5));
Assert.Throws<NotSupportedException>(() => natr.Update(series));
}
// ============== NATR Specific Tests ==============
[Fact]
public void Natr_RelationToAtr()
{
var natr = new Natr(14);
var bars = GenerateTestBars(100);
foreach (var bar in bars)
{
natr.Update(bar);
}
// NATR = (ATR / Close) * 100
double lastClose = bars.Last.Close;
double expectedNatr = (natr.Atr / lastClose) * 100.0;
Assert.Equal(expectedNatr, natr.Last.Value, 1e-6);
}
[Fact]
public void Natr_PeriodAffectsOutput()
{
var natr5 = new Natr(5);
var natr14 = new Natr(14);
var natr28 = new Natr(28);
var bars = GenerateTestBars(100);
foreach (var bar in bars)
{
natr5.Update(bar);
natr14.Update(bar);
natr28.Update(bar);
}
// All should produce valid values
Assert.True(double.IsFinite(natr5.Last.Value));
Assert.True(double.IsFinite(natr14.Last.Value));
Assert.True(double.IsFinite(natr28.Last.Value));
// Longer periods should generally be smoother (not necessarily higher/lower)
// Just verify they're all valid
Assert.True(natr5.Last.Value >= 0);
Assert.True(natr14.Last.Value >= 0);
Assert.True(natr28.Last.Value >= 0);
}
// ============== Static Batch Methods ==============
[Fact]
public void StaticBatch_Works()
{
var bars = GenerateTestBars(50);
var results = Natr.Batch(bars, 14);
Assert.Equal(50, results.Count);
Assert.True(double.IsFinite(results.Last.Value));
}
[Fact]
public void StaticBatch_DefaultPeriod()
{
var bars = GenerateTestBars(50);
var results = Natr.Batch(bars);
Assert.Equal(50, results.Count);
Assert.True(double.IsFinite(results.Last.Value));
}
// ============== Edge Cases ==============
[Fact]
public void SingleValue_ReturnsValue()
{
var natr = new Natr(14);
var bar = GenerateTestBars(1)[0];
var result = natr.Update(bar);
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Period1_Works()
{
var natr = new Natr(1);
var bars = GenerateTestBars(10);
foreach (var bar in bars)
{
var result = natr.Update(bar);
Assert.True(double.IsFinite(result.Value));
}
}
[Fact]
public void FlatRange_ProducesStableOutput()
{
var natr = new Natr(14);
// All bars have same values - ATR should be zero
for (int i = 0; i < 50; i++)
{
var bar = new TBar(DateTime.UtcNow.AddMinutes(i).Ticks, 100.0, 100.0, 100.0, 100.0, 1000.0);
natr.Update(bar);
}
// With no range, ATR and NATR should be 0
Assert.Equal(0.0, natr.Atr, 1e-6);
Assert.Equal(0.0, natr.Last.Value, 1e-6);
}
[Fact]
public void HighVolatility_ProducesHigherNatr()
{
var natrLow = new Natr(14);
var natrHigh = new Natr(14);
// Low volatility bars
for (int i = 0; i < 50; i++)
{
var bar = new TBar(DateTime.UtcNow.AddMinutes(i).Ticks, 100.0, 100.5, 99.5, 100.0, 1000.0);
natrLow.Update(bar);
}
// High volatility bars
for (int i = 0; i < 50; i++)
{
var bar = new TBar(DateTime.UtcNow.AddMinutes(i).Ticks, 100.0, 110.0, 90.0, 100.0, 1000.0);
natrHigh.Update(bar);
}
Assert.True(natrHigh.Last.Value > natrLow.Last.Value,
$"High vol NATR {natrHigh.Last.Value} should be > Low vol NATR {natrLow.Last.Value}");
}
// ============== Event Publishing ==============
[Fact]
public void PubEvent_Fires()
{
var natr = new Natr(14);
bool eventFired = false;
natr.Pub += (object? sender, in TValueEventArgs args) => eventFired = true;
var bar = GenerateTestBars(1)[0];
natr.Update(bar);
Assert.True(eventFired);
}
// ============== Additional Tests ==============
[Fact]
public void LargeDataset_Completes()
{
var natr = new Natr(14);
var bars = GenerateTestBars(5000);
foreach (var bar in bars)
{
natr.Update(bar);
}
Assert.True(natr.IsHot);
Assert.True(double.IsFinite(natr.Last.Value));
}
[Fact]
public void DifferentParameters_ProduceValidValues()
{
var bars = GenerateTestBars(200);
var natr1 = new Natr(5);
var natr2 = new Natr(14);
var natr3 = new Natr(28);
foreach (var bar in bars)
{
natr1.Update(bar);
natr2.Update(bar);
natr3.Update(bar);
}
Assert.True(double.IsFinite(natr1.Last.Value));
Assert.True(double.IsFinite(natr2.Last.Value));
Assert.True(double.IsFinite(natr3.Last.Value));
}
[Fact]
public void ConstructorFromTBarSeries_Works()
{
var bars = GenerateTestBars(100);
var natr = new Natr(bars, 14);
Assert.True(double.IsFinite(natr.Last.Value));
}
#pragma warning disable S2699 // Tests contain assertions - analyzer false positive
[Fact]
public void Prime_Works()
{
var natr = new Natr(5);
var values = new double[] { 1.0, 1.1, 0.9, 1.2, 0.8, 1.3, 1.0, 1.1, 0.95, 1.05 };
natr.Prime(values);
// Prime only sets ATR state (without close price, can't calculate NATR percentage)
// The Last value will be the ATR, not NATR percentage
Assert.True(double.IsFinite(natr.Last.Value), "Last value should be finite after Prime");
}
#pragma warning restore S2699
}
@@ -0,0 +1,351 @@
using OoplesFinance.StockIndicators;
using OoplesFinance.StockIndicators.Enums;
using OoplesFinance.StockIndicators.Models;
using Skender.Stock.Indicators;
using TALib;
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
/// <summary>
/// NATR validation tests.
/// NATR = (ATR / Close) × 100
/// Since external libraries don't have direct NATR, we validate by computing ATR
/// from external libraries and converting to NATR using the same formula.
/// Note: NATR and ATRP are mathematically identical - both are (ATR/Close)*100.
/// </summary>
public sealed class NatrValidationTests : IDisposable
{
private readonly ValidationTestData _testData;
private readonly ITestOutputHelper _output;
private bool _disposed;
public NatrValidationTests(ITestOutputHelper output)
{
_output = output;
_testData = new ValidationTestData();
}
public void Dispose()
{
Dispose(true);
}
private void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
_disposed = true;
if (disposing)
{
_testData?.Dispose();
}
}
[Fact]
public void Validate_Skender_Batch()
{
int[] periods = { 14 };
foreach (var period in periods)
{
// Calculate QuanTAlib NATR (batch TBarSeries)
var natr = new Natr(period);
var qResult = natr.Update(_testData.Bars);
// Calculate Skender ATR and convert to NATR
var sAtr = _testData.SkenderQuotes.GetAtr(period).ToList();
var closeValues = _testData.SkenderQuotes.ToList();
// Build expected NATR values: (ATR / Close) * 100
var expectedNatr = new List<double>();
for (int i = 0; i < sAtr.Count; i++)
{
double? atr = sAtr[i].Atr;
double close = (double)closeValues[i].Close;
if (atr.HasValue && close > 0)
{
expectedNatr.Add((atr.Value / close) * 100.0);
}
else
{
expectedNatr.Add(double.NaN);
}
}
// Compare last 100 records
ValidationHelper.VerifyData(qResult, expectedNatr, (s) => s, 100, ValidationHelper.SkenderTolerance);
}
_output.WriteLine("NATR Batch(TBarSeries) validated successfully against Skender ATR");
}
[Fact]
public void Validate_Skender_Streaming()
{
int[] periods = { 14 };
foreach (var period in periods)
{
// Calculate QuanTAlib NATR (streaming)
var natr = new Natr(period);
var qResults = new List<double>();
foreach (var item in _testData.Bars)
{
qResults.Add(natr.Update(item).Value);
}
// Calculate Skender ATR and convert to NATR
var sAtr = _testData.SkenderQuotes.GetAtr(period).ToList();
var closeValues = _testData.SkenderQuotes.ToList();
// Build expected NATR values
var expectedNatr = new List<double>();
for (int i = 0; i < sAtr.Count; i++)
{
double? atr = sAtr[i].Atr;
double close = (double)closeValues[i].Close;
if (atr.HasValue && close > 0)
{
expectedNatr.Add((atr.Value / close) * 100.0);
}
else
{
expectedNatr.Add(double.NaN);
}
}
// Compare last 100 records
ValidationHelper.VerifyData(qResults, expectedNatr, (s) => s, 100, ValidationHelper.SkenderTolerance);
}
_output.WriteLine("NATR Streaming validated successfully against Skender ATR");
}
[Fact]
public void Validate_Talib_Batch()
{
int[] periods = { 14 };
// Note: QuanTAlib NATR uses warmup-compensated RMA which gives slightly different
// results than TA-Lib's classic Wilder's approach. The difference (~4-7%) accumulates
// over 5000 bars but both implementations are mathematically valid.
// Using absolute tolerance of 0.10 to account for accumulated drift divergence
// QuanTAlib warmup-compensated RMA diverges from TA-Lib classic Wilder over time
const double NatrTolerance = 0.10;
// Prepare data for TA-Lib (double[])
double[] hData = _testData.Bars.High.Select(x => x.Value).ToArray();
double[] lData = _testData.Bars.Low.Select(x => x.Value).ToArray();
double[] cData = _testData.Bars.Close.Select(x => x.Value).ToArray();
double[] atrOutput = new double[hData.Length];
foreach (var period in periods)
{
// Calculate QuanTAlib NATR (batch TBarSeries)
var natr = new Natr(period);
var qResult = natr.Update(_testData.Bars);
// Calculate TA-Lib ATR
var retCode = TALib.Functions.Atr(hData, lData, cData, 0..^0, atrOutput, out var outRange, period);
Assert.Equal(TALib.Core.RetCode.Success, retCode);
int lookback = TALib.Functions.AtrLookback(period);
// Convert ATR to NATR: (ATR / Close) * 100
var expectedNatr = new double[atrOutput.Length];
for (int i = outRange.Start.Value; i < outRange.End.Value; i++)
{
double atr = atrOutput[i];
double close = cData[i];
expectedNatr[i] = close > 0 ? (atr / close) * 100.0 : double.NaN;
}
// Compare last 100 records
ValidationHelper.VerifyData(qResult, expectedNatr, outRange, lookback, tolerance: NatrTolerance);
}
_output.WriteLine("NATR Batch(TBarSeries) validated successfully against TA-Lib ATR");
}
[Fact]
public void Validate_Talib_Streaming()
{
int[] periods = { 14 };
// Note: QuanTAlib NATR uses warmup-compensated RMA which gives slightly different
// results than TA-Lib's classic Wilder's approach. The difference (~4-7%) accumulates
// over 5000 bars but both implementations are mathematically valid.
// Using absolute tolerance of 0.10 to account for accumulated drift divergence
// QuanTAlib warmup-compensated RMA diverges from TA-Lib classic Wilder over time
const double NatrTolerance = 0.10;
// Prepare data for TA-Lib (double[])
double[] hData = _testData.Bars.High.Select(x => x.Value).ToArray();
double[] lData = _testData.Bars.Low.Select(x => x.Value).ToArray();
double[] cData = _testData.Bars.Close.Select(x => x.Value).ToArray();
double[] atrOutput = new double[hData.Length];
foreach (var period in periods)
{
// Calculate QuanTAlib NATR (streaming)
var natr = new Natr(period);
var qResults = new List<double>();
foreach (var item in _testData.Bars)
{
qResults.Add(natr.Update(item).Value);
}
// Calculate TA-Lib ATR
var retCode = TALib.Functions.Atr(hData, lData, cData, 0..^0, atrOutput, out var outRange, period);
Assert.Equal(TALib.Core.RetCode.Success, retCode);
int lookback = TALib.Functions.AtrLookback(period);
// Convert ATR to NATR
var expectedNatr = new double[atrOutput.Length];
for (int i = outRange.Start.Value; i < outRange.End.Value; i++)
{
double atr = atrOutput[i];
double close = cData[i];
expectedNatr[i] = close > 0 ? (atr / close) * 100.0 : double.NaN;
}
// Compare last 100 records
ValidationHelper.VerifyData(qResults, expectedNatr, outRange, lookback, tolerance: NatrTolerance);
}
_output.WriteLine("NATR Streaming validated successfully against TA-Lib ATR");
}
[Fact]
public void Validate_Tulip_Batch()
{
int[] periods = { 14 };
// Prepare data for Tulip (double[])
double[] hData = _testData.Bars.High.Select(x => x.Value).ToArray();
double[] lData = _testData.Bars.Low.Select(x => x.Value).ToArray();
double[] cData = _testData.Bars.Close.Select(x => x.Value).ToArray();
foreach (var period in periods)
{
// Calculate QuanTAlib NATR (batch TBarSeries)
var natr = new Natr(period);
var qResult = natr.Update(_testData.Bars);
// Calculate Tulip ATR
var atrIndicator = Tulip.Indicators.atr;
double[][] inputs = { hData, lData, cData };
double[] options = { period };
// Tulip ATR lookback
int lookback = atrIndicator.Start(options);
double[][] outputs = { new double[hData.Length - lookback] };
atrIndicator.Run(inputs, options, outputs);
var tAtr = outputs[0];
// Convert ATR to NATR: (ATR / Close) * 100
var expectedNatr = new double[tAtr.Length];
for (int i = 0; i < tAtr.Length; i++)
{
int dataIndex = lookback + i;
double close = cData[dataIndex];
expectedNatr[i] = close > 0 ? (tAtr[i] / close) * 100.0 : double.NaN;
}
// Compare last 100 records
ValidationHelper.VerifyData(qResult, expectedNatr, lookback, tolerance: ValidationHelper.TulipTolerance);
}
_output.WriteLine("NATR Batch(TBarSeries) validated successfully against Tulip ATR");
}
[Fact]
public void Validate_Tulip_Streaming()
{
int[] periods = { 14 };
// Prepare data for Tulip (double[])
double[] hData = _testData.Bars.High.Select(x => x.Value).ToArray();
double[] lData = _testData.Bars.Low.Select(x => x.Value).ToArray();
double[] cData = _testData.Bars.Close.Select(x => x.Value).ToArray();
foreach (var period in periods)
{
// Calculate QuanTAlib NATR (streaming)
var natr = new Natr(period);
var qResults = new List<double>();
foreach (var item in _testData.Bars)
{
qResults.Add(natr.Update(item).Value);
}
// Calculate Tulip ATR
var atrIndicator = Tulip.Indicators.atr;
double[][] inputs = { hData, lData, cData };
double[] options = { period };
// Tulip ATR lookback
int lookback = atrIndicator.Start(options);
double[][] outputs = { new double[hData.Length - lookback] };
atrIndicator.Run(inputs, options, outputs);
var tAtr = outputs[0];
// Convert ATR to NATR
var expectedNatr = new double[tAtr.Length];
for (int i = 0; i < tAtr.Length; i++)
{
int dataIndex = lookback + i;
double close = cData[dataIndex];
expectedNatr[i] = close > 0 ? (tAtr[i] / close) * 100.0 : double.NaN;
}
// Compare last 100 records
ValidationHelper.VerifyData(qResults, expectedNatr, lookback, tolerance: ValidationHelper.TulipTolerance);
}
_output.WriteLine("NATR Streaming validated successfully against Tulip ATR");
}
[Fact]
public void Validate_Ooples_Batch()
{
int[] periods = { 14 };
// Prepare data for Ooples (List<TickerData>)
var ooplesData = _testData.SkenderQuotes.Select(q => new TickerData
{
Date = q.Date,
Close = (double)q.Close,
High = (double)q.High,
Low = (double)q.Low,
Open = (double)q.Open,
Volume = (double)q.Volume
}).ToList();
foreach (var period in periods)
{
// Calculate QuanTAlib NATR (batch TBarSeries)
var natr = new Natr(period);
var qResult = natr.Update(_testData.Bars);
// Calculate Ooples ATR
var stockData = new StockData(ooplesData);
var oAtr = stockData.CalculateAverageTrueRange(MovingAvgType.WildersSmoothingMethod, period).OutputValues.Values.First();
// Convert ATR to NATR
var expectedNatr = new List<double>();
for (int i = 0; i < oAtr.Count; i++)
{
double atr = oAtr[i];
double close = ooplesData[i].Close;
expectedNatr.Add(close > 0 ? (atr / close) * 100.0 : double.NaN);
}
// Compare last 100 records
ValidationHelper.VerifyData(qResult, expectedNatr, (s) => s, 100, ValidationHelper.OoplesTolerance);
}
_output.WriteLine("NATR Batch(TBarSeries) validated successfully against Ooples ATR");
}
}