From 5038889e976fbf117a8e5e4ada381c7d61278c5a Mon Sep 17 00:00:00 2001 From: moshferatu Date: Sat, 4 Nov 2023 06:02:22 -0700 Subject: [PATCH] Add Fair Value Gap (FVG) indicator --- indicators/FairValueGap.cs | 453 +++++++++++++++++++++++++++++++++++++ 1 file changed, 453 insertions(+) create mode 100644 indicators/FairValueGap.cs diff --git a/indicators/FairValueGap.cs b/indicators/FairValueGap.cs new file mode 100644 index 0000000..d2ccc1f --- /dev/null +++ b/indicators/FairValueGap.cs @@ -0,0 +1,453 @@ +#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; +using System.Globalization; +using NinjaTrader.NinjaScript.Indicators; +#endregion + +//This namespace holds Indicators in this folder and is required. Do not change it. +namespace NinjaTrader.NinjaScript.Indicators +{ + public class FairValueGap + { + public DateTime StartTime { get; set; } + + public DateTime EndTime { get; set; } + + public double StartPrice { get; set; } + + public double EndPrice { get; set; } + } + + [TypeConverter("NinjaTrader.NinjaScript.Indicators.FairValueGapsPropertyConverter")] + public class FairValueGaps : Indicator + { + public const int CHART_BARS = 0; + public const int OTHER_BARS = 1; + + private List fairValueGaps; + + protected override void OnStateChange() + { + if (State == State.SetDefaults) + { + Description = @"ICT Fair Value Gap (FVG)"; + Name = "Fair Value Gaps"; + Calculate = Calculate.OnPriceChange; + IsOverlay = true; + DisplayInDataBox = true; + DrawOnPricePanel = true; + DrawHorizontalGridLines = true; + DrawVerticalGridLines = true; + PaintPriceMarkers = true; + ScaleJustification = ScaleJustification.Right; + IsSuspendedWhileInactive = true; + MinimumGap = 1.0; + GapBars = CHART_BARS; + BarType = BarsPeriodType.Minute; + Period = 1; + GapUpColor = Brushes.LimeGreen; + GapUpOpacity = 25; + GapDownColor = Brushes.Red; + GapDownOpacity = 25; + } + else if (State == State.Configure) + { + fairValueGaps = new List(); + + if (GapBars != CHART_BARS) + { + AddDataSeries(Instrument.FullName, new BarsPeriod { + BarsPeriodType = BarType, Value = Period }, Bars.TradingHours.Name); + } + } + else if (State == State.Historical) + { + SetZOrder(-1); + } + } + + protected override void OnBarUpdate() + { + if (CurrentBar < 4) + return; + + if (GapBars == BarsInProgress && IsFirstTickOfBar) + { + double downGap = Lows[GapBars][3] - Highs[GapBars][1]; + if (downGap > MinimumGap) + { + fairValueGaps.Add(new FairValueGap + { + StartTime = Times[GapBars][3], + StartPrice = Lows[GapBars][3], + EndPrice = Highs[GapBars][1] + }); + } + + double upGap = Lows[GapBars][1] - Highs[GapBars][3]; + if (upGap > MinimumGap) + { + fairValueGaps.Add(new FairValueGap + { + StartTime = Times[GapBars][3], + StartPrice = Highs[GapBars][3], + EndPrice = Lows[GapBars][1] + }); + } + } + + var unfilledGaps = from fairValueGap in fairValueGaps + where fairValueGap.EndTime == default(DateTime) + select fairValueGap; + foreach (FairValueGap unfilledGap in unfilledGaps) + { + if (unfilledGap.EndPrice < unfilledGap.StartPrice && Highs[BarsInProgress][0] >= unfilledGap.StartPrice) + unfilledGap.EndTime = Times[BarsInProgress][0]; + else if (unfilledGap.EndPrice > unfilledGap.StartPrice && Lows[BarsInProgress][0] <= unfilledGap.StartPrice) + unfilledGap.EndTime = Times[BarsInProgress][0]; + } + } + + protected override void OnRender(ChartControl chartControl, ChartScale chartScale) + { + base.OnRender(chartControl, chartScale); + + SharpDX.Direct2D1.Brush fairValueGapUpBrush = GapUpColor.ToDxBrush(RenderTarget); + fairValueGapUpBrush.Opacity = GapUpOpacity / 100f; + + SharpDX.Direct2D1.Brush fairValueGapDownBrush = GapDownColor.ToDxBrush(RenderTarget); + fairValueGapDownBrush.Opacity = GapDownOpacity / 100f; + + // TODO: Only render gaps that would actually be visible on the screen. + foreach (FairValueGap fairValueGap in fairValueGaps) + { + int fairValueGapStartX = chartControl.GetXByTime(fairValueGap.StartTime); + + int fairValueGapEndX; + if (fairValueGap.EndTime != default(DateTime)) + fairValueGapEndX = chartControl.GetXByTime(fairValueGap.EndTime); + else + fairValueGapEndX = ChartPanel.X + ChartPanel.W; + + int fairValueGapStartY = chartScale.GetYByValue(Math.Max(fairValueGap.StartPrice, fairValueGap.EndPrice)); + int fairValueGapEndY = chartScale.GetYByValue(Math.Min(fairValueGap.StartPrice, fairValueGap.EndPrice)); + SharpDX.RectangleF fairValueGapRectangle = new SharpDX.RectangleF(fairValueGapStartX, fairValueGapStartY, + fairValueGapEndX - fairValueGapStartX, fairValueGapEndY - fairValueGapStartY); + RenderTarget.FillRectangle(fairValueGapRectangle, + fairValueGap.EndPrice > fairValueGap.StartPrice ? fairValueGapUpBrush : fairValueGapDownBrush); + } + + fairValueGapUpBrush.Dispose(); + fairValueGapDownBrush.Dispose(); + } + + public override string DisplayName + { + get { return Name; } + } + + #region Properties + [NinjaScriptProperty] + [Range(1, double.MaxValue)] + [Display(Name = "Minimum Gap Size", Description = "Minimum size of gaps to consider (in points)", Order = 1, GroupName = "Fair Value Gaps")] + public double MinimumGap + { get; set; } + + [TypeConverter(typeof(GapBarsConverter))] + [PropertyEditor("NinjaTrader.Gui.Tools.StringStandardValuesEditorKey")] + [RefreshProperties(RefreshProperties.All)] + [Display(Name = "Detect Gaps On", Description = "The bars used to detect gaps", Order = 2, GroupName = "Fair Value Gaps")] + public int GapBars + { get; set; } + + [TypeConverter(typeof(BarsPeriodTypeConverter))] + [PropertyEditor("NinjaTrader.Gui.Tools.StringStandardValuesEditorKey")] + [Display(Name = "Bar Type", Description = "Type of bars on which to detect gaps", Order = 3, GroupName = "Fair Value Gaps")] + public BarsPeriodType BarType + { get; set; } + + [NinjaScriptProperty] + [Range(1, int.MaxValue)] + [Display(Name = "Period", Description = "Period of the bars used to detect gaps", Order = 4, GroupName = "Fair Value Gaps")] + public int Period + { get; set; } + + [XmlIgnore] + [NinjaScriptProperty] + [Display(Name = "Gap Up", Description = "Color of the area of upward gaps", Order = 5, GroupName = "Fair Value Gaps")] + public Brush GapUpColor + { get; set; } + + [Browsable(false)] + public string GapUpColorSerialization + { + get { return Serialize.BrushToString(GapUpColor); } + set { GapUpColor = Serialize.StringToBrush(value); } + } + + [NinjaScriptProperty] + [Range(0, 100)] + [Display(Name = "Gap Up Opacity (%)", Description = "Opacity of the area of upward gaps", Order = 6, GroupName = "Fair Value Gaps")] + public int GapUpOpacity + { get; set; } + + [XmlIgnore] + [NinjaScriptProperty] + [Display(Name = "Gap Down", Description = "Color of the area of downward gaps", Order = 7, GroupName = "Fair Value Gaps")] + public Brush GapDownColor + { get; set; } + + [Browsable(false)] + public string GapDownColorSerialization + { + get { return Serialize.BrushToString(GapDownColor); } + set { GapDownColor = Serialize.StringToBrush(value); } + } + + [NinjaScriptProperty] + [Range(0, 100)] + [Display(Name = "Gap Down Opacity (%)", Description = "Opacity of the area of downward gaps", Order = 8, GroupName = "Fair Value Gaps")] + public int GapDownOpacity + { get; set; } + #endregion + } + + public class FairValueGapsPropertyConverter : IndicatorBaseConverter + { + public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object component, Attribute[] attrs) + { + FairValueGaps indicator = component as FairValueGaps; + + PropertyDescriptorCollection properties = base.GetPropertiesSupported(context) ? + base.GetProperties(context, component, attrs) : TypeDescriptor.GetProperties(component, attrs); + + if (indicator == null || properties == null) + return properties; + + PropertyDescriptor barType = properties["BarType"]; + PropertyDescriptor period = properties["Period"]; + + properties.Remove(barType); + properties.Remove(period); + + if (indicator.GapBars == FairValueGaps.OTHER_BARS) + { + properties.Add(barType); + properties.Add(period); + } + + return properties; + } + + public override bool GetPropertiesSupported(ITypeDescriptorContext context) + { return true; } + } +} + +public class GapBarsConverter : TypeConverter +{ + private const string CHART = "Chart Bars"; + private const string OTHER = "Other"; + + public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context) + { + List values = new List() { CHART, OTHER }; + return new StandardValuesCollection(values); + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + switch (value.ToString()) + { + case CHART: + return FairValueGaps.CHART_BARS; + case OTHER: + return FairValueGaps.OTHER_BARS; + } + return FairValueGaps.CHART_BARS; + } + + public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) + { + switch ((int)value) + { + case FairValueGaps.CHART_BARS: + return CHART; + case FairValueGaps.OTHER_BARS: + return OTHER; + } + return CHART; + } + + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { return true; } + + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) + { return true; } + + public override bool GetStandardValuesExclusive(ITypeDescriptorContext context) + { return true; } + + public override bool GetStandardValuesSupported(ITypeDescriptorContext context) + { return true; } +} + +public class BarsPeriodTypeConverter : TypeConverter +{ + private const string MINUTE = "Minute"; + private const string SECOND = "Second"; + private const string DAY = "Day"; + private const string WEEK = "Week"; + private const string MONTH = "Month"; + private const string YEAR = "Year"; + private const string RANGE = "Range"; + private const string TICK = "Tick"; + private const string VOLUME = "Volume"; + + public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context) + { + List values = new List() { MINUTE, SECOND, DAY, WEEK, MONTH, YEAR, RANGE, TICK, VOLUME }; + return new StandardValuesCollection(values); + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + switch (value.ToString()) + { + case MINUTE: + return BarsPeriodType.Minute; + case SECOND: + return BarsPeriodType.Second; + case DAY: + return BarsPeriodType.Day; + case WEEK: + return BarsPeriodType.Week; + case MONTH: + return BarsPeriodType.Month; + case YEAR: + return BarsPeriodType.Year; + case RANGE: + return BarsPeriodType.Range; + case TICK: + return BarsPeriodType.Tick; + case VOLUME: + return BarsPeriodType.Volume; + } + return BarsPeriodType.Minute; + } + + public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) + { + BarsPeriodType enumValue = (BarsPeriodType)Enum.Parse(typeof(BarsPeriodType), value.ToString()); + switch (enumValue) + { + case BarsPeriodType.Minute: + return MINUTE; + case BarsPeriodType.Second: + return SECOND; + case BarsPeriodType.Day: + return DAY; + case BarsPeriodType.Week: + return WEEK; + case BarsPeriodType.Month: + return MONTH; + case BarsPeriodType.Year: + return YEAR; + case BarsPeriodType.Range: + return RANGE; + case BarsPeriodType.Tick: + return TICK; + case BarsPeriodType.Volume: + return VOLUME; + } + return MINUTE; + } + + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { return true; } + + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) + { return true; } + + public override bool GetStandardValuesExclusive(ITypeDescriptorContext context) + { return true; } + + public override bool GetStandardValuesSupported(ITypeDescriptorContext context) + { return true; } +} + +#region NinjaScript generated code. Neither change nor remove. + +namespace NinjaTrader.NinjaScript.Indicators +{ + public partial class Indicator : NinjaTrader.Gui.NinjaScript.IndicatorRenderBase + { + private FairValueGaps[] cacheFairValueGaps; + public FairValueGaps FairValueGaps(double minimumGap, int period, Brush gapUpColor, int gapUpOpacity, Brush gapDownColor, int gapDownOpacity) + { + return FairValueGaps(Input, minimumGap, period, gapUpColor, gapUpOpacity, gapDownColor, gapDownOpacity); + } + + public FairValueGaps FairValueGaps(ISeries input, double minimumGap, int period, Brush gapUpColor, int gapUpOpacity, Brush gapDownColor, int gapDownOpacity) + { + if (cacheFairValueGaps != null) + for (int idx = 0; idx < cacheFairValueGaps.Length; idx++) + if (cacheFairValueGaps[idx] != null && cacheFairValueGaps[idx].MinimumGap == minimumGap && cacheFairValueGaps[idx].Period == period && cacheFairValueGaps[idx].GapUpColor == gapUpColor && cacheFairValueGaps[idx].GapUpOpacity == gapUpOpacity && cacheFairValueGaps[idx].GapDownColor == gapDownColor && cacheFairValueGaps[idx].GapDownOpacity == gapDownOpacity && cacheFairValueGaps[idx].EqualsInput(input)) + return cacheFairValueGaps[idx]; + return CacheIndicator(new FairValueGaps(){ MinimumGap = minimumGap, Period = period, GapUpColor = gapUpColor, GapUpOpacity = gapUpOpacity, GapDownColor = gapDownColor, GapDownOpacity = gapDownOpacity }, input, ref cacheFairValueGaps); + } + } +} + +namespace NinjaTrader.NinjaScript.MarketAnalyzerColumns +{ + public partial class MarketAnalyzerColumn : MarketAnalyzerColumnBase + { + public Indicators.FairValueGaps FairValueGaps(double minimumGap, int period, Brush gapUpColor, int gapUpOpacity, Brush gapDownColor, int gapDownOpacity) + { + return indicator.FairValueGaps(Input, minimumGap, period, gapUpColor, gapUpOpacity, gapDownColor, gapDownOpacity); + } + + public Indicators.FairValueGaps FairValueGaps(ISeries input , double minimumGap, int period, Brush gapUpColor, int gapUpOpacity, Brush gapDownColor, int gapDownOpacity) + { + return indicator.FairValueGaps(input, minimumGap, period, gapUpColor, gapUpOpacity, gapDownColor, gapDownOpacity); + } + } +} + +namespace NinjaTrader.NinjaScript.Strategies +{ + public partial class Strategy : NinjaTrader.Gui.NinjaScript.StrategyRenderBase + { + public Indicators.FairValueGaps FairValueGaps(double minimumGap, int period, Brush gapUpColor, int gapUpOpacity, Brush gapDownColor, int gapDownOpacity) + { + return indicator.FairValueGaps(Input, minimumGap, period, gapUpColor, gapUpOpacity, gapDownColor, gapDownOpacity); + } + + public Indicators.FairValueGaps FairValueGaps(ISeries input , double minimumGap, int period, Brush gapUpColor, int gapUpOpacity, Brush gapDownColor, int gapDownOpacity) + { + return indicator.FairValueGaps(input, minimumGap, period, gapUpColor, gapUpOpacity, gapDownColor, gapDownOpacity); + } + } +} + +#endregion