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); + } +}