diff --git a/src/a-d/Adx/Adx.Increments.cs b/src/a-d/Adx/Adx.Increments.cs
new file mode 100644
index 000000000..f0349e4f4
--- /dev/null
+++ b/src/a-d/Adx/Adx.Increments.cs
@@ -0,0 +1,128 @@
+namespace Skender.Stock.Indicators;
+
+///
+/// Interface for Average Directional Index (ADX) calculations.
+///
+public interface IAdx
+{
+ ///
+ /// Gets the number of periods to look back for the calculation.
+ ///
+ int LookbackPeriods { get; }
+}
+
+///
+/// Average Directional Index (ADX) from incremental reusable values.
+///
+public class AdxList : List, IAdx, IAddQuote, IAddReusable
+{
+ private readonly Queue _buffer;
+ private double _bufferSum;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The number of periods to look back for the calculation.
+ public AdxList(int lookbackPeriods)
+ {
+ Adx.Validate(lookbackPeriods);
+ LookbackPeriods = lookbackPeriods;
+
+ _buffer = new(lookbackPeriods);
+ _bufferSum = 0;
+ }
+
+ ///
+ /// Gets the number of periods to look back for the calculation.
+ ///
+ public int LookbackPeriods { get; init; }
+
+ ///
+ /// Adds a new value to the ADX list.
+ ///
+ /// The timestamp of the value.
+ /// The value to add.
+ public void Add(DateTime timestamp, double value)
+ {
+ // update buffer
+ if (_buffer.Count == LookbackPeriods)
+ {
+ _bufferSum -= _buffer.Dequeue();
+ }
+ _buffer.Enqueue(value);
+ _bufferSum += value;
+
+ // add nulls for incalculable periods
+ if (Count < LookbackPeriods - 1)
+ {
+ base.Add(new AdxResult(timestamp));
+ return;
+ }
+
+ // re/initialize as SMA
+ if (this[^1].Adx is null)
+ {
+ base.Add(new AdxResult(
+ timestamp,
+ _bufferSum / LookbackPeriods));
+ return;
+ }
+
+ // calculate ADX normally
+ base.Add(new AdxResult(
+ timestamp,
+ Adx.Increment(this[^1].Adx, value)));
+ }
+
+ ///
+ /// Adds a new reusable value to the ADX list.
+ ///
+ /// The reusable value to add.
+ /// Thrown when the value is null.
+ public void Add(IReusable value)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+ Add(value.Timestamp, value.Value);
+ }
+
+ ///
+ /// Adds a list of reusable values to the ADX list.
+ ///
+ /// The list of reusable values to add.
+ /// Thrown when the values list is null.
+ public void Add(IReadOnlyList values)
+ {
+ ArgumentNullException.ThrowIfNull(values);
+
+ for (int i = 0; i < values.Count; i++)
+ {
+ Add(values[i].Timestamp, values[i].Value);
+ }
+ }
+
+ ///
+ /// Adds a new quote to the ADX list.
+ ///
+ /// The quote to add.
+ /// Thrown when the quote is null.
+ public void Add(IQuote quote)
+ {
+ ArgumentNullException.ThrowIfNull(quote);
+ Add(quote.Timestamp, quote.Value);
+ }
+
+ ///
+ /// Adds a list of quotes to the ADX list.
+ ///
+ /// The list of quotes to add.
+ /// Thrown when the quotes list is null.
+ public void Add(IReadOnlyList quotes)
+ {
+ ArgumentNullException.ThrowIfNull(quotes);
+
+ for (int i = 0; i < quotes.Count; i++)
+ {
+ Add(quotes[i]);
+ }
+ }
+}
diff --git a/src/a-d/Adx/Adx.Utilities.cs b/src/a-d/Adx/Adx.Utilities.cs
index c73e8745b..92c0cbd2e 100644
--- a/src/a-d/Adx/Adx.Utilities.cs
+++ b/src/a-d/Adx/Adx.Utilities.cs
@@ -20,6 +20,17 @@ public static IReadOnlyList RemoveWarmupPeriods(
return results.Remove((2 * n) + 100);
}
+ ///
+ /// Increments the ADX value based on the last ADX value and the new price.
+ ///
+ /// The last ADX value.
+ /// The new price.
+ /// The incremented ADX value.
+ public static double? Increment(
+ double? lastAdx,
+ double newPrice)
+ => throw new NotImplementedException();
+
///
/// Validates the parameters for the ADX calculation.
///
diff --git a/tests/indicators/a-d/Adx/Adx.Increments.Tests.cs b/tests/indicators/a-d/Adx/Adx.Increments.Tests.cs
new file mode 100644
index 000000000..d2e3a0775
--- /dev/null
+++ b/tests/indicators/a-d/Adx/Adx.Increments.Tests.cs
@@ -0,0 +1,72 @@
+namespace Increments;
+
+[TestClass]
+public class Adx : IncrementsTestBase
+{
+ private const int lookbackPeriods = 14;
+
+ private static readonly IReadOnlyList reusables
+ = Quotes
+ .Cast()
+ .ToList();
+
+ private static readonly IReadOnlyList series
+ = Quotes.ToAdx(lookbackPeriods);
+
+ [TestMethod]
+ public void FromReusableSplit()
+ {
+ AdxList sut = new(lookbackPeriods);
+
+ foreach (IReusable item in reusables)
+ {
+ sut.Add(item.Timestamp, item.Value);
+ }
+
+ sut.Should().HaveCount(Quotes.Count);
+ sut.Should().BeEquivalentTo(series);
+ }
+
+ [TestMethod]
+ public void FromReusableItem()
+ {
+ AdxList sut = new(lookbackPeriods);
+
+ foreach (IReusable item in reusables) { sut.Add(item); }
+
+ sut.Should().HaveCount(Quotes.Count);
+ sut.Should().BeEquivalentTo(series);
+ }
+
+ [TestMethod]
+ public void FromReusableBatch()
+ {
+ AdxList sut = new(lookbackPeriods) { reusables };
+
+ sut.Should().HaveCount(Quotes.Count);
+ sut.Should().BeEquivalentTo(series);
+ }
+
+ [TestMethod]
+ public override void FromQuote()
+ {
+ AdxList sut = new(lookbackPeriods);
+
+ foreach (Quote q in Quotes) { sut.Add(q); }
+
+ sut.Should().HaveCount(Quotes.Count);
+ sut.Should().BeEquivalentTo(series);
+ }
+
+ [TestMethod]
+ public override void FromQuoteBatch()
+ {
+ AdxList sut = new(lookbackPeriods) { Quotes };
+
+ IReadOnlyList series
+ = Quotes.ToAdx(lookbackPeriods);
+
+ sut.Should().HaveCount(Quotes.Count);
+ sut.Should().BeEquivalentTo(series);
+ }
+}