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,184 @@
using TradingPlatform.BusinessLayer;
namespace QuanTAlib.Quantower.Tests;
public class AtrnIndicatorTests
{
[Fact]
public void AtrnIndicator_Constructor_SetsDefaults()
{
var indicator = new AtrnIndicator();
Assert.Equal(14, indicator.Period);
Assert.True(indicator.ShowColdValues);
Assert.Equal("ATRN - Average True Range Normalized", indicator.Name);
Assert.True(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void AtrnIndicator_MinHistoryDepths_EqualsZero()
{
var indicator = new AtrnIndicator();
Assert.Equal(0, AtrnIndicator.MinHistoryDepths);
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
public void AtrnIndicator_ShortName_IncludesPeriod()
{
var indicator = new AtrnIndicator { Period = 14 };
Assert.True(indicator.ShortName.Contains("ATRN", StringComparison.Ordinal));
Assert.True(indicator.ShortName.Contains("14", StringComparison.Ordinal));
}
[Fact]
public void AtrnIndicator_Initialize_CreatesInternalAtrn()
{
var indicator = new AtrnIndicator { Period = 10 };
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void AtrnIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new AtrnIndicator { Period = 5 };
indicator.Initialize();
// Add historical data
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
// Process update
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
// Line series should have a value
Assert.Equal(1, indicator.LinesSeries[0].Count);
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
}
[Fact]
public void AtrnIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new AtrnIndicator { Period = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
[Fact]
public void AtrnIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
{
var indicator = new AtrnIndicator { Period = 5 };
indicator.Initialize();
// Add initial bar first (NewTick requires at least one bar in historical data)
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
// Now NewTick should not throw an exception
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
// Assert that the indicator still exists (method completed without exception)
Assert.NotNull(indicator);
// NewTick updates the last bar in place or adds a new point depending on implementation
Assert.True(indicator.LinesSeries[0].Count >= 1);
}
[Fact]
public void AtrnIndicator_MultipleUpdates_ProducesCorrectSequence()
{
var indicator = new AtrnIndicator { Period = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
double[] closes = [100, 102, 105, 103, 107, 110];
foreach (var close in closes)
{
indicator.HistoricalData.AddBar(now, close, close + 5, close - 5, close);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
now = now.AddMinutes(1);
}
// All values should be finite
for (int i = 0; i < closes.Length; i++)
{
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(closes.Length - 1 - i)));
}
}
[Fact]
public void AtrnIndicator_Period_CanBeChanged()
{
var indicator = new AtrnIndicator { Period = 10 };
Assert.Equal(10, indicator.Period);
indicator.Period = 20;
Assert.Equal(20, indicator.Period);
}
[Fact]
public void AtrnIndicator_ShowColdValues_CanBeChanged()
{
var indicator = new AtrnIndicator { ShowColdValues = true };
Assert.True(indicator.ShowColdValues);
indicator.ShowColdValues = false;
Assert.False(indicator.ShowColdValues);
}
[Fact]
public void AtrnIndicator_ShortName_UpdatesWhenPeriodChanges()
{
var indicator = new AtrnIndicator { Period = 10 };
string initialName = indicator.ShortName;
Assert.True(initialName.Contains("10", StringComparison.Ordinal));
indicator.Period = 20;
string updatedName = indicator.ShortName;
Assert.True(updatedName.Contains("20", StringComparison.Ordinal));
}
[Fact]
public void AtrnIndicator_LineSeries_HasCorrectProperties()
{
var indicator = new AtrnIndicator { Period = 10 };
indicator.Initialize();
var lineSeries = indicator.LinesSeries[0];
Assert.True(lineSeries.Name.Contains("ATRN", StringComparison.Ordinal));
Assert.Equal(2, lineSeries.Width);
Assert.Equal(LineStyle.Solid, lineSeries.Style);
}
[Fact]
public void AtrnIndicator_SourceCodeLink_IsValid()
{
var indicator = new AtrnIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
Assert.Contains("Atrn.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
}
}
+478
View File
@@ -0,0 +1,478 @@
using Xunit;
namespace QuanTAlib.Tests;
public class AtrnTests
{
private readonly GBM _gbm;
private readonly TBarSeries _bars;
private const int DefaultPeriod = 14;
private const double Tolerance = 1e-10;
public AtrnTests()
{
_gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
_bars = _gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
}
#region Constructor Tests
[Fact]
public void Constructor_WithValidPeriod_SetsCorrectName()
{
var atrn = new Atrn(DefaultPeriod);
Assert.Equal($"Atrn({DefaultPeriod})", atrn.Name);
}
[Fact]
public void Constructor_WithZeroPeriod_ThrowsArgumentException()
{
var ex = Assert.Throws<ArgumentException>(() => new Atrn(0));
Assert.Equal("period", ex.ParamName);
}
[Fact]
public void Constructor_WithNegativePeriod_ThrowsArgumentException()
{
var ex = Assert.Throws<ArgumentException>(() => new Atrn(-1));
Assert.Equal("period", ex.ParamName);
}
[Fact]
public void Constructor_WithTBarSeries_InitializesState()
{
var atrn = new Atrn(_bars, DefaultPeriod);
Assert.True(atrn.Last.Value >= 0);
Assert.True(atrn.Last.Value <= 1);
}
#endregion
#region Basic Calculation Tests
[Fact]
public void Update_ReturnsValidTValue()
{
var atrn = new Atrn(DefaultPeriod);
var result = atrn.Update(_bars[0], isNew: true);
Assert.IsType<TValue>(result);
Assert.Equal(_bars[0].Time, result.Time);
}
[Fact]
public void Update_ReturnsValueInZeroOneRange()
{
var atrn = new Atrn(DefaultPeriod);
for (int i = 0; i < _bars.Count; i++)
{
var result = atrn.Update(_bars[i], isNew: true);
Assert.True(result.Value >= 0 && result.Value <= 1,
$"Value {result.Value} at index {i} is outside [0,1] range");
}
}
[Fact]
public void Last_ReturnsLatestValue()
{
var atrn = new Atrn(DefaultPeriod);
for (int i = 0; i < _bars.Count; i++)
{
var result = atrn.Update(_bars[i], true);
Assert.Equal(result.Value, atrn.Last.Value);
}
}
[Fact]
public void Name_IsAccessible()
{
var atrn = new Atrn(DefaultPeriod);
Assert.False(string.IsNullOrEmpty(atrn.Name));
}
#endregion
#region State and Bar Correction Tests
[Fact]
public void Update_WithIsNewTrue_AdvancesState()
{
var atrn = new Atrn(DefaultPeriod);
atrn.Update(_bars[0], true);
atrn.Update(_bars[1], true);
// State should advance - time should match latest bar
Assert.True(atrn.Last.Time == _bars[1].Time);
}
[Fact]
public void Update_WithIsNewFalse_RollsBackState()
{
var atrn = new Atrn(DefaultPeriod);
// Process several bars first
for (int i = 0; i < 50; i++)
{
atrn.Update(_bars[i], true);
}
// Update with new bar
atrn.Update(_bars[50], true);
double valueAfterNewBar = atrn.Last.Value;
// Create modified bar
var modifiedBar = new TBar(
_bars[50].Time,
_bars[50].Open * 1.1,
_bars[50].High * 1.1,
_bars[50].Low * 1.1,
_bars[50].Close * 1.1,
_bars[50].Volume
);
// Update with isNew=false (correction)
atrn.Update(modifiedBar, false);
var valueAfterCorrection = atrn.Last.Value;
// Correction should produce different value than original update
Assert.NotEqual(valueAfterNewBar, valueAfterCorrection);
}
[Fact]
public void Update_IterativeCorrections_RestoreState()
{
var atrn = new Atrn(DefaultPeriod);
// Process initial bars
for (int i = 0; i < 100; i++)
{
atrn.Update(_bars[i], true);
}
// Process more bars
for (int i = 100; i < 150; i++)
{
atrn.Update(_bars[i], true);
}
// Now correct bar 150 multiple times
var originalBar150 = _bars[149];
var result1 = atrn.Update(originalBar150, false);
// Correct again with same value
var result2 = atrn.Update(originalBar150, false);
Assert.Equal(result1.Value, result2.Value, Tolerance);
}
[Fact]
public void Reset_ClearsStateAndLastValue()
{
var atrn = new Atrn(DefaultPeriod);
// Process some data
for (int i = 0; i < 200; i++)
{
atrn.Update(_bars[i], true);
}
Assert.True(atrn.IsHot);
// Reset
atrn.Reset();
Assert.False(atrn.IsHot);
Assert.Equal(default, atrn.Last);
}
#endregion
#region Warmup and Convergence Tests
[Fact]
public void IsHot_BecomesTrueAfterWarmup()
{
var atrn = new Atrn(DefaultPeriod);
Assert.False(atrn.IsHot);
// Warmup is period + 10*period = 11*period
int warmupPeriod = DefaultPeriod + (10 * DefaultPeriod);
for (int i = 0; i < warmupPeriod + 50; i++)
{
atrn.Update(_bars[i], true);
}
Assert.True(atrn.IsHot);
}
[Fact]
public void WarmupPeriod_IsCorrectlySet()
{
var atrn = new Atrn(DefaultPeriod);
// Warmup = RMA warmup + lookback window
int expectedWarmup = DefaultPeriod + (10 * DefaultPeriod);
Assert.True(atrn.WarmupPeriod >= expectedWarmup - DefaultPeriod);
}
#endregion
#region Robustness Tests
[Fact]
public void Update_WithNaN_UsesLastValidValue()
{
var atrn = new Atrn(DefaultPeriod);
// Process some valid data
for (int i = 0; i < 50; i++)
{
atrn.Update(_bars[i], true);
}
// Create bar with NaN
var nanBar = new TBar(
DateTime.UtcNow,
double.NaN,
double.NaN,
double.NaN,
double.NaN,
100
);
var result = atrn.Update(nanBar, true);
// Should still produce a valid value
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Update_WithInfinity_UsesLastValidValue()
{
var atrn = new Atrn(DefaultPeriod);
// Process some valid data
for (int i = 0; i < 50; i++)
{
atrn.Update(_bars[i], true);
}
// Create bar with Infinity
var infBar = new TBar(
DateTime.UtcNow,
double.PositiveInfinity,
double.PositiveInfinity,
double.NegativeInfinity,
double.PositiveInfinity,
100
);
var result = atrn.Update(infBar, true);
// Should still produce a valid value
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Update_BatchNaN_RemainsStable()
{
var atrn = new Atrn(DefaultPeriod);
// Process valid data
for (int i = 0; i < 100; i++)
{
atrn.Update(_bars[i], true);
}
// Process multiple NaN bars
for (int i = 0; i < 10; i++)
{
var nanBar = new TBar(
DateTime.UtcNow.AddMinutes(i),
double.NaN,
double.NaN,
double.NaN,
double.NaN,
100
);
var result = atrn.Update(nanBar, true);
Assert.True(double.IsFinite(result.Value));
}
}
#endregion
#region Consistency Tests
[Fact]
public void BatchCalc_MatchesStreaming()
{
var streamingAtrn = new Atrn(DefaultPeriod);
var streamingResults = new List<double>();
for (int i = 0; i < _bars.Count; i++)
{
var result = streamingAtrn.Update(_bars[i], true);
streamingResults.Add(result.Value);
}
var batchResults = Atrn.Batch(_bars, DefaultPeriod);
// Compare last 100 values (after warmup)
int compareStart = Math.Max(0, streamingResults.Count - 100);
for (int i = compareStart; i < streamingResults.Count; i++)
{
Assert.Equal(streamingResults[i], batchResults[i].Value, Tolerance);
}
}
[Fact]
public void TBarSeries_MatchesStreaming()
{
var streamingAtrn = new Atrn(DefaultPeriod);
var streamingResults = new List<double>();
for (int i = 0; i < _bars.Count; i++)
{
var result = streamingAtrn.Update(_bars[i], true);
streamingResults.Add(result.Value);
}
var seriesAtrn = new Atrn(DefaultPeriod);
var seriesResults = seriesAtrn.Update(_bars);
// Compare last 100 values
int compareStart = Math.Max(0, streamingResults.Count - 100);
for (int i = compareStart; i < streamingResults.Count; i++)
{
Assert.Equal(streamingResults[i], seriesResults[i].Value, Tolerance);
}
}
[Fact]
public void Update_EmptyTSeries_ReturnsEmpty()
{
var atrn = new Atrn(DefaultPeriod);
var result = atrn.Update(new TSeries());
Assert.Empty(result);
Assert.Equal(0, atrn.Last.Value);
}
[Fact]
public void Calculate_ReturnsConfiguredIndicatorAndMatchingResults()
{
var bars = _gbm.Fetch(300, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var (results, indicator) = Atrn.Calculate(bars, DefaultPeriod);
var batch = Atrn.Batch(bars, DefaultPeriod);
Assert.NotNull(indicator);
Assert.True(indicator.WarmupPeriod >= DefaultPeriod + 10 * DefaultPeriod);
Assert.Equal(batch.Count, results.Count);
for (int i = 0; i < results.Count; i++)
{
Assert.Equal(batch[i].Value, results[i].Value, Tolerance);
}
}
#endregion
#region Chainability Tests
[Fact]
public void Pub_EventFires_OnUpdate()
{
var atrn = new Atrn(DefaultPeriod);
int eventCount = 0;
atrn.Pub += (object? sender, in TValueEventArgs args) => eventCount++;
for (int i = 0; i < 10; i++)
{
atrn.Update(_bars[i], true);
}
Assert.Equal(10, eventCount);
}
[Fact]
public void EventBasedChaining_Works()
{
var atrn1 = new Atrn(DefaultPeriod);
var sma = new Sma(5);
var receivedValues = new List<double>();
atrn1.Pub += (object? sender, in TValueEventArgs args) =>
{
sma.Update(args.Value, args.IsNew);
receivedValues.Add(args.Value.Value);
};
for (int i = 0; i < 50; i++)
{
atrn1.Update(_bars[i], true);
}
Assert.Equal(50, receivedValues.Count);
Assert.True(sma.Last.Value >= 0 && sma.Last.Value <= 1);
}
#endregion
#region Normalization Tests
[Fact]
public void Output_IsAlwaysNormalized()
{
var atrn = new Atrn(DefaultPeriod);
for (int i = 0; i < _bars.Count; i++)
{
var result = atrn.Update(_bars[i], true);
Assert.True(result.Value >= 0.0,
$"Value {result.Value} at index {i} is less than 0");
Assert.True(result.Value <= 1.0,
$"Value {result.Value} at index {i} is greater than 1");
}
}
[Fact]
public void ConstantVolatility_ReturnsStableValue()
{
var atrn = new Atrn(DefaultPeriod);
// Create bars with constant range
var constantBars = new TBarSeries();
for (int i = 0; i < 200; i++)
{
constantBars.Add(new TBar(
DateTime.UtcNow.AddMinutes(i),
100.0, // Open
105.0, // High
95.0, // Low
100.0, // Close
1000.0 // Volume
));
}
TValue lastResult = default;
for (int i = 0; i < constantBars.Count; i++)
{
lastResult = atrn.Update(constantBars[i], true);
}
// With constant volatility, value should be stable and within [0,1]
Assert.True(lastResult.Value >= 0.0 && lastResult.Value <= 1.0,
$"Expected value in [0,1] for constant volatility, got {lastResult.Value}");
}
#endregion
}
@@ -0,0 +1,346 @@
using Skender.Stock.Indicators;
using Xunit;
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
/// <summary>
/// Validation tests for ATRN (Average True Range Normalized).
/// ATRN is QuanTAlib-specific - it normalizes ATR to [0,1] using min-max scaling.
/// Validation focuses on:
/// 1. Underlying ATR matches external libraries
/// 2. Normalization logic is correct
/// 3. Output is always in [0,1] range
/// </summary>
public sealed class AtrnValidationTests : IDisposable
{
private readonly ValidationTestData _testData;
private readonly ITestOutputHelper _output;
private bool _disposed;
public AtrnValidationTests(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();
}
}
#region ATR Foundation Validation
/// <summary>
/// Validates that the underlying ATR calculation matches Skender.
/// Since ATRN = normalized(ATR), the ATR component must be accurate.
/// </summary>
[Fact]
public void UnderlyingAtr_MatchesSkender()
{
int period = 14;
// Get QuanTAlib ATR
var atr = new Atr(period);
var quantalibAtr = atr.Update(_testData.Bars);
// Get Skender ATR
var skenderResults = _testData.SkenderQuotes.GetAtr(period).ToList();
// Compare using ValidationHelper
ValidationHelper.VerifyData(quantalibAtr, skenderResults, (s) => s.Atr, tolerance: ValidationHelper.SkenderTolerance);
_output.WriteLine("Underlying ATR validated successfully against Skender");
}
#endregion
#region Normalization Validation
/// <summary>
/// Validates that ATRN output is always in [0,1] range.
/// </summary>
[Fact]
public void Atrn_AlwaysInZeroOneRange()
{
int period = 14;
var atrn = new Atrn(period);
for (int i = 0; i < _testData.Bars.Count; i++)
{
var result = atrn.Update(_testData.Bars[i], true);
Assert.True(result.Value >= 0.0,
$"ATRN at index {i} is {result.Value}, expected >= 0");
Assert.True(result.Value <= 1.0,
$"ATRN at index {i} is {result.Value}, expected <= 1");
}
_output.WriteLine("ATRN output range validated [0,1]");
}
/// <summary>
/// Validates the min-max normalization formula.
/// </summary>
[Fact]
public void Atrn_NormalizationFormula_IsCorrect()
{
int period = 14;
int lookbackWindow = 10 * period;
var atr = new Atr(period);
var atrn = new Atrn(period);
var atrValues = new List<double>();
for (int i = 0; i < _testData.Bars.Count; i++)
{
var atrResult = atr.Update(_testData.Bars[i], true);
atrValues.Add(atrResult.Value);
var atrnResult = atrn.Update(_testData.Bars[i], true);
// After warmup, verify normalization
if (i >= lookbackWindow)
{
// Get min/max of ATR over lookback window
int startIdx = Math.Max(0, atrValues.Count - lookbackWindow);
double minAtr = double.MaxValue;
double maxAtr = double.MinValue;
for (int j = startIdx; j < atrValues.Count; j++)
{
if (atrValues[j] < minAtr)
{
minAtr = atrValues[j];
}
if (atrValues[j] > maxAtr)
{
maxAtr = atrValues[j];
}
}
double currentAtr = atrValues[^1];
double expectedNormalized = minAtr < maxAtr
? (currentAtr - minAtr) / (maxAtr - minAtr)
: 0.5;
Assert.True(
Math.Abs(expectedNormalized - atrnResult.Value) < 1e-6,
$"Normalization mismatch at index {i}: expected={expectedNormalized}, actual={atrnResult.Value}"
);
}
}
_output.WriteLine("ATRN normalization formula validated");
}
/// <summary>
/// Validates that constant ATR produces stable normalized value in [0,1].
/// </summary>
[Fact]
public void Atrn_ConstantAtr_ReturnsStableValue()
{
int period = 14;
var atrn = new Atrn(period);
int lookbackWindow = 10 * period;
// Create bars with constant range (no gaps, constant high-low)
var constantBars = new TBarSeries();
double price = 100.0;
long startTime = DateTime.UtcNow.Ticks;
for (int i = 0; i < lookbackWindow + 100; i++)
{
constantBars.Add(new TBar(
startTime + i * TimeSpan.FromMinutes(1).Ticks,
price, // Open
price + 5.0, // High (constant +5)
price - 5.0, // Low (constant -5)
price, // Close (same as open, no gap)
1000.0 // Volume
));
}
TValue lastResult = default;
for (int i = 0; i < constantBars.Count; i++)
{
lastResult = atrn.Update(constantBars[i], true);
}
// With constant volatility, value should be stable and within [0,1]
Assert.True(
lastResult.Value >= 0.0 && lastResult.Value <= 1.0,
$"Expected value in [0,1] for constant ATR, got {lastResult.Value}"
);
_output.WriteLine("ATRN constant ATR returns stable value validated");
}
#endregion
#region Edge Cases
/// <summary>
/// Validates ATRN behavior with increasing volatility.
/// Higher current ATR relative to history should produce values closer to 1.
/// </summary>
[Fact]
public void Atrn_IncreasingVolatility_ApproachesOne()
{
int period = 14;
var atrn = new Atrn(period);
int lookbackWindow = 10 * period;
// Create bars with increasing volatility
var bars = new TBarSeries();
double price = 100.0;
long startTime = DateTime.UtcNow.Ticks;
for (int i = 0; i < lookbackWindow + 50; i++)
{
// Range increases over time
double range = 1.0 + (i * 0.1);
bars.Add(new TBar(
startTime + i * TimeSpan.FromMinutes(1).Ticks,
price,
price + range,
price - range,
price,
1000.0
));
}
TValue lastResult = default;
for (int i = 0; i < bars.Count; i++)
{
lastResult = atrn.Update(bars[i], true);
}
// With increasing volatility, the latest ATR should be near max
// So normalized value should be close to 1
Assert.True(
lastResult.Value > 0.8,
$"Expected value close to 1.0 for increasing volatility, got {lastResult.Value}"
);
_output.WriteLine("ATRN increasing volatility validated");
}
/// <summary>
/// Validates ATRN behavior with decreasing volatility.
/// Lower current ATR relative to history should produce values closer to 0.
/// </summary>
[Fact]
public void Atrn_DecreasingVolatility_ApproachesZero()
{
int period = 14;
var atrn = new Atrn(period);
int lookbackWindow = 10 * period;
// Create bars with decreasing volatility
var bars = new TBarSeries();
double price = 100.0;
long startTime = DateTime.UtcNow.Ticks;
for (int i = 0; i < lookbackWindow + 50; i++)
{
// Range decreases over time (but stays positive)
double range = Math.Max(0.1, 10.0 - (i * 0.05));
bars.Add(new TBar(
startTime + i * TimeSpan.FromMinutes(1).Ticks,
price,
price + range,
price - range,
price,
1000.0
));
}
TValue lastResult = default;
for (int i = 0; i < bars.Count; i++)
{
lastResult = atrn.Update(bars[i], true);
}
// With decreasing volatility, the latest ATR should be near min
// So normalized value should be close to 0
Assert.True(
lastResult.Value < 0.2,
$"Expected value close to 0.0 for decreasing volatility, got {lastResult.Value}"
);
_output.WriteLine("ATRN decreasing volatility validated");
}
/// <summary>
/// Validates different period settings produce valid results.
/// </summary>
[Theory]
[InlineData(5)]
[InlineData(10)]
[InlineData(14)]
[InlineData(20)]
[InlineData(50)]
public void Atrn_DifferentPeriods_ProducesValidResults(int period)
{
var atrn = new Atrn(period);
for (int i = 0; i < _testData.Bars.Count; i++)
{
var result = atrn.Update(_testData.Bars[i], true);
Assert.True(result.Value >= 0.0 && result.Value <= 1.0,
$"ATRN({period}) at index {i} is {result.Value}, expected in [0,1]");
}
}
#endregion
#region Streaming vs Batch Consistency
/// <summary>
/// Validates streaming matches batch calculation.
/// </summary>
[Fact]
public void Atrn_StreamingMatchesBatch()
{
int period = 14;
// Streaming
var streamingAtrn = new Atrn(period);
var streamingResults = new List<double>();
for (int i = 0; i < _testData.Bars.Count; i++)
{
var result = streamingAtrn.Update(_testData.Bars[i], true);
streamingResults.Add(result.Value);
}
// Batch
var batchResults = Atrn.Batch(_testData.Bars, period);
Assert.Equal(streamingResults.Count, batchResults.Count);
// Compare all values
for (int i = 0; i < streamingResults.Count; i++)
{
Assert.Equal(streamingResults[i], batchResults[i].Value, 1e-10);
}
_output.WriteLine("ATRN streaming matches batch validated");
}
#endregion
}