Custom Zig Zag / Price Action Swing Indicator

Can the Price action swing indicator on tradovate be upgraded to include more customized calculations. It’d be nice to have a setting to show swings based on points or ticks. Similar to Sierra Chart.

Sierra chart zig zag

Is it possible to update the “Price Action Swing” indicator to include a feature that shows the average tick/swing rotations based on a custom parameter that can look back for a certain period.

Something like this.

I am trying to work on this, but have run into a snag. When rename the indicator from “jigsaw” to its own name all of the zigzag styling goes away and the lines between the points become invisible and the text becomes black. The only way I can see the pivots is by using a white background in tradovate. How do I change get the coloring of the lines and text so they can be visible regardless of background color?

Here is the code I have so far:

const lodash = require("lodash");
const predef = require("./tools/predef");
const meta = require("./tools/meta");
const SMA = require("./tools/SMA");
const MovingHigh = require("./tools/MovingHigh");
const MovingLow = require("./tools/MovingLow");

//class priceActionSwing {
class zigZagPrice {    
    init() {
        this.swingVolume = new SMA();
        this.highest = new MovingHigh();
        this.lowest = new MovingLow();
        this.prevPoint = undefined;
        this.softPoint = undefined;
    }

    map(d, i, history) {
        let value;
        let newPoint;
        let lastPoint;
        //const percent = this.props.filter / 100;
        const reversalType = this.props.reversalType;
        const percent = this.props.reversalAmt / 100;
        const point = this.props.reversalAmt;
        const display = this.props.displayType;

        if (i === 0) {
            this.prevPoint = {
                timestamp: d.timestamp(),
                high: d.high(),
                low: d.low(),
                totalVolume: 0,
                intialVolume: 0
            };
        }

        const tickSize = history.contractInfo()
            .tickSize();

        // swingVolume is treated carefully throughout setting points and holding total volume for swings
        this.swingVolume.push(d.volume());
        this.highest.push(d.high(), i);
        this.lowest.push(d.low(), i);

        if (this.softPoint) {
            // Up
            if (this.softPoint.peak) {
                const p1 = this.softPoint.high;
                const low = d.low();

                // New 'Higher' Point?
                if (d.high() >= p1) {
                    this.softPoint.high = d.high();
                    this.softPoint.low = d.low();
                    this.softPoint.totalVolume = this.softPoint.intialVolume + this.swingVolume.sum();
                    this.softPoint.timestamp = d.timestamp();
                }// Reversed? Set 'High' point and track 'Low' candidate
                //else if  ((p1 - low) >= (percent * p1)){
                else if (((reversalType === "percent") && ((p1 - low) >= (percent * p1))) || ((reversalType === "point") && ((p1 - low) >= point))) {
                    newPoint = this.softPoint;
                    const intialVolume = (this.softPoint.intialVolume + this.swingVolume.sum()) - newPoint.totalVolume;
                    this.softPoint = {
                        timestamp: d.timestamp(),
                        high: d.high(),
                        low: d.low(),
                        peak: false,
                        intialVolume, // Save intialVolume before swingVolume.refresh
                        totalVolume: intialVolume
                    };
                }
            }// DOWN
            else {
                const p1 = this.softPoint.low;
                const high = d.high();

                // New 'Lower' Point?
                if (d.low() <= p1) {
                    this.softPoint.high = d.high();
                    this.softPoint.low = d.low();
                    this.softPoint.totalVolume = this.softPoint.intialVolume + this.swingVolume.sum();
                    this.softPoint.timestamp = d.timestamp();
                }// Reversed? Set 'Low' point and track 'High' candidate
                //else if ((high - p1) >= (percent * p1)) {
                else if (((reversalType === "percent") && ((high - p1) >= (percent * p1))) || ((reversalType === "point") && ((high - p1) >= point))) {
                    newPoint = this.softPoint;
                    const intialVolume = (this.softPoint.intialVolume + this.swingVolume.sum()) - newPoint.totalVolume;
                    this.softPoint = {
                        timestamp: d.timestamp(),
                        high: d.high(),
                        low: d.low(),
                        peak: true,
                        intialVolume, // Save intialVolume before swingVolume.refresh
                        totalVolume: intialVolume
                    };
                }
            }
        }
        else { // First Point
            // UP
            // if (d.high() - this.prevPoint.low >= percent * this.prevPoint.low)) {
            if (((reversalType === "percent") && (d.high() - this.prevPoint.low >= percent * this.prevPoint.low)) || ((reversalType === "point") && (d.high() - this.prevPoint.low >= point))) {
                this.softPoint = {
                    timestamp: d.timestamp(),
                    high: d.high(),
                    low: d.low(),
                    peak: true,
                    intialVolume: 0, // swingVolume will contain total volume for first swing
                    totalVolume: this.swingVolume.sum()
                };
            }
            // DOWN
            //else if (this.prevPoint.high - d.low() >= percent * this.prevPoint.high) {
            else if (((reversalType === "percent") && (this.prevPoint.high - d.low() >= percent * this.prevPoint.high)) || ((reversalType === "point") && (this.prevPoint.high - d.low() >= point))) {
                this.softPoint = {
                    timestamp: d.timestamp(),
                    high: d.high(),
                    low: d.low(),
                    peak: false,
                    intialVolume: 0, // swingVolume will contain total volume for first swing
                    totalVolume: this.swingVolume.sum()
                };
            }
        }

        if (i === history.data.length - 1) {
            lastPoint = this.softPoint;

            // No points determined within range, so just make one
            if (!lastPoint) {
                const peak = this.highest.current() - this.prevPoint.low > this.prevPoint.high - this.lowest.current();
                const index = peak ? this.highest.index() : this.lowest.index();
                const peakItem = history.getItem(index);
                lastPoint = {
                    timestamp: peakItem.timestamp(),
                    high: peakItem.high(),
                    low: peakItem.low(),
                    peak,
                    intialVolume: 0, // swingVolume will contain full volume for first swing
                    totalVolume: this.swingVolume.state.items.slice(0, index + 1)
                        .reduce((a, b) => a + b, 0)
                };
            }
        }

        if (lastPoint || newPoint) {
            const previous = this.prevPoint;
            const current = lastPoint || newPoint;

            const priceDiff = current.peak ? (current.high - previous.low)
                : (current.low - previous.high);
            
            const coordinates = {
                // Added condition to check for volume being NAN and removing it from text
                //text: isNaN(current.totalVolume) ? [undefined, `${Math.round(priceDiff / tickSize)},`] : [undefined, `${Math.round(priceDiff / tickSize)},${Math.round(current.totalVolume / 1000)}K`],
                //text: isNaN(current.totalVolume) ? [undefined, `${Math.round(priceDiff)},`] : [undefined, `${Math.round(priceDiff)},${Math.round(current.totalVolume / 1000)}K`],
                text: isNaN(current.totalVolume) ? display === "point" ? [undefined, `${Math.round(priceDiff)},`] : [undefined, `${Math.round(priceDiff / tickSize)},`] : display === "point" ? [undefined, `${Math.round(priceDiff)},${Math.round(current.totalVolume / 1000)}K`] : [undefined, `${Math.round(priceDiff / tickSize)},${Math.round(current.totalVolume / 1000)}K`],
                x: [
                    previous.timestamp,
                    current.timestamp
                ],
                y: [
                    previous.peak === false || current.peak ? previous.low : previous.high,
                    current.peak ? current.high : current.low
                ]
            };

            const upSwing = current.peak;
            const volumeUp = previous.totalVolume ? current.totalVolume > previous.totalVolume : false;

            // Last two swings
            if (lastPoint) {
                if (lastPoint.timestamp !== d.timestamp()) {
                    const priceDiff = current.peak ? (d.low() - current.high)
                        : (d.high() - current.low);
                    const lastVolume = (current.intialVolume + this.swingVolume.sum()) - current.totalVolume;
                    // Added condition to check for volume being NAN and removing it from text
                    //const lastData = isNaN(lastVolume) ? `${Math.round(priceDiff / tickSize)}` : `${Math.round(priceDiff / tickSize)},${Math.round(lastVolume / 1000)}K`;
                    const lastData = isNaN(lastVolume) ? `${Math.round(priceDiff)}` : `${Math.round(priceDiff)},${Math.round(lastVolume / 1000)}K`;
                    coordinates.text.push(lastData);

                    const currentGreen = (upSwing && volumeUp) || (!upSwing && !volumeUp);
                    const lastGreen = (!upSwing && (lastVolume > current.totalVolume)) || (upSwing && (lastVolume <= current.totalVolume));

                    //TESTING Line
                    //console.log("d", i, current, previous, priceDiff / tickSize, "upSwing", upSwing, "volumeUp", volumeUp, "lastVolume", lastVolume, current.totalVolume);

                    // Last swing estimated at current bar, seperate line style if needed, else add last swing to current style
                    if (currentGreen === lastGreen) {
                        coordinates.x.push(d.timestamp());
                        coordinates.y.push(current.peak ? d.low() : d.high());
                    }
                    else {
                        if (currentGreen) {
                            value = {
                                green: coordinates
                            };
                        }
                        else {
                            value = {
                                red: coordinates
                            };
                        }

                        if (lastGreen) {
                            value.green = lodash.cloneDeep(coordinates);
                            value.green.text.splice(1, 1);
                            value.green.x.shift();
                            value.green.y.shift();
                            value.green.x.push(d.timestamp());
                            value.green.y.push(current.peak ? d.low() : d.high());
                        }
                        else {
                            value.red = lodash.cloneDeep(coordinates);
                            value.red.text.splice(1, 1);
                            value.red.x.shift();
                            value.red.y.shift();
                            value.red.x.push(d.timestamp());
                            value.red.y.push(current.peak ? d.low() : d.high());
                        }
                    }
                }
            }

            if (!value && ((upSwing && volumeUp) || (!upSwing && !volumeUp))) {
                value = {
                    green: coordinates
                };
            }
            else if (!value && ((!upSwing && volumeUp) || (upSwing && !volumeUp))) {
                value = {
                    red: coordinates
                };
            }

            this.prevPoint = lodash.clone(current);
            this.swingVolume.reset();
        }
        
        return value;
    }

    filter(d) {
        return !!d;
    }
}

module.exports = {
    //name: "jigsaw",
    //title: "Pr.Act.Swing",
    //description: "Price Action Swing",
    //calculator: priceActionSwing,
    name: "zigZag_Count",
    description: "ZigZag Count",
    calculator: zigZagPrice,
    params: {
        reversalType: predef.paramSpecs.enum({
            point: 'Point',
            percent: 'Percent',
        }, 'point'),
        //filter: predef.paramSpecs.percent(0.10, 0.02, 0.01, 100.0),
        reversalAmt: predef.paramSpecs.number(6.5, 0.01, 0.01),
        displayType: predef.paramSpecs.enum({
            point: 'Points',
            tick: 'Ticks',
        }, 'point'),
    },
    inputType: meta.InputType.BARS,
    plotter: predef.plotters.zigzag(["green", "red"]),
    plots: {
        green: { title: "Green" },
        red: { title: "Red" },
    },
    schemeStyles: {
        dark: {
            green: predef.styles.plot("#00CC66"),
            red: predef.styles.plot("#B30000"),
        },
        light: {
            green: predef.styles.plot("#00CC66"),
            red: predef.styles.plot("#B30000"),
        },
    }
};