Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Buffer-style ADX incremental #1271

Merged
merged 7 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/test-performance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ jobs:
echo "## Stream indicators (with Quote caching)" >> $GITHUB_STEP_SUMMARY
cat Performance.StreamIndicators-report-github.md >> $GITHUB_STEP_SUMMARY

echo "## Incremental indicators (with buffer)" >> $GITHUB_STEP_SUMMARY
cat Performance.Incrementals-report-github.md >> $GITHUB_STEP_SUMMARY
echo "## Incrementing buffer-style indicators" >> $GITHUB_STEP_SUMMARY
cat Performance.BufferLists-report-github.md >> $GITHUB_STEP_SUMMARY

echo "## Utilities" >> $GITHUB_STEP_SUMMARY
cat Performance.Utility-report-github.md >> $GITHUB_STEP_SUMMARY
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
namespace Skender.Stock.Indicators;

/// <summary>
/// Interface for adding reusable incremental values to a list.
/// Interface for adding input values to a buffered list.
/// </summary>
public interface IAddReusable
public interface IBufferReusable
{
/// <summary>
/// Converts an incremental value into the next incremental indicator value and adds it to the list.
Expand All @@ -26,9 +26,9 @@ public interface IAddReusable
}

/// <summary>
/// Interface for adding incremental quotes to a list.
/// Interface for adding buffered quotes to a list.
/// </summary>
public interface IAddQuote
public interface IBufferQuote
{
/// <summary>
/// Converts an incremental quote into the next incremental indicator value and adds it to the list.
Expand Down
199 changes: 199 additions & 0 deletions src/a-d/Adx/Adx.BufferList.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
namespace Skender.Stock.Indicators;

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

/// <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 Queue<AdxBuffer>(lookbackPeriods);
}

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

/// <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);

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

/// <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]);
}
}

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;
}
}
2 changes: 2 additions & 0 deletions src/a-d/Adx/Adx.Models.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace Skender.Stock.Indicators;
/// <param name="Timestamp">Gets the timestamp of the result.</param>
/// <param name="Pdi">Gets the Positive Directional Indicator (PDI) value.</param>
/// <param name="Mdi">Gets the Negative Directional Indicator (MDI) value.</param>
/// <param name="Dx">Gets the Directional Index (DX) value.</param>
/// <param name="Adx">Gets the Average Directional Index (ADX) value.</param>
/// <param name="Adxr">Gets the Average Directional Movement Rating (ADXR) value.</param>
[Serializable]
Expand All @@ -14,6 +15,7 @@ public record AdxResult
DateTime Timestamp,
double? Pdi = null,
double? Mdi = null,
double? Dx = null,
double? Adx = null,
double? Adxr = null
) : IReusable
Expand Down
2 changes: 2 additions & 0 deletions src/a-d/Adx/Adx.StaticSeries.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ private static List<AdxResult> CalcAdx(

// ADX initialization period
// TODO: update healing, without requiring specific indexing
// see ADX BufferList for hint
else
{
sumDx += dx;
Expand All @@ -166,6 +167,7 @@ private static List<AdxResult> CalcAdx(
Timestamp: q.Timestamp,
Pdi: pdi,
Mdi: mdi,
Dx: dx.NaN2Null(),
Adx: adx.NaN2Null(),
Adxr: adxr.NaN2Null());

Expand Down
12 changes: 12 additions & 0 deletions src/a-d/Adx/IAdx.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Skender.Stock.Indicators;

/// <summary>
/// Interface for Average Directional Index (ADX) streaming and buffered list.
/// </summary>
public interface IAdx
{
/// <summary>
/// Gets the number of periods to look back for the calculation.
/// </summary>
int LookbackPeriods { get; }
}
15 changes: 9 additions & 6 deletions src/e-k/Ema/Ema.Increments.cs → src/e-k/Ema/Ema.BufferList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ namespace Skender.Stock.Indicators;
/// <summary>
/// Exponential Moving Average (EMA) from incremental reusable values.
/// </summary>
public class EmaList : List<EmaResult>, IEma, IAddQuote, IAddReusable
public class EmaList : List<EmaResult>, IEma, IBufferQuote, IBufferReusable
{
private readonly Queue<double> _buffer;
private double _bufferSum;
Expand All @@ -18,7 +18,7 @@ public EmaList(int lookbackPeriods)
LookbackPeriods = lookbackPeriods;
K = 2d / (lookbackPeriods + 1);

_buffer = new(lookbackPeriods);
_buffer = new Queue<double>(lookbackPeriods);
_bufferSum = 0;
}

Expand All @@ -30,7 +30,7 @@ public EmaList(int lookbackPeriods)
/// <summary>
/// Gets the smoothing factor for the calculation.
/// </summary>
public double K { get; init; }
public double K { get; private init; }

/// <summary>
/// Adds a new value to the EMA list.
Expand All @@ -44,6 +44,7 @@ public void Add(DateTime timestamp, double value)
{
_bufferSum -= _buffer.Dequeue();
}

_buffer.Enqueue(value);
_bufferSum += value;

Expand All @@ -54,8 +55,10 @@ public void Add(DateTime timestamp, double value)
return;
}

double? lastEma = this[^1].Ema;

// re/initialize as SMA
if (this[^1].Ema is null)
if (lastEma is null)
{
base.Add(new EmaResult(
timestamp,
Expand All @@ -66,7 +69,7 @@ public void Add(DateTime timestamp, double value)
// calculate EMA normally
base.Add(new EmaResult(
timestamp,
Ema.Increment(K, this[^1].Ema, value)));
Ema.Increment(K, lastEma.Value, value)));
}

/// <summary>
Expand Down Expand Up @@ -117,7 +120,7 @@ public void Add(IReadOnlyList<IQuote> quotes)

for (int i = 0; i < quotes.Count; i++)
{
Add(quotes[i]);
Add(quotes[i].Timestamp, quotes[i].Value);
}
}
}
2 changes: 1 addition & 1 deletion src/e-k/Ema/Ema.StreamHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ internal EmaHub(
public int LookbackPeriods { get; init; }

/// <inheritdoc/>
public double K { get; init; }
public double K { get; private init; }

/// <inheritdoc/>
public override string ToString() => hubName;
Expand Down
2 changes: 1 addition & 1 deletion tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ dotnet run -c Release --filter **
dotnet run --list
...
# Available Benchmarks:
#0 Incrementals
#0 BufferList
#1 SeriesIndicators
#2 StreamExternal
#3 StreamIndicators
Expand Down
2 changes: 1 addition & 1 deletion tests/indicators/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ dotnet run -c Release --filter **
dotnet run --list
...
# Available Benchmarks:
#0 Incrementals
#0 BufferLists
#1 SeriesIndicators
#2 StreamExternal
#3 StreamIndicators
Expand Down
4 changes: 2 additions & 2 deletions tests/indicators/TestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ public abstract class StaticSeriesTestBase : TestBase
}

/// <summary>
/// Base tests that all static indicators (series) should have.
/// Base tests that all buffered list indicators should have.
/// </summary>
public abstract class IncrementsTestBase : TestBase
public abstract class BufferListTestBase : TestBase
{
public abstract void FromQuote();

Expand Down
Binary file modified tests/indicators/a-d/Adx/Adx.Calc.xlsx
Binary file not shown.
Loading