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

Idea: Increment approach for ADX #1062

Open
ingted opened this issue May 20, 2023 · 2 comments
Open

Idea: Increment approach for ADX #1062

ingted opened this issue May 20, 2023 · 2 comments
Labels
enhancement New feature or request on hold On hold / waiting

Comments

@ingted
Copy link

ingted commented May 20, 2023

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...

@ingted ingted added the enhancement New feature or request label May 20, 2023
@DaveSkender DaveSkender changed the title Incrementally update Idea: Increment approach for ADX May 20, 2023
@DaveSkender DaveSkender added the on hold On hold / waiting label May 20, 2023
@DaveSkender
Copy link
Owner

DaveSkender commented May 20, 2023

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:

// incremental calculation
internal static double Increment(double newValue, double lastEma, double k)
=> lastEma + (k * (newValue - lastEma));

// incremental calculation
internal static double Increment(
List<(DateTime Date, double Value)> values,
int index,
int lookbackPeriods)
{
if (index < lookbackPeriods - 1)
{
return double.NaN;
}
double sum = 0;
for (int i = index - lookbackPeriods + 1; i <= index; i++)
{
sum += values[i].Value;
}
return sum / lookbackPeriods;
}

@DaveSkender
Copy link
Owner

Related to #1091

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request on hold On hold / waiting
Projects
Development

No branches or pull requests

2 participants