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:
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.
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.
Suggestion: Align the comparison conditions symmetrically across both loops and optionally expose a parameter like
AllowEqualHighLow
to allow users to toggle this behaviour.
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.
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(...)
andSwingLowBar(...)
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
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.