mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-22 12:38:06 +00:00
SIMD Refactor: Merge simd-dev into dev (#55)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) <aider@aider.chat> Co-authored-by: Warp <agent@warp.dev>
This commit is contained in:
co-authored by
Claude Opus 4.5
aider
Warp
parent
5bcdf8d614
commit
86fe32a682
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class AtrnIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 1000, 1, 0)]
|
||||
public int Period { get; set; } = 14;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Atrn _atrn = null!;
|
||||
private readonly LineSeries _series;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"ATRN {Period}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/volatility/atrn/Atrn.Quantower.cs";
|
||||
|
||||
public AtrnIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "ATRN - Average True Range Normalized";
|
||||
Description = "Normalizes ATR to [0,1] range using min-max scaling over a lookback window";
|
||||
|
||||
_series = new LineSeries(name: "ATRN", color: Color.Orange, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_atrn = new Atrn(Period);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
TBar bar = this.GetInputBar(args);
|
||||
TValue result = _atrn.Update(bar, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _atrn.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,450 @@
|
||||
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], 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], 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);
|
||||
}
|
||||
}
|
||||
|
||||
#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,339 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// ATRN: Average True Range Normalized
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// ATRN normalizes the ATR to a [0,1] range using min-max scaling over a lookback window.
|
||||
/// This makes volatility comparable across different price scales and time periods.
|
||||
///
|
||||
/// Calculation:
|
||||
/// 1. Calculate ATR using RMA smoothing
|
||||
/// 2. Find min/max ATR over lookback window (10 * period)
|
||||
/// 3. Normalize: (ATR - minATR) / (maxATR - minATR)
|
||||
/// 4. If maxATR equals minATR, return 0.5
|
||||
///
|
||||
/// Sources:
|
||||
/// Derived from ATR by J. Welles Wilder, normalized for cross-asset comparison.
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Atrn : AbstractBase
|
||||
{
|
||||
private readonly int _lookbackWindow;
|
||||
private readonly Rma _rma;
|
||||
private readonly RingBuffer _atrBuffer;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(
|
||||
TBar PrevBar,
|
||||
bool IsInitialized,
|
||||
double LastValidTr,
|
||||
double LastValidAtr);
|
||||
|
||||
private State _state;
|
||||
private State _p_state;
|
||||
|
||||
/// <summary>
|
||||
/// Creates ATRN with specified period.
|
||||
/// </summary>
|
||||
/// <param name="period">Period for ATR calculation (must be > 0)</param>
|
||||
public Atrn(int period)
|
||||
{
|
||||
if (period <= 0)
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
|
||||
_lookbackWindow = 10 * period;
|
||||
_rma = new Rma(period);
|
||||
_atrBuffer = new RingBuffer(_lookbackWindow);
|
||||
|
||||
Name = $"Atrn({period})";
|
||||
WarmupPeriod = _rma.WarmupPeriod + _lookbackWindow;
|
||||
_state = new State(default, false, 0.0, 0.0);
|
||||
_p_state = _state;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates ATRN with specified source and period.
|
||||
/// </summary>
|
||||
/// <param name="source">Source to subscribe to</param>
|
||||
/// <param name="period">Period for ATR calculation</param>
|
||||
public Atrn(ITValuePublisher source, int period) : this(period)
|
||||
{
|
||||
source.Pub += Handle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates ATRN from a TBarSeries.
|
||||
/// </summary>
|
||||
/// <param name="source">Bar series source</param>
|
||||
/// <param name="period">Period for ATR calculation</param>
|
||||
public Atrn(TBarSeries source, int period) : this(period)
|
||||
{
|
||||
var result = Update(source);
|
||||
if (result.Count > 0)
|
||||
{
|
||||
Last = result.Last;
|
||||
}
|
||||
}
|
||||
|
||||
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
|
||||
|
||||
/// <summary>
|
||||
/// True if the ATRN has warmed up and is providing valid results.
|
||||
/// </summary>
|
||||
public override bool IsHot => _rma.IsHot && _atrBuffer.Count >= _lookbackWindow;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the indicator state using the provided history.
|
||||
/// Note: ATRN needs OHLCV data. This Prime method expects pre-calculated TR values.
|
||||
/// </summary>
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
double tr = source[i];
|
||||
TValue atr = _rma.Update(new TValue(DateTime.UtcNow.AddMinutes(i), tr), true);
|
||||
_atrBuffer.Add(atr.Value);
|
||||
}
|
||||
|
||||
if (_atrBuffer.Count > 0)
|
||||
{
|
||||
double currentAtr = _atrBuffer[^1];
|
||||
double maxAtr = GetMax();
|
||||
double minAtr = GetMin();
|
||||
double normalized = minAtr < maxAtr ? (currentAtr - minAtr) / (maxAtr - minAtr) : 0.5;
|
||||
Last = new TValue(DateTime.UtcNow, normalized);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the ATRN state.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void Reset()
|
||||
{
|
||||
_rma.Reset();
|
||||
_atrBuffer.Clear();
|
||||
_state = new State(default, false, 0.0, 0.0);
|
||||
_p_state = _state;
|
||||
Last = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates ATRN with a new bar.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TBar input, bool isNew = true)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_p_state = _state;
|
||||
_atrBuffer.Snapshot();
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = _p_state;
|
||||
_atrBuffer.Restore();
|
||||
}
|
||||
|
||||
// Calculate True Range FIRST (before RMA update for bar correction)
|
||||
double tr;
|
||||
if (!_state.IsInitialized)
|
||||
{
|
||||
// First bar: TR = High - Low
|
||||
tr = input.High - input.Low;
|
||||
}
|
||||
else
|
||||
{
|
||||
double hl = input.High - input.Low;
|
||||
double hpc = Math.Abs(input.High - _state.PrevBar.Close);
|
||||
double lpc = Math.Abs(input.Low - _state.PrevBar.Close);
|
||||
tr = Math.Max(hl, Math.Max(hpc, lpc));
|
||||
}
|
||||
|
||||
// Handle non-finite values
|
||||
if (!double.IsFinite(tr))
|
||||
{
|
||||
tr = _state.LastValidTr;
|
||||
}
|
||||
|
||||
// Calculate ATR using RMA (now uses freshly computed TR for both new and correction paths)
|
||||
TValue atrResult = _rma.Update(new TValue(input.Time, tr), isNew);
|
||||
double currentAtr = atrResult.Value;
|
||||
|
||||
// Handle non-finite ATR
|
||||
if (!double.IsFinite(currentAtr))
|
||||
{
|
||||
currentAtr = _state.LastValidAtr;
|
||||
}
|
||||
|
||||
// Add to buffer for min-max calculation
|
||||
_atrBuffer.Add(currentAtr);
|
||||
|
||||
// Calculate normalized value
|
||||
double maxAtr = GetMax();
|
||||
double minAtr = GetMin();
|
||||
double normalized = minAtr < maxAtr ? (currentAtr - minAtr) / (maxAtr - minAtr) : 0.5;
|
||||
|
||||
// Update state
|
||||
if (isNew)
|
||||
{
|
||||
_state = new State(input, true, tr, currentAtr);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = _state with { LastValidTr = tr, LastValidAtr = currentAtr };
|
||||
}
|
||||
|
||||
TValue result = new(input.Time, normalized);
|
||||
Last = result;
|
||||
PubEvent(Last, isNew);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates ATRN with a TValue input.
|
||||
/// This treats the input value as the TR itself.
|
||||
/// </summary>
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_p_state = _state;
|
||||
_atrBuffer.Snapshot();
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = _p_state;
|
||||
_atrBuffer.Restore();
|
||||
}
|
||||
|
||||
double tr = input.Value;
|
||||
if (!double.IsFinite(tr))
|
||||
{
|
||||
tr = _state.LastValidTr;
|
||||
}
|
||||
|
||||
TValue atrResult = _rma.Update(new TValue(input.Time, tr), isNew);
|
||||
double currentAtr = atrResult.Value;
|
||||
|
||||
if (!double.IsFinite(currentAtr))
|
||||
{
|
||||
currentAtr = _state.LastValidAtr;
|
||||
}
|
||||
|
||||
_atrBuffer.Add(currentAtr);
|
||||
|
||||
double maxAtr = GetMax();
|
||||
double minAtr = GetMin();
|
||||
double normalized = minAtr < maxAtr ? (currentAtr - minAtr) / (maxAtr - minAtr) : 0.5;
|
||||
|
||||
_state = _state with { LastValidTr = tr, LastValidAtr = currentAtr };
|
||||
|
||||
TValue result = new(input.Time, normalized);
|
||||
Last = result;
|
||||
PubEvent(Last, isNew);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates ATRN from a TBarSeries.
|
||||
/// </summary>
|
||||
public TSeries Update(TBarSeries source)
|
||||
{
|
||||
if (source.Count == 0) return [];
|
||||
|
||||
var t = new List<long>(source.Count);
|
||||
var v = new List<double>(source.Count);
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
TValue result = Update(source[i], true);
|
||||
t.Add(result.Time);
|
||||
v.Add(result.Value);
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates ATRN from a TSeries (assumes values are already TR).
|
||||
/// </summary>
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
if (source.Count == 0) return [];
|
||||
|
||||
var t = new List<long>(source.Count);
|
||||
var v = new List<double>(source.Count);
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
TValue result = Update(source[i], true);
|
||||
t.Add(source[i].Time);
|
||||
v.Add(result.Value);
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates ATRN for the entire series using a new instance.
|
||||
/// </summary>
|
||||
public static TSeries Batch(TBarSeries source, int period)
|
||||
{
|
||||
var atrn = new Atrn(period);
|
||||
return atrn.Update(source);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double GetMax()
|
||||
{
|
||||
ReadOnlySpan<double> span = _atrBuffer.GetSpan();
|
||||
if (span.IsEmpty) return 0;
|
||||
|
||||
double max = double.MinValue;
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
{
|
||||
if (span[i] > max) max = span[i];
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double GetMin()
|
||||
{
|
||||
ReadOnlySpan<double> span = _atrBuffer.GetSpan();
|
||||
if (span.IsEmpty) return 0;
|
||||
|
||||
double min = double.MaxValue;
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
{
|
||||
if (span[i] < min) min = span[i];
|
||||
}
|
||||
return min;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
# ATRN: Average True Range Normalized
|
||||
|
||||
> "Context is everything. A \$5 ATR means nothing until you know the \$5 ATR from last month was \$2."
|
||||
|
||||
ATRN transforms the absolute ATR into a relative measure by normalizing it to a [0,1] scale using min-max scaling over a lookback window. This answers the question: "Is current volatility high or low *compared to recent history*?"
|
||||
|
||||
While ATR tells you *how much* an asset moves, ATRN tells you *how unusual* that movement is relative to the asset's own recent behavior. A value near 1 means volatility is at its recent high; a value near 0 means volatility is at its recent low; 0.5 means volatility is average.
|
||||
|
||||
## Historical Context
|
||||
|
||||
ATRN is a practical extension of Wilder's ATR, developed to solve the **context problem** in volatility analysis. Raw ATR values are meaningless in isolation—you need to compare them to something. Some traders compare ATR to price (ATRP/NATR), which gives a percentage. ATRN takes a different approach: it compares ATR to its own recent range.
|
||||
|
||||
This normalization approach is common in machine learning and signal processing, where inputs are scaled to [0,1] for better model performance. ATRN applies the same principle to volatility measurement.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
ATRN is built on three components:
|
||||
|
||||
1. **True Range (TR)**: Captures the full range of price movement including gaps.
|
||||
2. **RMA Smoothing**: Wilder's exponential average ($\alpha = 1/N$) to smooth TR into ATR.
|
||||
3. **Min-Max Normalization**: Scales ATR to [0,1] over a lookback window.
|
||||
|
||||
### The Lookback Window
|
||||
|
||||
The lookback window is set to $10 \times period$. For the default period of 14:
|
||||
- Lookback = 140 bars
|
||||
- This captures roughly 6-7 months of daily data
|
||||
- Provides stable min/max anchors while remaining responsive to regime changes
|
||||
|
||||
### Edge Case: Constant Volatility
|
||||
|
||||
When max ATR equals min ATR (perfectly constant volatility), the denominator becomes zero. ATRN returns 0.5 in this case—the midpoint—indicating "average" volatility by default.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### 1. True Range (TR)
|
||||
|
||||
$$
|
||||
TR_t = \max(H_t - L_t, |H_t - C_{t-1}|, |L_t - C_{t-1}|)
|
||||
$$
|
||||
|
||||
### 2. Average True Range (ATR)
|
||||
|
||||
$$
|
||||
ATR_t = \frac{ATR_{t-1} \times (N-1) + TR_t}{N}
|
||||
$$
|
||||
|
||||
### 3. Min-Max Normalization
|
||||
|
||||
$$
|
||||
ATRN_t = \frac{ATR_t - \min(ATR, W)}{\max(ATR, W) - \min(ATR, W)}
|
||||
$$
|
||||
|
||||
Where:
|
||||
- $W = 10 \times N$ (lookback window)
|
||||
- $\min(ATR, W)$ = minimum ATR over last $W$ bars
|
||||
- $\max(ATR, W)$ = maximum ATR over last $W$ bars
|
||||
|
||||
If $\max = \min$:
|
||||
|
||||
$$
|
||||
ATRN_t = 0.5
|
||||
$$
|
||||
|
||||
## Performance Profile
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| **Throughput** | 9 | High; O(W) for min-max scan per bar. |
|
||||
| **Allocations** | 0 | Zero-allocation in hot paths via RingBuffer. |
|
||||
| **Complexity** | O(W) | Linear in lookback window size. |
|
||||
| **Accuracy** | 10 | Exact min-max normalization. |
|
||||
| **Timeliness** | 5 | Lags due to RMA + lookback window context. |
|
||||
| **Overshoot** | 0 | Bounded to [0,1] by construction. |
|
||||
| **Smoothness** | 8 | Inherits RMA smoothness from ATR. |
|
||||
|
||||
## Validation
|
||||
|
||||
| Library | Status | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| **QuanTAlib** | ✅ | Reference implementation. |
|
||||
| **TA-Lib** | N/A | No direct equivalent; underlying ATR validated. |
|
||||
| **Skender** | N/A | No direct equivalent; underlying ATR validated. |
|
||||
| **Tulip** | N/A | No direct equivalent. |
|
||||
| **Ooples** | N/A | No direct equivalent. |
|
||||
|
||||
ATRN is a QuanTAlib-specific indicator. Validation confirms:
|
||||
1. Underlying ATR matches external libraries.
|
||||
2. Normalization formula produces values in [0,1].
|
||||
3. Constant volatility produces 0.5.
|
||||
4. Increasing volatility approaches 1.0.
|
||||
5. Decreasing volatility approaches 0.0.
|
||||
|
||||
## Interpretation Guide
|
||||
|
||||
| ATRN Value | Meaning | Trading Implications |
|
||||
| :--- | :--- | :--- |
|
||||
| **0.9 - 1.0** | Volatility at recent high | Extreme conditions; expand stops/targets |
|
||||
| **0.7 - 0.9** | Above average volatility | Trending or volatile market |
|
||||
| **0.4 - 0.6** | Average volatility | Normal conditions |
|
||||
| **0.2 - 0.4** | Below average volatility | Consolidation; potential breakout setup |
|
||||
| **0.0 - 0.2** | Volatility at recent low | Extreme quiet; mean reversion likely |
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
* **Scale Independence**: ATRN is relative to the asset's own history. An ATRN of 0.8 on AAPL is not comparable to 0.8 on BTC—they're measuring different things.
|
||||
|
||||
* **Lookback Sensitivity**: The 10×period lookback window defines "recent history." Shorter lookbacks react faster but may produce whipsaw signals. The default balances responsiveness and stability.
|
||||
|
||||
* **Lag**: Like all smoothed indicators, ATRN lags the actual volatility state. By the time ATRN hits 1.0, the volatility spike may already be fading.
|
||||
|
||||
* **Not a Directional Indicator**: ATRN measures the magnitude of volatility, not its direction. High ATRN can occur in both rallies and crashes.
|
||||
|
||||
## Use Cases
|
||||
|
||||
1. **Position Sizing**: Scale position size inversely with ATRN—smaller positions when ATRN is high, larger when low.
|
||||
|
||||
2. **Stop Loss Adaptation**: Tighter stops when ATRN is low (quiet market), wider stops when ATRN is high (volatile market).
|
||||
|
||||
3. **Regime Detection**: Use ATRN thresholds to switch between mean-reversion (low ATRN) and trend-following (high ATRN) strategies.
|
||||
|
||||
4. **Volatility Breakout**: Look for moves from ATRN < 0.2 to ATRN > 0.5 as potential breakout confirmation.
|
||||
@@ -0,0 +1,43 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Average True Range Normalized (ATRN)", "ATRN", overlay=false, format=format.percent, precision=2)
|
||||
|
||||
//@function Calculates the Average True Range Normalized (ATRN) relative to its maximum value over a longer period.
|
||||
//@param length The period length for the ATR calculation. The highest uses a length of 10 * length.
|
||||
//@returns The ATRN value, normalized relative to its maximum over the longer period.
|
||||
//@optimized Beta precomputation for RMA warmup compensation
|
||||
atrn(simple int length) =>
|
||||
if length <= 0
|
||||
runtime.error("Period must be greater than 0")
|
||||
var float prevClose = close
|
||||
float tr1 = high - low
|
||||
float tr2 = math.abs(high - prevClose)
|
||||
float tr3 = math.abs(low - prevClose)
|
||||
float trueRange = math.max(tr1, tr2, tr3)
|
||||
prevClose := close
|
||||
float alpha = 1.0 / float(length)
|
||||
float beta = 1.0 - alpha
|
||||
var float EPSILON = 1e-10
|
||||
var float raw_rma = 0.0
|
||||
var float e = 1.0
|
||||
float atrValue = na
|
||||
if not na(trueRange)
|
||||
raw_rma := (raw_rma * (length - 1) + trueRange) / length
|
||||
e *= beta
|
||||
atrValue := e > EPSILON ? raw_rma / (1.0 - e) : raw_rma
|
||||
int lookbackWindow = math.min(10 * length, bar_index + 1)
|
||||
float maxAtr = ta.highest(atrValue, lookbackWindow)
|
||||
float minAtr = ta.lowest(atrValue, lookbackWindow)
|
||||
minAtr < maxAtr ? (atrValue - minAtr) / (maxAtr - minAtr) : 0.5
|
||||
|
||||
// ---------- Main loop ----------
|
||||
|
||||
// Inputs
|
||||
i_length = input.int(14, "Length", minval=1, tooltip="Number of bars used for the ATR calculation")
|
||||
|
||||
// Calculation
|
||||
atrnValue = atrn(i_length)
|
||||
|
||||
// Plot
|
||||
plot(atrnValue, "ATRN", color=color.yellow, linewidth=2)
|
||||
Reference in New Issue
Block a user