// SPECTRE_Levels — auto-syncing daily levels for NinjaTrader 8. // // Install ONCE, then it pulls your levels straight from spectre.markets every session and redraws — // no files to download, no folders to manage. Your download-wizard preferences (which groups, colours, // custom labels, held %) are applied automatically because the server knows who you are from your key. // // 1. Documents\NinjaTrader 8\bin\Custom\Indicators\ ← drop this file here // 2. NinjaScript Editor → Compile (F5) // 3. Add "SPECTRE_Levels" to a chart → set your API key (copy it from spectre.markets → Files panel) // and the Symbol (NQ / ES / BTC …). Done. It refreshes itself. #region Using declarations using System; using System.Net; using System.Linq; using System.Threading; using System.Globalization; using System.ComponentModel.DataAnnotations; using System.Windows.Media; using NinjaTrader.Gui; // DashStyleHelper lives here — without this the dash line fails to compile using NinjaTrader.NinjaScript; using NinjaTrader.NinjaScript.DrawingTools; #endregion namespace NinjaTrader.NinjaScript.Indicators { public class SPECTRE_Levels : Indicator { // The first '#' row of the file, written by the server's stamp_export. private const string StampPrefix = "# SPECTRE levels"; private const string StampAsOf = "data as of"; private const string StampStale = "STALE"; private const string StatusTag = "spectre_status"; private DateTime lastFetchUtc = DateTime.MinValue; private DateTime refreshFailedSinceUtc = DateTime.MinValue; private string sessionDate; private bool sessionStale; private int levelsDrawn; [NinjaScriptProperty] [Display(Name = "API key", Description = "Your spectre.markets key (Files panel → 'auto-sync key')", Order = 1, GroupName = "SPECTRE")] public string ApiKey { get; set; } [NinjaScriptProperty] [Display(Name = "Symbol", Description = "NQ / ES / BTC … — which instrument's levels to pull", Order = 2, GroupName = "SPECTRE")] public string Sym { get; set; } [NinjaScriptProperty] [Range(2, 1440)] [Display(Name = "Refresh (minutes)", Description = "How often to re-pull (levels publish once daily: NQ/ES about 13:15 UTC, BTC/ETH/GC/CL about 23:15 UTC)", Order = 3, GroupName = "SPECTRE")] public int RefreshMinutes { get; set; } [NinjaScriptProperty] [Display(Name = "Server", Description = "Leave as is", Order = 4, GroupName = "SPECTRE")] public string BaseUrl { get; set; } protected override void OnStateChange() { if (State == State.SetDefaults) { Name = "SPECTRE_Levels"; Description = "SPECTRE daily levels — auto-synced from spectre.markets (gamma, pivots, volume, golden, …)."; IsOverlay = true; DrawOnPricePanel = true; IsSuspendedWhileInactive = true; ApiKey = ""; Sym = "NQ"; RefreshMinutes = 30; BaseUrl = "https://spectre.markets"; } } protected override void OnBarUpdate() { // Draw on load, then refresh every RefreshMinutes. The fetch is SYNCHRONOUS with a bounded // timeout and only runs on the last/realtime bar, so the lines draw reliably + at once, and // any problem prints to the NinjaScript Output window (Control Center -> New -> Output) // and shows on the chart's status line. if (CurrentBar < 0) return; if (State == State.Historical && CurrentBar < Count - (Calculate == Calculate.OnBarClose ? 2 : 1)) return; // wait for the latest bar: under OnBarClose the forming bar is never historical if ((DateTime.UtcNow - lastFetchUtc).TotalMinutes < RefreshMinutes) return; lastFetchUtc = DateTime.UtcNow; FetchAndDraw(); } private void FetchAndDraw() { try { if (string.IsNullOrEmpty(ApiKey)) { Print("SPECTRE_Levels: paste your API key in the indicator settings (copy it from spectre.markets -> Files panel)."); MarkRefreshFailed(); return; } ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11; string url = BaseUrl.TrimEnd('/') + "/api/levels/export?symbol=" + Uri.EscapeDataString(Sym) + "&platform=ninjatrader"; Print("SPECTRE_Levels: fetching " + Sym + " ..."); var req = (HttpWebRequest)WebRequest.Create(url); req.Timeout = 15000; req.Headers["X-API-Key"] = ApiKey; req.UserAgent = "SPECTRE-NT/1"; string csv; using (var resp = (HttpWebResponse)req.GetResponse()) using (var sr = new System.IO.StreamReader(resp.GetResponseStream())) csv = sr.ReadToEnd(); ReadStamp(csv); int n = DrawLevels(csv); levelsDrawn = n; refreshFailedSinceUtc = DateTime.MinValue; DrawStatus(); // after DrawLevels: its RemoveDrawObjects clears every tag Print("SPECTRE_Levels: drew " + n + " levels for " + Sym + " (next refresh in " + RefreshMinutes + " min)."); } catch (WebException wex) { var resp = wex.Response as HttpWebResponse; int code = resp != null ? (int)resp.StatusCode : 0; if (resp == null) Print("SPECTRE_Levels: fetch FAILED — " + wex.Status + " " + wex.Message + " (a firewall/proxy may be blocking NinjaTrader's internet access)."); else if ((int)resp.StatusCode == 403) Print("SPECTRE_Levels: 403 — chart files are free with an account: sign up at spectre.markets, or check that the API key is correct and not expired."); else if (code == 404) Print("SPECTRE_Levels: 404 — no levels for Symbol '" + Sym + "'. Check the Symbol setting (NQ / ES / BTC …)."); else if (code == 429) Print("SPECTRE_Levels: 429 — rate limited by spectre.markets. It retries at the next refresh."); else if (code >= 500) Print("SPECTRE_Levels: " + code + " — the spectre.markets server had a problem. It retries at the next refresh."); else Print("SPECTRE_Levels: fetch FAILED — HTTP " + code + " " + wex.Message); MarkRefreshFailed(); } catch (Exception ex) { Print("SPECTRE_Levels: error — " + ex.Message); MarkRefreshFailed(); } } // Only the LEADING '#' rows can carry the stamp. No stamp means the date is unknown, never stale. private void ReadStamp(string csv) { sessionDate = null; sessionStale = false; foreach (var row in csv.Replace("\r", "").Split('\n')) { string t = row.Trim(); if (t.Length == 0) continue; if (!t.StartsWith("#", StringComparison.Ordinal)) break; int at = t.IndexOf(StampAsOf, StringComparison.Ordinal); if (at < 0 || !t.StartsWith(StampPrefix, StringComparison.Ordinal)) continue; sessionDate = t.Substring(at + StampAsOf.Length).Trim(); sessionStale = t.IndexOf(StampStale, 0, at, StringComparison.Ordinal) >= 0; return; } } // The lines stay up on a failed refresh; the status line is what says they may be old. private void MarkRefreshFailed() { if (refreshFailedSinceUtc == DateTime.MinValue) refreshFailedSinceUtc = DateTime.UtcNow; DrawStatus(); } private void DrawStatus() { try { string s = "SPECTRE " + Sym + " levels"; if (levelsDrawn == 0) s += " - no levels loaded"; else s += sessionDate != null ? " - session " + sessionDate : " - session date unknown"; if (sessionStale) s += " - " + StampStale; if (refreshFailedSinceUtc != DateTime.MinValue) s += " - REFRESH FAILED since " + refreshFailedSinceUtc.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture) + " UTC (see NinjaScript Output)"; Draw.TextFixed(this, StatusTag, s, TextPosition.TopRight); } catch (Exception ex) { Print("SPECTRE_Levels status: " + ex.Message); } } private int DrawLevels(string csv) { int drawn = 0; try { RemoveDrawObjects(); var rows = csv.Replace("\r", "").Split('\n'); for (int i = 1; i < rows.Length; i++) // row 0 = header { if (string.IsNullOrWhiteSpace(rows[i])) continue; var c = rows[i].Split(','); if (c.Length < 5) continue; double price; if (!double.TryParse(c[0], NumberStyles.Any, CultureInfo.InvariantCulture, out price)) continue; string label = c[1]; Brush brush; try { brush = new SolidColorBrush((Color)ColorConverter.ConvertFromString(c[2])); brush.Freeze(); } catch { brush = Brushes.Gray; } int width; if (!int.TryParse(c[3], out width)) width = 1; int dash; if (!int.TryParse(c[4], out dash)) dash = 0; DashStyleHelper style = dash == 1 ? DashStyleHelper.Dash : DashStyleHelper.Solid; string tag = "spectre_" + i; Draw.HorizontalLine(this, tag, price, brush, style, width); Draw.Text(this, tag + "_lbl", label + " " + price.ToString("#,##0.########", CultureInfo.InvariantCulture), 0, price, brush); drawn++; } } catch (Exception ex) { Print("SPECTRE_Levels draw: " + ex.Message); } return drawn; } } }