DaveSkender / Stock.Indicators

Stock Indicators for .NET is a C# NuGet package that transforms raw equity, commodity, forex, or cryptocurrency financial market price quotes into technical indicators and trading insights. You'll need this essential data in the investment tools that you're building for algorithmic trading, technical analysis, machine learning, or visual charting.
https://dotnet.StockIndicators.dev
Apache License 2.0
980 stars 245 forks source link

Idea: Increment approach for ADX #1062

Open ingted opened 1 year ago

ingted commented 1 year ago

It seems like the indicator values are calculated for the whole input each time.

Is there any possibility to add incrementally update version?

Here are my workaround for the Adx indicator:

Adx.Api.cs

public static partial class Indicator
{

    public static Tuple<List<AdxResult>, AdxHelper> GetAdx<TQuote>(
        this IEnumerable<TQuote> quotes,
        AdxHelper ah,
        List<AdxResult> results,
        int start,
        int lookbackPeriods = 14
        )
        where TQuote : IQuote
    {
        return quotes
            .ToQuoteD()
            .CalcAdx(lookbackPeriods, ah, results, start);
    }
}

AdxSeries.cs

namespace Skender.Stock.Indicators;

public class AdxHelper
{
    public double prevHigh = 0;
    public double prevLow = 0;
    public double prevClose = 0;
    public double prevTrs = 0; // smoothed
    public double prevPdm = 0;
    public double prevMdm = 0;
    public double prevAdx = 0;

    public double sumTr = 0;
    public double sumPdm = 0;
    public double sumMdm = 0;
    public double sumDx = 0;
}
// AVERAGE DIRECTIONAL INDEX (SERIES)
public static partial class Indicator
{
    internal static Tuple<List<AdxResult>, AdxHelper> CalcAdx(
        this List<QuoteD> qdList,
        int lookbackPeriods,
        AdxHelper ah,
        List<AdxResult> results,
        int start = 0)
    {
        // check parameter arguments
        ValidateAdx(lookbackPeriods);

        // initialize
        int length = qdList.Count;
        if (results == null)
        {
            results = new(length);
        }

        //double prevHigh = 0;
        //double prevLow = 0;
        //double prevClose = 0;
        //double prevTrs = 0; // smoothed
        //double prevPdm = 0;
        //double prevMdm = 0;
        //double prevAdx = 0;

        //double sumTr = 0;
        //double sumPdm = 0;
        //double sumMdm = 0;
        //double sumDx = 0;
        if (ah == null)
        {
            ah = new AdxHelper();
        }
        // roll through quotes
        for (int i = start; i < length; i++)
        {
            QuoteD q = qdList[i];

            AdxResult r = new(q.Date);
            results.Add(r);

            // skip first period
            if (i == 0)
            {
                ah.prevHigh = q.High;
                ah.prevLow = q.Low;
                ah.prevClose = q.Close;
                continue;
            }

            double hmpc = Math.Abs(q.High - ah.prevClose);
            double lmpc = Math.Abs(q.Low - ah.prevClose);
            double hmph = q.High - ah.prevHigh;
            double plml = ah.prevLow - q.Low;

            double tr = Math.Max(q.High - q.Low, Math.Max(hmpc, lmpc));

            double pdm1 = hmph > plml ? Math.Max(hmph, 0) : 0;
            double mdm1 = plml > hmph ? Math.Max(plml, 0) : 0;

            ah.prevHigh = q.High;
            ah.prevLow = q.Low;
            ah.prevClose = q.Close;

            // initialization period
            if (i <= lookbackPeriods)
            {
                ah.sumTr += tr;
                ah.sumPdm += pdm1;
                ah.sumMdm += mdm1;
            }

            // skip DM initialization period
            if (i < lookbackPeriods)
            {
                continue;
            }

            // smoothed true range and directional movement
            double trs;
            double pdm;
            double mdm;

            if (i == lookbackPeriods)
            {
                trs = ah.sumTr;
                pdm = ah.sumPdm;
                mdm = ah.sumMdm;
            }
            else
            {
                trs = ah.prevTrs - (ah.prevTrs / lookbackPeriods) + tr;
                pdm = ah.prevPdm - (ah.prevPdm / lookbackPeriods) + pdm1;
                mdm = ah.prevMdm - (ah.prevMdm / lookbackPeriods) + mdm1;
            }

            ah.prevTrs = trs;
            ah.prevPdm = pdm;
            ah.prevMdm = mdm;

            if (trs is 0)
            {
                continue;
            }

            // directional increments
            double pdi = 100 * pdm / trs;
            double mdi = 100 * mdm / trs;

            r.Pdi = pdi;
            r.Mdi = mdi;

            // calculate ADX
            double dx = (pdi == mdi)
                ? 0
                : (pdi + mdi != 0)
                ? 100 * Math.Abs(pdi - mdi) / (pdi + mdi)
                : double.NaN;

            double adx;

            if (i > (2 * lookbackPeriods) - 1)
            {
                adx = ((ah.prevAdx * (lookbackPeriods - 1)) + dx) / lookbackPeriods;
                r.Adx = adx.NaN2Null();

                double? priorAdx = results[i + 1 - lookbackPeriods].Adx;

                r.Adxr = (adx + priorAdx).NaN2Null() / 2;
                ah.prevAdx = adx;
            }

            // initial ADX
            else if (i == (2 * lookbackPeriods) - 1)
            {
                ah.sumDx += dx;
                adx = ah.sumDx / lookbackPeriods;
                r.Adx = adx.NaN2Null();
                ah.prevAdx = adx;
            }

            // ADX initialization period
            else
            {
                ah.sumDx += dx;
            }
        }

        return Tuple.Create(results, ah);
    }

    // parameter validation
    private static void ValidateAdx(
        int lookbackPeriods)
    {
        // check parameter arguments
        if (lookbackPeriods <= 1)
        {
            throw new ArgumentOutOfRangeException(nameof(lookbackPeriods), lookbackPeriods,
                "Lookback periods must be greater than 1 for ADX.");
        }
    }
}

The usage in F#:


let l = List<Quote> (quotes |> Seq.filter (fun q -> q.Date <> mxDt) |> Seq.sortBy (fun q -> q.Date))

let mutable tpl = l.GetAdx(null, null, 0, 7)

l.Add (quotes |> Seq.find (fun q -> q.Date = mxDt))

tpl <- l.GetAdx(snd tpl, fst tpl, 412, 7) 

I just need to input the AdxResult list, the AdxHelper and the offset, it will only calculate start from the offset. I know this looks ugly... but I have no elegant way...

DaveSkender commented 1 year ago

Thanks @ingted. I'll keep this in mind when getting back to:

It might make sense for our next v3 preview to focus on providing all the static increments functions as part of the public API. That would allow users to do their own instrumentations. Adding something like this makes sense.

We've already started down this path anyway:

https://github.com/DaveSkender/Stock.Indicators/blob/fc34f3dfd3561ba7b04bb1f34e4219f533eb5c3e/src/e-k/Ema/Ema.Observer.cs#L43-L45

https://github.com/DaveSkender/Stock.Indicators/blob/fc34f3dfd3561ba7b04bb1f34e4219f533eb5c3e/src/s-z/Sma/Sma.Observer.cs#L40-L58

DaveSkender commented 1 year ago

Related to #1091