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
936 stars 231 forks source link

Add depth parameter to ZigZag indicator #694

Open islero opened 2 years ago

islero commented 2 years ago

the depth parameter to zigzag indicator will allow to have less noise at the chart and the high and low points will be more obvious. Currently the depth option is not available and simply equals to 0, this means that almost each bar will be H or L if set percentage to 1. gráfico-zigzag

https://tradeasy.tech/en/zig-zag/

DaveSkender commented 2 years ago

@islero thanks for contributing ideas. Please review our documentation for Zig Zag. I think the "depth" parameter you are seeking is called percentChange in our library and it is already available.

It may be confusing because there is a default value of 5, so the method can be used without specifying the parameter.

islero commented 2 years ago

@DaveSkender, thank you for your reply, I checked documentation and even code, but unfortunately, I've noticed that percentChange is obviously a Deviation percent What is Zig Zag_ - tradEAsy - Google Chrome 2022-0 By depth I meant the minimum distance (in # candles, integer value) between "H" and "H" or "L" and "L", so for example depth=10 means for example if "H" index is 4, the next "H" should be allowed at index 14, but it doesn't mean that exactly 14 will be the next "H". Need to consider that "H" and "L" points in between this depth=10 candles range are involved, but shouldn't be shown as "H" and "L" points.

I've override GetZigZag extension method locally and simply skipped if (trendUp && index - lastHighPoint.Index <= depth) continue; if (!trendUp && index - lastLowPoint.Index <= depth) continue; 10 candles in EvaluateNextPoint method, but this led to new extremums right after 10 candles, bcoz percentage change exceeded default percentChange=5.

So, honestly I've no idea how to implement that so far

DaveSkender commented 2 years ago

Oh, I see, this is a setting that sets a minimum number of bars between pivots? I'll take a look, though sounds a bit confusing to interpret if used with small "% deviation".

islero commented 2 years ago

@DaveSkender, this is a setting to set a min number of bars between extremums, like X bars between "H" and "H", same between "L" and "L", thanks :)

DaveSkender commented 2 years ago

See also discussion with @M22arius494

DaveSkender commented 2 years ago

I'm seeing here that the TradeEasy and TradingView have two different APIs. TradeEasy also includes a Backstep parameter that limits the minimum number of bars between H/L reversals. Do they both treat Depth the same way? Which one is right?

DaveSkender commented 2 years ago

This is what TradingView does with the last leg with a few depth variations. These don't look right to me:

1) The line segment should never go from L to L

image

2) It seems to have missed an obvious peak H:

image

I suspect you're trying to avoid this whipsaw scenario:

image

Without the depth parameter, this would usually be fixed by increasing the Deviation value:

image

anthonypuppo commented 1 year ago

There doesn't appear to be much standardization around this. Some sites indicate depth is the minimum bars between extremes while others say it should be extremes of the same time (maxima/minima). If going off the standard offered by TradingView, it appears they divide depth by two and use that as their search window?

[iH, pH] = pivots(high, math.floor(depth / 2), true)
[iL, pL] = pivots(low, math.floor(depth / 2), false)

Snippet from my implementation (where i is a standard i++ increment beginning at the last found ZigZag point):

var chunk = dataPoints.Skip(i).Take(depth).Select((x, y) => (DataPoint: x, Index: y + i)).ToList();
var chunkMin = chunk.MinBy((x) => x.DataPoint.Low);
var chunkMax = chunk.MaxBy((x) => x.DataPoint.High);

I then use the min/max as extreme points to check for reversals instead of just looking one ahead.

jkaa2 commented 1 year ago

Hi, did anyone managed to clone the zigzag indicator for the Tradeview with same params list (deviation, depth) ?, the library indicator yields different results for the same quote list compared to binance zigzag indicator,

Screenshot 2022-08-13 134526

tr5x commented 1 year ago

Hi,

Started using your amazing library recently and also encountered a problem where results are different comparing to TradingView, probably due to depth parameter. Especially it can be seen on recent data, where small price fluctuations are being registered as ZigZag points. If I increase the param of the indicator then the most recent extremes are not registered as ZigZag points too.

For example, the library still shows 1180.18 as the most recent low point but TradingView shows 1172.75.

image

Thanks!

DaveSkender commented 1 year ago

Thanks for the info. I’ll take a closer look at depth when I get around to this one.

Just an FYI to everyone, TradingView is not always a reliable standard nor is it a good source for basing accuracy. See https://github.com/DaveSkender/Stock.Indicators/discussions/801 for some examples, including some obvious problems I’d shown above.

We’ve been basing our definition on how it is described by Investopedia and by Chart School at stockcharts.com; but do agree that there’s no clear expert source author on this one. I usually defer to original expert author publications to clarify.

vedattaylan commented 1 year ago

Hi Dave. I have been using your indicators for a long time and you are very successful. I thank you for this. As a Tradingview user, I have to trust the data here. Currently, I am trading with your project with the data coming from the binance api and there is no problem. However, I have a request from you regarding the Zigzag indicator. Can we include the "Last Extend Bar" option in the ZigZag indicator? Is it possible? (I want to see the "H" value at the end)

01 02

I downloaded your project and reviewed the codes, but I didn't know how to make a change to enable this feature (Last Extended Bar). I also share the latest version of Tradingview ZigZag indicator with you. You can see exactly how they code. I hope it helps.

Kind regards.

ZigZag CodeView

`// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/ // © TradingView

//@version=5 library("ZigZag", overlay = true)

// ZigZag Library // v5, 2023.02.28

// This code was written using the recommendations from the Pine Script™ User Manual's Style Guide: // https://www.tradingview.com/pine-script-docs/en/v5/writing/Style_guide.html

//#region ———————————————————— Inputs

float deviationInput = input.float(5.0, "Deviation (%)", minval = 0.00001, maxval = 100.0) int depthInput = input.int(10, "Depth", minval = 1) color lineColorInput = input.color(#2962FF, "Line Color") bool extendInput = input.bool(true, "Extend to Last Bar") bool showPriceInput = input.bool(true, "Display Reversal Price") bool showVolInput = input.bool(true, "Display Cumulative Volume") bool showChgInput = input.bool(true, "Display Reversal Price Change", inline = "Price Rev") string priceDiffInput = input.string("Absolute", "", options = ["Absolute", "Percent"], inline = "Price Rev") //#endregion

//#region ———————————————————— Library functions

// @type Provides calculation and display attributes to ZigZag objects. // @field devThreshold The minimum percentage deviation from a point before the ZigZag will change direction. // @field depth The number of bars required for pivot detection. // @field lineColor Line color. // @field extendLast Condition allowing a line to connect the most recent pivot with the current close. // @field displayReversalPrice Condition to display the pivot price in the pivot label. // @field displayCumulativeVolume Condition to display the cumulative volume for the pivot segment in the pivot label. // @field displayReversalPriceChange Condition to display the change in price or percent from the previous pivot in the pivot label. // @field differencePriceMode Reversal change display mode. Options are "Absolute" or "Percent". // @field draw Condition to display lines and labels. // @field allowZigZagOnOneBar Condition to allow double pivots to occur ie. when a large bar makes both a pivot high and a pivot low. export type Settings float devThreshold = 5.0 int depth = 10 color lineColor = #2962FF bool extendLast = true bool displayReversalPrice = true bool displayCumulativeVolume = true bool displayReversalPriceChange = true string differencePriceMode = "Absolute" bool draw = true bool allowZigZagOnOneBar = true

// @type A coordinate containing bar, price, and time information.
// @field tm A value in UNIX time. // @field price A value on the Y axis (price). // @field barIndex A bar_index. export type Point int tm float price int barIndex

// @type A level of significance used to determine directional movement or potential support and resistance. // @field ln A line object connecting the start and end Point objects. // @field lb A label object to display pivot values. // @field isHigh A condition to determine if the pivot is a pivot high. // @field vol Volume for the pivot segment. // @field start The coordinate of the previous Point. // @field end The coordinate of the current Point. export type Pivot line ln label lb bool isHigh float vol Point start Point end

// @type An object to maintain Zig Zag settings, pivots, and volume. // @field settings Settings object to provide calculation and display attributes. // @field pivots An array of Pivot objects. // @field sumVol The volume sum for the pivot segment. // @field extend Pivot object used to project a line from the last pivot to the last bar. export type ZigZag Settings settings array pivots float sumVol = 0 Pivot extend = na

// @function Finds a pivot point if the src has not been exceeded over the length of bars. Finds pivot highs when isHigh is true, and pivot lows otherwise. // @param src (series float) Data series to calculate the pivot value. // @param length (series float) Length in bars required for pivot confirmation. // @param isHigh (simple bool) Condition to determine if the Pivot is a pivot high or pivot low. // @returns (Point) A Point object when a pivot is found, and na otherwise. findPivotPoint(series float src, series float length, simple bool isHigh) => float p = nz(src[length]) if length == 0 Point.new(time, p, bar_index) else if length 2 <= bar_index bool isFound = true for i = 0 to math.abs(length - 1) if (isHigh and src[i] > p) or (not isHigh and src[i] < p) isFound := false break for i = length + 1 to 2 length if (isHigh and src[i] >= p) or (not isHigh and src[i] <= p) isFound := false break if isFound Point.new(time[length], p, bar_index[length])

// @function Calculates the deviation percentage between the price and the basePrice. // @param basePrice (series float) Start price. // @param price (series float) End price. // @returns (float) Deviation percentage, signed. calcDev(series float basePrice, series float price) => float result = 100 * (price - basePrice) / basePrice

// @function Calculates the difference between the start and end point as a price or percentage difference and converts the value to a string variable. // @param start (series float) Start price. // @param end (series float) End price. // @param settings (series Settings) A Settings object. // @returns (string) A string representation of the difference between points. priceRotationDiff(series float start, series float end, Settings settings) => float diff = end - start string sign = math.sign(diff) > 0 ? "+" : "" string diffStr = settings.differencePriceMode == "Absolute" ? str.tostring(diff, format.mintick) : str.tostring(diff * 100 / start, format.percent) string result = str.format("({0}{1})", sign, diffStr)

// @function Creates a string variable containing the price, cumulative volume, and change in price for the pivot.
// @param start (series float) Start price. // @param end (series float) End price. // @param vol (series float) Volume. // @param settings (series Settings) A Settings object. // @returns (string) A string to be displayed in pivot labels. priceRotationAggregate(series float start, series float end, series float vol, Settings settings) => string str = "" if settings.displayReversalPrice str += str.tostring(end, format.mintick) + " " if settings.displayReversalPriceChange str += priceRotationDiff(start, end, settings) + " " if settings.displayCumulativeVolume str += "\n" + str.tostring(vol, format.volume) str

// @function Produces a label at the p Point if settings display attributes are enabled. // @param isHigh (series bool) Condition to determine the label color and location. // @param p (series Point) A Point object. // @param settings (series Settings) A Settings object. // @returns (void) Function has no return. makePivotLabel(series bool isHigh, Point p, Settings settings) => if settings.displayReversalPrice or settings.displayReversalPriceChange or settings.displayCumulativeVolume [yloc, txtColor] = switch isHigh => [yloc.abovebar, color.green] => [yloc.belowbar, color.red] label.new(p.tm, p.price, style = label.style_none, xloc = xloc.bar_time, yloc = yloc, textcolor = txtColor)

// @function Updates Pivot attributes including Point objects, volume, label text, and label and line object locations. // Can be used as a function or method. // @param this (series Pivot) Pivot object to be updated. // @param end (series Point) Point to set the Pivot to. // @param vol (series float) Volume of the Pivot. // @param settings (series Settings) A Settings object. // @returns (void) Function has no return. method updatePivot(Pivot this, Point end, float vol, Settings settings) => this.end := end this.vol := vol if not na(this.lb) this.lb.set_xy(this.end.tm, this.end.price) this.lb.set_text(priceRotationAggregate(this.start.price, this.end.price, this.vol, settings)) this.ln.set_xy2(this.end.tm, this.end.price)

// @function Creates a new Pivot object and assigns a line and label if enabled in the settings. // @param start (series Point) The start Point of the Pivot. // @param end (series Point) The end Point of the Pivot. // @param vol (series float) Volume of the Pivot. // @param isHigh (series bool) Condition to determine if the Pivot is a pivot high or pivot low. // @param settings (series settings) Settings object. // @returns (Pivot) The new Pivot object. newPivot(series Point start, series Point end, series float vol, series bool isHigh, series Settings settings) => Pivot p = Pivot.new(na, na, isHigh, vol, start, end) if settings.draw p.ln := line.new(start.tm, start.price, end.tm, end.price, xloc = xloc.bar_time, color = settings.lineColor, width = 2) p.lb := makePivotLabel(isHigh, end, settings) p.updatePivot(end, vol, settings) p

// @function Deletes line and label objects from this Pivot. // Can be used as a function or method. // @param this (series Pivot) A Pivot object. // @returns (void) Function has no return. method delete(series Pivot this) => if not na(this.ln) this.ln.delete() if not na(this.lb) this.lb.delete()

// @function Determines if price of the p Point is greater than the end price of this Pivot. // Can be used as a function or method. // @param this (series Pivot) A Pivot object. // @param p (series Point) A Point object. // @returns (bool) true if the price of p is greater than this Pivot price. method isMorePrice(series Pivot this, series Point p) => int m = this.isHigh ? 1 : -1 bool result = p.price m > this.end.price m

// @function Returns the last Pivot of this ZigZag if there is at least one Pivot to return, and na otherwise. // Can be used as a function or method. // @param this (series ZigZag) A ZigZag object. // @returns (Pivot) The last Pivot in the ZigZag. export method lastPivot(series ZigZag this) => int s = this.pivots.size() Pivot result = s > 0 ? this.pivots.get(s - 1) : na

// @function Updates the last Pivot of this ZigZag to the p Point and sets the volume to 0. // Can be used as a function or method. // @param this (series ZigZag) A ZigZag object. // @param p (series Point) The Point to set the Pivot to. // @returns (void) Function has no return. method updateLastPivot(series ZigZag this, series Point p) => Pivot lastPivot = this.lastPivot() if this.pivots.size() == 1 lastPivot.start := p if this.settings.draw lastPivot.ln.set_xy1(p.tm, p.price) lastPivot.updatePivot(p, lastPivot.vol + this.sumVol, this.settings) this.sumVol := 0

// @function Pushes a new Pivot into the array within this ZigZag. // Can be used as a function or method. // @param this (series ZigZag) A ZigZag object. // @param new (series Pivot) The Pivot to add to the ZigZag. // @returns (void) Function has no return. method newPivotFound(series ZigZag this, series Pivot new) => this.pivots.push(new) this.sumVol := 0

// @function Determines if a new ZigZag line has been found or the existing line needs updating by comparing new pivots to the existing ZigZag Point. Updates this ZigZag and returns true if either condition occurs. // Can be used as a function or method. // @param this (series ZigZag) A ZigZag object.
// @param isHigh (series bool) Condition to look for pivot high or pivot low. // @param p (Point) A Point object to compare to the current ZigZag Point. // @returns (bool) true if a new ZigZag line is found or last zigzag line has changed. method newPivotPointFound(series ZigZag this, simple bool isHigh, series Point p) => bool result = false Pivot lastPivot = this.lastPivot() if not na(lastPivot) if lastPivot.isHigh == isHigh if lastPivot.isMorePrice(p) this.updateLastPivot(p) result := true else float dev = calcDev(lastPivot.end.price, p.price) if (not lastPivot.isHigh and dev >= this.settings.devThreshold) or (lastPivot.isHigh and dev <= -1 * this.settings.devThreshold) newPivotFound(this, newPivot(lastPivot.end, p, this.sumVol, isHigh, this.settings)) result := true else this.newPivotFound(newPivot(p, p, this.sumVol, isHigh, this.settings)) result := true result

// @function Determines if a new ZigZag Point has been found. // Can be used as a function or method. // @param this (series ZigZag) a ZigZag object.
// @param src (series float) Data series to calculate the pivot. // @param isHigh (simple bool) Condition to look for pivot high or pivot low. // @param depth (series int) The length of bars to look for pivots. // @param registerPivot (series bool) Condition to determine whether or not to register a pivot. // @returns (bool) true if a new Zig Zag line is found or the last Zig Zag line has changed. method tryFindPivot(series ZigZag this, series float src, simple bool isHigh, series int depth, series bool registerPivot = true) => Point point = findPivotPoint(src, depth, isHigh) bool result = not na(point) and registerPivot ? this.newPivotPointFound(isHigh, point) : false

// @function Updates this ZigZag object with new pivots, volume, lines, labels. NOTE: The function must be called on every bar for accurate calculations. // Can be used as a function or method. // @param this (series ZigZag) a ZigZag object.
// @returns (bool) true if a new Zig Zag line is found or the last Zig Zag line has changed. export method update(series ZigZag this) => int depth = math.max(2, math.floor(this.settings.depth / 2)) this.sumVol += nz(volume[depth]) bool somethingChanged = this.tryFindPivot(high, true, depth) somethingChanged := this.tryFindPivot(low, false, depth, this.settings.allowZigZagOnOneBar or not somethingChanged) or somethingChanged Pivot lastPivot = this.lastPivot() float remVol = math.sum(volume, math.max(depth, 1)) if this.settings.extendLast and barstate.islast and not na(lastPivot) bool isHigh = not lastPivot.isHigh float curSeries = isHigh ? high : low Point end = Point.new(time, curSeries, bar_index) if na(this.extend) or somethingChanged if not na(this.extend) this.extend.delete() this.extend := newPivot(lastPivot.end, end, this.sumVol, isHigh, this.settings) this.extend.updatePivot(end, this.sumVol + remVol, this.settings) somethingChanged

// @function Instantiates a new ZigZag object with settings. If no settings are provided, a default ZigZag object is created. // @param settings (series Settings) A Settings object. // @returns (ZigZag) A new ZigZag instance. export newInstance(series Settings settings = na) => ZigZag result = ZigZag.new(na(settings) ? Settings.new() : settings, array.new()) //#endregion

//#region ———————————————————— Example Code

var Settings settings = Settings.new( deviationInput, depthInput, lineColorInput, extendInput, showPriceInput, showVolInput, showChgInput, priceDiffInput)

var ZigZag zigZag = newInstance(settings) zigZag.update() //#endregion `

DaveSkender commented 1 year ago

Can we include the "Last Extend Bar" option in the ZigZag indicator?

We do extend the line to the most recent bar already, are you just interested in having the "H" value? We'd left that off because it's not an actual confirmed pivot.

vedattaylan commented 1 year ago

We do extend the line to the most recent bar already, are you just interested in having the "H" value?

Yes sir. Thank you very much for answering my question. As you said, it's not a real confirmed pivot, but it's useful to know where the direction is momentarily. During the backtest, the stock is going down while the zigzag still signals bullish (zigzag last value is almost a day ago (L)) I would appreciate it if you would evaluate this option as parametric. Screenshot 2023-03-25 132306

DaveSkender commented 1 year ago

We do extend the line to the most recent bar already, are you just interested in having the "H" value?

Yes sir.

Would an IsBullish bool return value work for you? The reason I'd not want to use PointType is that having an actual value there is more meaningful for this indicator, whereas I think you're just looking for information on direction, right?

vedattaylan commented 1 year ago

Yes

DaveSkender commented 1 year ago

I still have a lot of uncertainty how depth might negatively impact the quality of this indicator. It doesn’t smell right. I can try to explain.

What I hear is that you essentially want to ignore quick reversals — for example, you don’t want to eat ice cream every day, but once a week is okay. 🍦

Where I struggle with this concept: if ice cream arrives on Monday, Tuesday, and Wednesday, why would you want to arbitrarily rule out Tuesday or Wednesday as the best day in the week to eat ice cream? It doesn’t add up, logically.

If your goal is to avoid whipsaw reversals, my recommendation is either increase your percentChange (deviation) or use a pre-smoother like ALMA to prevent random spikes from triggering reversals.

// use chaining to pre-smooth
// UPDATE: this is impossible, won’t work
var results = quotes
    .GetAlma(..)
    .GetZigZag(..);

I’ll keep this issue open for a while longer to see if there’s any strong, compelling counter arguments to my logic, but am leaning towards skipping this one.

DaveSkender commented 1 year ago

And now I’ll eat my words. You can’t chain ALMA to ZIG ZAG, it needs a full quote! 😵‍💫

I can update Zig Zag to allow this chaining option, but you’ll not have a high/low option for endType when using in this manner. This will also have a side benefit of enabling Zig Zag on all the basic quote transforms — the aggregate options like HLC3 can also solve the random spike problem.

DaveSkender commented 1 year ago

@anthonypuppo I think has the best lead here. If I understood it correctly, he’s saying a reversal point also has to be a Williams Fractal, where depth is the windowSpan parameter. This could enable my ice cream on Wednesday scenario, possibly.

vedattaylan commented 11 months ago

Hi Dave. From what I understand it seems unlikely. Thank you for your interest in this matter.