Try this one :
namespace NinjaTrader.NinjaScript.Indicators
{
[Gui.CategoryOrder(“Line Property”, 0)]
[Gui.CategoryOrder(“Labels”, 20)]
[Gui.CategoryOrder(“Alert”, 30)]
[Gui.CategoryOrder(“Email”, 40)]
public class LowHighMarker : Indicator
{
#region Variable Declaration
public enum SideEnum { Left, Right }
public enum PositionEnum { Below, Above }
private MenuItem HiLowMarker;
private MenuItem menuItem1;
private MenuItem menuItem2;
private MenuItem menuItem3;
private Separator mymenuseparator;
private SharpDX.Point clickPoint = new SharpDX.Point();
private ChartScale chartScale;
// private bool inited = false;
private int lastSoundPlay = -1;
private int myxcoord;
// REMOVED cached DX resources – they must be created inside OnRender
private List<int> Mxcoord;
private List<int> Mbar;
private List<double> Mtag;
private List<string> Mlabel;
// private double alertvalue;
// private string myalertfile = “Alert1.wav”;
private double MyHigh = double.NaN;
private double MyLow = double.NaN;
private string subject = “NinjaTrader Alert Triggered”;
private string body = “”;
private int convertedBarIndex = -1;
private double convertedPrice = double.NaN;
private bool isContextMenuInitialized = false;
// private bool clickSet = false;
#endregion
#region OnStateChange
protected override void OnStateChange()
{
try
{
if (State == State.SetDefaults)
{
Description = @"Mark important S/R levels on the chart.";
Name = "_LowHighMarker";
Calculate = Calculate.OnBarClose;
IsOverlay = true;
DisplayInDataBox = false;
DrawOnPricePanel = true;
DrawHorizontalGridLines = true;
DrawVerticalGridLines = true;
PaintPriceMarkers = true;
ScaleJustification = NinjaTrader.Gui.Chart.ScaleJustification.Right;
IsSuspendedWhileInactive = true;
Side = SideEnum.Right;
Size = 22;
Position = PositionEnum.Above;
Bold = false;
Opacity = 255;
SoundFile = @"Alert1.wav";
sendEMail = false;
EmailTo = @"noone@noone.com";
EmailBody = @"Body of the email";
soundAlerts = false;
LineWidth = 3;
Infocolor = Brushes.Blue;
MyHighLabel = "High:";
MyLowLabel = "Low:";
MA0DashStyle = DashStyleHelper.Solid;
}
else if (State == State.Historical)
{
if (ChartControl == null) return;
ChartControl.Dispatcher.Invoke(() =>
{
if (HiLowMarker == null)
{
ChartControl.ContextMenuOpening += ChartControl_ContextMenuOpening;
ChartControl.ContextMenuClosing += ChartControl_ContextMenuClosing;
mymenuseparator = new Separator();
HiLowMarker = new MenuItem { Header = "Support/Resistance Marker", Name = "HiLowMarker" };
menuItem1 = new MenuItem { Header = "Mark the High", Name = "menuItem1" };
menuItem2 = new MenuItem { Header = "Mark the Low", Name = "menuItem2" };
menuItem3 = new MenuItem { Header = "Clear All", Name = "menuItem3" };
menuItem1.Click += menuItem1_Click;
menuItem2.Click += menuItem2_Click;
menuItem3.Click += menuItem3_Click;
if (ChartPanel != null)
ChartPanel.MouseRightButtonDown += OnMouseClick;
}
});
if (ChartPanel != null)
{
foreach (ChartScale scale in ChartPanel.Scales)
if (scale.ScaleJustification == ScaleJustification)
{ chartScale = scale; break; }
}
}
else if (State == State.DataLoaded)
{
Mxcoord = new List<int>();
Mbar = new List<int>();
Mtag = new List<double>();
Mlabel = new List<string>();
}
else if (State == State.Terminated)
{
if (ChartControl == null) return;
ChartControl.Dispatcher.Invoke(() =>
{
if (menuItem1 != null) menuItem1.Click -= menuItem1_Click;
if (menuItem2 != null) menuItem2.Click -= menuItem2_Click;
if (menuItem3 != null) menuItem3.Click -= menuItem3_Click;
ChartControl.ContextMenuOpening -= ChartControl_ContextMenuOpening;
ChartControl.ContextMenuClosing -= ChartControl_ContextMenuClosing;
if (ChartPanel != null)
ChartPanel.MouseRightButtonDown -= OnMouseClick;
if (HiLowMarker != null && ChartControl.ContextMenu.Items.Contains(HiLowMarker))
ChartControl.ContextMenu.Items.Remove(HiLowMarker);
if (mymenuseparator != null && ChartControl.ContextMenu.Items.Contains(mymenuseparator))
ChartControl.ContextMenu.Items.Remove(mymenuseparator);
});
Mxcoord?.Clear(); Mbar?.Clear(); Mtag?.Clear(); Mlabel?.Clear();
}
}
catch (Exception ex)
{
Log("LowHighMarker: OnStateChange – " + ex.Message, LogLevel.Error);
}
}
#endregion
#region Mouse / Menu handlers
private void OnMouseClick(object sender, MouseEventArgs e)
{
if (ChartControl == null || ChartPanel == null || chartScale == null) return;
try
{
double clickX = e.GetPosition(ChartControl as IInputElement).X;
double clickY = e.GetPosition(ChartControl as IInputElement).Y;
clickPoint.X = ChartingExtensions.ConvertToHorizontalPixels(clickX, ChartControl.PresentationSource);
clickPoint.Y = ChartingExtensions.ConvertToVerticalPixels (clickY, ChartControl.PresentationSource);
ChartControl.InvalidateVisual();
e.Handled = true;
convertedPrice = Instrument?.MasterInstrument?.RoundToTickSize(chartScale.GetValueByY(clickPoint.Y)) ?? double.NaN;
int mousePointX = ChartControl.MouseDownPoint.X.ConvertToHorizontalPixels(ChartControl.PresentationSource);
convertedBarIndex = ChartBars?.GetBarIdxByX(ChartControl, mousePointX) ?? -1;
if (convertedBarIndex >= 0 && convertedBarIndex < BarsArray?[0]?.Count)
{
MyHigh = High.GetValueAt(convertedBarIndex);
MyLow = Low .GetValueAt(convertedBarIndex);
}
else
{
MyHigh = double.NaN;
MyLow = double.NaN;
}
myxcoord = (int)clickPoint.X;
}
catch (Exception ex)
{
Log("LowHighMarker: OnMouseClick – " + ex.Message, LogLevel.Error);
MyHigh = MyLow = double.NaN;
convertedBarIndex = -1;
}
}
private bool ValidateClickData() =>
myxcoord > 0 &&
convertedBarIndex >= 0 &&
convertedBarIndex < BarsArray?[0]?.Count &&
!double.IsNaN(MyHigh) &&
!double.IsNaN(MyLow);
private void menuItem1_Click(object sender, RoutedEventArgs e)
{
try {
if (ValidateClickData())
{
Mxcoord.Add(myxcoord);
Mbar.Add(convertedBarIndex);
Mtag.Add(MyHigh);
Mlabel.Add(MyHighLabel ?? "High:");
ChartControl?.InvalidateVisual(); // <-- NEW
}
}
catch (Exception ex) { Log("menuItem1_Click – " + ex.Message, LogLevel.Error); }
}
private void menuItem2_Click(object sender, RoutedEventArgs e)
{
try {
if (ValidateClickData())
{
Mxcoord.Add(myxcoord);
Mbar.Add(convertedBarIndex);
Mtag.Add(MyLow );
Mlabel.Add(MyLowLabel ?? "Low:" );
ChartControl?.InvalidateVisual(); // <-- NEW
}
}
catch (Exception ex) { Log("menuItem2_Click – " + ex.Message, LogLevel.Error); }
}
private void menuItem3_Click(object sender, RoutedEventArgs e)
{
try {
Mxcoord?.Clear();
Mbar?.Clear();
Mtag?.Clear();
Mlabel?.Clear();
ChartControl?.InvalidateVisual();
}
catch (Exception ex) { Log("menuItem3_Click – " + ex.Message, LogLevel.Error); }
}
#endregion
#region OnBarUpdate & Alert helpers
protected override void OnBarUpdate()
{
if (State != State.Realtime || ChartControl == null || Mtag?.Count == 0 || CurrentBar < 20)
return;
try
{
for (int i = 0; i < Mtag.Count; i++)
{
double level = Mtag[i];
if (Close[1] < level && Close[0] > level && lastSoundPlay != CurrentBar)
HandleAlert("Crossed Above", level, i);
else if (Close[1] > level && Close[0] < level && lastSoundPlay != CurrentBar)
HandleAlert("Crossed Below", level, i);
}
}
catch (Exception ex)
{
Log("LowHighMarker: OnBarUpdate – " + ex.Message, LogLevel.Error);
}
}
private void HandleAlert(string alertType, double alertValue, int markerIndex)
{
if (State != State.Realtime) return;
try
{
lastSoundPlay = CurrentBar;
string label = (markerIndex >= 0 && markerIndex < Mlabel?.Count) ? Mlabel[markerIndex] : "Level";
string msg = $"{alertType} Alert at ({label}) {alertValue:F2}";
if (soundAlerts && !string.IsNullOrWhiteSpace(SoundFile))
PlaySound(SoundFile);
Alert($"{Name}_{Instrument?.FullName}", Priority.High, msg, SoundFile, 60, Infocolor, Brushes.White);
if (sendEMail && !string.IsNullOrWhiteSpace(EmailTo))
{
string subj = $"Cross Alert on {Instrument?.FullName} {BarsPeriod?.Value} {BarsPeriod?.BarsPeriodType}";
string body = $"Price crossed {alertValue:F2} on {Instrument?.FullName} at {Time[0]}\n{EmailBody}";
SendMail(EmailTo, subj, body);
}
}
catch (Exception ex)
{
Log("HandleAlert – " + ex.Message, LogLevel.Error);
}
}
#endregion
#region Context-Menu events
private void ChartControl_ContextMenuOpening(object sender, ContextMenuEventArgs e)
{
var chartControl = sender as ChartControl;
if (chartControl == null || chartControl.ChartObjects.Any(o => o.IsSelected)) return;
try
{
if (!isContextMenuInitialized)
{
chartControl.Dispatcher.Invoke(() =>
{
if (!chartControl.ContextMenu.Items.Contains(mymenuseparator))
chartControl.ContextMenu.Items.Add(mymenuseparator);
if (!chartControl.ContextMenu.Items.Contains(HiLowMarker))
chartControl.ContextMenu.Items.Add(HiLowMarker);
HiLowMarker.Items.Clear();
HiLowMarker.Items.Add(menuItem1);
HiLowMarker.Items.Add(menuItem2);
HiLowMarker.Items.Add(menuItem3);
});
isContextMenuInitialized = true;
}
}
catch (Exception ex)
{
Log("ContextMenuOpening – " + ex.Message, LogLevel.Error);
}
}
private void ChartControl_ContextMenuClosing(object sender, ContextMenuEventArgs e)
{
try
{
if (HiLowMarker != null) HiLowMarker.Items.Clear();
isContextMenuInitialized = false;
}
catch (Exception ex)
{
Log("ContextMenuClosing – " + ex.Message, LogLevel.Error);
}
}
#endregion
#region OnRender
private SharpDX.Color ConvertBrushToDxColor(System.Windows.Media.Brush brush, float opacity)
{
if (brush is System.Windows.Media.SolidColorBrush scb)
{
Color c = scb.Color;
return new SharpDX.Color(c.R, c.G, c.B, (byte)(c.A * opacity));
}
return SharpDX.Color.White;
}
private SharpDX.Direct2D1.StrokeStyle GetStrokeStyle(DashStyleHelper dash, RenderTarget rt)
{
try
{
SharpDX.Direct2D1.DashStyle dxDash = SharpDX.Direct2D1.DashStyle.Solid;
float[] dashes = null; // <-- declare local variable
switch (dash)
{
case DashStyleHelper.Solid:
dxDash = SharpDX.Direct2D1.DashStyle.Solid;
break;
case DashStyleHelper.Dash:
dxDash = SharpDX.Direct2D1.DashStyle.Custom;
dashes = new[] { 6f, 6f };
break;
case DashStyleHelper.Dot:
dxDash = SharpDX.Direct2D1.DashStyle.Dot;
break;
case DashStyleHelper.DashDot:
dxDash = SharpDX.Direct2D1.DashStyle.Custom;
dashes = new[] { 6f, 3f, 1f, 3f };
break;
case DashStyleHelper.DashDotDot:
dxDash = SharpDX.Direct2D1.DashStyle.Custom;
dashes = new[] { 6f, 3f, 1f, 3f, 1f, 3f };
break;
}
var props = new StrokeStyleProperties
{
DashStyle = dxDash,
DashCap = CapStyle.Flat,
EndCap = CapStyle.Flat,
StartCap = CapStyle.Flat,
LineJoin = LineJoin.Miter
};
return new SharpDX.Direct2D1.StrokeStyle(rt.Factory, props, dashes);
}
catch
{
return new SharpDX.Direct2D1.StrokeStyle(rt.Factory, new StrokeStyleProperties());
}
}
protected override void OnRender(ChartControl chartControl, ChartScale chartScale)
{
if (chartControl == null || chartScale == null || ChartBars == null ||
RenderTarget == null || Mxcoord?.Count == 0 || Mtag?.Count == 0 || Mlabel?.Count == 0)
return;
try
{
// 1. Create resources locally (per RenderTarget)
var mediaColor = ((System.Windows.Media.SolidColorBrush)Infocolor).Color;
var dxColor = new SharpDX.Color4(
mediaColor.R / 255f, mediaColor.G / 255f, mediaColor.B / 255f,
(mediaColor.A * (Opacity / 255f)) / 255f);
using (var brush = new SharpDX.Direct2D1.SolidColorBrush(RenderTarget, dxColor))
using (var stroke = GetStrokeStyle(MA0DashStyle, RenderTarget))
{
int cnt = Math.Min(Math.Min(Mxcoord.Count, Mtag.Count), Mlabel.Count);
for (int i = 0; i < cnt; i++)
{
double price = Mtag[i];
string lbl = Mlabel[i];
int x = Mxcoord[i];
float y = (float)chartScale.GetYByValue(price);
float end = ChartPanel.X + ChartPanel.W;
RenderTarget.DrawLine(
new SharpDX.Vector2(x, y),
new SharpDX.Vector2(end, y),
brush, LineWidth, stroke);
string text = $"{lbl} ({price:F2})";
using (var tf = new TextFormat(
Core.Globals.DirectWriteFactory,
"Arial",
Bold ? SharpDX.DirectWrite.FontWeight.Bold : SharpDX.DirectWrite.FontWeight.Normal,
FontStyle.Normal,
Size))
using (var tl = new TextLayout(
Core.Globals.DirectWriteFactory,
text,
tf,
500, 50))
{
float labelX, labelY;
labelX = Side == SideEnum.Left
? x + 2
: ChartPanel.W - tl.Metrics.Width - 2;
labelY = Position == PositionEnum.Above
? y - Size - LineWidth - 2
: y + LineWidth + 2;
RenderTarget.DrawTextLayout(
new SharpDX.Vector2(labelX, labelY),
tl, brush);
}
}
}
}
catch (Exception ex)
{
Log("LowHighMarker: OnRender – " + ex.Message, LogLevel.Error);
}
}
#endregion
#region Properties
[Display(Name = "Label for Lows", GroupName = "Labels", Order = 0)]
public string MyLowLabel { get; set; }
[Display(Name = "Label for Highs", GroupName = "Labels", Order = 1)]
public string MyHighLabel { get; set; }
[Display(Name = "Font Size", GroupName = "Labels", Order = 2)]
public int Size { get; set; }
[Display(Name = "Bold", GroupName = "Labels", Order = 3)]
public bool Bold { get; set; }
[Display(Name = "Label Position", GroupName = "Labels", Order = 4)]
public PositionEnum Position { get; set; }
[Display(Name = "Label Side", GroupName = "Labels", Order = 5)]
public SideEnum Side { get; set; }
[Display(Name = "Send email alert?", GroupName = "Email", Order = 0)]
public bool sendEMail { get; set; }
[Display(Name = "Email To:", GroupName = "Email", Order = 2)]
public string EmailTo { get; set; }
[Display(Name = "Email Body:", GroupName = "Email", Order = 3)]
public string EmailBody { get; set; }
[Display(Name = "Sound alert?", GroupName = "Alert", Order = 0)]
public bool soundAlerts { get; set; }
[Display(Name = "Alert sound file", GroupName = "Alert", Order = 1)]
[PropertyEditor("NinjaTrader.Gui.Tools.FilePathPicker", Filter = "Wav Files (*.wav)|*.wav")]
public string SoundFile { get; set; }
[XmlIgnore]
[Display(Name = "Line & Text Color", GroupName = "Line Property", Order = 3)]
public System.Windows.Media.Brush Infocolor { get; set; }
[Browsable(false)]
public string InfocolorColorSerializable
{
get => Serialize.BrushToString(Infocolor);
set => Infocolor = Serialize.StringToBrush(value);
}
[Range(1, 10)]
[Display(Name = "Line Width", GroupName = "Line Property", Order = 2)]
public int LineWidth { get; set; }
[Range(0, 255)]
[Display(Name = "Line Opacity (0-255)", GroupName = "Line Property", Order = 4)]
public byte Opacity { get; set; }
[Display(Name = "Line DashStyle", GroupName = "Line Property", Order = 5)]
public DashStyleHelper MA0DashStyle { get; set; }
#endregion
}
}