From a6ea3d46552c552846113a31362f2b64fb9fd7eb Mon Sep 17 00:00:00 2001 From: Dave Skender <8432125+DaveSkender@users.noreply.github.com> Date: Mon, 11 Nov 2024 17:04:02 -0500 Subject: [PATCH] add ADX buffered list --- src/a-d/Adx/Adx.BufferList.cs | 199 ++++++++++++++++++ src/a-d/Adx/Adx.Increments.cs | 128 ----------- src/a-d/Adx/IAdx.cs | 12 ++ .../a-d/Adx/Adx.Increments.Tests.cs | 41 +--- 4 files changed, 212 insertions(+), 168 deletions(-) create mode 100644 src/a-d/Adx/Adx.BufferList.cs delete mode 100644 src/a-d/Adx/Adx.Increments.cs create mode 100644 src/a-d/Adx/IAdx.cs diff --git a/src/a-d/Adx/Adx.BufferList.cs b/src/a-d/Adx/Adx.BufferList.cs new file mode 100644 index 000000000..17a9d8d1d --- /dev/null +++ b/src/a-d/Adx/Adx.BufferList.cs @@ -0,0 +1,199 @@ +namespace Skender.Stock.Indicators; + +/// +/// Average Directional Index (ADX) from incremental reusable values. +/// +public class AdxList : List, IAdx, IBufferQuote +{ + private readonly Queue _buffer; + + /// + /// 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 Queue(lookbackPeriods); + } + + /// + /// Gets the number of periods to look back for the calculation. + /// + public int LookbackPeriods { get; init; } + + /// + /// 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); + + // update buffer + if (_buffer.Count == LookbackPeriods) + { + _buffer.Dequeue(); + } + + DateTime timestamp = quote.Timestamp; + + AdxBuffer curr = new( + (double)quote.High, + (double)quote.Low, + (double)quote.Close); + + // skip first period + if (Count == 0) + { + _buffer.Enqueue(curr); + base.Add(new AdxResult(timestamp)); + return; + } + + // get last, then add current object + AdxBuffer last = _buffer.Last(); + _buffer.Enqueue(curr); + + // calculate TR, PDM, and MDM + double hmpc = Math.Abs(curr.High - last.Close); + double lmpc = Math.Abs(curr.Low - last.Close); + double hmph = curr.High - last.High; + double plml = last.Low - curr.Low; + + curr.Tr = Math.Max(curr.High - curr.Low, Math.Max(hmpc, lmpc)); + + curr.Pdm1 = hmph > plml ? Math.Max(hmph, 0) : 0; + curr.Mdm1 = plml > hmph ? Math.Max(plml, 0) : 0; + + // skip incalculable + if (Count < LookbackPeriods) + { + base.Add(new AdxResult(timestamp)); + return; + } + + // re/initialize smooth TR and DM + if (Count >= LookbackPeriods && last.Trs == 0) + { + foreach (AdxBuffer buffer in _buffer) + { + curr.Trs += buffer.Tr; + curr.Pdm += buffer.Pdm1; + curr.Mdm += buffer.Mdm1; + } + } + + // normal movement calculations + else + { + curr.Trs = last.Trs - (last.Trs / LookbackPeriods) + curr.Tr; + curr.Pdm = last.Pdm - (last.Pdm / LookbackPeriods) + curr.Pdm1; + curr.Mdm = last.Mdm - (last.Mdm / LookbackPeriods) + curr.Mdm1; + } + + // skip incalculable periods + if (curr.Trs == 0) + { + base.Add(new AdxResult(timestamp)); + return; + } + + // directional increments + double pdi = 100 * curr.Pdm / curr.Trs; + double mdi = 100 * curr.Mdm / curr.Trs; + + // calculate directional index (DX) + curr.Dx = pdi - mdi == 0 + ? 0 + : pdi + mdi != 0 + ? 100 * Math.Abs(pdi - mdi) / (pdi + mdi) + : double.NaN; + + // skip incalculable ADX periods + if (Count < (2 * LookbackPeriods) - 1) + { + base.Add(new AdxResult(timestamp, + Pdi: pdi.NaN2Null(), + Mdi: mdi.NaN2Null(), + Dx: curr.Dx.NaN2Null())); + + return; + } + + double adxr = double.NaN; + + // re/initialize ADX + if (Count >= (2 * LookbackPeriods) - 1 && double.IsNaN(last.Adx)) + { + double sumDx = 0; + + foreach (AdxBuffer buffer in _buffer) + { + sumDx += buffer.Dx; + } + + curr.Adx = sumDx / LookbackPeriods; + } + + // normal ADX calculation + else + { + curr.Adx + = ((last.Adx * (LookbackPeriods - 1)) + curr.Dx) + / LookbackPeriods; + + AdxBuffer first = _buffer.Peek(); + adxr = (curr.Adx + first.Adx) / 2; + } + + AdxResult r = new( + Timestamp: timestamp, + Pdi: pdi, + Mdi: mdi, + Dx: curr.Dx.NaN2Null(), + Adx: curr.Adx.NaN2Null(), + Adxr: adxr.NaN2Null()); + + base.Add(r); + } + + /// + /// 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]); + } + } + + internal class AdxBuffer( + double high, + double low, + double close) + { + internal double High { get; init; } = high; + internal double Low { get; init; } = low; + internal double Close { get; init; } = close; + + internal double Tr { get; set; } = double.NaN; + internal double Pdm1 { get; set; } = double.NaN; + internal double Mdm1 { get; set; } = double.NaN; + + internal double Trs { get; set; } + internal double Pdm { get; set; } + internal double Mdm { get; set; } + + internal double Dx { get; set; } = double.NaN; + internal double Adx { get; set; } = double.NaN; + } +} diff --git a/src/a-d/Adx/Adx.Increments.cs b/src/a-d/Adx/Adx.Increments.cs deleted file mode 100644 index f0349e4f4..000000000 --- a/src/a-d/Adx/Adx.Increments.cs +++ /dev/null @@ -1,128 +0,0 @@ -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/IAdx.cs b/src/a-d/Adx/IAdx.cs new file mode 100644 index 000000000..b02cc1644 --- /dev/null +++ b/src/a-d/Adx/IAdx.cs @@ -0,0 +1,12 @@ +namespace Skender.Stock.Indicators; + +/// +/// Interface for Average Directional Index (ADX) streaming and buffered list. +/// +public interface IAdx +{ + /// + /// Gets the number of periods to look back for the calculation. + /// + int LookbackPeriods { get; } +} diff --git a/tests/indicators/a-d/Adx/Adx.Increments.Tests.cs b/tests/indicators/a-d/Adx/Adx.Increments.Tests.cs index d2e3a0775..e0789bee2 100644 --- a/tests/indicators/a-d/Adx/Adx.Increments.Tests.cs +++ b/tests/indicators/a-d/Adx/Adx.Increments.Tests.cs @@ -1,52 +1,13 @@ namespace Increments; [TestClass] -public class Adx : IncrementsTestBase +public class Adx : BufferListTestBase { 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() {