NinjaTrader - ZigZag Labels
Jun 28, 2023

This modified ZigZag indicator plots the rotation text above/below the swing high/low.


#region Using declarations
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Xml.Serialization;
using NinjaTrader.Cbi;
using NinjaTrader.Gui;
using NinjaTrader.Gui.Chart;
using NinjaTrader.Gui.SuperDom;
using NinjaTrader.Data;
using NinjaTrader.NinjaScript;
using NinjaTrader.Core.FloatingPoint;
using NinjaTrader.NinjaScript.DrawingTools;
#endregion

//This namespace holds indicators in this folder and is required. Do not change it.
namespace NinjaTrader.NinjaScript.Indicators
{
	/// <summary>
	/// The ZigZag indicator shows trend lines filtering out changes below a defined level.
	/// </summary>
	public class ZigZagLabels : Indicator
	{
		private Series<double>		zigZagHighZigZags;
		private Series<double>		zigZagLowZigZags;
		private Series<double>		zigZagHighSeries;
		private Series<double>		zigZagLowSeries;

		private double				currentZigZagHigh;
		private double				currentZigZagLow;
		private int					lastSwingIdx;
		private double				lastSwingPrice;
		private int 				startIndex;
		private int					trendDir;
		
		private string swingHighName;
		private string swingLowName;
		
		protected override void OnStateChange()
		{
			if (State == State.SetDefaults)
			{
				Description					= NinjaTrader.Custom.Resource.NinjaScriptIndicatorDescriptionZigZag;
				Name						= "ZigZagLabels";
				DeviationType				= DeviationType.Points;
				DeviationValue				= 0.5;
				DisplayInDataBox			= false;
				DrawOnPricePanel			= false;
				IsSuspendedWhileInactive	= true;
				IsOverlay					= true;
				PaintPriceMarkers			= false;
				UseHighLow					= false;

				AddPlot(Brushes.DodgerBlue, NinjaTrader.Custom.Resource.NinjaScriptIndicatorNameZigZag);

				DisplayInDataBox			= false;
				PaintPriceMarkers			= false;
			}
			else if (State == State.Configure)
			{
				currentZigZagHigh	= 0;
				currentZigZagLow	= 0;
				lastSwingIdx		= -1;
				lastSwingPrice		= 0.0;
				trendDir			= 0; // 1 = trend up, -1 = trend down, init = 0
				startIndex 			= int.MinValue;
			}
			else if (State == State.DataLoaded)
			{
				zigZagHighZigZags	= new Series<double>(this, MaximumBarsLookBack.Infinite);
				zigZagLowZigZags	= new Series<double>(this, MaximumBarsLookBack.Infinite);
				zigZagHighSeries	= new Series<double>(this, MaximumBarsLookBack.Infinite);
				zigZagLowSeries		= new Series<double>(this, MaximumBarsLookBack.Infinite);
			}
		}

		// Returns the number of bars ago a zig zag low occurred. Returns a value of -1 if a zig zag low is not found within the look back period.
		public int LowBar(int barsAgo, int instance, int lookBackPeriod)
		{
			if (instance < 1)
				throw new Exception(string.Format(NinjaTrader.Custom.Resource.ZigZagLowBarInstanceGreaterEqual, GetType().Name, instance));
			if (barsAgo < 0)
				throw new Exception(string.Format(NinjaTrader.Custom.Resource.ZigZigLowBarBarsAgoGreaterEqual, GetType().Name, barsAgo));
			if (barsAgo >= Count)
				throw new Exception(string.Format(NinjaTrader.Custom.Resource.ZigZagLowBarBarsAgoOutOfRange, GetType().Name, (Count - 1), barsAgo));

			Update();
			for (int idx = CurrentBar - barsAgo - 1; idx >= CurrentBar - barsAgo - 1 - lookBackPeriod; idx--)
			{
				if (idx < 0)
					return -1;
				if (idx >= zigZagLowZigZags.Count)
					continue;

				if (!zigZagLowZigZags.IsValidDataPointAt(idx))
					continue;

				if (instance == 1) // 1-based, < to be save
					return CurrentBar - idx;

				instance--;
			}

			return -1;
		}

		// Returns the number of bars ago a zig zag high occurred. Returns a value of -1 if a zig zag high is not found within the look back period.
		public int HighBar(int barsAgo, int instance, int lookBackPeriod)
		{
			if (instance < 1)
				throw new Exception(string.Format(NinjaTrader.Custom.Resource.ZigZagHighBarInstanceGreaterEqual, GetType().Name, instance));
			if (barsAgo < 0)
				throw new Exception(string.Format(NinjaTrader.Custom.Resource.ZigZigHighBarBarsAgoGreaterEqual, GetType().Name, barsAgo));
			if (barsAgo >= Count)
				throw new Exception(string.Format(NinjaTrader.Custom.Resource.ZigZagHighBarBarsAgoOutOfRange, GetType().Name, (Count - 1), barsAgo));

			Update();
			for (int idx = CurrentBar - barsAgo - 1; idx >= CurrentBar - barsAgo - 1 - lookBackPeriod; idx--)
			{
				if (idx < 0)
					return -1;
				if (idx >= zigZagHighZigZags.Count)
					continue;

				if (!zigZagHighZigZags.IsValidDataPointAt(idx))
					continue;

				if (instance <= 1) // 1-based, < to be save
					return CurrentBar - idx;

				instance--;
			}

			return -1;
		}

		protected override void OnBarUpdate()
		{
			if (CurrentBar < 2) // Need at least 3 bars to calculate Low/High
			{
				zigZagHighSeries[0]		= 0;
				zigZagLowSeries[0] 		= 0;
				return;
			}

			// Initialization
			if (lastSwingPrice == 0.0)
				lastSwingPrice = Input[0];

			ISeries<double> highSeries	= High;
			ISeries<double> lowSeries	= Low;

			if (!UseHighLow)
			{
				highSeries	= Input;
				lowSeries	= Input;
			}

			// Calculation always for 1-bar ago !
			bool isSwingHigh			= highSeries[1].ApproxCompare(highSeries[0]) >= 0
											&& highSeries[1].ApproxCompare(highSeries[2]) >= 0;
			bool isSwingLow				= lowSeries[1].ApproxCompare(lowSeries[0]) <= 0
											&& lowSeries[1].ApproxCompare(lowSeries[2]) <= 0;
			bool isOverHighDeviation	= (DeviationType == DeviationType.Percent && IsPriceGreater(highSeries[1], (lastSwingPrice * (1.0 + DeviationValue / 100.0))))
											|| (DeviationType == DeviationType.Points && IsPriceGreater(highSeries[1], lastSwingPrice + DeviationValue));
			bool isOverLowDeviation		= (DeviationType == DeviationType.Percent && IsPriceGreater(lastSwingPrice * (1.0 - DeviationValue / 100.0), lowSeries[1]))
											|| (DeviationType == DeviationType.Points && IsPriceGreater(lastSwingPrice - DeviationValue, lowSeries[1]));

			double	saveValue	= 0.0;
			bool	addHigh		= false;
			bool	addLow		= false;
			bool	updateHigh	= false;
			bool	updateLow	= false;

			if (!isSwingHigh && !isSwingLow)
			{
				zigZagHighSeries[0] = currentZigZagHigh;
				zigZagLowSeries[0]	= currentZigZagLow;
				return;
			}

			if (trendDir <= 0 && isSwingHigh && isOverHighDeviation)
			{
				saveValue	= highSeries[1];
				addHigh		= true;
				trendDir	= 1;
			}
			else if (trendDir >= 0 && isSwingLow && isOverLowDeviation)
			{
				saveValue	= lowSeries[1];
				addLow		= true;
				trendDir	= -1;
			}
			else if (trendDir == 1 && isSwingHigh && IsPriceGreater(highSeries[1], lastSwingPrice))
			{
				saveValue	= highSeries[1];
				updateHigh	= true;
			}
			else if (trendDir == -1 && isSwingLow && IsPriceGreater(lastSwingPrice, lowSeries[1]))
			{
				saveValue	= lowSeries[1];
				updateLow	= true;
			}

			if (addHigh || addLow || updateHigh || updateLow)
			{
				if (updateHigh && lastSwingIdx >= 0)
				{
					zigZagHighZigZags.Reset(CurrentBar - lastSwingIdx);
					Value.Reset(CurrentBar - lastSwingIdx);
				}
				else if (updateLow && lastSwingIdx >= 0)
				{
					zigZagLowZigZags.Reset(CurrentBar - lastSwingIdx);
					Value.Reset(CurrentBar - lastSwingIdx);
				}

				if (addHigh || updateHigh)
				{
					zigZagHighZigZags[1]	= saveValue;
					currentZigZagHigh 		= saveValue;
					zigZagHighSeries[1]		= currentZigZagHigh;
					Value[1]				= currentZigZagHigh;
				}
				else if (addLow || updateLow)
				{
					zigZagLowZigZags[1]	= saveValue;
					currentZigZagLow 	= saveValue;
					zigZagLowSeries[1]	= currentZigZagLow;
					Value[1]			= currentZigZagLow;
				}

				lastSwingIdx	= CurrentBar - 1;
				lastSwingPrice	= saveValue;
			}

			zigZagHighSeries[0]	= currentZigZagHigh;
			zigZagLowSeries[0]	= currentZigZagLow;
			
			if (addHigh || updateHigh)
			{
				string text = string.Format("{0}", lastSwingPrice - currentZigZagLow);
				
				if (DeviationType == DeviationType.Percent)
					text = string.Format("{0,0:N2}%", (lastSwingPrice / currentZigZagLow - 1) * 100);
				
				if (addHigh)
					swingHighName = string.Format("{0} ZZ", CurrentBar);
				
				Draw.Text(this, swingHighName, text , 0, High[0] + 5 * TickSize, Brushes.Green);
			}
			else if (addLow || updateLow)
			{
				string text = string.Format("{0}", lastSwingPrice - currentZigZagHigh);
				
				if (DeviationType == DeviationType.Percent)
					text = string.Format("{0,0:N2}%", (lastSwingPrice / currentZigZagHigh - 1) * 100);
				
				if (addLow)
					swingLowName = string.Format("{0} ZZ", CurrentBar);
				
				Draw.Text(this, swingLowName, text , 0, Low[0] - 5 * TickSize, Brushes.Red);
			}
			
			if (startIndex == int.MinValue && (zigZagHighZigZags.IsValidDataPoint(1) && zigZagHighZigZags[1] != zigZagHighZigZags[2] || zigZagLowZigZags.IsValidDataPoint(1) && zigZagLowZigZags[1] != zigZagLowZigZags[2]))
				startIndex = CurrentBar - (Calculate == Calculate.OnBarClose ? 2 : 1);
		}

		#region Properties
		/// <summary>
		/// Gets the ZigZag high points.
		/// </summary>
		[NinjaScriptProperty]
		[Display(ResourceType = typeof(Custom.Resource), Name = "DeviationType", GroupName = "NinjaScriptParameters", Order = 0)]
		public DeviationType DeviationType
		{ get; set; }

		[Range(0, int.MaxValue), NinjaScriptProperty]
		[Display(ResourceType = typeof(Custom.Resource), Name = "DeviationValue", GroupName = "NinjaScriptParameters", Order = 1)]
		public double DeviationValue
		{ get; set; }

		[NinjaScriptProperty]
		[Display(ResourceType = typeof(Custom.Resource), Name = "UseHighLow", GroupName = "NinjaScriptParameters", Order = 2)]
		public bool UseHighLow
		{ get; set; }

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

		/// <summary>
		/// Gets the ZigZag low points.
		/// </summary>
		[Browsable(false)]
		[XmlIgnore()]
		public Series<double> ZigZagLow
		{
			get
			{
				Update();
				return zigZagLowSeries;
			}
		}
		#endregion

		#region Miscellaneous
		private bool IsPriceGreater(double a, double b)
		{
			return a.ApproxCompare(b) > 0;
		}

		public override void OnCalculateMinMax()
		{
			MinValue = double.MaxValue;
			MaxValue = double.MinValue;
			
			if (BarsArray[0] == null || ChartBars == null || startIndex == int.MinValue)
				return;

			for (int seriesCount = 0; seriesCount < Values.Length; seriesCount++)
			{
				for (int idx = ChartBars.FromIndex - Displacement; idx <= ChartBars.ToIndex + Displacement; idx++)
				{
					if (idx < 0 || idx > Bars.Count - 1 - (Calculate == NinjaTrader.NinjaScript.Calculate.OnBarClose ? 1 : 0))
						continue;
					
					if (zigZagHighZigZags.IsValidDataPointAt(idx))
						MaxValue = Math.Max(MaxValue, zigZagHighZigZags.GetValueAt(idx));
					
					if (zigZagLowZigZags.IsValidDataPointAt(idx))
						MinValue = Math.Min(MinValue, zigZagLowZigZags.GetValueAt(idx));
				}
			}
		}

		protected override Point[] OnGetSelectionPoints(ChartControl chartControl, ChartScale chartScale)
		{
			if (!IsSelected || Count == 0 || Plots[0].Brush.IsTransparent() || startIndex == int.MinValue)
				return new System.Windows.Point[0];

			List<System.Windows.Point> points = new List<System.Windows.Point>();

			int lastIndex	= Calculate == NinjaTrader.NinjaScript.Calculate.OnBarClose ? ChartBars.ToIndex - 1 : ChartBars.ToIndex - 2;

			for (int i = Math.Max(0, ChartBars.FromIndex - Displacement); i <= Math.Max(lastIndex, Math.Min(Bars.Count - (Calculate == NinjaTrader.NinjaScript.Calculate.OnBarClose ? 2 : 1), lastIndex - Displacement)); i++)
			{
				int x = (chartControl.BarSpacingType == BarSpacingType.TimeBased || chartControl.BarSpacingType == BarSpacingType.EquidistantMulti && i + Displacement >= ChartBars.Count
					? chartControl.GetXByTime(ChartBars.GetTimeByBarIdx(chartControl, i + Displacement))
					: chartControl.GetXByBarIndex(ChartBars, i + Displacement));

				if (Value.IsValidDataPointAt(i))
					points.Add(new System.Windows.Point(x, chartScale.GetYByValue(Value.GetValueAt(i))));
			}
			return points.ToArray();
		}

		protected override void OnRender(Gui.Chart.ChartControl chartControl, Gui.Chart.ChartScale chartScale)
		{
			if (Bars == null || chartControl == null || startIndex == int.MinValue)
				return;

			IsValidDataPointAt(Bars.Count - 1 - (Calculate == NinjaTrader.NinjaScript.Calculate.OnBarClose ? 1 : 0)); // Make sure indicator is calculated until last (existing) bar
			int preDiff = 1;
			for (int i = ChartBars.FromIndex - 1; i >= 0; i--)
			{
				if (i - Displacement < startIndex || i - Displacement > Bars.Count - 1 - (Calculate == NinjaTrader.NinjaScript.Calculate.OnBarClose ? 1 : 0))
					break;

				bool isHigh	= zigZagHighZigZags.IsValidDataPointAt(i - Displacement);
				bool isLow	= zigZagLowZigZags.IsValidDataPointAt(i - Displacement);

				if (isHigh || isLow)
					break;

				preDiff++;
			}

			preDiff -= (Displacement < 0 ? Displacement : 0 - Displacement);

			int postDiff = 0;
			for (int i = ChartBars.ToIndex; i <= zigZagHighZigZags.Count; i++)
			{
				if (i - Displacement < startIndex || i - Displacement > Bars.Count - 1 - (Calculate == NinjaTrader.NinjaScript.Calculate.OnBarClose ? 1 : 0))
					break;

				bool isHigh	= zigZagHighZigZags.IsValidDataPointAt(i - Displacement);
				bool isLow	= zigZagLowZigZags.IsValidDataPointAt(i - Displacement);

				if (isHigh || isLow)
					break;

				postDiff++;
			}

			postDiff += (Displacement < 0 ? 0 - Displacement : Displacement);

			int		lastIdx		= -1;
			double	lastValue	= -1;
			SharpDX.Direct2D1.PathGeometry	g		= null;
			SharpDX.Direct2D1.GeometrySink	sink	= null;

			for (int idx = ChartBars.FromIndex - preDiff; idx <= ChartBars.ToIndex + postDiff; idx++)
			{
				if (idx < startIndex || idx > Bars.Count - (Calculate == NinjaTrader.NinjaScript.Calculate.OnBarClose ? 2 : 1) || idx < Math.Max(BarsRequiredToPlot - Displacement, Displacement))
					continue;

				bool isHigh	= zigZagHighZigZags.IsValidDataPointAt(idx);
				bool isLow	= zigZagLowZigZags.IsValidDataPointAt(idx);

				if (!isHigh && !isLow)
					continue;
				
				double value = isHigh ? zigZagHighZigZags.GetValueAt(idx) : zigZagLowZigZags.GetValueAt(idx);
				
				if (lastIdx >= startIndex)
				{
					float x1	= (chartControl.BarSpacingType == BarSpacingType.TimeBased || chartControl.BarSpacingType == BarSpacingType.EquidistantMulti && idx + Displacement >= ChartBars.Count
						? chartControl.GetXByTime(ChartBars.GetTimeByBarIdx(chartControl, idx + Displacement))
						: chartControl.GetXByBarIndex(ChartBars, idx + Displacement));
					float y1	= chartScale.GetYByValue(value);

					if (sink == null)
					{
						float x0	= (chartControl.BarSpacingType == BarSpacingType.TimeBased || chartControl.BarSpacingType == BarSpacingType.EquidistantMulti && lastIdx + Displacement >= ChartBars.Count
						? chartControl.GetXByTime(ChartBars.GetTimeByBarIdx(chartControl, lastIdx + Displacement))
						: chartControl.GetXByBarIndex(ChartBars, lastIdx + Displacement));
						float y0	= chartScale.GetYByValue(lastValue);
						g			= new SharpDX.Direct2D1.PathGeometry(Core.Globals.D2DFactory);
						sink		= g.Open();
						sink.BeginFigure(new SharpDX.Vector2(x0, y0), SharpDX.Direct2D1.FigureBegin.Hollow);
					}
					sink.AddLine(new SharpDX.Vector2(x1, y1));
				}

				// Save as previous point
				lastIdx		= idx;
				lastValue	= value;
			}

			if (sink != null)
			{
				sink.EndFigure(SharpDX.Direct2D1.FigureEnd.Open);
				sink.Close();
			}

			if (g != null)
			{
				var oldAntiAliasMode = RenderTarget.AntialiasMode;
				RenderTarget.AntialiasMode = SharpDX.Direct2D1.AntialiasMode.PerPrimitive;
				RenderTarget.DrawGeometry(g, Plots[0].BrushDX, Plots[0].Width, Plots[0].StrokeStyle);
				RenderTarget.AntialiasMode = oldAntiAliasMode;
				g.Dispose();
				RemoveDrawObject("NinjaScriptInfo");
			}
			else
				Draw.TextFixed(this, "NinjaScriptInfo", NinjaTrader.Custom.Resource.ZigZagDeviationValueError, TextPosition.BottomRight);
		}
		#endregion
	}
}

#region NinjaScript generated code. Neither change nor remove.

namespace NinjaTrader.NinjaScript.Indicators
{
	public partial class Indicator : NinjaTrader.Gui.NinjaScript.IndicatorRenderBase
	{
		private ZigZagLabels[] cacheZigZagLabels;
		public ZigZagLabels ZigZagLabels(DeviationType deviationType, double deviationValue, bool useHighLow)
		{
			return ZigZagLabels(Input, deviationType, deviationValue, useHighLow);
		}

		public ZigZagLabels ZigZagLabels(ISeries<double> input, DeviationType deviationType, double deviationValue, bool useHighLow)
		{
			if (cacheZigZagLabels != null)
				for (int idx = 0; idx < cacheZigZagLabels.Length; idx++)
					if (cacheZigZagLabels[idx] != null && cacheZigZagLabels[idx].DeviationType == deviationType && cacheZigZagLabels[idx].DeviationValue == deviationValue && cacheZigZagLabels[idx].UseHighLow == useHighLow && cacheZigZagLabels[idx].EqualsInput(input))
						return cacheZigZagLabels[idx];
			return CacheIndicator<ZigZagLabels>(new ZigZagLabels(){ DeviationType = deviationType, DeviationValue = deviationValue, UseHighLow = useHighLow }, input, ref cacheZigZagLabels);
		}
	}
}

namespace NinjaTrader.NinjaScript.MarketAnalyzerColumns
{
	public partial class MarketAnalyzerColumn : MarketAnalyzerColumnBase
	{
		public Indicators.ZigZagLabels ZigZagLabels(DeviationType deviationType, double deviationValue, bool useHighLow)
		{
			return indicator.ZigZagLabels(Input, deviationType, deviationValue, useHighLow);
		}

		public Indicators.ZigZagLabels ZigZagLabels(ISeries<double> input , DeviationType deviationType, double deviationValue, bool useHighLow)
		{
			return indicator.ZigZagLabels(input, deviationType, deviationValue, useHighLow);
		}
	}
}

namespace NinjaTrader.NinjaScript.Strategies
{
	public partial class Strategy : NinjaTrader.Gui.NinjaScript.StrategyRenderBase
	{
		public Indicators.ZigZagLabels ZigZagLabels(DeviationType deviationType, double deviationValue, bool useHighLow)
		{
			return indicator.ZigZagLabels(Input, deviationType, deviationValue, useHighLow);
		}

		public Indicators.ZigZagLabels ZigZagLabels(ISeries<double> input , DeviationType deviationType, double deviationValue, bool useHighLow)
		{
			return indicator.ZigZagLabels(input, deviationType, deviationValue, useHighLow);
		}
	}
}

#endregion

Comments