jbn / ZigZag

Python library for identifying the peaks and valleys of a time series.
BSD 3-Clause "New" or "Revised" License
444 stars 178 forks source link

Calculate threshold as multiples of ATR #11

Open HumanRupert opened 3 years ago

HumanRupert commented 3 years ago

Something like this feature in NinjaTrader or this script in TradingView.

I've got time to implement it. But I'm not sure how active this repo is. In any case, HMU if you need a maintainer.

flowtrader2016 commented 3 years ago

Did you get a response @akhtariali ? If not, would you consider forking the repo and adding your High/Low and ATR updates? I would be very interested in the functionality you mention especially if you can code a functional version that uses the candle High/Low.

HumanRupert commented 3 years ago

@flowtrader2016 I ended up implementing this code in Python, using ta-lib to calculate ATR. I'll send @jbn an email to see if he looks for maintainers/collaborators.

flowtrader2016 commented 3 years ago

@flowtrader2016 I ended up implementing this code in Python, using ta-lib to calculate ATR. I'll send @jbn an email to see if he looks for maintainers/collaborators.

I would love to see your code @akhtariali Is it something you could share please? Thanks

HumanRupert commented 3 years ago

@flowtrader2016 Sure thing.

zigzags = []

def calc_change_since_pivot(row, key):
    current = row[key]
    last_pivot = zigzags[-1]["Value"]
    if(last_pivot == 0): last_pivot = 1 ** (-100) # avoid division by 0
    perc_change_since_pivot = (current - last_pivot) / abs(last_pivot)
    return perc_change_since_pivot

def get_zigzag(row, taip=None):
    if(taip == "Peak"): key = "High"
    elif(taip == "Trough"): key = "Low"
    else: key = "Close"

    return {
        "Date": row["Date"],
        "Value": row[key],
        "Type": taip 
    }

for ix, row in hist.iterrows():
    threshold = row["ATR"] / row["Open"] * ATR_MULTIPILIER

    # handle first point
    is_starting = ix == 0
    if(is_starting):
        zigzags.append(get_zigzag(row))
        continue

    # handle first line
    is_first_line = len(zigzags) == 1
    if(is_first_line):
        perc_change_since_pivot = calc_change_since_pivot(row, "Close")

        if(abs(perc_change_since_pivot) >= threshold):
            if(perc_change_since_pivot > 0):
                zigzags.append(get_zigzag(row, "Peak"))
                zigzags[0]["Type"] = "Trough"
            else: 
                zigzags.append(get_zigzag(row, "Trough"))
                zigzags[0]["Type"] = "Peak"
        continue

    # handle other lines
    is_trough = zigzags[-2]["Value"] > zigzags[-1]["Value"]
    is_ending = ix == len(hist.index) - 1
    last_pivot = float(zigzags[-1]["Value"])

    # based on last pivot type, look for reversal or continuation
    if(is_trough):
        perc_change_since_pivot = calc_change_since_pivot(row, "High")
        is_reversing = (perc_change_since_pivot >= threshold) or is_ending
        is_continuing = row["Low"] <= last_pivot
        if (is_continuing): zigzags[-1] = get_zigzag(row, "Trough")
        elif (is_reversing): zigzags.append(get_zigzag(row, "Peak"))
    else:
        perc_change_since_pivot = calc_change_since_pivot(row, "Low")
        is_reversing = (perc_change_since_pivot <= -threshold) or is_ending
        is_continuing = row["High"] >= last_pivot
        if(is_continuing): zigzags[-1] = get_zigzag(row, "Peak")
        elif (is_reversing): zigzags.append(get_zigzag(row, "Trough"))

zigzags = pd.DataFrame(zigzags)
zigzags["PrevExt"] = zigzags.Value.shift(2)
print(zigzags.head())
print("\n")

# Note: 2nd index extremum is always compared with the 0th index, which is not actually an extremum, so ignore them.
higher_highs = zigzags.dropna()
higher_highs = higher_highs.loc[(higher_highs["Value"] > higher_highs["PrevExt"]) & (higher_highs["Type"] == "Peak") & (higher_highs.index != 2)]
print(higher_highs.head())
print("\n")

lower_lows = zigzags.dropna()
lower_lows = lower_lows.loc[(lower_lows["Value"] < lower_lows["PrevExt"]) & (lower_lows["Type"] == "Trough") & (lower_lows.index != 2)]
print(lower_lows.head())

With a l'l bit of matplotlib magic, you'll end up with:

fig, ax = plt.subplots()

candlestick_ohlc(ax, hist.values, width=0.6, colorup='green', colordown='red', alpha=0.8)
ax.set_xlabel('Date')
ax.set_ylabel('Price')
date_format = mpl_dates.DateFormatter('%d-%m-%Y')
ax.xaxis.set_major_formatter(date_format)

fig.autofmt_xdate()

zigzags.plot(ax=ax, x="Date", y="Value", legend=False)

for ix, row in lower_lows.iterrows():
    plt.plot(row["Date"], row["Value"], "rv")

for ix, row in higher_highs.iterrows():
    plt.plot(row["Date"], row["Value"], "g^")

plt.show()
Screen Shot 2021-05-08 at 16 06 31
flowtrader2016 commented 3 years ago

@flowtrader2016 Sure thing.

Hi @akhtariali ,

I tried to use your code with this test dataframe:

def genMockDataFrame(days,start_price,colName,startDate,seed=None): 

    periods = days*24
    np.random.seed(seed)
    steps = np.random.normal(loc=0, scale=0.0018, size=periods)
    steps[0]=0
    P = start_price+np.cumsum(steps)
    P = [round(i,4) for i in P]

    fxDF = pd.DataFrame({ 
        'ticker':np.repeat( [colName], periods ),
        'date':np.tile( pd.date_range(startDate, periods=periods, freq='H'), 1 ),
        'price':(P)})
    fxDF.index = pd.to_datetime(fxDF.date)
    fxDF = fxDF.price.resample('D').ohlc()
    return fxDF

hist = genMockDataFrame(100,1.1904,'eurusd','19/3/2020',seed=1)

I'm not sure what value to use for the MULTIPLIER and receive list index out of range error when trying different values. Would be great to see it working with this/any test data.

Thanks

HumanRupert commented 3 years ago

@flowtrader2016 I use yfinance to fetch data. ATR_MULTIPLIER is used to calculate the % threshold of divergence since the last pivot to calculate reversals–threshold = ATR / open * MULTIPLIER.

flowtrader2016 commented 3 years ago

@apologeticallypervy did you receive a response? If not it would be great to see you create a new repo with your zigzag code.

HumanRupert commented 3 years ago

No luck. I'm having crazy times right now; will take a look at it ASAP.

graceyangfan commented 3 years ago

@HumanRupert thanks for you share,It's beautiful