Skip to content

Commit

Permalink
add failing tests (TDD), incrementor shell (incomplete)
Browse files Browse the repository at this point in the history
  • Loading branch information
DaveSkender committed Nov 11, 2024
1 parent c676ed9 commit b4d724e
Show file tree
Hide file tree
Showing 3 changed files with 211 additions and 0 deletions.
128 changes: 128 additions & 0 deletions src/a-d/Adx/Adx.Increments.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
namespace Skender.Stock.Indicators;

/// <summary>
/// Interface for Average Directional Index (ADX) calculations.
/// </summary>
public interface IAdx
{
/// <summary>
/// Gets the number of periods to look back for the calculation.
/// </summary>
int LookbackPeriods { get; }
}

/// <summary>
/// Average Directional Index (ADX) from incremental reusable values.
/// </summary>
public class AdxList : List<AdxResult>, IAdx, IAddQuote, IAddReusable
{
private readonly Queue<double> _buffer;
private double _bufferSum;

/// <summary>
/// Initializes a new instance of the <see cref="AdxList"/> class.
/// </summary>
/// <param name="lookbackPeriods">The number of periods to look back for the calculation.</param>
public AdxList(int lookbackPeriods)
{
Adx.Validate(lookbackPeriods);
LookbackPeriods = lookbackPeriods;

_buffer = new(lookbackPeriods);
_bufferSum = 0;
}

/// <summary>
/// Gets the number of periods to look back for the calculation.
/// </summary>
public int LookbackPeriods { get; init; }

/// <summary>
/// Adds a new value to the ADX list.
/// </summary>
/// <param name="timestamp">The timestamp of the value.</param>
/// <param name="value">The value to add.</param>
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)));
}

/// <summary>
/// Adds a new reusable value to the ADX list.
/// </summary>
/// <param name="value">The reusable value to add.</param>
/// <exception cref="ArgumentNullException">Thrown when the value is null.</exception>
public void Add(IReusable value)
{
ArgumentNullException.ThrowIfNull(value);
Add(value.Timestamp, value.Value);
}

/// <summary>
/// Adds a list of reusable values to the ADX list.
/// </summary>
/// <param name="values">The list of reusable values to add.</param>
/// <exception cref="ArgumentNullException">Thrown when the values list is null.</exception>
public void Add(IReadOnlyList<IReusable> values)
{
ArgumentNullException.ThrowIfNull(values);

for (int i = 0; i < values.Count; i++)
{
Add(values[i].Timestamp, values[i].Value);
}
}

/// <summary>
/// Adds a new quote to the ADX list.
/// </summary>
/// <param name="quote">The quote to add.</param>
/// <exception cref="ArgumentNullException">Thrown when the quote is null.</exception>
public void Add(IQuote quote)
{
ArgumentNullException.ThrowIfNull(quote);
Add(quote.Timestamp, quote.Value);
}

/// <summary>
/// Adds a list of quotes to the ADX list.
/// </summary>
/// <param name="quotes">The list of quotes to add.</param>
/// <exception cref="ArgumentNullException">Thrown when the quotes list is null.</exception>
public void Add(IReadOnlyList<IQuote> quotes)
{
ArgumentNullException.ThrowIfNull(quotes);

for (int i = 0; i < quotes.Count; i++)
{
Add(quotes[i]);
}
}
}
11 changes: 11 additions & 0 deletions src/a-d/Adx/Adx.Utilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ public static IReadOnlyList<AdxResult> RemoveWarmupPeriods(
return results.Remove((2 * n) + 100);
}

/// <summary>
/// Increments the ADX value based on the last ADX value and the new price.
/// </summary>
/// <param name="lastAdx">The last ADX value.</param>
/// <param name="newPrice">The new price.</param>
/// <returns>The incremented ADX value.</returns>
public static double? Increment(
double? lastAdx,
double newPrice)
=> throw new NotImplementedException();

/// <summary>
/// Validates the parameters for the ADX calculation.
/// </summary>
Expand Down
72 changes: 72 additions & 0 deletions tests/indicators/a-d/Adx/Adx.Increments.Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
namespace Increments;

[TestClass]
public class Adx : IncrementsTestBase
{
private const int lookbackPeriods = 14;

private static readonly IReadOnlyList<IReusable> reusables
= Quotes
.Cast<IReusable>()
.ToList();

private static readonly IReadOnlyList<AdxResult> 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<AdxResult> series
= Quotes.ToAdx(lookbackPeriods);

sut.Should().HaveCount(Quotes.Count);
sut.Should().BeEquivalentTo(series);
}
}

0 comments on commit b4d724e

Please sign in to comment.