diff --git a/indicators/News.cs b/indicators/News.cs new file mode 100644 index 0000000..d16ff48 --- /dev/null +++ b/indicators/News.cs @@ -0,0 +1,348 @@ +#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 + { + // TODO: Date should be determined programatically. + private string newsUrl = "https://www.forexfactory.com/calendar?day=" + DateTime.Now.ToString("MMMdd.yyyy").ToLower(); + + private List newsEvents = new List(); + + 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 + { + HttpClient client = new HttpClient(); + client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64)"); + + 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); + + newsEvents.Add(new NewsEvent { Time = time, Currency = currency, Title = title }); + } + } + } + catch (Exception ex) + { + Print("Error parsing news events: " + ex.Message); + } + } + + protected override void OnBarUpdate() { } + + 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 newsEvents) + { + 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