mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-22 20:48:04 +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,189 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class AdrIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void AdrIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new AdrIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.Equal(AdrMethod.Sma, indicator.Method);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("ADR - Average Daily Range", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdrIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new AdrIndicator { Period = 20, Method = AdrMethod.Ema };
|
||||
Assert.Equal("ADR 20 Ema", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdrIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new AdrIndicator();
|
||||
|
||||
Assert.Equal(0, AdrIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdrIndicator_Initialize_CreatesInternalAdr()
|
||||
{
|
||||
var indicator = new AdrIndicator();
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdrIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new AdrIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data with volatility
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 2, 1000);
|
||||
|
||||
// Process update for each bar to simulate history loading
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
Assert.True(val > 0); // ADR should be positive with volatility
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdrIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new AdrIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 2, 1000);
|
||||
}
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Add new bar
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(20), 120, 128, 115, 125, 1500);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdrIndicator_DifferentPeriods_Work()
|
||||
{
|
||||
int[] periods = { 5, 10, 14, 20, 50 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var indicator = new AdrIndicator { Period = period };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 60; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 2, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val), $"Period {period} should produce finite value");
|
||||
Assert.True(val > 0, $"Period {period} should produce positive ADR");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdrIndicator_DifferentMethods_Work()
|
||||
{
|
||||
AdrMethod[] methods = { AdrMethod.Sma, AdrMethod.Ema, AdrMethod.Wma };
|
||||
|
||||
foreach (var method in methods)
|
||||
{
|
||||
var indicator = new AdrIndicator { Period = 14, Method = method };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 2, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val), $"Method {method} should produce finite value");
|
||||
Assert.True(val > 0, $"Method {method} should produce positive ADR");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdrIndicator_Period_CanBeChanged()
|
||||
{
|
||||
var indicator = new AdrIndicator();
|
||||
Assert.Equal(14, indicator.Period);
|
||||
|
||||
indicator.Period = 20;
|
||||
Assert.Equal(20, indicator.Period);
|
||||
|
||||
indicator.Period = 5;
|
||||
Assert.Equal(5, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdrIndicator_Method_CanBeChanged()
|
||||
{
|
||||
var indicator = new AdrIndicator();
|
||||
Assert.Equal(AdrMethod.Sma, indicator.Method);
|
||||
|
||||
indicator.Method = AdrMethod.Ema;
|
||||
Assert.Equal(AdrMethod.Ema, indicator.Method);
|
||||
|
||||
indicator.Method = AdrMethod.Wma;
|
||||
Assert.Equal(AdrMethod.Wma, indicator.Method);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdrIndicator_ShowColdValues_CanBeToggled()
|
||||
{
|
||||
var indicator = new AdrIndicator();
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = true;
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdrIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new AdrIndicator();
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Adr.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class AdrIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 1000, 1, 0)]
|
||||
public int Period { get; set; } = 14;
|
||||
|
||||
[InputParameter("Method", sortIndex: 2, variants: new object[] {
|
||||
"SMA", AdrMethod.Sma,
|
||||
"EMA", AdrMethod.Ema,
|
||||
"WMA", AdrMethod.Wma
|
||||
})]
|
||||
public AdrMethod Method { get; set; } = AdrMethod.Sma;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Adr _adr = null!;
|
||||
private readonly LineSeries _series;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"ADR {Period} {Method}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/volatility/adr/Adr.Quantower.cs";
|
||||
|
||||
public AdrIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "ADR - Average Daily Range";
|
||||
Description = "Measures the average price movement range over a specified period";
|
||||
|
||||
_series = new LineSeries(name: "ADR", color: Color.Yellow, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_adr = new Adr(Period, Method);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
TBar bar = this.GetInputBar(args);
|
||||
TValue result = _adr.Update(bar, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _adr.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,556 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class AdrTests
|
||||
{
|
||||
// ============== Constructor & Parameter Validation ==============
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidatesInput()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Adr(0));
|
||||
Assert.Throws<ArgumentException>(() => new Adr(-1));
|
||||
|
||||
var adr = new Adr(14);
|
||||
Assert.NotNull(adr);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidatesMethod()
|
||||
{
|
||||
var adrSma = new Adr(14, AdrMethod.Sma);
|
||||
var adrEma = new Adr(14, AdrMethod.Ema);
|
||||
var adrWma = new Adr(14, AdrMethod.Wma);
|
||||
|
||||
Assert.NotNull(adrSma);
|
||||
Assert.NotNull(adrEma);
|
||||
Assert.NotNull(adrWma);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InvalidMethod_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Adr(14, (AdrMethod)99));
|
||||
}
|
||||
|
||||
// ============== Basic Functionality ==============
|
||||
|
||||
[Fact]
|
||||
public void BasicCalculation_DoesNotCrash()
|
||||
{
|
||||
var adr = new Adr(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
adr.Update(bar);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(adr.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_ReturnsValue()
|
||||
{
|
||||
var adr = new Adr(14);
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
|
||||
Assert.Equal(0, adr.Last.Value);
|
||||
|
||||
TValue result = adr.Update(bar);
|
||||
|
||||
Assert.True(result.Value > 0);
|
||||
Assert.Equal(result.Value, adr.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FirstValue_ReturnsHighMinusLow()
|
||||
{
|
||||
var adr = new Adr(14);
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000);
|
||||
// First bar range = High - Low = 110 - 90 = 20
|
||||
// With SMA(14), first value = 20 (only one value in the average)
|
||||
|
||||
TValue result = adr.Update(bar);
|
||||
|
||||
Assert.Equal(20.0, result.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Properties_Accessible()
|
||||
{
|
||||
var adr = new Adr(14);
|
||||
|
||||
Assert.Equal(0, adr.Last.Value);
|
||||
Assert.False(adr.IsHot);
|
||||
Assert.Contains("Adr", adr.Name, StringComparison.Ordinal);
|
||||
Assert.True(adr.WarmupPeriod > 0);
|
||||
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
adr.Update(bar);
|
||||
|
||||
Assert.NotEqual(0, adr.Last.Value);
|
||||
}
|
||||
|
||||
// ============== Smoothing Method Tests ==============
|
||||
|
||||
[Fact]
|
||||
public void SmaMethod_Works()
|
||||
{
|
||||
var adr = new Adr(5, AdrMethod.Sma);
|
||||
var baseTime = DateTime.UtcNow;
|
||||
|
||||
// Feed 5 bars with consistent range of 10
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var bar = new TBar(baseTime.AddMinutes(i), 100, 105, 95, 100, 1000);
|
||||
adr.Update(bar);
|
||||
}
|
||||
|
||||
// SMA of [10, 10, 10, 10, 10] = 10
|
||||
Assert.Equal(10.0, adr.Last.Value, 1e-10);
|
||||
Assert.True(adr.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmaMethod_Works()
|
||||
{
|
||||
var adr = new Adr(5, AdrMethod.Ema);
|
||||
var baseTime = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var bar = new TBar(baseTime.AddMinutes(i), 100, 105, 95, 100, 1000);
|
||||
adr.Update(bar);
|
||||
}
|
||||
|
||||
// EMA should converge to 10 with constant input of 10
|
||||
Assert.Equal(10.0, adr.Last.Value, 0.01);
|
||||
Assert.True(adr.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WmaMethod_Works()
|
||||
{
|
||||
var adr = new Adr(5, AdrMethod.Wma);
|
||||
var baseTime = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var bar = new TBar(baseTime.AddMinutes(i), 100, 105, 95, 100, 1000);
|
||||
adr.Update(bar);
|
||||
}
|
||||
|
||||
// WMA of [10, 10, 10, 10, 10] = 10
|
||||
Assert.Equal(10.0, adr.Last.Value, 1e-10);
|
||||
Assert.True(adr.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentMethods_ProduceDifferentResults()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.2);
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var adrSma = new Adr(14, AdrMethod.Sma);
|
||||
var adrEma = new Adr(14, AdrMethod.Ema);
|
||||
var adrWma = new Adr(14, AdrMethod.Wma);
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
adrSma.Update(bar);
|
||||
adrEma.Update(bar);
|
||||
adrWma.Update(bar);
|
||||
}
|
||||
|
||||
// Different methods should produce slightly different results
|
||||
// (though with constant input they'd be the same)
|
||||
Assert.True(double.IsFinite(adrSma.Last.Value));
|
||||
Assert.True(double.IsFinite(adrEma.Last.Value));
|
||||
Assert.True(double.IsFinite(adrWma.Last.Value));
|
||||
}
|
||||
|
||||
// ============== State Management & Bar Correction ==============
|
||||
|
||||
[Fact]
|
||||
public void Calc_IsNew_AcceptsParameter()
|
||||
{
|
||||
var adr = new Adr(14);
|
||||
|
||||
// Bar1: H-L = 105-95 = 10
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
adr.Update(bar1, isNew: true);
|
||||
double value1 = adr.Last.Value;
|
||||
|
||||
// Bar2: H-L = 120-100 = 20 (different range from bar1)
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 102, 120, 100, 108, 1000);
|
||||
adr.Update(bar2, isNew: true);
|
||||
double value2 = adr.Last.Value;
|
||||
|
||||
// With different ranges, the SMA should change
|
||||
Assert.NotEqual(value1, value2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_IsNew_False_UpdatesValue()
|
||||
{
|
||||
var adr = new Adr(14);
|
||||
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
adr.Update(bar1, isNew: true);
|
||||
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 102, 110, 100, 108, 1000);
|
||||
adr.Update(bar2, isNew: true);
|
||||
double beforeUpdate = adr.Last.Value;
|
||||
|
||||
var bar2Modified = new TBar(DateTime.UtcNow.AddMinutes(1), 102, 120, 90, 108, 1000);
|
||||
adr.Update(bar2Modified, isNew: false);
|
||||
double afterUpdate = adr.Last.Value;
|
||||
|
||||
Assert.NotEqual(beforeUpdate, afterUpdate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNew_Consistency()
|
||||
{
|
||||
var adr = new Adr(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Feed first 99
|
||||
for (int i = 0; i < 99; i++)
|
||||
{
|
||||
adr.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Update with 100th point (isNew=true)
|
||||
adr.Update(bars[99], true);
|
||||
|
||||
// Update with modified 100th point (isNew=false)
|
||||
var modifiedBar = new TBar(bars[99].Time, bars[99].Open, bars[99].High + 10.0, bars[99].Low - 10.0, bars[99].Close, bars[99].Volume);
|
||||
double val2 = adr.Update(modifiedBar, false).Value;
|
||||
|
||||
// Create new instance and feed up to modified
|
||||
var adr2 = new Adr(14);
|
||||
for (int i = 0; i < 99; i++)
|
||||
{
|
||||
adr2.Update(bars[i]);
|
||||
}
|
||||
double val3 = adr2.Update(modifiedBar, true).Value;
|
||||
|
||||
Assert.Equal(val3, val2, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var adr = new Adr(5);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
|
||||
var bars = gbm.Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Feed 10 new values
|
||||
TBar tenthBar = default;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
tenthBar = bars[i];
|
||||
adr.Update(tenthBar, isNew: true);
|
||||
}
|
||||
|
||||
// Remember state after 10 values
|
||||
double stateAfterTen = adr.Last.Value;
|
||||
|
||||
// Generate 9 corrections with isNew=false (different values)
|
||||
for (int i = 10; i < 19; i++)
|
||||
{
|
||||
adr.Update(bars[i], isNew: false);
|
||||
}
|
||||
|
||||
// Feed the remembered 10th bar again with isNew=false
|
||||
TValue finalResult = adr.Update(tenthBar, isNew: false);
|
||||
|
||||
// State should match the original state after 10 values
|
||||
Assert.Equal(stateAfterTen, finalResult.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_Works()
|
||||
{
|
||||
var adr = new Adr(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars) adr.Update(bar);
|
||||
|
||||
double lastVal = adr.Last.Value;
|
||||
Assert.NotEqual(0, lastVal);
|
||||
|
||||
adr.Reset();
|
||||
Assert.Equal(0, adr.Last.Value);
|
||||
Assert.False(adr.IsHot);
|
||||
|
||||
// After reset, should accept new values
|
||||
adr.Update(bars[0]);
|
||||
Assert.NotEqual(0, adr.Last.Value);
|
||||
}
|
||||
|
||||
// ============== Warmup & Convergence ==============
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BecomesTrueAfterWarmup()
|
||||
{
|
||||
var adr = new Adr(5, AdrMethod.Sma);
|
||||
|
||||
Assert.False(adr.IsHot);
|
||||
|
||||
var baseTime = DateTime.UtcNow;
|
||||
int steps = 0;
|
||||
while (!adr.IsHot && steps < 100)
|
||||
{
|
||||
var bar = new TBar(baseTime.AddMinutes(steps), 100, 110, 90, 100, 1000);
|
||||
adr.Update(bar);
|
||||
steps++;
|
||||
}
|
||||
|
||||
Assert.True(adr.IsHot);
|
||||
// SMA with period 5 should become hot after 5 bars
|
||||
Assert.Equal(5, steps);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_IsPositive()
|
||||
{
|
||||
var adr = new Adr(14);
|
||||
Assert.True(adr.WarmupPeriod > 0);
|
||||
|
||||
var adr2 = new Adr(20);
|
||||
Assert.True(adr2.WarmupPeriod > 0);
|
||||
|
||||
// WarmupPeriod should increase with the period parameter
|
||||
Assert.True(adr2.WarmupPeriod >= adr.WarmupPeriod);
|
||||
}
|
||||
|
||||
// ============== NaN/Infinity Handling ==============
|
||||
|
||||
[Fact]
|
||||
public void NaN_Input_HandledGracefully()
|
||||
{
|
||||
var adr = new Adr(5);
|
||||
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
adr.Update(bar1);
|
||||
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 102, 110, 98, 108, 1000);
|
||||
adr.Update(bar2);
|
||||
|
||||
// Feed bar with NaN values - range will be NaN, should be handled
|
||||
var barWithNaN = new TBar(DateTime.UtcNow.AddMinutes(2), double.NaN, 115, 100, 112, 1000);
|
||||
var resultAfterNaN = adr.Update(barWithNaN);
|
||||
|
||||
// Result should be finite (NaN range treated as 0)
|
||||
Assert.True(double.IsFinite(resultAfterNaN.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_Input_HandledGracefully()
|
||||
{
|
||||
var adr = new Adr(5);
|
||||
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
adr.Update(bar1);
|
||||
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 102, 110, 98, 108, 1000);
|
||||
adr.Update(bar2);
|
||||
|
||||
// Feed bar with Infinity
|
||||
var barWithInf = new TBar(DateTime.UtcNow.AddMinutes(2), 108, double.PositiveInfinity, 100, 112, 1000);
|
||||
var resultAfterInf = adr.Update(barWithInf);
|
||||
|
||||
// Result should be finite (infinite range treated as 0)
|
||||
Assert.True(double.IsFinite(resultAfterInf.Value));
|
||||
}
|
||||
|
||||
// ============== Consistency Tests ==============
|
||||
|
||||
[Fact]
|
||||
public void BatchCalc_MatchesIterativeCalc()
|
||||
{
|
||||
var adrIterative = new Adr(14);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Calculate iteratively
|
||||
var iterativeResults = new TSeries();
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
iterativeResults.Add(adrIterative.Update(bar));
|
||||
}
|
||||
|
||||
// Calculate batch
|
||||
var batchResults = Adr.Batch(bars, 14);
|
||||
|
||||
// Compare
|
||||
Assert.Equal(iterativeResults.Count, batchResults.Count);
|
||||
for (int i = 0; i < iterativeResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(iterativeResults[i].Value, batchResults[i].Value, 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TBarSeries_Update_MatchesStreaming()
|
||||
{
|
||||
var adr1 = new Adr(14);
|
||||
var adr2 = new Adr(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Streaming
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
adr1.Update(bar);
|
||||
}
|
||||
|
||||
// Batch
|
||||
adr2.Update(bars);
|
||||
|
||||
Assert.Equal(adr1.Last.Value, adr2.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Chainability_Works()
|
||||
{
|
||||
var adr = new Adr(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var result = adr.Update(bars);
|
||||
Assert.Equal(50, result.Count);
|
||||
Assert.Equal(adr.Last.Value, result.Last.Value);
|
||||
}
|
||||
|
||||
// ============== Range Calculation Tests ==============
|
||||
|
||||
[Fact]
|
||||
public void Range_EqualsHighMinusLow()
|
||||
{
|
||||
var adr = new Adr(1, AdrMethod.Sma);
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 120, 90, 110, 1000);
|
||||
// Range = 120 - 90 = 30
|
||||
|
||||
var result = adr.Update(bar);
|
||||
Assert.Equal(30.0, result.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoGapConsideration_UnlikeAtr()
|
||||
{
|
||||
// ADR should NOT consider gaps like ATR does
|
||||
var adr = new Adr(14);
|
||||
|
||||
// Bar1: C=100
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 110, 90, 100, 1000);
|
||||
adr.Update(bar1);
|
||||
// Range = 110 - 90 = 20
|
||||
|
||||
// Bar2: Gap up - O=120, H=130, L=115, C=125
|
||||
// ADR Range = 130 - 115 = 15 (ignores gap from close 100)
|
||||
// ATR would use max(15, |130-100|=30, |115-100|=15) = 30
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 120, 130, 115, 125, 1000);
|
||||
var result = adr.Update(bar2);
|
||||
|
||||
// With SMA(14), after 2 bars: (20 + 15) / 2 = 17.5
|
||||
Assert.Equal(17.5, result.Value, 1e-10);
|
||||
}
|
||||
|
||||
// ============== Static Batch Method ==============
|
||||
|
||||
[Fact]
|
||||
public void StaticBatch_Works()
|
||||
{
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var results = Adr.Batch(bars, 14);
|
||||
|
||||
Assert.Equal(50, results.Count);
|
||||
Assert.True(double.IsFinite(results.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaticBatch_WithMethod_Works()
|
||||
{
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var resultsSma = Adr.Batch(bars, 14, AdrMethod.Sma);
|
||||
var resultsEma = Adr.Batch(bars, 14, AdrMethod.Ema);
|
||||
var resultsWma = Adr.Batch(bars, 14, AdrMethod.Wma);
|
||||
|
||||
Assert.Equal(50, resultsSma.Count);
|
||||
Assert.Equal(50, resultsEma.Count);
|
||||
Assert.Equal(50, resultsWma.Count);
|
||||
|
||||
Assert.True(double.IsFinite(resultsSma.Last.Value));
|
||||
Assert.True(double.IsFinite(resultsEma.Last.Value));
|
||||
Assert.True(double.IsFinite(resultsWma.Last.Value));
|
||||
}
|
||||
|
||||
// ============== Edge Cases ==============
|
||||
|
||||
[Fact]
|
||||
public void SingleBar_ReturnsValidResult()
|
||||
{
|
||||
var adr = new Adr(14);
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000);
|
||||
|
||||
var result = adr.Update(bar);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.Equal(20.0, result.Value, 1e-10); // H-L = 110-90 = 20
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Period1_Works()
|
||||
{
|
||||
var adr = new Adr(1);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
var result = adr.Update(bar);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
Assert.True(adr.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FlatBars_ZeroRange()
|
||||
{
|
||||
var adr = new Adr(5);
|
||||
|
||||
// All bars have same OHLC values (no range)
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), 100, 100, 100, 100, 1000);
|
||||
adr.Update(bar);
|
||||
}
|
||||
|
||||
// ADR should be 0 for flat bars
|
||||
Assert.Equal(0.0, adr.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NegativeRange_TreatedAsZero()
|
||||
{
|
||||
var adr = new Adr(5);
|
||||
|
||||
// Bar with Low > High (invalid data)
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 90, 110, 100, 1000); // H=90, L=110 -> range = -20
|
||||
var result = adr.Update(bar);
|
||||
|
||||
// Negative range should be treated as 0
|
||||
Assert.Equal(0.0, result.Value, 1e-10);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// ADR Validation Tests
|
||||
///
|
||||
/// Note: ADR (Average Daily Range) is a simple indicator that calculates
|
||||
/// the moving average of High-Low ranges. Unlike ATR, it doesn't account
|
||||
/// for gaps. Most external libraries don't have a direct ADR implementation,
|
||||
/// so we validate against our own manual calculations and cross-validate
|
||||
/// between smoothing methods.
|
||||
/// </summary>
|
||||
public sealed class AdrValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _testData;
|
||||
private readonly ITestOutputHelper _output;
|
||||
private bool _disposed;
|
||||
|
||||
public AdrValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
_testData = new ValidationTestData();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_testData?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ManualCalculation_Sma()
|
||||
{
|
||||
int period = 14;
|
||||
|
||||
// Calculate ADR using our implementation
|
||||
var adr = new Adr(period, AdrMethod.Sma);
|
||||
var qResult = adr.Update(_testData.Bars);
|
||||
|
||||
// Calculate manually: SMA of (High - Low)
|
||||
var ranges = new List<double>();
|
||||
for (int i = 0; i < _testData.Bars.Count; i++)
|
||||
{
|
||||
var bar = _testData.Bars[i];
|
||||
ranges.Add(bar.High - bar.Low);
|
||||
}
|
||||
|
||||
var sma = new Sma(period);
|
||||
var manualResult = new List<double>();
|
||||
foreach (var range in ranges)
|
||||
{
|
||||
manualResult.Add(sma.Update(new TValue(DateTime.UtcNow, range)).Value);
|
||||
}
|
||||
|
||||
// Compare last 100 records
|
||||
int compareCount = Math.Min(100, qResult.Count);
|
||||
int startIdx = qResult.Count - compareCount;
|
||||
|
||||
for (int i = 0; i < compareCount; i++)
|
||||
{
|
||||
Assert.Equal(manualResult[startIdx + i], qResult[startIdx + i].Value, 1e-10);
|
||||
}
|
||||
|
||||
_output.WriteLine("ADR SMA validated successfully against manual calculation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ManualCalculation_Ema()
|
||||
{
|
||||
int period = 14;
|
||||
|
||||
// Calculate ADR using our implementation
|
||||
var adr = new Adr(period, AdrMethod.Ema);
|
||||
var qResult = adr.Update(_testData.Bars);
|
||||
|
||||
// Calculate manually: EMA of (High - Low)
|
||||
var ranges = new List<double>();
|
||||
for (int i = 0; i < _testData.Bars.Count; i++)
|
||||
{
|
||||
var bar = _testData.Bars[i];
|
||||
ranges.Add(bar.High - bar.Low);
|
||||
}
|
||||
|
||||
var ema = new Ema(period);
|
||||
var manualResult = new List<double>();
|
||||
foreach (var range in ranges)
|
||||
{
|
||||
manualResult.Add(ema.Update(new TValue(DateTime.UtcNow, range)).Value);
|
||||
}
|
||||
|
||||
// Compare last 100 records
|
||||
int compareCount = Math.Min(100, qResult.Count);
|
||||
int startIdx = qResult.Count - compareCount;
|
||||
|
||||
for (int i = 0; i < compareCount; i++)
|
||||
{
|
||||
Assert.Equal(manualResult[startIdx + i], qResult[startIdx + i].Value, 1e-10);
|
||||
}
|
||||
|
||||
_output.WriteLine("ADR EMA validated successfully against manual calculation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ManualCalculation_Wma()
|
||||
{
|
||||
int period = 14;
|
||||
|
||||
// Calculate ADR using our implementation
|
||||
var adr = new Adr(period, AdrMethod.Wma);
|
||||
var qResult = adr.Update(_testData.Bars);
|
||||
|
||||
// Calculate manually: WMA of (High - Low)
|
||||
var ranges = new List<double>();
|
||||
for (int i = 0; i < _testData.Bars.Count; i++)
|
||||
{
|
||||
var bar = _testData.Bars[i];
|
||||
ranges.Add(bar.High - bar.Low);
|
||||
}
|
||||
|
||||
var wma = new Wma(period);
|
||||
var manualResult = new List<double>();
|
||||
foreach (var range in ranges)
|
||||
{
|
||||
manualResult.Add(wma.Update(new TValue(DateTime.UtcNow, range)).Value);
|
||||
}
|
||||
|
||||
// Compare last 100 records
|
||||
int compareCount = Math.Min(100, qResult.Count);
|
||||
int startIdx = qResult.Count - compareCount;
|
||||
|
||||
for (int i = 0; i < compareCount; i++)
|
||||
{
|
||||
Assert.Equal(manualResult[startIdx + i], qResult[startIdx + i].Value, 1e-10);
|
||||
}
|
||||
|
||||
_output.WriteLine("ADR WMA validated successfully against manual calculation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Streaming_MatchesBatch_Sma()
|
||||
{
|
||||
int period = 14;
|
||||
|
||||
// Calculate batch
|
||||
var adrBatch = new Adr(period, AdrMethod.Sma);
|
||||
var batchResult = adrBatch.Update(_testData.Bars);
|
||||
|
||||
// Calculate streaming
|
||||
var adrStream = new Adr(period, AdrMethod.Sma);
|
||||
var streamResult = new List<double>();
|
||||
foreach (var bar in _testData.Bars)
|
||||
{
|
||||
streamResult.Add(adrStream.Update(bar).Value);
|
||||
}
|
||||
|
||||
// Compare all records
|
||||
Assert.Equal(batchResult.Count, streamResult.Count);
|
||||
for (int i = 0; i < batchResult.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult[i].Value, streamResult[i], 1e-10);
|
||||
}
|
||||
|
||||
_output.WriteLine("ADR SMA Streaming validated successfully against Batch");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Streaming_MatchesBatch_Ema()
|
||||
{
|
||||
int period = 14;
|
||||
|
||||
// Calculate batch
|
||||
var adrBatch = new Adr(period, AdrMethod.Ema);
|
||||
var batchResult = adrBatch.Update(_testData.Bars);
|
||||
|
||||
// Calculate streaming
|
||||
var adrStream = new Adr(period, AdrMethod.Ema);
|
||||
var streamResult = new List<double>();
|
||||
foreach (var bar in _testData.Bars)
|
||||
{
|
||||
streamResult.Add(adrStream.Update(bar).Value);
|
||||
}
|
||||
|
||||
// Compare all records (use 1e-8 tolerance for EMA due to floating-point drift)
|
||||
Assert.Equal(batchResult.Count, streamResult.Count);
|
||||
for (int i = 0; i < batchResult.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult[i].Value, streamResult[i], 1e-8);
|
||||
}
|
||||
|
||||
_output.WriteLine("ADR EMA Streaming validated successfully against Batch");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Streaming_MatchesBatch_Wma()
|
||||
{
|
||||
int period = 14;
|
||||
|
||||
// Calculate batch
|
||||
var adrBatch = new Adr(period, AdrMethod.Wma);
|
||||
var batchResult = adrBatch.Update(_testData.Bars);
|
||||
|
||||
// Calculate streaming
|
||||
var adrStream = new Adr(period, AdrMethod.Wma);
|
||||
var streamResult = new List<double>();
|
||||
foreach (var bar in _testData.Bars)
|
||||
{
|
||||
streamResult.Add(adrStream.Update(bar).Value);
|
||||
}
|
||||
|
||||
// Compare all records
|
||||
Assert.Equal(batchResult.Count, streamResult.Count);
|
||||
for (int i = 0; i < batchResult.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult[i].Value, streamResult[i], 1e-10);
|
||||
}
|
||||
|
||||
_output.WriteLine("ADR WMA Streaming validated successfully against Batch");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_MultiplePeriods()
|
||||
{
|
||||
int[] periods = { 5, 10, 14, 20, 50 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate ADR for each period
|
||||
var adrSma = new Adr(period, AdrMethod.Sma);
|
||||
var adrEma = new Adr(period, AdrMethod.Ema);
|
||||
var adrWma = new Adr(period, AdrMethod.Wma);
|
||||
|
||||
var resultSma = adrSma.Update(_testData.Bars);
|
||||
var resultEma = adrEma.Update(_testData.Bars);
|
||||
var resultWma = adrWma.Update(_testData.Bars);
|
||||
|
||||
// Verify all results are finite and positive (or zero for flat bars)
|
||||
Assert.True(double.IsFinite(resultSma.Last.Value), $"SMA Period {period} should produce finite value");
|
||||
Assert.True(double.IsFinite(resultEma.Last.Value), $"EMA Period {period} should produce finite value");
|
||||
Assert.True(double.IsFinite(resultWma.Last.Value), $"WMA Period {period} should produce finite value");
|
||||
|
||||
Assert.True(resultSma.Last.Value >= 0, $"SMA Period {period} should produce non-negative value");
|
||||
Assert.True(resultEma.Last.Value >= 0, $"EMA Period {period} should produce non-negative value");
|
||||
Assert.True(resultWma.Last.Value >= 0, $"WMA Period {period} should produce non-negative value");
|
||||
}
|
||||
|
||||
_output.WriteLine("ADR validated successfully across multiple periods");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_RangeIsAlwaysNonNegative()
|
||||
{
|
||||
// ADR should always produce non-negative values (average of non-negative ranges)
|
||||
var adr = new Adr(14, AdrMethod.Sma);
|
||||
var result = adr.Update(_testData.Bars);
|
||||
|
||||
foreach (var val in result)
|
||||
{
|
||||
Assert.True(val.Value >= 0, "ADR should always be non-negative");
|
||||
}
|
||||
|
||||
_output.WriteLine("ADR validated: all values are non-negative");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_AdrLessThanOrEqualToAtr()
|
||||
{
|
||||
// ADR should generally be <= ATR because ATR accounts for gaps
|
||||
// which can only increase the range, not decrease it
|
||||
int period = 14;
|
||||
|
||||
var adr = new Adr(period, AdrMethod.Sma);
|
||||
var atr = new Atr(period);
|
||||
|
||||
// Note: ATR uses RMA (Wilder's smoothing) not SMA, so we compare
|
||||
// the underlying concept rather than exact values
|
||||
// For bars without gaps, ADR range = ATR true range
|
||||
// For bars with gaps, ATR true range >= ADR range
|
||||
|
||||
foreach (var bar in _testData.Bars)
|
||||
{
|
||||
adr.Update(bar);
|
||||
atr.Update(bar);
|
||||
}
|
||||
|
||||
// Both should be finite and positive
|
||||
Assert.True(double.IsFinite(adr.Last.Value));
|
||||
Assert.True(double.IsFinite(atr.Last.Value));
|
||||
Assert.True(adr.Last.Value >= 0);
|
||||
Assert.True(atr.Last.Value >= 0);
|
||||
|
||||
_output.WriteLine($"ADR: {adr.Last.Value:F4}, ATR: {atr.Last.Value:F4}");
|
||||
_output.WriteLine("ADR and ATR validated: both produce valid results");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// ADR: Average Daily Range
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// ADR measures the average price movement range over a specified period.
|
||||
/// Unlike ATR, ADR uses only the High-Low range without accounting for gaps.
|
||||
///
|
||||
/// Calculation:
|
||||
/// 1. Daily Range = High - Low
|
||||
/// 2. ADR = MA(Daily Range, period)
|
||||
///
|
||||
/// Supports three smoothing methods:
|
||||
/// - SMA (Simple Moving Average) - default
|
||||
/// - EMA (Exponential Moving Average)
|
||||
/// - WMA (Weighted Moving Average)
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Adr : AbstractBase
|
||||
{
|
||||
private readonly AbstractBase _ma;
|
||||
private ITValuePublisher? _source;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Creates ADR with specified period and smoothing method.
|
||||
/// </summary>
|
||||
/// <param name="period">Period for ADR calculation (must be > 0)</param>
|
||||
/// <param name="method">Smoothing method (default: SMA)</param>
|
||||
public Adr(int period, AdrMethod method = AdrMethod.Sma)
|
||||
{
|
||||
if (period <= 0)
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
|
||||
_ma = method switch
|
||||
{
|
||||
AdrMethod.Sma => new Sma(period),
|
||||
AdrMethod.Ema => new Ema(period),
|
||||
AdrMethod.Wma => new Wma(period),
|
||||
_ => throw new ArgumentException($"Invalid smoothing method: {method}", nameof(method))
|
||||
};
|
||||
|
||||
Name = $"Adr({period},{method})";
|
||||
WarmupPeriod = _ma.WarmupPeriod;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates ADR with specified source, period, and smoothing method.
|
||||
/// </summary>
|
||||
/// <param name="source">Source to subscribe to</param>
|
||||
/// <param name="period">Period for ADR calculation</param>
|
||||
/// <param name="method">Smoothing method (default: SMA)</param>
|
||||
public Adr(ITValuePublisher source, int period, AdrMethod method = AdrMethod.Sma) : this(period, method)
|
||||
{
|
||||
_source = source;
|
||||
source.Pub += Handle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates ADR from a TBarSeries.
|
||||
/// </summary>
|
||||
/// <param name="source">Bar series source</param>
|
||||
/// <param name="period">Period for ADR calculation</param>
|
||||
/// <param name="method">Smoothing method (default: SMA)</param>
|
||||
public Adr(TBarSeries source, int period, AdrMethod method = AdrMethod.Sma) : this(period, method)
|
||||
{
|
||||
var ranges = CalculateRanges(source);
|
||||
_ma.Prime(ranges.Values);
|
||||
Last = _ma.Last;
|
||||
}
|
||||
|
||||
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
|
||||
|
||||
/// <summary>
|
||||
/// True if the ADR has warmed up and is providing valid results.
|
||||
/// </summary>
|
||||
public override bool IsHot => _ma.IsHot;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the indicator state using the provided history.
|
||||
/// Note: ADR needs OHLCV data to calculate range properly.
|
||||
/// This Prime method expects pre-calculated range values.
|
||||
/// </summary>
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
_ma.Prime(source);
|
||||
Last = _ma.Last;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the ADR state.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void Reset()
|
||||
{
|
||||
_ma.Reset();
|
||||
Last = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates ADR with a new bar.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TBar input, bool isNew = true)
|
||||
{
|
||||
double range = input.High - input.Low;
|
||||
|
||||
// Handle invalid range values
|
||||
if (!double.IsFinite(range) || range < 0)
|
||||
{
|
||||
range = 0;
|
||||
}
|
||||
|
||||
TValue result = _ma.Update(new TValue(input.Time, range), isNew);
|
||||
Last = result;
|
||||
PubEvent(Last, isNew);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates ADR with a TValue input.
|
||||
/// This treats the input value as the range itself.
|
||||
/// </summary>
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
TValue result = _ma.Update(input, isNew);
|
||||
Last = result;
|
||||
PubEvent(Last, isNew);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates ADR from a TBarSeries.
|
||||
/// </summary>
|
||||
public TSeries Update(TBarSeries source)
|
||||
{
|
||||
if (source.Count == 0) return [];
|
||||
|
||||
// Calculate range series
|
||||
TSeries rangeSeries = CalculateRanges(source);
|
||||
|
||||
// Run MA on ranges
|
||||
var result = _ma.Update(rangeSeries);
|
||||
Last = _ma.Last;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates ADR from a TSeries (assumes values are already ranges).
|
||||
/// </summary>
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
var result = _ma.Update(source);
|
||||
Last = _ma.Last;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the ADR and unsubscribes from the source.
|
||||
/// </summary>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposing && _source != null)
|
||||
{
|
||||
_source.Pub -= Handle;
|
||||
_source = null;
|
||||
}
|
||||
_disposed = true;
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates High-Low ranges from bar series.
|
||||
/// </summary>
|
||||
private static TSeries CalculateRanges(TBarSeries source)
|
||||
{
|
||||
var t = new List<long>(source.Count);
|
||||
var v = new List<double>(source.Count);
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
var bar = source[i];
|
||||
double range = bar.High - bar.Low;
|
||||
|
||||
// Handle invalid values
|
||||
if (!double.IsFinite(range) || range < 0)
|
||||
{
|
||||
range = 0;
|
||||
}
|
||||
|
||||
t.Add(bar.Time);
|
||||
v.Add(range);
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates ADR for the entire series using a new instance.
|
||||
/// </summary>
|
||||
public static TSeries Batch(TBarSeries source, int period, AdrMethod method = AdrMethod.Sma)
|
||||
{
|
||||
var adr = new Adr(period, method);
|
||||
return adr.Update(source);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Smoothing method for ADR calculation.
|
||||
/// </summary>
|
||||
public enum AdrMethod
|
||||
{
|
||||
/// <summary>Simple Moving Average</summary>
|
||||
Sma = 1,
|
||||
/// <summary>Exponential Moving Average</summary>
|
||||
Ema = 2,
|
||||
/// <summary>Weighted Moving Average</summary>
|
||||
Wma = 3
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
# ADR: Average Daily Range
|
||||
|
||||
> "The simplest measure is often the most useful. Why complicate what doesn't need complicating?"
|
||||
|
||||
The Average Daily Range (ADR) measures the average distance between High and Low prices over a specified period. Unlike its cousin ATR, ADR ignores gaps entirely. It answers a straightforward question: "How much does this asset typically move within a single bar?"
|
||||
|
||||
This simplicity is ADR's strength. When you don't care about overnight gaps—perhaps you're day trading or analyzing intraday bars—ADR gives you exactly what you need without the complexity of True Range calculations.
|
||||
|
||||
## Historical Context
|
||||
|
||||
ADR predates ATR conceptually. Traders have been calculating average ranges since price charts existed. While Wilder formalized ATR in 1978 to account for gaps, the original range-based volatility measure never disappeared.
|
||||
|
||||
ADR remains popular among:
|
||||
|
||||
- **Day traders**: Gaps don't matter when you close positions before the session ends.
|
||||
- **Intraday analysts**: 5-minute bars rarely gap; High-Low is the relevant measure.
|
||||
- **Forex traders**: 24-hour markets gap infrequently; ADR and ATR often produce nearly identical results.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
ADR uses composition to delegate smoothing to proven moving average implementations. The range calculation is trivial; the smoothing method determines ADR's character.
|
||||
|
||||
### Core Formula
|
||||
|
||||
$$
|
||||
Range_t = High_t - Low_t
|
||||
$$
|
||||
|
||||
### Smoothing Options
|
||||
|
||||
1. **SMA (Simple Moving Average)**: Equal weight to all bars in the period. Classic, stable, but can be "jumpy" when old values drop off.
|
||||
2. **EMA (Exponential Moving Average)**: More recent bars weighted higher ($\alpha = 2/(N+1)$). Responsive to recent volatility changes.
|
||||
3. **WMA (Weighted Moving Average)**: Linear weighting. Middle ground between SMA and EMA.
|
||||
|
||||
### The Gap Non-Problem
|
||||
|
||||
ADR intentionally ignores gaps. This is not a flaw—it's a feature.
|
||||
|
||||
- **Scenario**: Close = 100. Next Open = 110. High = 112. Low = 109.
|
||||
- **ADR Range**: $112 - 109 = 3$.
|
||||
- **ATR Range**: $112 - 100 = 12$.
|
||||
|
||||
If you're trading intraday and won't hold through the gap, ADR's 3 is the relevant number, not ATR's 12.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### 1. Daily Range (DR)
|
||||
|
||||
$$
|
||||
DR_t = H_t - L_t
|
||||
$$
|
||||
|
||||
Where:
|
||||
|
||||
- $H_t$: Current High
|
||||
- $L_t$: Current Low
|
||||
|
||||
### 2. Average Daily Range (ADR)
|
||||
|
||||
$$
|
||||
ADR_t = MA(DR, N, method)
|
||||
$$
|
||||
|
||||
Where $MA$ is one of:
|
||||
|
||||
**SMA:**
|
||||
$$
|
||||
ADR_t = \frac{1}{N} \sum_{i=0}^{N-1} DR_{t-i}
|
||||
$$
|
||||
|
||||
**EMA:**
|
||||
$$
|
||||
ADR_t = \alpha \cdot DR_t + (1 - \alpha) \cdot ADR_{t-1}, \quad \alpha = \frac{2}{N+1}
|
||||
$$
|
||||
|
||||
**WMA:**
|
||||
$$
|
||||
ADR_t = \frac{\sum_{i=0}^{N-1} (N-i) \cdot DR_{t-i}}{\sum_{i=0}^{N-1} (N-i)}
|
||||
$$
|
||||
|
||||
## Performance Profile
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| **Throughput** | 10 | High; O(1) via EMA, O(N) initial for SMA/WMA. |
|
||||
| **Allocations** | 0 | Zero-allocation in hot paths. |
|
||||
| **Complexity** | O(1) | Streaming updates are constant time. |
|
||||
| **Accuracy** | 10 | Simple calculation; no numerical edge cases. |
|
||||
| **Timeliness** | 5-7 | Depends on smoothing method (EMA most responsive). |
|
||||
| **Overshoot** | 0 | Absolute measure; cannot overshoot. |
|
||||
| **Smoothness** | 6-8 | Depends on smoothing method (SMA smoothest). |
|
||||
|
||||
## Validation
|
||||
|
||||
| Library | Status | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| **QuanTAlib** | ✅ | Validated. |
|
||||
| **Manual SMA** | ✅ | Matches manual High-Low SMA calculation. |
|
||||
| **Manual EMA** | ✅ | Matches manual High-Low EMA calculation. |
|
||||
| **Manual WMA** | ✅ | Matches manual High-Low WMA calculation. |
|
||||
|
||||
**Note**: ADR is not a standard indicator in TA-Lib, Skender, Tulip, or Ooples. Validation is performed against manual calculations and cross-method consistency checks.
|
||||
|
||||
## ADR vs ATR: When to Use Which
|
||||
|
||||
| Scenario | Use ADR | Use ATR |
|
||||
| :--- | :---: | :---: |
|
||||
| Day trading (no overnight holds) | ✅ | |
|
||||
| Intraday charts (1m, 5m, 15m) | ✅ | |
|
||||
| 24-hour markets (Forex, Crypto) | ✅ | ✅ |
|
||||
| Swing trading (overnight holds) | | ✅ |
|
||||
| Daily charts with gaps | | ✅ |
|
||||
| Position sizing through gaps | | ✅ |
|
||||
|
||||
### Common Pitfalls
|
||||
|
||||
- **Confusing ADR with ATR**: They measure different things. ADR ignores gaps; ATR accounts for them. Know which you need.
|
||||
- **Wrong smoothing method**: SMA is stable but can jump when old values exit the window. EMA is smoother for trending volatility. Match the method to your use case.
|
||||
- **Scale dependence**: Like ATR, ADR is absolute. An ADR of 5 on a \$100 stock is 5% volatility; on a \$10 stock, it's 50% volatility. Normalize if comparing across assets.
|
||||
- **Assuming direction**: High ADR means wide bars, not up or down. Crashes and rallies both produce high ADR.
|
||||
@@ -0,0 +1,62 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Average Daily Range (ADR)", "ADR", overlay=false)
|
||||
|
||||
//@function Calculates Average Daily Range with choice of smoothing method
|
||||
//@param length Period for smoothing calculations
|
||||
//@param method Smoothing method (1=SMA, 2=EMA, 3=WMA)
|
||||
//@returns float ADR value
|
||||
//@optimized for performance and dirty data
|
||||
adr(simple int length, simple int method = 1) =>
|
||||
if length <= 0
|
||||
runtime.error("Length must be greater than 0")
|
||||
if method < 1 or method > 3
|
||||
runtime.error("Method must be 1 (SMA), 2 (EMA), or 3 (WMA)")
|
||||
var int p = math.max(1, length)
|
||||
var int head = 0
|
||||
var int count = 0
|
||||
var array<float> buffer = array.new_float(p, na)
|
||||
var float sum = 0.0
|
||||
var float wsum = 0.0
|
||||
float dayRange = high - low
|
||||
float oldest = array.get(buffer, head)
|
||||
if not na(oldest)
|
||||
sum -= oldest
|
||||
count -= 1
|
||||
sum += dayRange
|
||||
count += 1
|
||||
array.set(buffer, head, dayRange)
|
||||
head := (head + 1) % p
|
||||
var float EPSILON = 1e-10
|
||||
var float raw_ema = 0.0
|
||||
var float e = 1.0
|
||||
float result = na
|
||||
if method == 1 // SMA
|
||||
result := nz(sum / count, dayRange)
|
||||
else if method == 2 // EMA
|
||||
float alpha = 1.0/float(length)
|
||||
raw_ema := (raw_ema * (length - 1) + dayRange) / length
|
||||
e := (1 - alpha) * e
|
||||
result := e > EPSILON ? raw_ema / (1.0 - e) : raw_ema
|
||||
else // WMA
|
||||
wsum := 0.0
|
||||
float weight = length
|
||||
for i = 0 to length - 1
|
||||
wsum += nz(array.get(buffer, (head - i - 1 + p) % p)) * weight
|
||||
weight -= 1.0
|
||||
float divisor = length * (length + 1) / 2
|
||||
result := wsum / divisor
|
||||
result
|
||||
|
||||
// ---------- Main loop ----------
|
||||
|
||||
// Inputs
|
||||
i_length = input.int(14, "Length", minval=1, maxval=500, tooltip="Number of bars to average the daily range over")
|
||||
i_method = input.int(1, "Method", minval=1, maxval=3, tooltip="1=SMA, 2=EMA, 3=WMA")
|
||||
|
||||
// Calculation
|
||||
adrValue = adr(i_length, i_method)
|
||||
|
||||
// Plot
|
||||
plot(adrValue, "ADR", color=color.yellow, linewidth=2)
|
||||
Reference in New Issue
Block a user