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:
Miha Kralj
2026-01-18 19:02:03 -08:00
committed by GitHub
co-authored by Claude Opus 4.5 aider Warp
parent 5bcdf8d614
commit 86fe32a682
1750 changed files with 198235 additions and 80539 deletions
+65
View File
@@ -0,0 +1,65 @@
using TradingPlatform.BusinessLayer;
using QuanTAlib;
namespace QuanTAlib.Tests;
public class AdxrIndicatorTests
{
[Fact]
public void AdxrIndicator_Constructor_SetsDefaults()
{
var indicator = new AdxrIndicator();
Assert.Equal(14, indicator.Period);
Assert.True(indicator.ShowColdValues);
Assert.Equal("ADXR - Average Directional Movement Rating", indicator.Name);
Assert.True(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void AdxrIndicator_MinHistoryDepths_EqualsZero()
{
var indicator = new AdxrIndicator { Period = 20 };
Assert.Equal(0, AdxrIndicator.MinHistoryDepths);
IWatchlistIndicator watchlistIndicator = indicator;
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
}
[Fact]
public void AdxrIndicator_Initialize_CreatesInternalAdxr()
{
var indicator = new AdxrIndicator { Period = 14 };
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist (ADXR)
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void AdxrIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new AdxrIndicator { Period = 5 };
indicator.Initialize();
// Add historical data
var now = DateTime.UtcNow;
// Need enough bars for Period
for (int i = 0; i < 20; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
// 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 adxr = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(adxr));
}
}
+50
View File
@@ -0,0 +1,50 @@
using System.Drawing;
using System.Runtime.CompilerServices;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
[SkipLocalsInit]
public sealed class AdxrIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Period", sortIndex: 1, 1, 1000, 1, 0)]
public int Period { get; set; } = 14;
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private Adxr _adxr = null!;
private readonly LineSeries _adxrSeries;
public static int MinHistoryDepths => 0;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => $"ADXR {Period}";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/momentum/adxr/Adxr.Quantower.cs";
public AdxrIndicator()
{
OnBackGround = true;
SeparateWindow = true;
Name = "ADXR - Average Directional Movement Rating";
Description = "Quantifies the change in momentum of the ADX";
_adxrSeries = new LineSeries(name: "ADXR", color: Color.Orange, width: 2, style: LineStyle.Solid);
AddLineSeries(_adxrSeries);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnInit()
{
_adxr = new Adxr(Period);
base.OnInit();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnUpdate(UpdateArgs args)
{
TValue result = _adxr.Update(this.GetInputBar(args), args.IsNewBar());
_adxrSeries.SetValue(result.Value, _adxr.IsHot, ShowColdValues);
}
}
+271
View File
@@ -0,0 +1,271 @@
namespace QuanTAlib;
public class AdxrTests
{
[Fact]
public void BasicCalculation_DoesNotCrash()
{
var adxr = new Adxr(14);
var gbm = new GBM();
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars)
{
adxr.Update(bar);
}
Assert.True(double.IsFinite(adxr.Last.Value));
}
[Fact]
public void IsNew_Consistency()
{
var adxr = new Adxr(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++)
{
adxr.Update(bars[i]);
}
// Update with 100th point (isNew=true)
adxr.Update(bars[99], true);
// Update with modified 100th point (isNew=false)
var modifiedBar = new TBar(bars[99].Time, bars[99].Open, bars[99].High + 1.0, bars[99].Low - 1.0, bars[99].Close, bars[99].Volume);
var val2 = adxr.Update(modifiedBar, false);
// Create new instance and feed up to modified
var adxr2 = new Adxr(14);
for (int i = 0; i < 99; i++)
{
adxr2.Update(bars[i]);
}
var val3 = adxr2.Update(modifiedBar, true);
Assert.Equal(val3.Value, val2.Value, 1e-9);
}
[Fact]
public void Reset_Works()
{
var adxr = new Adxr(14);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars)
{
adxr.Update(bar);
}
adxr.Reset();
Assert.Equal(0, adxr.Last.Value);
Assert.False(adxr.IsHot);
// Feed again
foreach (var bar in bars)
{
adxr.Update(bar);
}
Assert.True(double.IsFinite(adxr.Last.Value));
}
[Fact]
public void TBarSeries_Update_Matches_Streaming()
{
var adxr = new Adxr(14);
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var streamingResults = new List<double>();
foreach (var bar in bars)
{
streamingResults.Add(adxr.Update(bar).Value);
}
var adxr2 = new Adxr(14);
var seriesResults = adxr2.Update(bars);
Assert.Equal(streamingResults.Count, seriesResults.Count);
for (int i = 0; i < seriesResults.Count; i++)
{
Assert.Equal(streamingResults[i], seriesResults.Values[i], 1e-9);
}
}
[Fact]
public void StaticCalculate_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var adxr = new Adxr(14);
var streamingResults = new List<double>();
foreach (var bar in bars)
{
streamingResults.Add(adxr.Update(bar).Value);
}
var staticResults = Adxr.Batch(bars, 14);
Assert.Equal(streamingResults.Count, staticResults.Count);
for (int i = 0; i < staticResults.Count; i++)
{
Assert.Equal(streamingResults[i], staticResults.Values[i], 1e-9);
}
}
[Fact]
public void Constructor_InvalidParameters_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new Adxr(0));
Assert.Throws<ArgumentException>(() => new Adxr(-1));
}
[Fact]
public void Chainability_Works()
{
var adxr = new Adxr(14);
var gbm = new GBM();
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Test TBarSeries chain
var result = adxr.Update(bars);
Assert.NotNull(result);
Assert.IsType<TSeries>(result);
// Test TBar chain (returns TValue)
var result2 = adxr.Update(bars[0]);
Assert.IsType<TValue>(result2);
}
[Fact]
public void IterativeCorrections_RestoreToOriginalState()
{
var adxr = new Adxr(5);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
// Feed 20 new values
TBar twentiethInput = default;
for (int i = 0; i < 20; i++)
{
var bar = gbm.Next(isNew: true);
twentiethInput = bar;
adxr.Update(bar, isNew: true);
}
// Remember state after 20 values
double stateAfterTwenty = adxr.Last.Value;
// Generate 9 corrections with isNew=false (different values)
for (int i = 0; i < 9; i++)
{
var bar = gbm.Next(isNew: false);
adxr.Update(bar, isNew: false);
}
// Feed the remembered 20th input again with isNew=false
TValue finalResult = adxr.Update(twentiethInput, isNew: false);
// State should match the original state after 20 values
Assert.Equal(stateAfterTwenty, finalResult.Value, 1e-10);
}
[Fact]
public void IsHot_BecomesTrueWhenBufferFull()
{
var adxr = new Adxr(5);
var gbm = new GBM();
Assert.False(adxr.IsHot);
// ADXR needs more warmup than just period (ADX warmup + period)
// Feed bars until IsHot becomes true
int count = 0;
while (!adxr.IsHot && count < 100)
{
var bar = gbm.Next(isNew: true);
adxr.Update(bar, isNew: true);
count++;
}
Assert.True(adxr.IsHot);
Assert.True(count > 5); // Should take more than period bars
}
[Fact]
public void NaN_Input_UsesLastValidValue()
{
var adxr = new Adxr(5);
var gbm = new GBM();
var bars = gbm.Fetch(30, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Feed some valid bars first
for (int i = 0; i < 25; i++)
{
adxr.Update(bars[i]);
}
// Create a bar with NaN values
var nanBar = new TBar(DateTime.UtcNow, double.NaN, double.NaN, double.NaN, double.NaN, double.NaN);
var result = adxr.Update(nanBar);
// Should not crash and should return a finite value
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Infinity_Input_UsesLastValidValue()
{
var adxr = new Adxr(5);
var gbm = new GBM();
var bars = gbm.Fetch(30, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Feed some valid bars first
for (int i = 0; i < 25; i++)
{
adxr.Update(bars[i]);
}
// Create a bar with Infinity values
var infBar = new TBar(DateTime.UtcNow, double.PositiveInfinity, double.PositiveInfinity, double.NegativeInfinity, double.PositiveInfinity, double.PositiveInfinity);
var result = adxr.Update(infBar);
// Should not crash and should return a finite value
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void AllModes_ProduceSameResult()
{
// Arrange
const int period = 5;
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// 1. Batch Mode (static method)
var batchSeries = Adxr.Batch(bars, period);
double expected = batchSeries.Last.Value;
// 2. Streaming Mode (instance, one bar at a time)
var streamingInd = new Adxr(period);
foreach (var bar in bars)
{
streamingInd.Update(bar);
}
double streamingResult = streamingInd.Last.Value;
// 3. Instance Update with TBarSeries
var instanceInd = new Adxr(period);
var instanceResult = instanceInd.Update(bars);
double instanceValue = instanceResult.Last.Value;
// Assert all modes produce identical results
Assert.Equal(expected, streamingResult, precision: 9);
Assert.Equal(expected, instanceValue, precision: 9);
}
}
@@ -0,0 +1,70 @@
using TALib;
using QuanTAlib.Tests;
namespace QuanTAlib;
public sealed class AdxrValidationTests : IDisposable
{
private readonly ValidationTestData _data;
public AdxrValidationTests()
{
_data = new ValidationTestData();
}
public void Dispose()
{
_data.Dispose();
}
[Fact]
public void MatchesTalib()
{
var adxr = new Adxr(14);
var results = new List<double>();
for (int i = 0; i < _data.Bars.Count; i++)
{
var res = adxr.Update(_data.Bars[i]);
results.Add(res.Value);
}
double[] hData = _data.Bars.High.Select(x => x.Value).ToArray();
double[] lData = _data.Bars.Low.Select(x => x.Value).ToArray();
double[] cData = _data.Bars.Close.Select(x => x.Value).ToArray();
double[] outReal = new double[_data.Bars.Count];
var retCode = Functions.Adxr(hData, lData, cData, 0..^0, outReal, out var outRange, 14);
Assert.Equal(Core.RetCode.Success, retCode);
int lookback = Functions.AdxrLookback(14);
ValidationHelper.VerifyData(results, outReal, outRange, lookback);
}
[Fact]
public void MatchesTulip()
{
var adxr = new Adxr(14);
var results = new List<double>();
for (int i = 0; i < _data.Bars.Count; i++)
{
var res = adxr.Update(_data.Bars[i]);
results.Add(res.Value);
}
double[] hData = _data.Bars.High.Select(x => x.Value).ToArray();
double[] lData = _data.Bars.Low.Select(x => x.Value).ToArray();
double[] cData = _data.Bars.Close.Select(x => x.Value).ToArray();
double[][] inputs = { hData, lData, cData };
double[] options = { 14 };
var adxrInd = Tulip.Indicators.adxr;
double[][] outputs = { new double[hData.Length - adxrInd.Start(options)] };
adxrInd.Run(inputs, options, outputs);
double[] tulipResults = outputs[0];
int lookback = adxrInd.Start(options);
ValidationHelper.VerifyData(results, tulipResults, lookback);
}
}
+231
View File
@@ -0,0 +1,231 @@
using System.Buffers;
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// ADXR: Average Directional Movement Rating
/// </summary>
/// <remarks>
/// ADXR quantifies the change in momentum of the ADX. It is calculated by averaging
/// the current ADX value and the ADX value from 'Period' bars ago.
///
/// Calculation:
/// ADXR = (ADX + ADX[Period]) / 2
///
/// Sources:
/// https://www.investopedia.com/terms/a/adxr.asp
/// "New Concepts in Technical Trading Systems" by J. Welles Wilder
/// </remarks>
[SkipLocalsInit]
public sealed class Adxr : ITValuePublisher
{
private readonly int _period;
private readonly Adx _adx;
private readonly RingBuffer _adxHistory;
private readonly RingBuffer _p_adxHistory;
/// <summary>
/// Display name for the indicator.
/// </summary>
public string Name { get; }
public event TValuePublishedHandler? Pub;
/// <summary>
/// Current ADXR value.
/// </summary>
public TValue Last { get; private set; }
/// <summary>
/// True if the ADXR has warmed up and is providing valid results.
/// </summary>
public bool IsHot => _adx.IsHot && _adxHistory.IsFull;
/// <summary>
/// The number of bars required for the indicator to warm up.
/// </summary>
public int WarmupPeriod { get; }
/// <summary>
/// Creates ADXR with specified period.
/// </summary>
/// <param name="period">Period for ADXR calculation (must be > 0)</param>
public Adxr(int period)
{
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
_period = period;
Name = $"Adxr({period})";
_adx = new Adx(period);
// We need the ADX value from 'period' bars ago.
// TA-Lib uses (Period-1) lag for ADXR.
_adxHistory = new RingBuffer(period - 1);
_p_adxHistory = new RingBuffer(period - 1);
// ADXR needs valid ADX from 'period' bars ago.
// ADX takes 2*period to warm up.
// So ADXR takes 2*period + period - 1 to warm up.
WarmupPeriod = _adx.WarmupPeriod + period - 1;
}
/// <summary>
/// Resets the ADXR state.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset()
{
_adx.Reset();
_adxHistory.Clear();
_p_adxHistory.Clear();
Last = default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TBar input, bool isNew = true)
{
// Update ADX first
TValue adxResult = _adx.Update(input, isNew);
double currentAdx = adxResult.Value;
if (isNew)
{
_p_adxHistory.CopyFrom(_adxHistory);
}
else
{
_adxHistory.CopyFrom(_p_adxHistory);
}
double prevAdx = double.NaN;
if (_adxHistory.IsFull)
{
prevAdx = _adxHistory.Oldest;
}
_adxHistory.Add(currentAdx);
// Calculate ADXR: average of current ADX and ADX from 'period' bars ago
// When prevAdx is NaN (insufficient history), use currentAdx as fallback
double adxr = double.IsNaN(prevAdx)
? currentAdx
: (currentAdx + prevAdx) * 0.5;
Last = new TValue(input.Time, adxr);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
return Last;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
{
return Update(new TBar(input.Time, input.Value, input.Value, input.Value, input.Value, 0), isNew);
}
public TSeries Update(TBarSeries source)
{
if (source.Count == 0) return new TSeries([], []);
int len = source.Count;
var v = new double[len];
Calculate(source.High.Values, source.Low.Values, source.Close.Values, _period, v);
var tList = new List<long>(len);
var vList = new List<double>(v);
var times = source.Open.Times;
for (int i = 0; i < len; i++)
{
tList.Add(times[i]);
}
Reset();
for (int i = 0; i < len; i++)
{
Update(source[i], isNew: true);
}
return new TSeries(tList, vList);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> high, ReadOnlySpan<double> low, ReadOnlySpan<double> close, int period, Span<double> destination)
{
int len = high.Length;
if (len == 0 || len != low.Length || len != close.Length || len != destination.Length)
{
if (destination.Length > 0)
{
destination.Clear();
}
return;
}
const int StackallocThreshold = 256;
double[]? rentedAdx = null;
scoped Span<double> adxSpan;
if (len <= StackallocThreshold)
{
adxSpan = stackalloc double[len];
}
else
{
rentedAdx = ArrayPool<double>.Shared.Rent(len);
adxSpan = rentedAdx.AsSpan(0, len);
}
try
{
Adx.Calculate(high, low, close, period, adxSpan);
destination.Clear();
int lag = period - 1;
if (lag <= 0)
{
adxSpan.CopyTo(destination);
return;
}
if (lag >= len)
{
return;
}
ReadOnlySpan<double> current = adxSpan[lag..];
ReadOnlySpan<double> previous = adxSpan[..(len - lag)];
Span<double> destTail = destination[lag..];
SimdExtensions.Add(current, previous, destTail);
SimdExtensions.Scale(destTail, 0.5, destTail);
}
finally
{
if (rentedAdx != null)
ArrayPool<double>.Shared.Return(rentedAdx);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TSeries Batch(TBarSeries source, int period)
{
if (source.Count == 0) return new TSeries([], []);
int len = source.Count;
var v = new double[len];
Calculate(source.High.Values, source.Low.Values, source.Close.Values, period, v);
var tList = new List<long>(len);
var times = source.Open.Times;
for (int i = 0; i < len; i++)
{
tList.Add(times[i]);
}
return new TSeries(tList, [.. v]);
}
}
+77
View File
@@ -0,0 +1,77 @@
# ADXR: Average Directional Movement Rating
> If ADX is the speedometer, ADXR is the cruise control setting. It smooths out the acceleration to tell you if the trend has staying power.
The Average Directional Movement Rating (ADXR) is a smoothed version of the ADX. It dampens the volatility of the ADX itself, providing a more stable—albeit significantly more lagging—measure of trend strength. It is primarily used to rate the efficacy of trend-following strategies before capital is committed.
## Historical Context
J. Welles Wilder Jr. introduced ADXR alongside ADX in *New Concepts in Technical Trading Systems* (1978). His goal was simple: ADX can be erratic. By averaging the current ADX with a past ADX, he created a metric that ignores short-term fluctuations in trend strength.
It is effectively a "momentum of momentum" indicator, smoothed to the point of geological stability.
## Architecture & Physics
ADXR is a composite indicator. It does not interact with price directly; it interacts with the output of the ADX.
1. **Dependency**: It instantiates and maintains a full `Adx` indicator internally.
2. **History**: It maintains a circular buffer of historical ADX values.
3. **Averaging**: It computes the arithmetic mean of the current ADX and the ADX from `Period - 1` bars ago.
### The Lag Trade-off
ADXR is intentionally slow.
* **ADX** lags price because of its multiple smoothing layers.
* **ADXR** lags ADX because it averages the current value with a value from the distant past.
This double lag makes ADXR useless for entry timing. Its only valid architectural purpose is **regime filtering**: determining *if* a trend-following system should be active, not *when* it should trade.
## Mathematical Foundation
The formula is deceptively simple, but relies on the complex ADX calculation underneath.
$$ ADXR_t = \frac{ADX_t + ADX_{t-(n-1)}}{2} $$
Where:
* $ADX_t$ is the current ADX value.
* $n$ is the Period (typically 14).
* $ADX_{t-(n-1)}$ is the ADX value from `n-1` periods ago.
*Note: The `n-1` lag is used to match TA-Lib's implementation exactly. Some sources cite `n`, but standard reference implementations use `n-1`.*
## Performance Profile
The performance cost is dominated by the underlying ADX calculation. The ADXR step itself is trivial.
### Zero-Allocation Design
The implementation uses a circular buffer (`RingBuffer`) to store historical ADX values, ensuring O(1) access and zero heap allocations during the update cycle.
| Metric | Score | Notes |
| :--- | :--- | :--- |
| **Throughput** | 6ns | 6ns / bar (Apple M1 Max). |
| **Allocations** | 0 | Hot path is allocation-free. |
| **Complexity** | O(1) | Ring buffer access is constant time. |
| **Accuracy** | 10/10 | Matches TA-Lib to 1e-9. |
| **Timeliness** | 1/10 | Double lag (ADX + History). |
| **Overshoot** | 10/10 | Extremely stable. |
| **Smoothness** | 10/10 | Extremely stable trend rating. |
## Validation
Validation is performed against industry-standard libraries.
| Library | Status | Notes |
| :--- | :--- | :--- |
| **QuanTAlib** | ✅ | Validated. |
| **TA-Lib** | ✅ | Matches `TA_ADXR` to 1e-9. |
| **Skender** | N/A | Not implemented in Skender. |
| **Tulip** | ✅ | Matches `ti.adxr`. |
| **Ooples** | N/A | Not implemented. |
### Common Pitfalls
* **Using for Entries**: Do not use ADXR crossovers for entries. The signal is too late.
* **Short Periods**: Using a short period (e.g., 3) defeats the purpose of ADXR. If you want responsiveness, use ADX. ADXR is for stability.
+55
View File
@@ -0,0 +1,55 @@
// The MIT License (MIT)
// © mihakralj
//@version=6
indicator("Average Directional Movement Index Rating (ADXR)", "ADXR", overlay=false)
//@function Calculates ADX Rating (ADXR) using current and historical ADX values
//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/dynamics/adxr.md
//@param period Number of bars used in ADX calculation
//@param rating_period Number of bars between current and historical ADX
//@returns tuple of ADXR value, ADX value, +DI, -DI
adxr(simple int period, simple int rating_period) =>
if period <= 0
runtime.error("Period must be greater than 0")
if rating_period <= 0
runtime.error("Rating period must be greater than 0")
var float EPSILON = 1e-10
float alpha = 1.0/float(period)
float tr = na(close[1]) ? high - low : math.max(high - low, math.max(math.abs(high - close[1]), math.abs(low - close[1])))
float plus_dm = na(high[1]) ? 0.0 : high - high[1] > low[1] - low and high - high[1] > 0 ? high - high[1] : 0.0
float minus_dm = na(low[1]) ? 0.0 : low[1] - low > high - high[1] and low[1] - low > 0 ? low[1] - low : 0.0
var float e = 1.0
var float tr_raw = na
tr_raw := na(tr_raw) ? tr : (tr_raw * (period - 1) + tr) / period
float tr_smooth = e > EPSILON ? tr_raw / (1.0 - e) : tr_raw
var float pdm_raw = na
pdm_raw := na(pdm_raw) ? plus_dm : (pdm_raw * (period - 1) + plus_dm) / period
float plus_dm_smooth = e > EPSILON ? pdm_raw / (1.0 - e) : pdm_raw
var float mdm_raw = na
mdm_raw := na(mdm_raw) ? minus_dm : (mdm_raw * (period - 1) + minus_dm) / period
float minus_dm_smooth = e > EPSILON ? mdm_raw / (1.0 - e) : mdm_raw
float plus_di = tr_smooth != 0.0 ? math.min(100 * plus_dm_smooth / tr_smooth, 50.0) : 0.0
float minus_di = tr_smooth != 0.0 ? math.min(100 * minus_dm_smooth / tr_smooth, 50.0) : 0.0
float dx = plus_di + minus_di != 0.0 ? 100 * math.abs(plus_di - minus_di) / (plus_di + minus_di) : 0.0
var float adx_raw = na
adx_raw := na(adx_raw) ? 0.0 : (adx_raw * (period - 1) + dx) / period
float adx_value = e > EPSILON ? adx_raw / (1.0 - e) : adx_raw
e *= (1 - alpha)
float historical_adx = adx_value[math.min(rating_period, bar_index)]
float adxr_value = (adx_value + nz(historical_adx,0)) / 2.0
[adxr_value, adx_value, plus_di, minus_di]
// ---------- Main loop ----------
// Inputs
i_period = input.int(14, "ADX Period", minval=1, tooltip="Number of bars used in ADX calculation")
i_rating_period = input.int(14, "Rating Period", minval=1, tooltip="Number of bars between current and historical ADX")
// Calculate ADXR
[adxr_value, adx_value, plus_di, minus_di] = adxr(i_period, i_rating_period)
// Plot
plot(adxr_value, "ADXR", color=color.yellow, linewidth=2)
plot(adx_value, "ADX", color=color.yellow, linewidth=2)
plot(plus_di, "+DI", color=color.yellow, linewidth=2)
plot(minus_di, "-DI", color=color.yellow, linewidth=2)