#region Using declarations using NinjaTrader.Gui; using NinjaTrader.Gui.Chart; using NinjaTrader.Gui.Tools; using SharpDX; using SharpDX.DirectWrite; using System; using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Net.Http; using System.Text.RegularExpressions; using System.Windows.Media; using System.Xml.Serialization; #endregion //This namespace holds Indicators in this folder and is required. Do not change it. namespace NinjaTrader.NinjaScript.Indicators { public class NewsEvent { public string Time { get; set; } public string Currency { get; set; } public string Title { get; set; } } public class News : Indicator { private List newsEvents = new List(); private DateTime currentDate = DateTime.Now.Date; protected override void OnStateChange() { if (State == State.SetDefaults) { Description = @"Displays economic news events for the current day on the chart"; Name = "News"; Calculate = Calculate.OnBarClose; IsOverlay = true; ScaleJustification = ScaleJustification.Right; IsSuspendedWhileInactive = true; HeaderColor = Brushes.Yellow; HeaderTextColor = Brushes.Black; EventTextColor = Brushes.White; HeaderFont = new SimpleFont("Arial", 12); EventFont = new SimpleFont("Arial", 12); HorizontalPadding = 20; VerticalPadding = 5; YOffset = 0; } else if (State == State.Configure) { RequestNewsEvents(); } else if (State == State.Historical) { SetZOrder(-1); // Display behind bars on chart. } } private async void RequestNewsEvents() { try { newsEvents.Clear(); HttpClient client = new HttpClient(); client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64)"); string newsUrl = "https://nfs.faireconomy.media/ff_calendar_thisweek.json"; HttpResponseMessage response = await client.GetAsync(newsUrl); response.EnsureSuccessStatusCode(); string responseBody = await response.Content.ReadAsStringAsync(); ParseNewsEvents(responseBody); ForceRefresh(); } catch (Exception e) { Print("Error loading news events: " + e.Message); } } private void ParseNewsEvents(string json) { try { var events = Regex.Matches(json, @"\{[^}]*\}"); foreach (Match eventMatch in events) { string eventString = eventMatch.Value; string date = ExtractJsonValue(eventString, "date"); string currency = ExtractJsonValue(eventString, "country"); string title = ExtractJsonValue(eventString, "title"); DateTime eventTime = DateTime.Parse(date); if (eventTime.Date != currentDate) continue; string time = eventTime.ToString("hh:mmtt").ToLower(); newsEvents.Add(new NewsEvent { Time = time, Currency = currency, Title = title }); } } catch (Exception ex) { Print("Error parsing news events: " + ex.Message); } } private string ExtractJsonValue(string json, string key) { string pattern = $"\"{key}\":\"(.*?)\""; Match match = Regex.Match(json, pattern); return match.Success ? match.Groups[1].Value : string.Empty; } protected override void OnBarUpdate() { if (State == State.Realtime) { if (DateTime.Now.Date != currentDate) { RequestNewsEvents(); currentDate = DateTime.Now.Date; } } } protected override void OnRender(ChartControl chartControl, ChartScale chartScale) { base.OnRender(chartControl, chartScale); int maxColWidth = 0; int maxHeaderHeight = 0; int maxRowHeight = 0; // Determine maximum column width and row height. using (TextFormat headerTextFormat = HeaderFont.ToDirectWriteTextFormat(), eventTextFormat = EventFont.ToDirectWriteTextFormat()) { var headerSize = MeasureString("Time", headerTextFormat); maxColWidth = Math.Max(maxColWidth, headerSize.Width); maxHeaderHeight = Math.Max(maxHeaderHeight, headerSize.Height); headerSize = MeasureString("Currency", headerTextFormat); maxColWidth = Math.Max(maxColWidth, headerSize.Width); headerSize = MeasureString("Event", headerTextFormat); maxColWidth = Math.Max(maxColWidth, headerSize.Width); foreach (var newsEvent in new List(newsEvents)) // New list in order to avoid concurrent modification { var timeSize = MeasureString(newsEvent.Time, eventTextFormat); var currencySize = MeasureString(newsEvent.Currency, eventTextFormat); var titleSize = MeasureString(newsEvent.Title, eventTextFormat); maxColWidth = Math.Max(maxColWidth, Math.Max(timeSize.Width, Math.Max(currencySize.Width, titleSize.Width))); maxRowHeight = Math.Max(maxRowHeight, Math.Max(timeSize.Height, Math.Max(currencySize.Height, titleSize.Height))); } } maxColWidth += HorizontalPadding * 2; int headerHeight = maxHeaderHeight + (VerticalPadding * 2); int rowHeight = maxRowHeight + (VerticalPadding * 2); int x = ChartPanel.W - (maxColWidth * 3) - HorizontalPadding; int y = YOffset; // Draw header row. using (TextFormat headerTextFormat = HeaderFont.ToDirectWriteTextFormat()) { string[] headers = { "Time", "Currency", "Event" }; foreach (var header in headers) { RectangleF headerRect = new RectangleF(x, y, maxColWidth, headerHeight); RenderTarget.FillRectangle(headerRect, HeaderColor.ToDxBrush(RenderTarget)); var size = MeasureString(header, headerTextFormat); int textX = x + (maxColWidth - size.Width) / 2; // Center the text horizontally. int textY = y + (headerHeight - size.Height) / 2; // Center the text vertically. DrawText(header, textX, textY, HeaderTextColor, headerTextFormat, headerHeight); x += maxColWidth; } } // Reset x coordinate for news events. x = ChartPanel.W - (maxColWidth * 3) - HorizontalPadding; y += headerHeight; // Draw news events. using (TextFormat eventTextFormat = EventFont.ToDirectWriteTextFormat()) { foreach (var newsEvent in new List(newsEvents)) { var timeSize = MeasureString(newsEvent.Time, eventTextFormat); var currencySize = MeasureString(newsEvent.Currency, eventTextFormat); var titleSize = MeasureString(newsEvent.Title, eventTextFormat); RectangleF timeRect = new RectangleF(x, y, maxColWidth, rowHeight); DrawText(newsEvent.Time, x + (maxColWidth - timeSize.Width) / 2, y + (rowHeight - timeSize.Height) / 2, EventTextColor, eventTextFormat, rowHeight); x += maxColWidth; RectangleF currencyRect = new RectangleF(x, y, maxColWidth, rowHeight); DrawText(newsEvent.Currency, x + (maxColWidth - currencySize.Width) / 2, y + (rowHeight - currencySize.Height) / 2, EventTextColor, eventTextFormat, rowHeight); x += maxColWidth; RectangleF titleRect = new RectangleF(x, y, maxColWidth, rowHeight); DrawText(newsEvent.Title, x + (maxColWidth - titleSize.Width) / 2, y + (rowHeight - titleSize.Height) / 2, EventTextColor, eventTextFormat, rowHeight); x = ChartPanel.W - (maxColWidth * 3) - HorizontalPadding; y += rowHeight; } } } private Size2 MeasureString(string text, TextFormat textFormat) { using (var textLayout = new TextLayout(Core.Globals.DirectWriteFactory, text, textFormat, float.PositiveInfinity, float.PositiveInfinity)) { return new Size2((int)Math.Ceiling(textLayout.Metrics.Width), (int)Math.Ceiling(textLayout.Metrics.Height)); } } private void DrawText(string text, int x, int y, Brush brush, TextFormat textFormat, int height) { TextLayout textLayout = new TextLayout(Core.Globals.DirectWriteFactory, text, textFormat, 500, height); Vector2 textOrigin = new Vector2(x, y); RenderTarget.DrawTextLayout(textOrigin, textLayout, brush.ToDxBrush(RenderTarget)); textLayout.Dispose(); } public override string DisplayName { get { return Name; } } #region Properties [XmlIgnore] [Display(Name = "Header Row", GroupName = "News", Order = 1)] public Brush HeaderColor { get; set; } [Browsable(false)] public string HeaderColorSerialization { get { return Serialize.BrushToString(HeaderColor); } set { HeaderColor = Serialize.StringToBrush(value); } } [XmlIgnore] [Display(Name = "Header Text", GroupName = "News", Order = 2)] public Brush HeaderTextColor { get; set; } [Browsable(false)] public string HeaderTextColorSerialization { get { return Serialize.BrushToString(HeaderTextColor); } set { HeaderTextColor = Serialize.StringToBrush(value); } } [XmlIgnore] [Display(Name = "Event Text", GroupName = "News", Order = 3)] public Brush EventTextColor { get; set; } [Browsable(false)] public string EventTextColorSerialization { get { return Serialize.BrushToString(EventTextColor); } set { EventTextColor = Serialize.StringToBrush(value); } } [Range(0, int.MaxValue)] [Display(Name = "Horizontal Padding", GroupName = "News", Order = 4)] public int HorizontalPadding { get; set; } [Range(0, int.MaxValue)] [Display(Name = "Vertical Padding", GroupName = "News", Order = 5)] public int VerticalPadding { get; set; } [Range(0, int.MaxValue)] [Display(Name = "Y-Offset", GroupName = "News", Order = 6)] public int YOffset { get; set; } [Display(Name = "Header Font", GroupName = "News", Order = 7)] public SimpleFont HeaderFont { get; set; } [Display(Name = "Event Font", GroupName = "News", Order = 8)] public SimpleFont EventFont { get; set; } #endregion } } #region NinjaScript generated code. Neither change nor remove. namespace NinjaTrader.NinjaScript.Indicators { public partial class Indicator : NinjaTrader.Gui.NinjaScript.IndicatorRenderBase { private News[] cacheNews; public News News() { return News(Input); } public News News(ISeries input) { if (cacheNews != null) for (int idx = 0; idx < cacheNews.Length; idx++) if (cacheNews[idx] != null && cacheNews[idx].EqualsInput(input)) return cacheNews[idx]; return CacheIndicator(new News(), input, ref cacheNews); } } } namespace NinjaTrader.NinjaScript.MarketAnalyzerColumns { public partial class MarketAnalyzerColumn : MarketAnalyzerColumnBase { public Indicators.News News() { return indicator.News(Input); } public Indicators.News News(ISeries input ) { return indicator.News(input); } } } namespace NinjaTrader.NinjaScript.Strategies { public partial class Strategy : NinjaTrader.Gui.NinjaScript.StrategyRenderBase { public Indicators.News News() { return indicator.News(Input); } public Indicators.News News(ISeries input ) { return indicator.News(input); } } } #endregion