mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-17 01:58:06 +00:00
more volatilty
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class TrIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void TrIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new TrIndicator();
|
||||
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("TR - True Range", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrIndicator_ShortName_IsTr()
|
||||
{
|
||||
var indicator = new TrIndicator();
|
||||
Assert.Equal("TR", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrIndicator_MinHistoryDepths_EqualsOne()
|
||||
{
|
||||
var indicator = new TrIndicator();
|
||||
|
||||
Assert.Equal(1, TrIndicator.MinHistoryDepths);
|
||||
Assert.Equal(1, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrIndicator_Initialize_CreatesInternalTr()
|
||||
{
|
||||
var indicator = new TrIndicator();
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new TrIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data with varying ranges
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
double range = 2 + (i % 5);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + range, basePrice - range, basePrice + 1, 1000);
|
||||
|
||||
// Process update for each bar to simulate history loading
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
Assert.True(val >= 0, "True Range should be non-negative");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new TrIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 2, 1000);
|
||||
}
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Add new bar with gap up
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(20), 130, 135, 125, 133, 1500);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrIndicator_ShowColdValues_CanBeToggled()
|
||||
{
|
||||
var indicator = new TrIndicator();
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = true;
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new TrIndicator();
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Tr.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrIndicator_FirstBar_UsesHighMinusLow()
|
||||
{
|
||||
var indicator = new TrIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// First bar: High=110, Low=90, so TR should be 20
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(20.0, val, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrIndicator_GapUp_CapturesGap()
|
||||
{
|
||||
var indicator = new TrIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// First bar: close at 100
|
||||
indicator.HistoricalData.AddBar(now, 98, 102, 98, 100, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Second bar: gap up to 110-115, so TR = max(5, 15, 10) = 15
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 112, 115, 110, 113, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(15.0, val, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrIndicator_GapDown_CapturesGap()
|
||||
{
|
||||
var indicator = new TrIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// First bar: close at 100
|
||||
indicator.HistoricalData.AddBar(now, 98, 102, 98, 100, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Second bar: gap down to 85-90, so TR = max(5, 10, 15) = 15
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 88, 90, 85, 87, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(15.0, val, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrIndicator_NoGap_EqualsHighMinusLow()
|
||||
{
|
||||
var indicator = new TrIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// First bar: close at 100
|
||||
indicator.HistoricalData.AddBar(now, 98, 102, 98, 100, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Second bar: no gap, H=108, L=92, pC=100, so TR = max(16, 8, 8) = 16
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 99, 108, 92, 105, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(16.0, val, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrIndicator_HigherVolatility_ProducesHigherTr()
|
||||
{
|
||||
var indicator1 = new TrIndicator();
|
||||
var indicator2 = new TrIndicator();
|
||||
indicator1.Initialize();
|
||||
indicator2.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Indicator 1: low volatility (narrow range)
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double basePrice = 100;
|
||||
indicator1.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 1, basePrice - 1, basePrice + 0.5, 1000);
|
||||
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// Indicator 2: high volatility (wide range)
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double basePrice = 100;
|
||||
indicator2.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 10, basePrice - 10, basePrice + 2, 1000);
|
||||
indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double lowVol = indicator1.LinesSeries[0].GetValue(0);
|
||||
double highVol = indicator2.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(lowVol));
|
||||
Assert.True(double.IsFinite(highVol));
|
||||
Assert.True(highVol > lowVol, "Higher volatility bars should produce higher TR value");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrIndicator_FlatBar_ProducesZero()
|
||||
{
|
||||
var indicator = new TrIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// Flat bar: H=L=O=C
|
||||
indicator.HistoricalData.AddBar(now, 100, 100, 100, 100, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(0.0, val, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrIndicator_FlatBarWithGap_CapturesGap()
|
||||
{
|
||||
var indicator = new TrIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// First bar: close at 100
|
||||
indicator.HistoricalData.AddBar(now, 100, 100, 100, 100, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Second bar: flat but at 105 (gap of 5)
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 105, 105, 105, 105, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(5.0, val, 10); // Gap = |105-100| = 5
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrIndicator_IsHotImmediately()
|
||||
{
|
||||
var indicator = new TrIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// TR has warmup of 1, so should be hot after first bar
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Value should be valid (not cold)
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
Assert.True(val >= 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrIndicator_UsesAllOhlcComponents()
|
||||
{
|
||||
// TR uses H, L, and previous Close - verify it captures gaps properly
|
||||
var indicator = new TrIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// First bar: standard range
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 100, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
double firstTr = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(10.0, firstTr, 10); // H-L = 105-95 = 10
|
||||
|
||||
// Second bar: big gap up (prevClose=100, current range 150-160)
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 155, 160, 150, 158, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
double secondTr = indicator.LinesSeries[0].GetValue(0);
|
||||
// TR = max(10, 60, 50) = 60
|
||||
Assert.Equal(60.0, secondTr, 10);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class TrIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Tr _tr = null!;
|
||||
private readonly LineSeries _series;
|
||||
|
||||
public static int MinHistoryDepths => 1;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => "TR";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/volatility/tr/Tr.Quantower.cs";
|
||||
|
||||
public TrIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "TR - True Range";
|
||||
Description = "True Range measures the maximum price movement including gaps from the previous close. It is the foundation for ATR (Average True Range).";
|
||||
|
||||
_series = new LineSeries(name: "TR", color: IndicatorExtensions.Volatility, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_tr = new Tr();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
TBar bar = this.GetInputBar(args);
|
||||
TValue result = _tr.Update(bar, isNew: args.IsNewBar());
|
||||
_series.SetValue(result.Value, _tr.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,548 @@
|
||||
// TR Unit Tests
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class TrTests
|
||||
{
|
||||
private readonly GBM _gbm;
|
||||
private const double Tolerance = 1e-10;
|
||||
|
||||
public TrTests()
|
||||
{
|
||||
_gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
}
|
||||
|
||||
private TBarSeries GenerateBars(int count)
|
||||
{
|
||||
_gbm.Reset(DateTime.UtcNow.Ticks);
|
||||
return _gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
#region Constructor Tests
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultParameters_SetsCorrectValues()
|
||||
{
|
||||
var tr = new Tr();
|
||||
Assert.Equal("Tr", tr.Name);
|
||||
Assert.Equal(1, tr.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithSource_SubscribesToEvents()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var tr = new Tr(source);
|
||||
source.Add(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.NotEqual(default, tr.Last);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Basic Calculation Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_FirstBar_ReturnsHighMinusLow()
|
||||
{
|
||||
var tr = new Tr();
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 105, 98, 102, 1000);
|
||||
var result = tr.Update(bar);
|
||||
// First bar: TR = High - Low = 105 - 98 = 7
|
||||
Assert.Equal(7.0, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_SecondBar_CalculatesTrueRange()
|
||||
{
|
||||
var tr = new Tr();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// First bar: Close = 100
|
||||
tr.Update(new TBar(time.AddSeconds(-1), 99, 101, 97, 100, 1000));
|
||||
|
||||
// Second bar: H=105, L=98, prevClose=100
|
||||
// TR1 = 105 - 98 = 7
|
||||
// TR2 = |105 - 100| = 5
|
||||
// TR3 = |98 - 100| = 2
|
||||
// TR = max(7, 5, 2) = 7
|
||||
var result = tr.Update(new TBar(time, 100, 105, 98, 103, 1000));
|
||||
Assert.Equal(7.0, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_GapUp_UsesPrevClose()
|
||||
{
|
||||
var tr = new Tr();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// First bar: Close = 100
|
||||
tr.Update(new TBar(time.AddSeconds(-1), 99, 101, 97, 100, 1000));
|
||||
|
||||
// Gap up bar: H=115, L=110, prevClose=100
|
||||
// TR1 = 115 - 110 = 5
|
||||
// TR2 = |115 - 100| = 15
|
||||
// TR3 = |110 - 100| = 10
|
||||
// TR = max(5, 15, 10) = 15
|
||||
var result = tr.Update(new TBar(time, 112, 115, 110, 113, 1000));
|
||||
Assert.Equal(15.0, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_GapDown_UsesPrevClose()
|
||||
{
|
||||
var tr = new Tr();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// First bar: Close = 100
|
||||
tr.Update(new TBar(time.AddSeconds(-1), 99, 101, 97, 100, 1000));
|
||||
|
||||
// Gap down bar: H=90, L=85, prevClose=100
|
||||
// TR1 = 90 - 85 = 5
|
||||
// TR2 = |90 - 100| = 10
|
||||
// TR3 = |85 - 100| = 15
|
||||
// TR = max(5, 10, 15) = 15
|
||||
var result = tr.Update(new TBar(time, 88, 90, 85, 87, 1000));
|
||||
Assert.Equal(15.0, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsNonNegative()
|
||||
{
|
||||
var tr = new Tr();
|
||||
var bars = GenerateBars(100);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = tr.Update(bars[i]);
|
||||
Assert.True(result.Value >= 0, $"TR should be non-negative, got {result.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithTValue_ReturnsZeroRange()
|
||||
{
|
||||
var tr = new Tr();
|
||||
// When using TValue, H=L=C, so range is always 0 for first bar
|
||||
var result = tr.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.Equal(0.0, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IsHot and WarmupPeriod Tests
|
||||
|
||||
[Fact]
|
||||
public void IsHot_AfterFirstBar_ReturnsTrue()
|
||||
{
|
||||
var tr = new Tr();
|
||||
Assert.False(tr.IsHot);
|
||||
|
||||
tr.Update(new TBar(DateTime.UtcNow, 99, 101, 97, 100, 1000));
|
||||
Assert.True(tr.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_EqualsOne()
|
||||
{
|
||||
var tr = new Tr();
|
||||
Assert.Equal(1, tr.WarmupPeriod);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region State and Bar Correction Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewTrue_AdvancesState()
|
||||
{
|
||||
var tr = new Tr();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
tr.Update(new TBar(time.AddSeconds(-2), 99, 101, 97, 100, 1000), isNew: true);
|
||||
var val1 = tr.Update(new TBar(time.AddSeconds(-1), 100, 105, 98, 103, 1000), isNew: true);
|
||||
|
||||
// New sequence with different previous close
|
||||
var tr2 = new Tr();
|
||||
tr2.Update(new TBar(time.AddSeconds(-2), 99, 101, 97, 95, 1000), isNew: true);
|
||||
var val2 = tr2.Update(new TBar(time.AddSeconds(-1), 100, 105, 98, 103, 1000), isNew: true);
|
||||
|
||||
// Different previous close should produce different TR
|
||||
Assert.NotEqual(val1.Value, val2.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewFalse_RollsBackState()
|
||||
{
|
||||
var tr = new Tr();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Build up history
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var bar = GenerateBars(1)[0];
|
||||
tr.Update(bar, isNew: true);
|
||||
}
|
||||
|
||||
var lastBar = GenerateBars(1)[0];
|
||||
|
||||
// New bar
|
||||
var result1 = tr.Update(new TBar(time, lastBar.Open, 110, 90, 100, 1000), isNew: true);
|
||||
|
||||
// Update same bar with different values - should rollback
|
||||
var result2 = tr.Update(new TBar(time, lastBar.Open, 120, 80, 100, 1000), isNew: false);
|
||||
|
||||
// Different range should produce different result
|
||||
Assert.NotEqual(result1.Value, result2.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IterativeCorrections_RestoreState()
|
||||
{
|
||||
var tr = new Tr();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Build history
|
||||
tr.Update(new TBar(time.AddSeconds(-1), 99, 101, 97, 100, 1000), isNew: true);
|
||||
|
||||
// Start a new bar
|
||||
var newBarResult = tr.Update(new TBar(time, 100, 110, 95, 105, 1000), isNew: true);
|
||||
|
||||
// Multiple corrections
|
||||
_ = tr.Update(new TBar(time, 100, 115, 90, 105, 1000), isNew: false);
|
||||
_ = tr.Update(new TBar(time, 100, 120, 85, 105, 1000), isNew: false);
|
||||
var correction3 = tr.Update(new TBar(time, 100, 110, 95, 105, 1000), isNew: false);
|
||||
|
||||
// Going back to original values should restore original result
|
||||
Assert.Equal(newBarResult.Value, correction3.Value, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reset Tests
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var tr = new Tr();
|
||||
|
||||
var bars = GenerateBars(10);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
tr.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.True(tr.IsHot);
|
||||
|
||||
tr.Reset();
|
||||
|
||||
Assert.False(tr.IsHot);
|
||||
Assert.Equal(default, tr.Last);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_AllowsReuseOfIndicator()
|
||||
{
|
||||
var tr = new Tr();
|
||||
var bars = GenerateBars(10);
|
||||
|
||||
// First run
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
tr.Update(bars[i]);
|
||||
}
|
||||
var firstResult = tr.Last;
|
||||
|
||||
tr.Reset();
|
||||
|
||||
// Second run with same data
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
tr.Update(bars[i]);
|
||||
}
|
||||
var secondResult = tr.Last;
|
||||
|
||||
Assert.Equal(firstResult.Value, secondResult.Value, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region NaN and Infinity Handling Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_NaNHigh_UsesLastValidValue()
|
||||
{
|
||||
var tr = new Tr();
|
||||
|
||||
tr.Update(new TBar(DateTime.UtcNow.AddSeconds(-1), 99, 101, 97, 100, 1000));
|
||||
_ = tr.Update(new TBar(DateTime.UtcNow, 100, 110, 95, 105, 1000));
|
||||
|
||||
var nanResult = tr.Update(new TBar(DateTime.UtcNow.AddSeconds(1), 100, double.NaN, 90, 95, 1000));
|
||||
|
||||
Assert.True(double.IsFinite(nanResult.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_NaNLow_UsesLastValidValue()
|
||||
{
|
||||
var tr = new Tr();
|
||||
|
||||
tr.Update(new TBar(DateTime.UtcNow.AddSeconds(-1), 99, 101, 97, 100, 1000));
|
||||
tr.Update(new TBar(DateTime.UtcNow, 100, 110, 95, 105, 1000));
|
||||
|
||||
var nanResult = tr.Update(new TBar(DateTime.UtcNow.AddSeconds(1), 100, 115, double.NaN, 112, 1000));
|
||||
|
||||
Assert.True(double.IsFinite(nanResult.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_InfinityInput_UsesLastValidValue()
|
||||
{
|
||||
var tr = new Tr();
|
||||
|
||||
tr.Update(new TBar(DateTime.UtcNow.AddSeconds(-1), 99, 101, 97, 100, 1000));
|
||||
tr.Update(new TBar(DateTime.UtcNow, 100, 110, 95, 105, 1000));
|
||||
|
||||
var infResult = tr.Update(new TBar(DateTime.UtcNow.AddSeconds(1), 100, double.PositiveInfinity, 90, 95, 1000));
|
||||
|
||||
Assert.True(double.IsFinite(infResult.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_WithNaN_ProducesSafeOutput()
|
||||
{
|
||||
double[] highs = [101, 110, double.NaN, 108, 115];
|
||||
double[] lows = [97, 95, 92, 90, 100];
|
||||
double[] closes = [100, 105, 95, 102, 110];
|
||||
double[] output = new double[5];
|
||||
|
||||
Tr.Batch(highs, lows, closes, output);
|
||||
|
||||
foreach (var val in output)
|
||||
{
|
||||
Assert.True(double.IsFinite(val));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Mode Consistency Tests
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceConsistentResults()
|
||||
{
|
||||
const int dataLen = 100;
|
||||
var bars = GenerateBars(dataLen);
|
||||
|
||||
// Mode 1: Streaming
|
||||
var tr1 = new Tr();
|
||||
for (int i = 0; i < dataLen; i++)
|
||||
{
|
||||
tr1.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
// Mode 2: Batch via TBarSeries
|
||||
var batchResult = Tr.Calculate(bars);
|
||||
|
||||
// Mode 3: Span-based
|
||||
double[] highs = new double[dataLen];
|
||||
double[] lows = new double[dataLen];
|
||||
double[] closes = new double[dataLen];
|
||||
double[] spanOutput = new double[dataLen];
|
||||
|
||||
for (int i = 0; i < dataLen; i++)
|
||||
{
|
||||
highs[i] = bars[i].High;
|
||||
lows[i] = bars[i].Low;
|
||||
closes[i] = bars[i].Close;
|
||||
}
|
||||
|
||||
Tr.Batch(highs, lows, closes, spanOutput);
|
||||
|
||||
// Compare last 50 values
|
||||
int compareStart = dataLen - 50;
|
||||
for (int i = compareStart; i < dataLen; i++)
|
||||
{
|
||||
double batch = batchResult[i].Value;
|
||||
double span = spanOutput[i];
|
||||
|
||||
// Batch and Span should match exactly
|
||||
Assert.Equal(batch, span, Tolerance);
|
||||
}
|
||||
|
||||
// Final values should match
|
||||
Assert.Equal(tr1.Last.Value, batchResult[dataLen - 1].Value, 1e-8);
|
||||
Assert.Equal(tr1.Last.Value, spanOutput[dataLen - 1], 1e-8);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Span API Tests
|
||||
|
||||
[Fact]
|
||||
public void Batch_ValidatesOutputLength()
|
||||
{
|
||||
double[] highs = [101, 102, 103];
|
||||
double[] lows = [99, 98, 97];
|
||||
double[] closes = [100, 101, 102];
|
||||
double[] output = new double[2]; // Too short
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Tr.Batch(highs, lows, closes, output));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_ValidatesInputLengths()
|
||||
{
|
||||
double[] highs = [101, 102, 103];
|
||||
double[] lows = [99, 98]; // Wrong length
|
||||
double[] closes = [100, 101, 102];
|
||||
double[] output = new double[3];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Tr.Batch(highs, lows, closes, output));
|
||||
Assert.Equal("low", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_EmptyInput_ProducesNoOutput()
|
||||
{
|
||||
double[] highs = [];
|
||||
double[] lows = [];
|
||||
double[] closes = [];
|
||||
double[] output = [];
|
||||
|
||||
Tr.Batch(highs, lows, closes, output);
|
||||
// Should not throw
|
||||
Assert.Empty(output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_MatchesStreamingMode()
|
||||
{
|
||||
const int dataLen = 50;
|
||||
var bars = GenerateBars(dataLen);
|
||||
|
||||
double[] highs = new double[dataLen];
|
||||
double[] lows = new double[dataLen];
|
||||
double[] closes = new double[dataLen];
|
||||
|
||||
for (int i = 0; i < dataLen; i++)
|
||||
{
|
||||
highs[i] = bars[i].High;
|
||||
lows[i] = bars[i].Low;
|
||||
closes[i] = bars[i].Close;
|
||||
}
|
||||
|
||||
// Streaming
|
||||
var tr = new Tr();
|
||||
for (int i = 0; i < dataLen; i++)
|
||||
{
|
||||
tr.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Batch
|
||||
double[] batchOutput = new double[dataLen];
|
||||
Tr.Batch(highs, lows, closes, batchOutput);
|
||||
|
||||
// Compare final value
|
||||
Assert.Equal(tr.Last.Value, batchOutput[dataLen - 1], 1e-8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_LargeDataset_NoStackOverflow()
|
||||
{
|
||||
const int dataLen = 10000;
|
||||
double[] highs = new double[dataLen];
|
||||
double[] lows = new double[dataLen];
|
||||
double[] closes = new double[dataLen];
|
||||
double[] output = new double[dataLen];
|
||||
|
||||
// Fill with realistic data
|
||||
double price = 100.0;
|
||||
var rng = new Random(42);
|
||||
for (int i = 0; i < dataLen; i++)
|
||||
{
|
||||
double volatility = 0.02;
|
||||
double high = price * (1 + rng.NextDouble() * volatility);
|
||||
double low = price * (1 - rng.NextDouble() * volatility);
|
||||
double close = low + rng.NextDouble() * (high - low);
|
||||
|
||||
highs[i] = high;
|
||||
lows[i] = low;
|
||||
closes[i] = close;
|
||||
price = close;
|
||||
}
|
||||
|
||||
Tr.Batch(highs, lows, closes, output);
|
||||
|
||||
// Verify all outputs are valid
|
||||
for (int i = 0; i < dataLen; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]));
|
||||
Assert.True(output[i] >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Chainability Tests
|
||||
|
||||
[Fact]
|
||||
public void Pub_FiresOnUpdate()
|
||||
{
|
||||
var tr = new Tr();
|
||||
int eventCount = 0;
|
||||
|
||||
tr.Pub += (object? sender, in TValueEventArgs args) => eventCount++;
|
||||
|
||||
tr.Update(new TBar(DateTime.UtcNow.AddSeconds(0), 99, 101, 97, 100, 1000));
|
||||
tr.Update(new TBar(DateTime.UtcNow.AddSeconds(1), 100, 105, 98, 103, 1000));
|
||||
tr.Update(new TBar(DateTime.UtcNow.AddSeconds(2), 102, 108, 100, 106, 1000));
|
||||
|
||||
Assert.Equal(3, eventCount);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TBarSeries Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_TBarSeries_ReturnsCorrectLength()
|
||||
{
|
||||
var tr = new Tr();
|
||||
var bars = GenerateBars(50);
|
||||
|
||||
var result = tr.Update(bars);
|
||||
|
||||
Assert.Equal(50, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Static_TBarSeries_Works()
|
||||
{
|
||||
var bars = GenerateBars(50);
|
||||
|
||||
var result = Tr.Calculate(bars);
|
||||
|
||||
Assert.Equal(50, result.Count);
|
||||
Assert.All(result.Values.ToArray(), v => Assert.True(v >= 0));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Prime Tests
|
||||
|
||||
[Fact]
|
||||
public void Prime_SetsInitialState()
|
||||
{
|
||||
var tr = new Tr();
|
||||
double[] warmupData = [100, 101, 102, 103, 104];
|
||||
|
||||
tr.Prime(warmupData);
|
||||
|
||||
Assert.True(tr.IsHot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,660 @@
|
||||
namespace QuanTAlib.Test;
|
||||
|
||||
using Xunit;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for TR (True Range).
|
||||
/// TR = max(High - Low, |High - prevClose|, |Low - prevClose|)
|
||||
/// First bar uses High - Low only.
|
||||
/// </summary>
|
||||
public class TrValidationTests
|
||||
{
|
||||
private static TBarSeries GenerateTestData(int count = 100)
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
// === Mathematical Validation ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates the TR formula: max(H-L, |H-pC|, |L-pC|)
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_Formula_IsCorrect()
|
||||
{
|
||||
double high = 105.0;
|
||||
double low = 95.0;
|
||||
double prevClose = 100.0;
|
||||
|
||||
double tr1 = high - low; // 10
|
||||
double tr2 = Math.Abs(high - prevClose); // 5
|
||||
double tr3 = Math.Abs(low - prevClose); // 5
|
||||
|
||||
double expected = Math.Max(tr1, Math.Max(tr2, tr3)); // 10
|
||||
|
||||
Assert.Equal(10.0, expected, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates TR with gap up scenario.
|
||||
/// Gap up: prevClose below current Low, so |H-pC| > H-L
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_GapUp_CapturesGap()
|
||||
{
|
||||
double high = 115.0;
|
||||
double low = 110.0;
|
||||
double prevClose = 100.0; // Gap up from 100 to 110-115
|
||||
|
||||
double tr1 = high - low; // 5
|
||||
double tr2 = Math.Abs(high - prevClose); // 15
|
||||
double tr3 = Math.Abs(low - prevClose); // 10
|
||||
|
||||
double expected = Math.Max(tr1, Math.Max(tr2, tr3)); // 15
|
||||
|
||||
Assert.Equal(15.0, expected, 10);
|
||||
Assert.True(expected > tr1, "TR should capture the gap, exceeding H-L range");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates TR with gap down scenario.
|
||||
/// Gap down: prevClose above current High, so |L-pC| > H-L
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_GapDown_CapturesGap()
|
||||
{
|
||||
double high = 95.0;
|
||||
double low = 90.0;
|
||||
double prevClose = 110.0; // Gap down from 110 to 90-95
|
||||
|
||||
double tr1 = high - low; // 5
|
||||
double tr2 = Math.Abs(high - prevClose); // 15
|
||||
double tr3 = Math.Abs(low - prevClose); // 20
|
||||
|
||||
double expected = Math.Max(tr1, Math.Max(tr2, tr3)); // 20
|
||||
|
||||
Assert.Equal(20.0, expected, 10);
|
||||
Assert.True(expected > tr1, "TR should capture the gap, exceeding H-L range");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates TR when prevClose is within H-L range (no gap).
|
||||
/// In this case TR = H - L
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_NoGap_EqualsHighMinusLow()
|
||||
{
|
||||
double high = 105.0;
|
||||
double low = 95.0;
|
||||
double prevClose = 100.0; // Within range
|
||||
|
||||
double tr1 = high - low; // 10
|
||||
double tr2 = Math.Abs(high - prevClose); // 5
|
||||
double tr3 = Math.Abs(low - prevClose); // 5
|
||||
|
||||
double expected = Math.Max(tr1, Math.Max(tr2, tr3)); // 10
|
||||
|
||||
Assert.Equal(tr1, expected, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates first bar uses H - L only.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_FirstBar_UsesHighMinusLow()
|
||||
{
|
||||
var tr = new Tr();
|
||||
var bar = new TBar(DateTime.UtcNow.Ticks, 100, 110, 90, 105, 1000);
|
||||
|
||||
var result = tr.Update(bar);
|
||||
|
||||
Assert.Equal(20.0, result.Value, 10); // 110 - 90 = 20
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates second bar uses full TR formula.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_SecondBar_UsesFullFormula()
|
||||
{
|
||||
var tr = new Tr();
|
||||
|
||||
// First bar: close at 100
|
||||
var bar1 = new TBar(DateTime.UtcNow.Ticks, 98, 102, 98, 100, 1000);
|
||||
tr.Update(bar1);
|
||||
|
||||
// Second bar: gap up, H=115, L=110, pC=100
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1).Ticks, 110, 115, 110, 113, 1000);
|
||||
var result = tr.Update(bar2);
|
||||
|
||||
// TR = max(5, 15, 10) = 15
|
||||
Assert.Equal(15.0, result.Value, 10);
|
||||
}
|
||||
|
||||
// === Streaming Validation ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates streaming calculation matches manual calculation.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_StreamingMatchesManual()
|
||||
{
|
||||
var tr = new Tr();
|
||||
var bars = GenerateTestData(50);
|
||||
|
||||
double? prevClose = null;
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var bar = bars[i];
|
||||
var result = tr.Update(bar);
|
||||
|
||||
double expected;
|
||||
if (prevClose == null)
|
||||
{
|
||||
expected = bar.High - bar.Low;
|
||||
}
|
||||
else
|
||||
{
|
||||
double tr1 = bar.High - bar.Low;
|
||||
double tr2 = Math.Abs(bar.High - prevClose.Value);
|
||||
double tr3 = Math.Abs(bar.Low - prevClose.Value);
|
||||
expected = Math.Max(tr1, Math.Max(tr2, tr3));
|
||||
}
|
||||
|
||||
Assert.Equal(expected, result.Value, 10);
|
||||
prevClose = bar.Close;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates batch calculation matches streaming.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_BatchMatchesStreaming()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
|
||||
// Streaming
|
||||
var streamingTr = new Tr();
|
||||
var streamingResults = new double[bars.Count];
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingResults[i] = streamingTr.Update(bars[i]).Value;
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchOutput = new double[bars.Count];
|
||||
Tr.Batch(bars, batchOutput);
|
||||
|
||||
// Compare all values
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i], batchOutput[i], 10);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates TBarSeries batch matches streaming.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_TBarSeriesBatchMatchesStreaming()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
|
||||
// Streaming
|
||||
var streamingTr = new Tr();
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingTr.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Batch via TBarSeries
|
||||
var batchResult = Tr.Calculate(bars);
|
||||
|
||||
Assert.Equal(streamingTr.Last.Value, batchResult.Last.Value, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates span-based batch matches streaming.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_SpanBatchMatchesStreaming()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
|
||||
// Streaming
|
||||
var streamingTr = new Tr();
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingTr.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Extract OHLC
|
||||
var highs = new double[bars.Count];
|
||||
var lows = new double[bars.Count];
|
||||
var closes = new double[bars.Count];
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
highs[i] = bars[i].High;
|
||||
lows[i] = bars[i].Low;
|
||||
closes[i] = bars[i].Close;
|
||||
}
|
||||
|
||||
// Span batch
|
||||
var output = new double[bars.Count];
|
||||
Tr.Batch(highs, lows, closes, output);
|
||||
|
||||
Assert.Equal(streamingTr.Last.Value, output[^1], 10);
|
||||
}
|
||||
|
||||
// === Property Validation ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates TR is always non-negative.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_Output_IsNonNegative()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
var tr = new Tr();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = tr.Update(bars[i]);
|
||||
Assert.True(result.Value >= 0, $"TR should be non-negative at bar {i}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates TR >= High - Low for all bars (since it's the max of three components).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_GreaterOrEqualToHighMinusLow()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
var tr = new Tr();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var bar = bars[i];
|
||||
var result = tr.Update(bar);
|
||||
double hlRange = bar.High - bar.Low;
|
||||
|
||||
Assert.True(result.Value >= hlRange - 1e-10,
|
||||
$"TR should be >= H-L at bar {i}. TR={result.Value}, H-L={hlRange}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates TR output is always finite.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_Output_IsFinite()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
var tr = new Tr();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = tr.Update(bars[i]);
|
||||
Assert.True(double.IsFinite(result.Value), $"TR should be finite at bar {i}");
|
||||
}
|
||||
}
|
||||
|
||||
// === Edge Cases ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates handling of flat bars (H = L).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_FlatBars_HandledCorrectly()
|
||||
{
|
||||
var tr = new Tr();
|
||||
|
||||
// First bar: flat
|
||||
var bar1 = new TBar(DateTime.UtcNow.Ticks, 100, 100, 100, 100, 1000);
|
||||
var result1 = tr.Update(bar1);
|
||||
Assert.Equal(0.0, result1.Value, 10);
|
||||
|
||||
// Second bar: flat but different price (gap)
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1).Ticks, 105, 105, 105, 105, 1000);
|
||||
var result2 = tr.Update(bar2);
|
||||
Assert.Equal(5.0, result2.Value, 10); // |105-100| = 5
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates handling of very large gaps.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_LargeGaps_HandledCorrectly()
|
||||
{
|
||||
var tr = new Tr();
|
||||
|
||||
// First bar at 100
|
||||
var bar1 = new TBar(DateTime.UtcNow.Ticks, 100, 101, 99, 100, 1000);
|
||||
tr.Update(bar1);
|
||||
|
||||
// Second bar with huge gap up
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1).Ticks, 200, 202, 198, 200, 1000);
|
||||
var result = tr.Update(bar2);
|
||||
|
||||
// TR = max(4, 102, 98) = 102
|
||||
Assert.Equal(102.0, result.Value, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates handling of very small ranges.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_SmallRanges_HandledCorrectly()
|
||||
{
|
||||
var tr = new Tr();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bar = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i).Ticks,
|
||||
100.0, 100.001, 99.999, 100.0, 1000
|
||||
);
|
||||
var result = tr.Update(bar);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.True(result.Value >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates bar correction works correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_BarCorrection_WorksCorrectly()
|
||||
{
|
||||
var tr = new Tr();
|
||||
var bars = GenerateTestData(20);
|
||||
|
||||
// Feed initial bars
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
tr.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
// Add new bar
|
||||
tr.Update(bars[15], isNew: true);
|
||||
double afterNew = tr.Last.Value;
|
||||
|
||||
// Correct with different bar (much larger range)
|
||||
var correctedBar = new TBar(
|
||||
bars[15].Time,
|
||||
100, 200, 50, 150, 1000
|
||||
);
|
||||
tr.Update(correctedBar, isNew: false);
|
||||
double afterCorrection = tr.Last.Value;
|
||||
|
||||
// Restore original
|
||||
tr.Update(bars[15], isNew: false);
|
||||
double afterRestore = tr.Last.Value;
|
||||
|
||||
Assert.NotEqual(afterNew, afterCorrection);
|
||||
Assert.Equal(afterNew, afterRestore, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates iterative corrections converge.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_IterativeCorrections_Converge()
|
||||
{
|
||||
var tr = new Tr();
|
||||
var bars = GenerateTestData(20);
|
||||
|
||||
// Feed bars
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
tr.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
// Multiple corrections on same bar
|
||||
for (int j = 0; j < 5; j++)
|
||||
{
|
||||
var tempBar = new TBar(
|
||||
bars[14].Time,
|
||||
100 + j, 110 + j, 90 + j, 105 + j, 1000
|
||||
);
|
||||
tr.Update(tempBar, isNew: false);
|
||||
}
|
||||
|
||||
// Final correction back to original
|
||||
tr.Update(bars[14], isNew: false);
|
||||
double afterCorrections = tr.Last.Value;
|
||||
|
||||
// Fresh calculation
|
||||
var trFresh = new Tr();
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
trFresh.Update(bars[i], isNew: true);
|
||||
}
|
||||
double freshValue = trFresh.Last.Value;
|
||||
|
||||
Assert.Equal(freshValue, afterCorrections, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates Reset clears state completely.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_Reset_ClearsState()
|
||||
{
|
||||
var tr = new Tr();
|
||||
var bars = GenerateTestData(30);
|
||||
|
||||
// Feed bars
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
tr.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Reset
|
||||
tr.Reset();
|
||||
|
||||
// State should be cleared
|
||||
Assert.False(tr.IsHot);
|
||||
Assert.Equal(default, tr.Last);
|
||||
|
||||
// Feed bars again
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
tr.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Fresh indicator
|
||||
var trFresh = new Tr();
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
trFresh.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.Equal(trFresh.Last.Value, tr.Last.Value, 10);
|
||||
}
|
||||
|
||||
// === Consistency Tests ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates stability over repeated runs with same seed.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_Stability_ConsistentOverRepeatedRuns()
|
||||
{
|
||||
var results = new List<double>();
|
||||
|
||||
for (int run = 0; run < 3; run++)
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var tr = new Tr();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
tr.Update(bars[i]);
|
||||
}
|
||||
results.Add(tr.Last.Value);
|
||||
}
|
||||
|
||||
Assert.Equal(results[0], results[1], 15);
|
||||
Assert.Equal(results[1], results[2], 15);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates TR responds to volatility regime changes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_RespondsToVolatilityChange()
|
||||
{
|
||||
var tr = new Tr();
|
||||
var lowVolResults = new List<double>();
|
||||
var highVolResults = new List<double>();
|
||||
|
||||
// Low volatility regime
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var bar = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i).Ticks,
|
||||
100.0, 101.0, 99.0, 100.0, 1000
|
||||
);
|
||||
lowVolResults.Add(tr.Update(bar).Value);
|
||||
}
|
||||
|
||||
// High volatility regime
|
||||
for (int i = 20; i < 40; i++)
|
||||
{
|
||||
var bar = new TBar(
|
||||
DateTime.UtcNow.AddMinutes(i).Ticks,
|
||||
100.0, 110.0, 90.0, 100.0, 1000
|
||||
);
|
||||
highVolResults.Add(tr.Update(bar).Value);
|
||||
}
|
||||
|
||||
double avgLowVol = lowVolResults.Skip(1).Average(); // Skip first (no gap reference)
|
||||
double avgHighVol = highVolResults.Average();
|
||||
|
||||
Assert.True(avgHighVol > avgLowVol * 5,
|
||||
$"High vol TR ({avgHighVol:F2}) should be much larger than low vol ({avgLowVol:F2})");
|
||||
}
|
||||
|
||||
// === WarmupPeriod Validation ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates WarmupPeriod is 1 (TR is hot immediately).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_WarmupPeriod_IsOne()
|
||||
{
|
||||
var tr = new Tr();
|
||||
Assert.Equal(1, tr.WarmupPeriod);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates IsHot is true after first bar.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_IsHot_AfterFirstBar()
|
||||
{
|
||||
var tr = new Tr();
|
||||
Assert.False(tr.IsHot);
|
||||
|
||||
var bar = new TBar(DateTime.UtcNow.Ticks, 100, 105, 95, 102, 1000);
|
||||
tr.Update(bar);
|
||||
|
||||
Assert.True(tr.IsHot);
|
||||
}
|
||||
|
||||
// === NaN/Infinity Handling ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates NaN high uses last valid value.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_NaNHigh_UsesLastValid()
|
||||
{
|
||||
var tr = new Tr();
|
||||
|
||||
var bar1 = new TBar(DateTime.UtcNow.Ticks, 100, 105, 95, 100, 1000);
|
||||
tr.Update(bar1);
|
||||
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1).Ticks, 100, double.NaN, 95, 100, 1000);
|
||||
var result = tr.Update(bar2);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates NaN low uses last valid value.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_NaNLow_UsesLastValid()
|
||||
{
|
||||
var tr = new Tr();
|
||||
|
||||
var bar1 = new TBar(DateTime.UtcNow.Ticks, 100, 105, 95, 100, 1000);
|
||||
tr.Update(bar1);
|
||||
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1).Ticks, 100, 105, double.NaN, 100, 1000);
|
||||
var result = tr.Update(bar2);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates NaN close uses last valid value.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_NaNClose_UsesLastValid()
|
||||
{
|
||||
var tr = new Tr();
|
||||
|
||||
var bar1 = new TBar(DateTime.UtcNow.Ticks, 100, 105, 95, 100, 1000);
|
||||
tr.Update(bar1);
|
||||
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1).Ticks, 100, 105, 95, double.NaN, 1000);
|
||||
var result = tr.Update(bar2);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates Infinity values are handled.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_Infinity_UsesLastValid()
|
||||
{
|
||||
var tr = new Tr();
|
||||
|
||||
var bar1 = new TBar(DateTime.UtcNow.Ticks, 100, 105, 95, 100, 1000);
|
||||
tr.Update(bar1);
|
||||
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1).Ticks, 100, double.PositiveInfinity, 95, 100, 1000);
|
||||
var result = tr.Update(bar2);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates batch handles NaN values.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Tr_BatchNaN_HandledCorrectly()
|
||||
{
|
||||
var highs = new double[] { 105, 106, double.NaN, 108, 109 };
|
||||
var lows = new double[] { 95, 96, 97, double.NaN, 99 };
|
||||
var closes = new double[] { 100, 101, 102, 103, double.NaN };
|
||||
var output = new double[5];
|
||||
|
||||
Tr.Batch(highs, lows, closes, output);
|
||||
|
||||
for (int i = 0; i < output.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]), $"Output at index {i} should be finite");
|
||||
Assert.True(output[i] >= 0, $"Output at index {i} should be non-negative");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
// True Range (TR) Indicator
|
||||
// Measures the maximum price movement including gaps from the previous close
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// TR: True Range
|
||||
/// A volatility measure that captures the maximum price movement including gaps.
|
||||
/// True Range accounts for overnight gaps by comparing current High-Low range
|
||||
/// against the previous close.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Calculation steps:</b>
|
||||
/// <list type="number">
|
||||
/// <item>Calculate three ranges: (High - Low), |High - prevClose|, |Low - prevClose|</item>
|
||||
/// <item>True Range = max(all three ranges)</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <b>Key characteristics:</b>
|
||||
/// <list type="bullet">
|
||||
/// <item>Bar-by-bar calculation (no smoothing)</item>
|
||||
/// <item>Always positive (absolute values used for gap calculations)</item>
|
||||
/// <item>First bar uses High - Low (no previous close available)</item>
|
||||
/// <item>Foundation for ATR (Average True Range)</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <b>Sources:</b>
|
||||
/// J. Welles Wilder Jr. (1978). "New Concepts in Technical Trading Systems"
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Tr : AbstractBase
|
||||
{
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(
|
||||
double PrevClose,
|
||||
double LastValidHigh,
|
||||
double LastValidLow,
|
||||
double LastValidClose,
|
||||
double LastTr,
|
||||
int Count
|
||||
);
|
||||
private State _s;
|
||||
private State _ps;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the Tr class.
|
||||
/// </summary>
|
||||
public Tr()
|
||||
{
|
||||
WarmupPeriod = 1;
|
||||
Name = "Tr";
|
||||
_s = new State(double.NaN, 0, 0, 0, 0, 0);
|
||||
_ps = _s;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the Tr class with a source.
|
||||
/// </summary>
|
||||
/// <param name="source">The data source for chaining.</param>
|
||||
public Tr(ITValuePublisher source) : this()
|
||||
{
|
||||
source.Pub += Handle;
|
||||
}
|
||||
|
||||
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
|
||||
|
||||
/// <summary>
|
||||
/// True if the indicator has enough data for valid results.
|
||||
/// </summary>
|
||||
public override bool IsHot => _s.Count >= WarmupPeriod;
|
||||
|
||||
/// <summary>
|
||||
/// Computes the True Range for given bar values.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double ComputeTrueRange(double high, double low, double prevClose)
|
||||
{
|
||||
double tr1 = high - low;
|
||||
double tr2 = Math.Abs(high - prevClose);
|
||||
double tr3 = Math.Abs(low - prevClose);
|
||||
return Math.Max(tr1, Math.Max(tr2, tr3));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a TValue input.
|
||||
/// For TR, this treats the value as a close price (uses value for H, L, and C).
|
||||
/// Prefer Update(TBar) for standard OHLC data.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
// For TValue input, treat it as if H=L=C (no range)
|
||||
return UpdateCore(input.Time, input.Value, input.Value, input.Value, isNew);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a new bar (preferred method).
|
||||
/// </summary>
|
||||
/// <param name="bar">The input bar.</param>
|
||||
/// <param name="isNew">Whether this is a new bar or an update.</param>
|
||||
/// <returns>The calculated True Range value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TBar bar, bool isNew = true)
|
||||
{
|
||||
return UpdateCore(bar.Time, bar.High, bar.Low, bar.Close, isNew);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a bar series.
|
||||
/// </summary>
|
||||
/// <param name="source">The source bar series.</param>
|
||||
/// <returns>A TSeries containing the True Range values.</returns>
|
||||
public TSeries Update(TBarSeries source)
|
||||
{
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
var tSpan = CollectionsMarshal.AsSpan(t);
|
||||
var vSpan = CollectionsMarshal.AsSpan(v);
|
||||
|
||||
// Extract OHLC data
|
||||
Span<double> highs = len <= 128 ? stackalloc double[len] : new double[len];
|
||||
Span<double> lows = len <= 128 ? stackalloc double[len] : new double[len];
|
||||
Span<double> closes = len <= 128 ? stackalloc double[len] : new double[len];
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
highs[i] = source[i].High;
|
||||
lows[i] = source[i].Low;
|
||||
closes[i] = source[i].Close;
|
||||
tSpan[i] = source[i].Time;
|
||||
}
|
||||
|
||||
Batch(highs, lows, closes, vSpan);
|
||||
|
||||
// Update internal state
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
Update(source[i], isNew: true);
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
var tSpan = CollectionsMarshal.AsSpan(t);
|
||||
var vSpan = CollectionsMarshal.AsSpan(v);
|
||||
var values = source.Values;
|
||||
|
||||
// When using TSeries (close prices only), TR = |close[i] - close[i-1]| (gap-based)
|
||||
// This is a degenerate case - prefer TBarSeries
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
tSpan[i] = source.Times[i];
|
||||
vSpan[i] = (i == 0) ? 0 : Math.Abs(values[i] - values[i - 1]);
|
||||
}
|
||||
|
||||
// Update internal state
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
Update(new TValue(source.Times[i], values[i]), isNew: true);
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private TValue UpdateCore(long timeTicks, double high, double low, double close, bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_ps = _s;
|
||||
}
|
||||
else
|
||||
{
|
||||
_s = _ps;
|
||||
}
|
||||
|
||||
var s = _s;
|
||||
|
||||
// Handle non-finite values - use last valid values
|
||||
if (!double.IsFinite(high))
|
||||
{
|
||||
high = s.LastValidHigh;
|
||||
}
|
||||
else
|
||||
{
|
||||
s.LastValidHigh = high;
|
||||
}
|
||||
|
||||
if (!double.IsFinite(low))
|
||||
{
|
||||
low = s.LastValidLow;
|
||||
}
|
||||
else
|
||||
{
|
||||
s.LastValidLow = low;
|
||||
}
|
||||
|
||||
if (!double.IsFinite(close))
|
||||
{
|
||||
close = s.LastValidClose;
|
||||
}
|
||||
else
|
||||
{
|
||||
s.LastValidClose = close;
|
||||
}
|
||||
|
||||
double tr;
|
||||
if (s.Count == 0 || !double.IsFinite(s.PrevClose))
|
||||
{
|
||||
// First bar or no previous close: use High - Low
|
||||
tr = high - low;
|
||||
}
|
||||
else
|
||||
{
|
||||
tr = ComputeTrueRange(high, low, s.PrevClose);
|
||||
}
|
||||
|
||||
if (!double.IsFinite(tr) || tr < 0)
|
||||
{
|
||||
tr = s.LastTr;
|
||||
}
|
||||
else
|
||||
{
|
||||
s.LastTr = tr;
|
||||
}
|
||||
|
||||
// Update state
|
||||
s.PrevClose = close;
|
||||
if (isNew)
|
||||
{
|
||||
s.Count++;
|
||||
}
|
||||
|
||||
_s = s;
|
||||
|
||||
Last = new TValue(timeTicks, tr);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
Update(new TValue(DateTime.UtcNow, source[i]), isNew: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Reset()
|
||||
{
|
||||
_s = new State(double.NaN, 0, 0, 0, 0, 0);
|
||||
_ps = _s;
|
||||
Last = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates True Range for a bar series (static).
|
||||
/// </summary>
|
||||
/// <param name="source">The source bar series.</param>
|
||||
/// <returns>A TSeries containing the True Range values.</returns>
|
||||
public static TSeries Calculate(TBarSeries source)
|
||||
{
|
||||
var tr = new Tr();
|
||||
return tr.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch calculation using spans for OHLC data.
|
||||
/// </summary>
|
||||
/// <param name="high">High prices.</param>
|
||||
/// <param name="low">Low prices.</param>
|
||||
/// <param name="close">Close prices.</param>
|
||||
/// <param name="output">Output True Range values.</param>
|
||||
public static void Batch(
|
||||
ReadOnlySpan<double> high,
|
||||
ReadOnlySpan<double> low,
|
||||
ReadOnlySpan<double> close,
|
||||
Span<double> output)
|
||||
{
|
||||
int len = high.Length;
|
||||
if (low.Length != len || close.Length != len)
|
||||
{
|
||||
throw new ArgumentException("All input spans must have the same length", nameof(low));
|
||||
}
|
||||
if (output.Length < len)
|
||||
{
|
||||
throw new ArgumentException("Output span must be at least as long as input spans", nameof(output));
|
||||
}
|
||||
|
||||
if (len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
double lastValidHigh = 0;
|
||||
double lastValidLow = 0;
|
||||
double lastValidClose = 0;
|
||||
double lastTr = 0;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double h = high[i];
|
||||
double l = low[i];
|
||||
double c = close[i];
|
||||
|
||||
// Handle non-finite values
|
||||
if (!double.IsFinite(h))
|
||||
{
|
||||
h = lastValidHigh;
|
||||
}
|
||||
else
|
||||
{
|
||||
lastValidHigh = h;
|
||||
}
|
||||
|
||||
if (!double.IsFinite(l))
|
||||
{
|
||||
l = lastValidLow;
|
||||
}
|
||||
else
|
||||
{
|
||||
lastValidLow = l;
|
||||
}
|
||||
|
||||
double tr;
|
||||
if (i == 0)
|
||||
{
|
||||
// First bar: use High - Low
|
||||
tr = h - l;
|
||||
}
|
||||
else
|
||||
{
|
||||
double prevClose = close[i - 1];
|
||||
if (!double.IsFinite(prevClose))
|
||||
{
|
||||
// Fall back to last valid close from previous bars
|
||||
prevClose = lastValidClose;
|
||||
}
|
||||
tr = ComputeTrueRange(h, l, prevClose);
|
||||
}
|
||||
|
||||
// Update lastValidClose AFTER computing TR so fallback uses previous bar's close
|
||||
if (double.IsFinite(c))
|
||||
{
|
||||
lastValidClose = c;
|
||||
}
|
||||
|
||||
if (!double.IsFinite(tr) || tr < 0)
|
||||
{
|
||||
tr = lastTr;
|
||||
}
|
||||
else
|
||||
{
|
||||
lastTr = tr;
|
||||
}
|
||||
|
||||
output[i] = tr;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch calculation using a TBarSeries (convenience overload).
|
||||
/// </summary>
|
||||
/// <param name="source">The source bar series.</param>
|
||||
/// <param name="output">Output True Range values.</param>
|
||||
public static void Batch(TBarSeries source, Span<double> output)
|
||||
{
|
||||
int len = source.Count;
|
||||
if (output.Length < len)
|
||||
{
|
||||
throw new ArgumentException("Output span must be at least as long as source", nameof(output));
|
||||
}
|
||||
|
||||
if (len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Span<double> highs = len <= 128 ? stackalloc double[len] : new double[len];
|
||||
Span<double> lows = len <= 128 ? stackalloc double[len] : new double[len];
|
||||
Span<double> closes = len <= 128 ? stackalloc double[len] : new double[len];
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
highs[i] = source[i].High;
|
||||
lows[i] = source[i].Low;
|
||||
closes[i] = source[i].Close;
|
||||
}
|
||||
|
||||
Batch(highs, lows, closes, output);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
# TR: True Range
|
||||
|
||||
> "The true measure of volatility isn't just where price traveled within the bar, but whether it leaped from where it was."
|
||||
|
||||
True Range (TR) is a volatility measure that captures the maximum price movement for each bar, including any gap from the previous close. Developed by J. Welles Wilder Jr. in 1978, TR forms the foundation for Average True Range (ATR) and numerous other volatility-based indicators. Unlike simple High-Low range, TR accounts for overnight gaps and opening jumps, providing a complete picture of price movement.
|
||||
|
||||
## Historical Context
|
||||
|
||||
J. Welles Wilder Jr. introduced True Range in his seminal 1978 book "New Concepts in Technical Trading Systems." This same work introduced many other foundational indicators including RSI, ATR, Parabolic SAR, and the ADX family.
|
||||
|
||||
Wilder recognized that the traditional High-Low range fails to capture the full extent of price movement when markets gap at the open. A stock might have a narrow intraday range but a massive overnight gap—the simple High-Low would miss this volatility entirely. True Range solves this by considering the previous close as a potential extreme.
|
||||
|
||||
The elegance of TR lies in its simplicity: take the maximum of three simple calculations. This approach captures all possible price extremes while requiring minimal data (just High, Low, Close, and the previous Close). TR became the building block for ATR, which Wilder used extensively for stop-loss placement and position sizing.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
### 1. Three Range Components
|
||||
|
||||
True Range considers three potential extremes:
|
||||
|
||||
$$
|
||||
TR_1 = H_t - L_t
|
||||
$$
|
||||
|
||||
$$
|
||||
TR_2 = |H_t - C_{t-1}|
|
||||
$$
|
||||
|
||||
$$
|
||||
TR_3 = |L_t - C_{t-1}|
|
||||
$$
|
||||
|
||||
where:
|
||||
|
||||
- $H_t, L_t$ = High, Low of current bar
|
||||
- $C_{t-1}$ = Close of previous bar
|
||||
|
||||
### 2. True Range Calculation
|
||||
|
||||
The True Range is the maximum of all three components:
|
||||
|
||||
$$
|
||||
TR_t = \max(TR_1, TR_2, TR_3)
|
||||
$$
|
||||
|
||||
Expanded:
|
||||
|
||||
$$
|
||||
TR_t = \max(H_t - L_t, |H_t - C_{t-1}|, |L_t - C_{t-1}|)
|
||||
$$
|
||||
|
||||
### 3. First Bar Handling
|
||||
|
||||
For the first bar (no previous close available):
|
||||
|
||||
$$
|
||||
TR_0 = H_0 - L_0
|
||||
$$
|
||||
|
||||
The implementation uses only the High-Low range when there's no history to reference.
|
||||
|
||||
### 4. Scenario Analysis
|
||||
|
||||
**No Gap (prevClose within H-L range):**
|
||||
|
||||
When $L_t \leq C_{t-1} \leq H_t$:
|
||||
|
||||
$$
|
||||
TR_t = H_t - L_t
|
||||
$$
|
||||
|
||||
The intraday range captures all movement since the gap components ($TR_2$, $TR_3$) are smaller.
|
||||
|
||||
**Gap Up (prevClose below Low):**
|
||||
|
||||
When $C_{t-1} < L_t$:
|
||||
|
||||
$$
|
||||
TR_t = H_t - C_{t-1} = TR_2
|
||||
$$
|
||||
|
||||
The gap up contributes to the range; price moved from yesterday's close to today's high.
|
||||
|
||||
**Gap Down (prevClose above High):**
|
||||
|
||||
When $C_{t-1} > H_t$:
|
||||
|
||||
$$
|
||||
TR_t = C_{t-1} - L_t = TR_3
|
||||
$$
|
||||
|
||||
The gap down contributes; price moved from yesterday's close to today's low.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Why Three Components?
|
||||
|
||||
Consider a price bar with these values:
|
||||
|
||||
- Yesterday close: $C_{t-1} = 100$
|
||||
- Today open: $O_t = 95$ (gap down)
|
||||
- Today high: $H_t = 98$
|
||||
- Today low: $L_t = 93$
|
||||
- Today close: $C_t = 96$
|
||||
|
||||
Traditional range: $H_t - L_t = 98 - 93 = 5$
|
||||
|
||||
True Range components:
|
||||
|
||||
- $TR_1 = 98 - 93 = 5$
|
||||
- $TR_2 = |98 - 100| = 2$
|
||||
- $TR_3 = |93 - 100| = 7$
|
||||
|
||||
$$
|
||||
TR_t = \max(5, 2, 7) = 7
|
||||
$$
|
||||
|
||||
The market actually moved 7 points (from 100 down to 93), not just 5. TR captures this correctly.
|
||||
|
||||
### Geometric Interpretation
|
||||
|
||||
True Range measures the maximum vertical distance price could have traveled from the previous close through today's bar. Visualize it as:
|
||||
|
||||
```
|
||||
Gap Down Case:
|
||||
┌── prevClose (100)
|
||||
│
|
||||
│ ← TR = 7
|
||||
│
|
||||
Today's Bar: [93 ──── 98]
|
||||
Low High
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
1. **Non-negativity**: $TR_t \geq 0$ always
|
||||
2. **Lower bound**: $TR_t \geq H_t - L_t$ (always at least the intraday range)
|
||||
3. **Unit**: Same unit as price (dollars, points, etc.)
|
||||
4. **No smoothing**: TR is a bar-by-bar calculation with no lookback
|
||||
5. **Immediate**: TR is "hot" after just one bar (WarmupPeriod = 1)
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode, Scalar)
|
||||
|
||||
Per-bar operations:
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| SUB | 3 | 1 | 3 |
|
||||
| ABS | 2 | 1 | 2 |
|
||||
| MAX | 2 | 1 | 2 |
|
||||
| **Total** | — | — | **~7 cycles** |
|
||||
|
||||
TR is extremely lightweight—one of the cheapest indicators to compute. No logarithms, no division, no transcendental functions.
|
||||
|
||||
### Batch Mode (512 values, SIMD/FMA)
|
||||
|
||||
| Operation | Scalar Ops | SIMD Ops (AVX2) | Speedup |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| Subtractions | 1536 | 192 | 8× |
|
||||
| Absolute values | 1024 | 128 | 8× |
|
||||
| Maximum | 1024 | 128 | 8× |
|
||||
|
||||
All operations vectorize perfectly with AVX2. No sequential dependencies limit SIMD utilization.
|
||||
|
||||
### Memory Profile
|
||||
|
||||
- **Per instance:** ~48 bytes (state struct)
|
||||
- **No ring buffer required** (only needs previous close)
|
||||
- **100 instances:** ~4.8 KB
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **Accuracy** | 10/10 | Exact calculation, no approximations |
|
||||
| **Timeliness** | 10/10 | Zero lag, bar-by-bar |
|
||||
| **Smoothness** | 2/10 | Unsmoothed, can be jagged |
|
||||
| **Simplicity** | 10/10 | Three comparisons, one max |
|
||||
| **Foundation** | 10/10 | Building block for ATR, etc. |
|
||||
|
||||
## Validation
|
||||
|
||||
TR is universally implemented with identical formulas:
|
||||
|
||||
| Library | Status | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **TA-Lib** | ✅ | Exact match (TRANGE function) |
|
||||
| **Skender** | ✅ | Exact match |
|
||||
| **Tulip** | ✅ | Exact match |
|
||||
| **OoplesFinance** | ✅ | Exact match |
|
||||
| **PineScript** | ✅ | Matches tr.pine reference |
|
||||
| **Manual** | ✅ | Validated against Wilder formula |
|
||||
|
||||
TR is one of the most consistently implemented indicators across all libraries.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **First bar handling**: The first bar has no previous close. The implementation uses High-Low for the first bar. Some implementations return NaN for the first bar—this one returns a valid (though incomplete) value.
|
||||
|
||||
2. **Confusing TR with ATR**: TR is the raw, unsmoothed value per bar. ATR is TR smoothed over a period. TR can be very volatile; ATR provides a more stable volatility estimate.
|
||||
|
||||
3. **Unit dependency**: TR is in the same units as price. A $500 stock might have TR=10 while a $50 stock has TR=1, even if percentage volatility is identical. Use NATR (Normalized ATR) or ATRP (ATR Percent) for percentage-based comparisons.
|
||||
|
||||
4. **Gap sensitivity**: TR captures gaps, which may or may not be desirable. For intraday-only volatility, use High-Low range instead.
|
||||
|
||||
5. **Comparing across assets**: Don't compare raw TR values across different-priced assets. TR=5 means different things for a $20 stock vs a $200 stock.
|
||||
|
||||
6. **Weekend/holiday gaps**: TR will capture large gaps after market closures. This may inflate volatility estimates around holidays. Some strategies filter these bars.
|
||||
|
||||
## Trading Applications
|
||||
|
||||
### Stop-Loss Placement (via ATR)
|
||||
|
||||
TR is the foundation for ATR-based stops:
|
||||
|
||||
```
|
||||
Long stop = Entry - (ATR × multiplier)
|
||||
Short stop = Entry + (ATR × multiplier)
|
||||
where ATR = smoothed TR
|
||||
```
|
||||
|
||||
### Position Sizing
|
||||
|
||||
Use TR/ATR for volatility-adjusted position sizing:
|
||||
|
||||
```
|
||||
Position size = (Account × Risk%) / (ATR × multiplier)
|
||||
```
|
||||
|
||||
Higher TR means more volatility, so smaller position.
|
||||
|
||||
### Breakout Detection
|
||||
|
||||
Large TR spikes indicate significant price movement:
|
||||
|
||||
```
|
||||
If TR_today > 2 × ATR_14: Potential breakout
|
||||
Monitor for continuation or reversal
|
||||
```
|
||||
|
||||
### Volatility Filtering
|
||||
|
||||
Filter trades based on minimum TR:
|
||||
|
||||
```
|
||||
If TR < threshold: Skip trade (too quiet, potential whipsaw)
|
||||
If TR > threshold: Proceed (sufficient volatility for trend)
|
||||
```
|
||||
|
||||
### Gap Analysis
|
||||
|
||||
Compare TR to High-Low range to quantify gap impact:
|
||||
|
||||
```
|
||||
Gap contribution = TR - (High - Low)
|
||||
If gap contribution > 50% of TR: Significant gap move
|
||||
```
|
||||
|
||||
## Relationship to Other Indicators
|
||||
|
||||
| Indicator | Relationship to TR |
|
||||
| :--- | :--- |
|
||||
| **ATR** | Smoothed TR (RMA/Wilder's MA) |
|
||||
| **NATR** | ATR / Close × 100 |
|
||||
| **ATRP** | ATR / Close × 100 (same as NATR) |
|
||||
| **Keltner Channel** | Uses ATR for band width |
|
||||
| **Chandelier Exit** | Uses ATR for trailing stop |
|
||||
| **SuperTrend** | Uses ATR for trend bands |
|
||||
| **ADX** | Uses TR in denominator for normalization |
|
||||
|
||||
## References
|
||||
|
||||
- Wilder, J. W. (1978). *New Concepts in Technical Trading Systems*. Trend Research.
|
||||
- Kaufman, P. J. (2013). *Trading Systems and Methods* (5th ed.). Wiley.
|
||||
- Murphy, J. J. (1999). *Technical Analysis of the Financial Markets*. New York Institute of Finance.
|
||||
Reference in New Issue
Block a user