#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.Net.Http; using SharpDX.DirectWrite; using SharpDX; using System.Xml.Linq; using System.Xml; using System.Text.RegularExpressions; using System.Windows.Markup; #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://www.forexfactory.com/calendar?day=" + DateTime.Now.ToString("MMMdd.yyyy").ToLower(); 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 html) { try { // Regex to match the rows with class "calendar__row" var rowRegex = new Regex(@"]*class=""[^""]*calendar__row[^""]*""[^>]*>(.*?)", RegexOptions.Singleline); // Regex to extract time and title from the row var timeRegex = new Regex(@"]*class=""[^""]*calendar__time[^""]*""[^>]*>(.*?)", RegexOptions.Singleline); var currencyRegex = new Regex(@"]*class=""[^""]*calendar__currency[^""]*""[^>]*>(.*?)", RegexOptions.Singleline); var titleRegex = new Regex(@"]*class=""[^""]*calendar__event-title[^""]*""[^>]*>(.*?)", RegexOptions.Singleline); var rowMatches = rowRegex.Matches(html); foreach (Match rowMatch in rowMatches) { string rowContent = rowMatch.Groups[1].Value; var timeMatch = timeRegex.Match(rowContent); var currencyMatch = currencyRegex.Match(rowContent); var titleMatch = titleRegex.Match(rowContent); if (timeMatch.Success && titleMatch.Success) { string time = timeMatch.Groups[1].Value.Trim(); string currency = currencyMatch.Groups[1].Value.Trim(); string title = titleMatch.Groups[1].Value.Trim(); // Remove any extraneous HTML tags from the time string time = Regex.Replace(time, "<.*?>", string.Empty); // Convert the time to the user's time zone time = ConvertToUserTimeZone(time); newsEvents.Add(new NewsEvent { Time = time, Currency = currency, Title = title }); } } } catch (Exception ex) { Print("Error parsing news events: " + ex.Message); } } private string ConvertToUserTimeZone(string time) { try { // Assumed to be in US Eastern Time DateTime easternTime = DateTime.ParseExact(time, "h:mmtt", System.Globalization.CultureInfo.InvariantCulture); TimeZoneInfo easternTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time"); DateTime userTime = TimeZoneInfo.ConvertTime(easternTime, easternTimeZone, Core.Globals.GeneralOptions.TimeZoneInfo); // Format the time as "hh:mmtt" (e.g., "06:30am") return userTime.ToString("hh:mmtt").ToLower(); } catch (Exception) { return time; // Return original time if conversion fails } } 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 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 [NinjaScriptProperty] [XmlIgnore] [Display(Name = "Header Row", GroupName = "News", Order = 1)] public Brush HeaderColor { get; set; } [NinjaScriptProperty] [XmlIgnore] [Display(Name = "Header Text", GroupName = "News", Order = 2)] public Brush HeaderTextColor { get; set; } [NinjaScriptProperty] [XmlIgnore] [Display(Name = "Event Text", GroupName = "News", Order = 3)] public Brush EventTextColor { get; set; } [NinjaScriptProperty] [Range(0, int.MaxValue)] [Display(Name = "Horizontal Padding", GroupName = "News", Order = 4)] public int HorizontalPadding { get; set; } [NinjaScriptProperty] [Range(0, int.MaxValue)] [Display(Name = "Vertical Padding", GroupName = "News", Order = 5)] public int VerticalPadding { get; set; } [NinjaScriptProperty] [Range(0, int.MaxValue)] [Display(Name = "Y-Offset", GroupName = "News", Order = 6)] public int YOffset { get; set; } [NinjaScriptProperty] [Display(Name = "Header Font", GroupName = "News", Order = 7)] public SimpleFont HeaderFont { get; set; } [NinjaScriptProperty] [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(Brush headerColor, Brush headerTextColor, Brush eventTextColor, int horizontalPadding, int verticalPadding, int yOffset, SimpleFont headerFont, SimpleFont eventFont) { return News(Input, headerColor, headerTextColor, eventTextColor, horizontalPadding, verticalPadding, yOffset, headerFont, eventFont); } public News News(ISeries input, Brush headerColor, Brush headerTextColor, Brush eventTextColor, int horizontalPadding, int verticalPadding, int yOffset, SimpleFont headerFont, SimpleFont eventFont) { if (cacheNews != null) for (int idx = 0; idx < cacheNews.Length; idx++) if (cacheNews[idx] != null && cacheNews[idx].HeaderColor == headerColor && cacheNews[idx].HeaderTextColor == headerTextColor && cacheNews[idx].EventTextColor == eventTextColor && cacheNews[idx].HorizontalPadding == horizontalPadding && cacheNews[idx].VerticalPadding == verticalPadding && cacheNews[idx].YOffset == yOffset && cacheNews[idx].HeaderFont == headerFont && cacheNews[idx].EventFont == eventFont && cacheNews[idx].EqualsInput(input)) return cacheNews[idx]; return CacheIndicator(new News(){ HeaderColor = headerColor, HeaderTextColor = headerTextColor, EventTextColor = eventTextColor, HorizontalPadding = horizontalPadding, VerticalPadding = verticalPadding, YOffset = yOffset, HeaderFont = headerFont, EventFont = eventFont }, input, ref cacheNews); } } } namespace NinjaTrader.NinjaScript.MarketAnalyzerColumns { public partial class MarketAnalyzerColumn : MarketAnalyzerColumnBase { public Indicators.News News(Brush headerColor, Brush headerTextColor, Brush eventTextColor, int horizontalPadding, int verticalPadding, int yOffset, SimpleFont headerFont, SimpleFont eventFont) { return indicator.News(Input, headerColor, headerTextColor, eventTextColor, horizontalPadding, verticalPadding, yOffset, headerFont, eventFont); } public Indicators.News News(ISeries input , Brush headerColor, Brush headerTextColor, Brush eventTextColor, int horizontalPadding, int verticalPadding, int yOffset, SimpleFont headerFont, SimpleFont eventFont) { return indicator.News(input, headerColor, headerTextColor, eventTextColor, horizontalPadding, verticalPadding, yOffset, headerFont, eventFont); } } } namespace NinjaTrader.NinjaScript.Strategies { public partial class Strategy : NinjaTrader.Gui.NinjaScript.StrategyRenderBase { public Indicators.News News(Brush headerColor, Brush headerTextColor, Brush eventTextColor, int horizontalPadding, int verticalPadding, int yOffset, SimpleFont headerFont, SimpleFont eventFont) { return indicator.News(Input, headerColor, headerTextColor, eventTextColor, horizontalPadding, verticalPadding, yOffset, headerFont, eventFont); } public Indicators.News News(ISeries input , Brush headerColor, Brush headerTextColor, Brush eventTextColor, int horizontalPadding, int verticalPadding, int yOffset, SimpleFont headerFont, SimpleFont eventFont) { return indicator.News(input, headerColor, headerTextColor, eventTextColor, horizontalPadding, verticalPadding, yOffset, headerFont, eventFont); } } } #endregion