System Swing Indicator – Logic Bug, Performance Issue, and Suggested Refactor

During comparative benchmarking and diagnostic testing of the system-provided Swing indicator, I identified a few issues that may impact both the accuracy and performance of the current implementation:


:red_exclamation_mark: 1. Performance Issue – Missing break in Validation Loops

The following loops that validate swing highs/lows lack a break statement once the condition fails:

for (int i = 0; i < Strength; i++)
    if (((double) lastHighCache[i]).ApproxCompare(swingHighCandidateValue) >= 0)
        isSwingHigh = false;

for (int i = Strength + 1; i < lastHighCache.Count; i++)
    if (((double) lastHighCache[i]).ApproxCompare(swingHighCandidateValue) > 0)
        isSwingHigh = false;

Suggestion: Introduce an early break; when isSwingHigh = false is detected to avoid unnecessary comparisons, especially with larger Strength values.

:warning:2. Bug – Asymmetric Comparison Logic

In the validation logic, one loop uses >= while the other uses >, leading to inconsistent detection of swing highs or lows — particularly when multiple equal highs/lows are present:

// Asymmetric swing high checks
if (((double) lastHighCache[i]).ApproxCompare(swingHighCandidateValue) >= 0)
...
if (((double) lastHighCache[i]).ApproxCompare(swingHighCandidateValue) > 0)

This results in false negatives or missed swings depending on where the equal value appears.

:white_check_mark: Suggestion: Align the comparison conditions symmetrically across both loops and optionally expose a parameter like AllowEqualHighLow to allow users to toggle this behaviour.

:rocket: 3. Performance – Overuse of ApproxCompare on Price Series

Currently, ApproxCompare is applied to all input types. This incurs unnecessary floating-point logic on inputs that are already known to be exchange-supplied OHLC price series.

:white_check_mark: Suggestion: Restrict .ApproxCompare(...) usage only for non-price series (e.g., calculated indicators), and use standard ==, <, > operators when Input is PriceSeries || Input is Bars.

Suggested Refactor

To address all the above issues, I’ve implemented a version called SwingSlidingWindow that:

  • Uses sliding window logic for forward-looking validation
  • Supports price and calculated inputs
  • Offers an AllowEqualHighLow property
  • Implements symmetric comparisons with early exits (break)
  • Provides public access to SwingHighBar(...) and SwingLowBar(...) for strategy use
  • Delivers better performance and consistent swing logic
#region Using declarations

using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Windows.Media;
using System.Xml.Serialization;
using NinjaTrader.Data;
using NinjaTrader.Gui;
using NinjaTrader.NinjaScript;
using NinjaTrader.Core.FloatingPoint;
using NinjaTrader.NinjaScript.DrawingTools;
using NinjaTrader.NinjaScript.Indicators;

#endregion

namespace NinjaTrader.NinjaScript.Indicators.TradingTools
{
    [CategoryOrder("Version", 0)]
    [CategoryOrder("Parameters", 1)]
    public class SwingSlidingWindow : Indicator
    {
        private Series<double> swingHighSeries;
        private Series<double> swingLowSeries;
        private bool isPriceSeries;
        private int windowSize;

        private double lastSwingHigh = double.NaN;
        private double lastSwingLow = double.NaN;

        protected override void OnStateChange()
        {
            if (State == State.SetDefaults)
            {
                Name = "SwingSlidingWindow";
                Description = "Swing High/Low detector using sliding window logic.";
                Calculate = Calculate.OnBarClose;
                IsOverlay = true;

                AddPlot(new Stroke(Brushes.DarkCyan, 2), PlotStyle.Dot, "SwingHigh");
                AddPlot(new Stroke(Brushes.Goldenrod, 2), PlotStyle.Dot, "SwingLow");

                Strength = 5;
                AllowEqualHighLow = true;
                Version = "3.3";
            }
            else if (State == State.DataLoaded)
            {
                swingHighSeries = new Series<double>(this);
                swingLowSeries = new Series<double>(this);
                windowSize = 2 * Strength + 1;
                isPriceSeries = Input is PriceSeries || Input is Bars;
            }
        }

        protected override void OnBarUpdate()
        {
            if (CurrentBar < windowSize)
            {
                swingHighSeries[0] = 0.0;
                swingLowSeries[0] = 0.0;
                SwingHighPlot[0] = 0.0;
                SwingLowPlot[0] = 0.0;
                return;
            }

            int center = Strength;
            double centerHigh = isPriceSeries ? High[Strength] : Input[Strength];
            double centerLow = isPriceSeries ? Low[Strength] : Input[Strength];

            bool isSwingHigh = true;
            bool isSwingLow = true;

            // High loop
            for (int i = 0; i < windowSize; i++)
            {
                if (i == center)
                    continue;

                if (isPriceSeries)
                {
                    if ((AllowEqualHighLow ? High[i] > centerHigh : High[i] >= centerHigh))
                    {
                        isSwingHigh = false;
                        break;
                    }
                }
                else
                {
                    if ((AllowEqualHighLow ? Input[i].ApproxCompare(centerHigh) > 0 : Input[i].ApproxCompare(centerHigh) >= 0))
                    {
                        isSwingHigh = false;
                        break;
                    }
                }
            }

            // Low loop
            for (int i = 0; i < windowSize; i++)
            {
                if (i == center)
                    continue;

                if (isPriceSeries)
                {
                    if ((AllowEqualHighLow ? Low[i] < centerLow : Low[i] <= centerLow))
                    {
                        isSwingLow = false;
                        break;
                    }
                }
                else
                {
                    if ((AllowEqualHighLow ? Input[i].ApproxCompare(centerLow) < 0 : Input[i].ApproxCompare(centerLow) <= 0))
                    {
                        isSwingLow = false;
                        break;
                    }
                }
            }

            // Handle Swing High
            if (isSwingHigh)
            {
                double swingValueHigh = isPriceSeries ? High[Strength] : Input[Strength];
                lastSwingHigh = swingValueHigh;
                for (int i = 0; i <= Strength; i++)
                {
                    swingHighSeries[i] = swingValueHigh;
                    SwingHighPlot[i] = swingValueHigh;
                }
            }
            else
            {
                swingHighSeries[0] = lastSwingHigh;
                double currentValue = isPriceSeries ? High[0] : Input[0];

                if (lastSwingHigh != 0.0 && currentValue > lastSwingHigh)
                {
                    SwingHighPlot[0] = double.NaN;
                }
                else
                {
                    SwingHighPlot[0] = SwingHighPlot[1];
                }
            }

            // Handle Swing Low
            if (isSwingLow)
            {
                double swingValueLow = isPriceSeries ? Low[Strength] : Input[Strength];
                lastSwingLow = swingValueLow;
                for (int i = 0; i <= Strength; i++)
                {
                    swingLowSeries[i] = swingValueLow;
                    SwingLowPlot[i] = swingValueLow;
                }
            }
            else
            {
                swingLowSeries[0] = lastSwingLow;
                double currentValue = isPriceSeries ? Low[0] : Input[0];

                if (lastSwingLow != 0.0 && currentValue < lastSwingLow)
                {
                    SwingLowPlot[0] = double.NaN;
                }
                else
                {
                    SwingLowPlot[0] = SwingLowPlot[1];
                }
            }
        }

        #region Properties

        [NinjaScriptProperty]
        [Range(1, int.MaxValue)]
        [Display(Name = "Strength", GroupName = "Parameters", Order = 1)]
        public int Strength { get; set; }

        [NinjaScriptProperty]
        [Display(Name = "Allow Equal Highs/Lows", GroupName = "Parameters", Order = 2)]
        public bool AllowEqualHighLow { get; set; }

        [Display(Name = "Version", GroupName = "Version", Order = 0)]
        [ReadOnly(true)]
        public string Version { get; set; }

        [Browsable(false)]
        [XmlIgnore()]
        public Series<double> SwingHigh
        {
            get
            {
                Update();
                return swingHighSeries;
            }
        }

        private Series<double> SwingHighPlot
        {
            get
            {
                Update();
                return Values[0];
            }
        }

        [Browsable(false)]
        [XmlIgnore()]
        public Series<double> SwingLow
        {
            get
            {
                Update();
                return swingLowSeries;
            }
        }

        private Series<double> SwingLowPlot
        {
            get
            {
                Update();
                return Values[1];
            }
        }

        #endregion

        #region Helper Methods

        public int SwingHighBar(int barsAgo, int instance, int lookBackPeriod)
        {
            int found = 0;
            for (int i = barsAgo; i < Math.Min(CurrentBar, lookBackPeriod + barsAgo); i++)
            {
                if (!double.IsNaN(SwingHighPlot[i]))
                {
                    if (found == instance)
                        return i;
                    found++;
                }
            }
            return -1;
        }

        public int SwingLowBar(int barsAgo, int instance, int lookBackPeriod)
        {
            int found = 0;
            for (int i = barsAgo; i < Math.Min(CurrentBar, lookBackPeriod + barsAgo); i++)
            {
                if (!double.IsNaN(SwingLowPlot[i]))
                {
                    if (found == instance)
                        return i;
                    found++;
                }
            }
            return -1;
        }

        #endregion
    }
}

#region NinjaScript generated code. Neither change nor remove.

namespace NinjaTrader.NinjaScript.Indicators
{
    public partial class Indicator : NinjaTrader.Gui.NinjaScript.IndicatorRenderBase
    {
        private TradingTools.SwingSlidingWindow[] cacheSwingSlidingWindow;
        public TradingTools.SwingSlidingWindow SwingSlidingWindow(int strength)
        {
            return SwingSlidingWindow(Input, strength);
        }

        public TradingTools.SwingSlidingWindow SwingSlidingWindow(ISeries<double> input, int strength)
        {
            if (cacheSwingSlidingWindow != null)
                for (int idx = 0; idx < cacheSwingSlidingWindow.Length; idx++)
                    if (cacheSwingSlidingWindow[idx] != null && cacheSwingSlidingWindow[idx].Strength == strength && cacheSwingSlidingWindow[idx].EqualsInput(input))
                        return cacheSwingSlidingWindow[idx];
            return CacheIndicator<TradingTools.SwingSlidingWindow>(new TradingTools.SwingSlidingWindow() { Strength = strength }, input, ref cacheSwingSlidingWindow);
        }
    }
}

namespace NinjaTrader.NinjaScript.MarketAnalyzerColumns
{
    public partial class MarketAnalyzerColumn : MarketAnalyzerColumnBase
    {
        public Indicators.TradingTools.SwingSlidingWindow SwingSlidingWindow(int strength)
        {
            return indicator.SwingSlidingWindow(Input, strength);
        }

        public Indicators.TradingTools.SwingSlidingWindow SwingSlidingWindow(ISeries<double> input, int strength)
        {
            return indicator.SwingSlidingWindow(input, strength);
        }
    }
}

namespace NinjaTrader.NinjaScript.Strategies
{
    public partial class Strategy : NinjaTrader.Gui.NinjaScript.StrategyRenderBase
    {
        public Indicators.TradingTools.SwingSlidingWindow SwingSlidingWindow(int strength)
        {
            return indicator.SwingSlidingWindow(Input, strength);
        }

        public Indicators.TradingTools.SwingSlidingWindow SwingSlidingWindow(ISeries<double> input, int strength)
        {
            return indicator.SwingSlidingWindow(input, strength);
        }
    }
}

#endregion

:white_check_mark: Summary

These issues can lead to missed or misclassified swing points and unnecessary CPU usage in high-frequency backtesting. A review and refactor of the system Swing indicator may be warranted.

Happy to share diagnostic test results and benchmark comparisons if helpful.

Looks like a ChatGPT reply. :grinning_face: If you are super concerned about performance then you should ask it to optimize it as close to constant or logarithmic time and space complexity as possible.

What I had reported was a bug in the logic of the Swing System Indicator (asymmetric determination of a swing around the swing bar for equal high or low bars). This results in false negatives or missed swings depending on where the equal value appears.

I have used an AI model to help me diagnose the bug and how to address it. As a result of this the AI model has also identified performance improvement in how it is working as I saw high cpu usage on the performance monitor when I run the swing indicator on every chart and I call it continuously in my own indicators over the course of the day, particularly on lower timeframe charts.

The point of this post was for the NinjaTrader development team to look at this bug and also at the same time see if it’s performance can be improved similar to the alternative implementation I had posted.

Good luck trying to report your possible bug on a community forum where the staff are not active. These are so minimal and the priority is so low. Considering their track record, these will most likely be in a deep backlog somewhere for the next 20 years or something if they considered your suggestion.

Did you consider that the asymmetric comparison was intentional and not a bug? Adding a break in a simple loop like these can save some iterations, but modern CPUs can do millions of floating-point comparisons per second. Even the overhead call for ApproxCompare is minimal. The savings is probably small in microseconds to seconds and the performance gain is probably not meaningful.