#region Using declarations using System; using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Globalization; using System.IO; using System.Net; using System.Text.RegularExpressions; using System.Threading; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Xml.Serialization; using NinjaTrader.Gui; using NinjaTrader.Gui.Chart; using NinjaTrader.NinjaScript; using TextFactory = SharpDX.DirectWrite.Factory; using DxBrush = SharpDX.Direct2D1.SolidColorBrush; using DxTextFormat = SharpDX.DirectWrite.TextFormat; using DxStrokeStyle = SharpDX.Direct2D1.StrokeStyle; using DxStrokeProps = SharpDX.Direct2D1.StrokeStyleProperties; #endregion // Robs Levels // Fetches the NQ / ES board from https://robstrades.com/levels and plots it. // Seller / buyer bands are shaded zones. Walls, opens, and prior-day levels are lines. // A chart button refetches the board. Paste from Copy NQ / Copy ES still works if the site is down. // // Install: copy this file to // Documents\NinjaTrader 8\bin\Custom\Indicators\RobsLevels.cs // then NinjaScript Editor > Compile (F5). Add RobsLevels to an NQ, MNQ, ES, or MES chart. namespace NinjaTrader.NinjaScript.Indicators { public class RobsLevels : Indicator { private const string PageUrl = "https://robstrades.com/levels"; private const string UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"; private Button fetchButton; private bool buttonAdded; private int fetching; private string statusText = "Fetch Levels"; private string updatedText = ""; private readonly object gate = new object(); private List lines = new List(); private List zones = new List(); private TextFactory textFactory; private class LevelLine { public string Label; public double Price; public bool Gold; public bool Dashed; public bool Thick; } private class LevelBand { public string Label; public double Low; public double High; public bool Seller; } protected override void OnStateChange() { if (State == State.SetDefaults) { Description = "Plots Robs Trades seller/buyer bands, walls, and key levels from robstrades.com/levels."; Name = "RobsLevels"; Calculate = Calculate.OnBarClose; IsOverlay = true; DisplayInDataBox = false; DrawOnPricePanel = true; PaintPriceMarkers = false; IsSuspendedWhileInactive = true; ScaleJustification = NinjaTrader.Gui.Chart.ScaleJustification.Right; FetchOnLoad = true; ShowPriorClose = false; ShowBuyerBands = true; ShowSellerBands = true; NqPaste = ""; EsPaste = ""; } else if (State == State.Historical) { if (ChartControl != null) ChartControl.Dispatcher.InvokeAsync(new Action(AddButton)); } else if (State == State.Realtime) { if (FetchOnLoad) QueueFetch(); } else if (State == State.Terminated) { if (ChartControl != null) ChartControl.Dispatcher.InvokeAsync(new Action(RemoveButton)); if (textFactory != null) { textFactory.Dispose(); textFactory = null; } } } protected override void OnBarUpdate() { } private void AddButton() { if (buttonAdded || UserControlCollection == null) return; fetchButton = new Button { Content = "Fetch Levels", Height = 24, MinWidth = 132, Margin = new Thickness(8, 28, 0, 0), Padding = new Thickness(10, 0, 10, 0), HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Top, Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(20, 20, 20)), Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(244, 236, 220)), BorderBrush = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(196, 162, 122)), FontSize = 12 }; fetchButton.Click += OnFetchClick; UserControlCollection.Add(fetchButton); buttonAdded = true; SetButton(statusText); } private void RemoveButton() { if (fetchButton != null) { fetchButton.Click -= OnFetchClick; if (UserControlCollection != null) UserControlCollection.Remove(fetchButton); fetchButton = null; } buttonAdded = false; } private void OnFetchClick(object sender, RoutedEventArgs e) { QueueFetch(); } private void QueueFetch() { if (Interlocked.CompareExchange(ref fetching, 1, 0) != 0) return; SetButton("Fetching..."); ThreadPool.QueueUserWorkItem(delegate { try { string symbol = ResolveSymbol(); List nextLines; List nextZones; string updated; LoadBoard(symbol, out nextLines, out nextZones, out updated); lock (gate) { lines = nextLines; zones = nextZones; updatedText = updated; statusText = symbol + " " + (string.IsNullOrEmpty(updated) ? "updated" : updated); } Print("RobsLevels loaded " + symbol + " " + nextZones.Count + " bands " + nextLines.Count + " levels"); } catch (Exception ex) { lock (gate) { statusText = "Fetch failed — using paste"; } Print("RobsLevels fetch failed: " + ex.Message); try { ApplyPaste(ResolveSymbol()); } catch (Exception pasteEx) { Print("RobsLevels paste failed: " + pasteEx.Message); } } finally { Interlocked.Exchange(ref fetching, 0); string label; lock (gate) label = statusText; SetButton(label); if (ChartControl != null) ChartControl.Dispatcher.InvokeAsync(new Action(delegate { ChartControl.InvalidateVisual(); })); } }); } private string ResolveSymbol() { string name = ""; if (Instrument != null && Instrument.MasterInstrument != null) name = Instrument.MasterInstrument.Name ?? ""; if (name.IndexOf("NQ", StringComparison.OrdinalIgnoreCase) >= 0) return "NQ"; if (name.IndexOf("ES", StringComparison.OrdinalIgnoreCase) >= 0) return "ES"; return "NQ"; } private void SetButton(string text) { if (ChartControl == null || fetchButton == null) return; ChartControl.Dispatcher.InvokeAsync(new Action(delegate { if (fetchButton != null) fetchButton.Content = text; })); } private void LoadBoard(string symbol, out List nextLines, out List nextZones, out string updated) { ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; string page = HttpGet(PageUrl, false); Match script = Regex.Match(page, @"/assets/levels-[A-Za-z0-9_-]+\.js"); if (!script.Success) throw new InvalidOperationException("Could not find the levels script on " + PageUrl); string js = HttpGet("https://robstrades.com" + script.Value, false); Match hash = Regex.Match(js, @"[a-f0-9]{64}"); if (!hash.Success) throw new InvalidOperationException("Could not find the levels feed id."); string payload = HttpGet("https://robstrades.com/_serverFn/" + hash.Value, true); ParseBoard(payload, symbol, out nextLines, out nextZones, out updated); } private static string HttpGet(string url, bool serverFn) { HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url); req.Method = "GET"; req.Timeout = 20000; req.UserAgent = UserAgent; req.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; req.Accept = serverFn ? "application/x-tss-framed, application/x-ndjson, application/json" : "text/html,application/json"; if (serverFn) { req.Referer = PageUrl; req.Headers["x-tsr-serverfn"] = "true"; } using (HttpWebResponse resp = (HttpWebResponse)req.GetResponse()) using (Stream stream = resp.GetResponseStream()) using (StreamReader reader = new StreamReader(stream)) return reader.ReadToEnd(); } private void ParseBoard(string payload, string symbol, out List nextLines, out List nextZones, out string updated) { object decoded = DecodeTss(ParseJson(payload)); Dictionary result = AsDict(decoded); if (result != null && result.ContainsKey("result")) result = AsDict(result["result"]); if (result == null) throw new InvalidOperationException("Levels feed was not readable."); updated = FormatUpdated(result.ContainsKey("updatedAt") ? Convert.ToString(result["updatedAt"]) : ""); nextLines = new List(); nextZones = new List(); List symbols = AsList(result.ContainsKey("symbols") ? result["symbols"] : null); Dictionary board = null; if (symbols != null) { foreach (object item in symbols) { Dictionary row = AsDict(item); if (row == null || !row.ContainsKey("symbol")) continue; if (string.Equals(Convert.ToString(row["symbol"]), symbol, StringComparison.OrdinalIgnoreCase)) { board = row; break; } } } if (board != null) { List levels = AsList(board.ContainsKey("levels") ? board["levels"] : null); if (levels != null) { foreach (object item in levels) { Dictionary row = AsDict(item); if (row == null || !row.ContainsKey("price")) continue; string key = row.ContainsKey("key") ? Convert.ToString(row["key"]) : ""; string label = row.ContainsKey("label") ? Convert.ToString(row["label"]) : key; if (string.Equals(key, "pdc", StringComparison.OrdinalIgnoreCase) && !ShowPriorClose) continue; double price; if (!TryDouble(row["price"], out price)) continue; bool today = string.Equals(key, "topen", StringComparison.OrdinalIgnoreCase); nextLines.Add(new LevelLine { Label = label, Price = price, Gold = today, Dashed = false, Thick = today }); } } } List bands = AsList(result.ContainsKey("bands") ? result["bands"] : null); if (bands != null) { foreach (object item in bands) { Dictionary row = AsDict(item); if (row == null) continue; string rowSymbol = row.ContainsKey("symbol") ? Convert.ToString(row["symbol"]) : ""; if (!string.Equals(rowSymbol, symbol, StringComparison.OrdinalIgnoreCase)) continue; string status = row.ContainsKey("status") ? Convert.ToString(row["status"]) : "active"; if (!string.Equals(status, "active", StringComparison.OrdinalIgnoreCase)) continue; string kind = row.ContainsKey("kind") ? Convert.ToString(row["kind"]) : "band"; string label = row.ContainsKey("label") ? Convert.ToString(row["label"]) : "Band"; double low, high; if (!TryDouble(row.ContainsKey("low") ? row["low"] : null, out low)) continue; if (!TryDouble(row.ContainsKey("high") ? row["high"] : null, out high)) high = low; if (string.Equals(kind, "scalp", StringComparison.OrdinalIgnoreCase)) { nextLines.Add(new LevelLine { Label = string.IsNullOrEmpty(label) ? "Scalp" : label, Price = low, Gold = true, Dashed = true, Thick = false }); continue; } bool seller = label.IndexOf("sell", StringComparison.OrdinalIgnoreCase) >= 0; if (seller && !ShowSellerBands) continue; if (!seller && !ShowBuyerBands) continue; nextZones.Add(new LevelBand { Label = seller ? "Seller band" : "Buyer band", Low = Math.Min(low, high), High = Math.Max(low, high), Seller = seller }); } } } private void ApplyPaste(string symbol) { string src = string.Equals(symbol, "ES", StringComparison.OrdinalIgnoreCase) ? EsPaste : NqPaste; if (string.IsNullOrWhiteSpace(src)) return; List nextLines = new List(); List nextZones = new List(); string[] rows = src.Replace("\r", "").Split('\n'); foreach (string raw in rows) { string row = raw.Trim(); if (row.Length == 0 || row.StartsWith("#") || row.StartsWith("//")) continue; string[] bits = row.Split(new char[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); if (bits.Length < 2) continue; string kind = bits[0].ToUpperInvariant(); if (kind == "CLOSE" && !ShowPriorClose) continue; double p1; double p2 = 0; if (!double.TryParse(bits[1], NumberStyles.Any, CultureInfo.InvariantCulture, out p1)) continue; bool has2 = bits.Length > 2 && double.TryParse(bits[2], NumberStyles.Any, CultureInfo.InvariantCulture, out p2); int nameStart = (kind == "BUY" || kind == "SELL") && has2 ? 3 : 2; string name = ""; for (int i = nameStart; i < bits.Length; i++) name = name.Length == 0 ? bits[i] : name + " " + bits[i]; if (name.Length == 0) name = kind; if ((kind == "BUY" || kind == "SELL") && has2) { bool seller = kind == "SELL"; if (seller && !ShowSellerBands) continue; if (!seller && !ShowBuyerBands) continue; nextZones.Add(new LevelBand { Label = name, Low = Math.Min(p1, p2), High = Math.Max(p1, p2), Seller = seller }); } else { bool gold = kind == "TODAY" || kind == "SCALP" || kind == "OPEN"; nextLines.Add(new LevelLine { Label = name, Price = p1, Gold = gold, Dashed = kind == "SCALP", Thick = kind == "TODAY" }); } } lock (gate) { if (nextLines.Count + nextZones.Count == 0) return; lines = nextLines; zones = nextZones; statusText = symbol + " paste " + nextZones.Count + " bands"; } } private static object DecodeTss(object node) { Dictionary obj = node as Dictionary; if (obj == null || !obj.ContainsKey("t")) return node; int t = Convert.ToInt32(obj["t"], CultureInfo.InvariantCulture); if (t == 0 || t == 1 || t == 2) return obj.ContainsKey("s") ? obj["s"] : null; if (t == 9) { List list = new List(); List items = obj.ContainsKey("a") ? obj["a"] as List : null; if (items != null) { foreach (object item in items) list.Add(DecodeTss(item)); } return list; } if (t == 10) { Dictionary dict = new Dictionary(); Dictionary p = obj.ContainsKey("p") ? obj["p"] as Dictionary : null; List keys = p != null && p.ContainsKey("k") ? p["k"] as List : null; List vals = p != null && p.ContainsKey("v") ? p["v"] as List : null; if (keys != null && vals != null) { int n = Math.Min(keys.Count, vals.Count); for (int i = 0; i < n; i++) dict[Convert.ToString(keys[i])] = DecodeTss(vals[i]); } return dict; } return null; } private static object ParseJson(string text) { int i = 0; return ReadJson(text, ref i); } private static object ReadJson(string text, ref int i) { SkipWs(text, ref i); if (i >= text.Length) return null; char c = text[i]; if (c == '{') return ReadObject(text, ref i); if (c == '[') return ReadArray(text, ref i); if (c == '"') return ReadString(text, ref i); if (c == 't' || c == 'f') return ReadLiteral(text, ref i); if (c == 'n') { ReadLiteral(text, ref i); return null; } return ReadNumber(text, ref i); } private static Dictionary ReadObject(string text, ref int i) { Dictionary dict = new Dictionary(); i++; SkipWs(text, ref i); if (i < text.Length && text[i] == '}') { i++; return dict; } while (i < text.Length) { SkipWs(text, ref i); string key = ReadString(text, ref i); SkipWs(text, ref i); if (i < text.Length && text[i] == ':') i++; dict[key] = ReadJson(text, ref i); SkipWs(text, ref i); if (i < text.Length && text[i] == ',') { i++; continue; } if (i < text.Length && text[i] == '}') i++; break; } return dict; } private static List ReadArray(string text, ref int i) { List list = new List(); i++; SkipWs(text, ref i); if (i < text.Length && text[i] == ']') { i++; return list; } while (i < text.Length) { list.Add(ReadJson(text, ref i)); SkipWs(text, ref i); if (i < text.Length && text[i] == ',') { i++; continue; } if (i < text.Length && text[i] == ']') i++; break; } return list; } private static string ReadString(string text, ref int i) { if (i >= text.Length || text[i] != '"') return ""; i++; System.Text.StringBuilder sb = new System.Text.StringBuilder(); while (i < text.Length) { char c = text[i++]; if (c == '"') break; if (c == '\\' && i < text.Length) { char e = text[i++]; if (e == 'n') sb.Append('\n'); else if (e == 'r') sb.Append('\r'); else if (e == 't') sb.Append('\t'); else if (e == 'u' && i + 4 <= text.Length) { int code; int.TryParse(text.Substring(i, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out code); sb.Append((char)code); i += 4; } else sb.Append(e); } else sb.Append(c); } return sb.ToString(); } private static object ReadNumber(string text, ref int i) { int start = i; if (i < text.Length && (text[i] == '-' || text[i] == '+')) i++; while (i < text.Length && (char.IsDigit(text[i]) || text[i] == '.' || text[i] == 'e' || text[i] == 'E' || text[i] == '+' || text[i] == '-')) i++; string raw = text.Substring(start, i - start); double value; double.TryParse(raw, NumberStyles.Any, CultureInfo.InvariantCulture, out value); return value; } private static bool ReadLiteral(string text, ref int i) { if (text.IndexOf("true", i, StringComparison.Ordinal) == i) { i += 4; return true; } if (text.IndexOf("false", i, StringComparison.Ordinal) == i) { i += 5; return false; } if (text.IndexOf("null", i, StringComparison.Ordinal) == i) i += 4; return false; } private static void SkipWs(string text, ref int i) { while (i < text.Length && char.IsWhiteSpace(text[i])) i++; } private static Dictionary AsDict(object value) { return value as Dictionary; } private static List AsList(object value) { return value as List; } private static bool TryDouble(object value, out double price) { price = 0; if (value == null) return false; return double.TryParse(Convert.ToString(value, CultureInfo.InvariantCulture), NumberStyles.Any, CultureInfo.InvariantCulture, out price); } private static string FormatUpdated(string raw) { if (string.IsNullOrEmpty(raw)) return ""; DateTime utc; if (!DateTime.TryParse(raw, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out utc)) return raw; try { TimeZoneInfo ct = TimeZoneInfo.FindSystemTimeZoneById("Central Standard Time"); DateTime local = TimeZoneInfo.ConvertTimeFromUtc(utc, ct); return local.ToString("h:mm tt", CultureInfo.InvariantCulture) + " CT"; } catch { return utc.ToString("HH:mm", CultureInfo.InvariantCulture) + " UTC"; } } private string PriceText(double price) { return price.ToString("0.00", CultureInfo.InvariantCulture); } protected override void OnRender(ChartControl chartControl, ChartScale chartScale) { base.OnRender(chartControl, chartScale); if (RenderTarget == null || chartScale == null || ChartPanel == null || IsInHitTest) return; List drawLines; List drawZones; lock (gate) { drawLines = lines; drawZones = zones; } if ((drawLines == null || drawLines.Count == 0) && (drawZones == null || drawZones.Count == 0)) return; if (textFactory == null) textFactory = new TextFactory(); float left = ChartPanel.X; float width = ChartPanel.W; float right = left + width; float panelTop = ChartPanel.Y; float panelBottom = ChartPanel.Y + ChartPanel.H; using (DxTextFormat labelFormat = new DxTextFormat(textFactory, "Segoe UI", 12f)) using (DxTextFormat bandFormat = new DxTextFormat(textFactory, "Segoe UI", 12f)) using (DxBrush white = new DxBrush(RenderTarget, new SharpDX.Color4(1f, 1f, 1f, 0.92f))) using (DxBrush whiteSoft = new DxBrush(RenderTarget, new SharpDX.Color4(1f, 1f, 1f, 0.55f))) using (DxBrush gold = new DxBrush(RenderTarget, new SharpDX.Color4(196f / 255f, 162f / 255f, 122f / 255f, 1f))) using (DxBrush sellerFill = new DxBrush(RenderTarget, new SharpDX.Color4(1f, 1f, 1f, 0.10f))) using (DxBrush sellerEdge = new DxBrush(RenderTarget, new SharpDX.Color4(1f, 1f, 1f, 0.28f))) using (DxBrush buyerFill = new DxBrush(RenderTarget, new SharpDX.Color4(196f / 255f, 162f / 255f, 122f / 255f, 0.16f))) using (DxBrush buyerEdge = new DxBrush(RenderTarget, new SharpDX.Color4(196f / 255f, 162f / 255f, 122f / 255f, 0.85f))) using (DxStrokeStyle dashed = new DxStrokeStyle(RenderTarget.Factory, new DxStrokeProps { DashStyle = SharpDX.Direct2D1.DashStyle.Dash })) { labelFormat.TextAlignment = SharpDX.DirectWrite.TextAlignment.Trailing; labelFormat.ParagraphAlignment = SharpDX.DirectWrite.ParagraphAlignment.Center; bandFormat.TextAlignment = SharpDX.DirectWrite.TextAlignment.Center; bandFormat.ParagraphAlignment = SharpDX.DirectWrite.ParagraphAlignment.Center; if (drawZones != null) { foreach (LevelBand zone in drawZones) { float yHigh = (float)chartScale.GetYByValue(zone.High); float yLow = (float)chartScale.GetYByValue(zone.Low); float top = Math.Min(yHigh, yLow); float bottom = Math.Max(yHigh, yLow); if (bottom < panelTop || top > panelBottom) continue; SharpDX.RectangleF rect = new SharpDX.RectangleF(left, top, width, Math.Max(1f, bottom - top)); RenderTarget.FillRectangle(rect, zone.Seller ? sellerFill : buyerFill); RenderTarget.DrawRectangle(rect, zone.Seller ? sellerEdge : buyerEdge, 1f); string text = zone.Label + "\n" + PriceText(zone.Low) + " - " + PriceText(zone.High); RenderTarget.DrawText(text, bandFormat, rect, white); } } if (drawLines != null) { List ordered = new List(drawLines); ordered.Sort(delegate (LevelLine a, LevelLine b) { return b.Price.CompareTo(a.Price); }); float lastLabelY = float.MinValue; foreach (LevelLine level in ordered) { float y = (float)chartScale.GetYByValue(level.Price); if (y < panelTop - 4 || y > panelBottom + 4) continue; DxBrush brush = level.Gold ? gold : white; if (level.Dashed) { RenderTarget.DrawLine(new SharpDX.Vector2(left, y), new SharpDX.Vector2(right, y), brush, 1f, dashed); } else { RenderTarget.DrawLine(new SharpDX.Vector2(left, y), new SharpDX.Vector2(right, y), level.Gold ? gold : whiteSoft, level.Thick ? 1.6f : 1f); } float labelY = y; if (lastLabelY > float.MinValue && labelY - lastLabelY < 14f) labelY = lastLabelY + 14f; lastLabelY = labelY; SharpDX.RectangleF labelRect = new SharpDX.RectangleF(right - 230f, labelY - 8f, 222f, 16f); RenderTarget.DrawText(level.Label + " " + PriceText(level.Price), labelFormat, labelRect, brush); } } } } #region Properties [Display(Name = "Fetch on load", Description = "Pull the board when the chart starts.", Order = 1, GroupName = "Source")] public bool FetchOnLoad { get; set; } [Display(Name = "Show prior close", Description = "Prior day close is hidden on the TradingView script. Turn on to plot it.", Order = 2, GroupName = "Source")] public bool ShowPriorClose { get; set; } [Display(Name = "Show seller bands", Order = 3, GroupName = "Source")] public bool ShowSellerBands { get; set; } [Display(Name = "Show buyer bands", Order = 4, GroupName = "Source")] public bool ShowBuyerBands { get; set; } [Display(Name = "NQ paste", Description = "Fallback. Paste Copy NQ from robstrades.com/levels. Used only if the fetch fails.", Order = 5, GroupName = "Paste fallback")] public string NqPaste { get; set; } [Display(Name = "ES paste", Description = "Fallback. Paste Copy ES from robstrades.com/levels. Used only if the fetch fails.", Order = 6, GroupName = "Paste fallback")] public string EsPaste { get; set; } #endregion } }