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()
{