From cd20c466ddf4abd1878dca36e69fbd5ca2b8e387 Mon Sep 17 00:00:00 2001 From: moshferatu Date: Sat, 6 Jan 2024 06:26:19 -0800 Subject: [PATCH] Add indicator for detecting pivots occurring at candles with above average volume --- indicators/VolumeSpikePivots.cs | 271 ++++++++++++++++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 indicators/VolumeSpikePivots.cs diff --git a/indicators/VolumeSpikePivots.cs b/indicators/VolumeSpikePivots.cs new file mode 100644 index 0000000..534c5d8 --- /dev/null +++ b/indicators/VolumeSpikePivots.cs @@ -0,0 +1,271 @@ +#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.Gui.Tools; +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 +{ + public class VolumeSpikePivotInfo + { + public bool IsPivotHigh { get; set; } + public bool IsPivotLow { get; set; } + public double Level { get; set; } + public int StartBarIndex { get; set; } + public int EndBarIndex { get; set; } + } + + public class VolumeSpikePivots : Indicator + { + public Series DetectedPivot; + + private List detectedPivots = new List(); + + private Series wasPivotHighBreachedLast; + + protected override void OnStateChange() + { + if (State == State.SetDefaults) + { + Description = @"Detects pivots which occurred on candles with above average volume"; + Name = "Volume Spike Pivots"; + Calculate = Calculate.OnPriceChange; + IsOverlay = true; + DisplayInDataBox = true; + DrawOnPricePanel = true; + DrawHorizontalGridLines = true; + DrawVerticalGridLines = true; + PaintPriceMarkers = true; + ScaleJustification = ScaleJustification.Right; + IsSuspendedWhileInactive = true; + BarsToCheck = 5; + VolumeAveragePeriod = 20; + VolumeMultiple = 1.5; + PivotHighStroke = new Stroke(Brushes.LimeGreen, 3); + PivotLowStroke = new Stroke(Brushes.Red, 3); + } + if (State == State.DataLoaded) + { + wasPivotHighBreachedLast = new Series(this, MaximumBarsLookBack.Infinite); + DetectedPivot = new Series(this, MaximumBarsLookBack.Infinite); + } + else if (State == State.Historical) + { + SetZOrder(-1); // Display behind bars on chart. + } + } + + protected override void OnBarUpdate() + { + if (CurrentBar < BarsToCheck || CurrentBar < VolumeAveragePeriod) + return; + + if (!IsFirstTickOfBar) + return; + + wasPivotHighBreachedLast[0] = wasPivotHighBreachedLast[1]; + + foreach (var pivot in detectedPivots) + { + if (pivot.EndBarIndex == 0) + { + if (pivot.IsPivotHigh && Close[1] > pivot.Level) + { + pivot.EndBarIndex = CurrentBar - 1; + wasPivotHighBreachedLast[0] = true; + } + else if (pivot.IsPivotLow && Close[1] < pivot.Level) + { + pivot.EndBarIndex = CurrentBar - 1; + wasPivotHighBreachedLast[0] = false; + } + } + } + + bool isPivotHigh = true; + bool isPivotLow = true; + + int centerBarIndex = (int)Math.Ceiling(BarsToCheck / 2.0); + + for (int i = 1; i <= BarsToCheck; i++) + { + if (i == centerBarIndex) continue; + + if (High[centerBarIndex] < High[i]) isPivotHigh = false; + if (Low[centerBarIndex] > Low[i]) isPivotLow = false; + } + + bool existsPivotHighWithoutEndAtSamePrice = detectedPivots.Any( + p => p.IsPivotHigh && p.EndBarIndex == 0 && p.Level == High[centerBarIndex]); + + bool existsPivotLowWithoutEndAtSamePrice = detectedPivots.Any( + p => p.IsPivotLow && p.EndBarIndex == 0 && p.Level == Low[centerBarIndex]); + + double averageVolume = 0; + for (int i = 0; i < VolumeAveragePeriod; i++) + { + averageVolume += Volume[i]; + } + averageVolume /= VolumeAveragePeriod; + + if (isPivotHigh && !existsPivotHighWithoutEndAtSamePrice && Volume[centerBarIndex] > averageVolume * VolumeMultiple) + { + VolumeSpikePivotInfo detectedPivot = new VolumeSpikePivotInfo { + IsPivotHigh = true, Level = High[centerBarIndex], StartBarIndex = CurrentBar - centerBarIndex }; + detectedPivots.Add(detectedPivot); + DetectedPivot[0] = detectedPivot; + } + + if (isPivotLow && !existsPivotLowWithoutEndAtSamePrice && Volume[centerBarIndex] > averageVolume * VolumeMultiple) + { + VolumeSpikePivotInfo detectedPivot = new VolumeSpikePivotInfo { + IsPivotLow = true, Level = Low[centerBarIndex], StartBarIndex = CurrentBar - centerBarIndex }; + detectedPivots.Add(detectedPivot); + DetectedPivot[0] = detectedPivot; + } + } + + protected override void OnRender(ChartControl chartControl, ChartScale chartScale) + { + base.OnRender(chartControl, chartScale); + + foreach (var pivot in detectedPivots) + { + int startX = chartControl.GetXByBarIndex(ChartBars, pivot.StartBarIndex); + int endX = pivot.EndBarIndex == 0 ? ChartPanel.X + ChartPanel.W : chartControl.GetXByBarIndex(ChartBars, pivot.EndBarIndex); + + var stroke = pivot.IsPivotHigh ? PivotHighStroke : pivot.IsPivotLow ? PivotLowStroke : new Stroke(Brushes.Gray); + DrawHorizontalLine(startX, endX, chartScale.GetYByValue(pivot.Level), stroke); + } + } + + private void DrawHorizontalLine(int startX, int endX, int y, Stroke stroke) + { + SharpDX.Direct2D1.Brush dxBrush = stroke.Brush.ToDxBrush(RenderTarget); + RenderTarget.DrawLine( + new SharpDX.Vector2(startX, y), + new SharpDX.Vector2(endX, y), + dxBrush, stroke.Width, stroke.StrokeStyle + ); + dxBrush.Dispose(); + } + + public override string DisplayName + { + get { return Name; } + } + + [NinjaScriptProperty] + [Range(1, int.MaxValue)] + [Display(Name = "Bars to Check", Order = 1, GroupName = "Volume Spike Pivots")] + public int BarsToCheck + { get; set; } + + [NinjaScriptProperty] + [Range(1, int.MaxValue)] + [Display(Name = "Volume Average Period", Order = 2, GroupName = "Volume Spike Pivots")] + public int VolumeAveragePeriod { get; set; } + + [NinjaScriptProperty] + [Range(1.0, double.MaxValue)] + [Display(Name = "Volume Multiple", Order = 3, GroupName = "Volume Spike Pivots")] + public double VolumeMultiple { get; set; } + + [NinjaScriptProperty] + [Display(Name = "Pivot High Stroke", Order = 4, GroupName = "Volume Spike Pivots")] + public Stroke PivotHighStroke { get; set; } + + [NinjaScriptProperty] + [Display(Name = "Pivot Low Stroke", Order = 5, GroupName = "Volume Spike Pivots")] + public Stroke PivotLowStroke { get; set; } + + [Browsable(false)] + [XmlIgnore] + public Series WasPivotHighBreachedLast + { + get { return wasPivotHighBreachedLast; } + } + + [Browsable(false)] + [XmlIgnore] + public Series DetectedPivots + { + get { return DetectedPivot; } + } + } +} + +#region NinjaScript generated code. Neither change nor remove. + +namespace NinjaTrader.NinjaScript.Indicators +{ + public partial class Indicator : NinjaTrader.Gui.NinjaScript.IndicatorRenderBase + { + private VolumeSpikePivots[] cacheVolumeSpikePivots; + public VolumeSpikePivots VolumeSpikePivots(int barsToCheck, int volumeAveragePeriod, double volumeMultiple, Stroke pivotHighStroke, Stroke pivotLowStroke) + { + return VolumeSpikePivots(Input, barsToCheck, volumeAveragePeriod, volumeMultiple, pivotHighStroke, pivotLowStroke); + } + + public VolumeSpikePivots VolumeSpikePivots(ISeries input, int barsToCheck, int volumeAveragePeriod, double volumeMultiple, Stroke pivotHighStroke, Stroke pivotLowStroke) + { + if (cacheVolumeSpikePivots != null) + for (int idx = 0; idx < cacheVolumeSpikePivots.Length; idx++) + if (cacheVolumeSpikePivots[idx] != null && cacheVolumeSpikePivots[idx].BarsToCheck == barsToCheck && cacheVolumeSpikePivots[idx].VolumeAveragePeriod == volumeAveragePeriod && cacheVolumeSpikePivots[idx].VolumeMultiple == volumeMultiple && cacheVolumeSpikePivots[idx].PivotHighStroke == pivotHighStroke && cacheVolumeSpikePivots[idx].PivotLowStroke == pivotLowStroke && cacheVolumeSpikePivots[idx].EqualsInput(input)) + return cacheVolumeSpikePivots[idx]; + return CacheIndicator(new VolumeSpikePivots(){ BarsToCheck = barsToCheck, VolumeAveragePeriod = volumeAveragePeriod, VolumeMultiple = volumeMultiple, PivotHighStroke = pivotHighStroke, PivotLowStroke = pivotLowStroke }, input, ref cacheVolumeSpikePivots); + } + } +} + +namespace NinjaTrader.NinjaScript.MarketAnalyzerColumns +{ + public partial class MarketAnalyzerColumn : MarketAnalyzerColumnBase + { + public Indicators.VolumeSpikePivots VolumeSpikePivots(int barsToCheck, int volumeAveragePeriod, double volumeMultiple, Stroke pivotHighStroke, Stroke pivotLowStroke) + { + return indicator.VolumeSpikePivots(Input, barsToCheck, volumeAveragePeriod, volumeMultiple, pivotHighStroke, pivotLowStroke); + } + + public Indicators.VolumeSpikePivots VolumeSpikePivots(ISeries input , int barsToCheck, int volumeAveragePeriod, double volumeMultiple, Stroke pivotHighStroke, Stroke pivotLowStroke) + { + return indicator.VolumeSpikePivots(input, barsToCheck, volumeAveragePeriod, volumeMultiple, pivotHighStroke, pivotLowStroke); + } + } +} + +namespace NinjaTrader.NinjaScript.Strategies +{ + public partial class Strategy : NinjaTrader.Gui.NinjaScript.StrategyRenderBase + { + public Indicators.VolumeSpikePivots VolumeSpikePivots(int barsToCheck, int volumeAveragePeriod, double volumeMultiple, Stroke pivotHighStroke, Stroke pivotLowStroke) + { + return indicator.VolumeSpikePivots(Input, barsToCheck, volumeAveragePeriod, volumeMultiple, pivotHighStroke, pivotLowStroke); + } + + public Indicators.VolumeSpikePivots VolumeSpikePivots(ISeries input , int barsToCheck, int volumeAveragePeriod, double volumeMultiple, Stroke pivotHighStroke, Stroke pivotLowStroke) + { + return indicator.VolumeSpikePivots(input, barsToCheck, volumeAveragePeriod, volumeMultiple, pivotHighStroke, pivotLowStroke); + } + } +} + +#endregion