#region Using declarations using System; using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Net; using System.IO; using System.Text; using System.Threading; using NinjaTrader.Cbi; using NinjaTrader.Gui; using NinjaTrader.Gui.Chart; using NinjaTrader.Gui.Tools; using NinjaTrader.Data; using NinjaTrader.NinjaScript; using NinjaTrader.NinjaScript.Strategies; using NinjaTrader.Core.FloatingPoint; #endregion // ============================================================================ // WinnersCircleAutoTrader // ---------------------------------------------------------------------------- // A user-side strategy that polls a personal webhook URL on the Winner's // Circle signal service and places orders via an ATM template. // // CONFIG (strategy parameters): // WebhookUrl — your personal URL from the dashboard Auto-Trade tab // (e.g. https://yourapp.up.railway.app/api/user-webhook/) // AtmTemplate — name of an ATM strategy template you've created in NT // (Tools → ATM Strategies) // PollSeconds — how often to check for new signals (default 2s) // AllowBuy — accept BUY signals? // AllowSell — accept SELL signals? // RequireLiveMode — only trade when the strategy is Connected + live // // PLACEMENT // Add this strategy once on any chart/instrument; it places on the // instrument reported IN THE SIGNAL PAYLOAD, not the chart's instrument. // Keep the strategy running while the market is open. // // SAFETY // - Duplicate-delivery protection: server marks signals delivered // atomically, but we also remember recent IDs. // - If NT is offline > 5 minutes, queued signals auto-expire server-side. // - Honors AllowBuy / AllowSell locally as a second safety net. // ============================================================================ namespace NinjaTrader.NinjaScript.Strategies { public class WinnersCircleAutoTrader : Strategy { private System.Timers.Timer pollTimer; private readonly HashSet seenIds = new HashSet(); private readonly object lockObj = new object(); private bool isPolling = false; private int consecutiveFailures = 0; protected override void OnStateChange() { if (State == State.SetDefaults) { Description = @"Winner's Circle Auto-Trader — polls your personal webhook URL and places ATM trades."; Name = "WinnersCircleAutoTrader"; Calculate = Calculate.OnBarClose; EntriesPerDirection = 100; EntryHandling = EntryHandling.AllEntries; IsExitOnSessionCloseStrategy = true; ExitOnSessionCloseSeconds = 30; IsFillLimitOnTouch = false; MaximumBarsLookBack = MaximumBarsLookBack.TwoHundredFiftySix; OrderFillResolution = OrderFillResolution.Standard; Slippage = 0; StartBehavior = StartBehavior.WaitUntilFlat; TimeInForce = TimeInForce.Day; TraceOrders = false; RealtimeErrorHandling = RealtimeErrorHandling.StopCancelClose; StopTargetHandling = StopTargetHandling.PerEntryExecution; BarsRequiredToTrade = 0; WebhookUrl = "https://your-app.up.railway.app/api/user-webhook/REPLACE_WITH_YOUR_TOKEN"; AtmTemplate = "WinnersCircle_Default"; PollSeconds = 2; AllowBuy = true; AllowSell = true; RequireLiveMode = true; } else if (State == State.Configure) { // Nothing — we don't need bars data. } else if (State == State.Realtime) { StartPolling(); } else if (State == State.Terminated) { StopPolling(); } } protected override void OnBarUpdate() { /* unused — polling-driven */ } // ────────────────────────────────────────────────────────────────── // Polling loop // ────────────────────────────────────────────────────────────────── private void StartPolling() { if (pollTimer != null) return; if (string.IsNullOrWhiteSpace(WebhookUrl) || WebhookUrl.Contains("REPLACE_WITH_YOUR_TOKEN")) { Print("[WinnersCircle] ERROR: WebhookUrl is not configured. Paste your personal URL from the dashboard Auto-Trade tab."); return; } if (string.IsNullOrWhiteSpace(AtmTemplate)) { Print("[WinnersCircle] ERROR: AtmTemplate is not set. Create one in Tools → ATM Strategies first."); return; } int interval = Math.Max(1, PollSeconds) * 1000; pollTimer = new System.Timers.Timer(interval); pollTimer.Elapsed += (s, e) => PollOnce(); pollTimer.AutoReset = true; pollTimer.Start(); Print("[WinnersCircle] Auto-trader started. Polling every " + PollSeconds + "s."); } private void StopPolling() { if (pollTimer != null) { try { pollTimer.Stop(); pollTimer.Dispose(); } catch {} pollTimer = null; Print("[WinnersCircle] Auto-trader stopped."); } } private void PollOnce() { lock (lockObj) { if (isPolling) return; isPolling = true; } try { if (RequireLiveMode && Connection.PrimaryHost.Connections.All(c => c.Status != ConnectionStatus.Connected)) return; string body = HttpGet(WebhookUrl, 5000); if (string.IsNullOrEmpty(body)) { consecutiveFailures++; return; } consecutiveFailures = 0; // We don't ship a JSON parser dependency; do minimal parse. List> signals = ExtractSignals(body); if (signals == null || signals.Count == 0) return; foreach (var sig in signals) { long id = ParseLong(sig, "id"); if (id > 0) { lock (seenIds) { if (seenIds.Contains(id)) continue; seenIds.Add(id); } // Trim memory lock (seenIds) { if (seenIds.Count > 500) seenIds.Clear(); } } string sym = ParseString(sig, "symbol"); string dir = ParseString(sig, "direction"); double entry = ParseDouble(sig, "entry"); int qty = (int)ParseLong(sig, "qty"); if (qty <= 0) qty = 1; if (string.IsNullOrWhiteSpace(sym) || string.IsNullOrWhiteSpace(dir)) continue; if (dir == "BUY" && !AllowBuy) continue; if (dir == "SELL" && !AllowSell) continue; TriggerAtm(id, sym, dir, qty, entry); } } catch (Exception ex) { consecutiveFailures++; Print("[WinnersCircle] poll error: " + ex.Message); } finally { lock (lockObj) { isPolling = false; } } } // ────────────────────────────────────────────────────────────────── // Order placement via ATM // ────────────────────────────────────────────────────────────────── private void TriggerAtm(long queueId, string symbol, string direction, int qty, double fallbackEntry) { try { // Resolve the instrument from the signal symbol. We take the // user's default front month contract of that root — NinjaTrader // handles continuous contract resolution. Instrument inst = Instrument.GetInstrument(symbol); if (inst == null) { Print("[WinnersCircle] cannot resolve instrument: " + symbol + " — skipping signal #" + queueId); return; } OrderAction action = direction == "BUY" ? OrderAction.Buy : OrderAction.SellShort; string atmId = "WC_" + queueId + "_" + DateTime.UtcNow.Ticks; string atmTemplate = AtmTemplate; // Use Strategy's AtmStrategyCreate to place a market order atomically // with the ATM template (which carries SL + TP + trailing definitions). AtmStrategyCreate(action, OrderType.Market, 0, 0, TimeInForce.Day, Guid.NewGuid().ToString("N"), atmTemplate, atmId, (atmCallbackErrorCode, atmCallbackNativeError, atmCallbackStrategyId) => { if (atmCallbackErrorCode == ErrorCode.NoError) Print("[WinnersCircle] ✅ ATM placed — " + direction + " " + symbol + " x" + qty + " (signal #" + queueId + ")"); else Print("[WinnersCircle] ❌ ATM failed (" + atmCallbackErrorCode + "): " + atmCallbackNativeError); Ack(queueId, atmCallbackErrorCode == ErrorCode.NoError, atmCallbackStrategyId); }); } catch (Exception ex) { Print("[WinnersCircle] TriggerAtm error: " + ex.Message); Ack(queueId, false, ex.Message); } } // ────────────────────────────────────────────────────────────────── // HTTP helpers // ────────────────────────────────────────────────────────────────── private string HttpGet(string url, int timeoutMs) { try { var req = (HttpWebRequest)WebRequest.Create(url); req.Method = "GET"; req.Timeout = timeoutMs; req.UserAgent = "WinnersCircleAutoTrader/1.0"; using (var resp = (HttpWebResponse)req.GetResponse()) using (var sr = new StreamReader(resp.GetResponseStream())) return sr.ReadToEnd(); } catch (WebException we) { // 404 on rotated token, etc if (we.Response is HttpWebResponse hr) Print("[WinnersCircle] HTTP " + (int)hr.StatusCode + " from server — check your WebhookUrl"); return null; } catch (Exception ex) { Print("[WinnersCircle] HTTP error: " + ex.Message); return null; } } private void Ack(long queueId, bool ok, object payload) { try { // Best-effort; never throws up. string ackUrl = WebhookUrl + (WebhookUrl.EndsWith("/") ? "ack" : "/ack"); string json = "{\"queueId\":" + queueId + ",\"result\":\"" + (ok ? "ok" : "error") + "\"" + ",\"detail\":\"" + EscapeJson(payload == null ? "" : payload.ToString()) + "\"}"; var req = (HttpWebRequest)WebRequest.Create(ackUrl); req.Method = "POST"; req.Timeout = 3000; req.ContentType = "application/json"; req.UserAgent = "WinnersCircleAutoTrader/1.0"; byte[] bytes = Encoding.UTF8.GetBytes(json); req.ContentLength = bytes.Length; using (var s = req.GetRequestStream()) s.Write(bytes, 0, bytes.Length); using (var resp = (HttpWebResponse)req.GetResponse()) { /* discard */ } } catch { /* swallow */ } } private string EscapeJson(string s) { if (string.IsNullOrEmpty(s)) return ""; return s.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", " ").Replace("\r", " "); } // ────────────────────────────────────────────────────────────────── // Minimal JSON extraction — dependency-free. Assumes server response // shape { "signals": [ { ... } ] } matching user_signal_queue rows. // We pull each object, then parse its keys. Robust enough for the // known payload, not a full JSON parser. // ────────────────────────────────────────────────────────────────── private List> ExtractSignals(string json) { var result = new List>(); if (string.IsNullOrEmpty(json)) return result; int arrStart = json.IndexOf("\"signals\""); if (arrStart < 0) return result; int bracket = json.IndexOf('[', arrStart); if (bracket < 0) return result; int depth = 0; int objStart = -1; for (int i = bracket; i < json.Length; i++) { char c = json[i]; if (c == '{') { if (depth == 0) objStart = i; depth++; } else if (c == '}') { depth--; if (depth == 0 && objStart >= 0) { string obj = json.Substring(objStart, i - objStart + 1); result.Add(ParseObject(obj)); objStart = -1; } } else if (c == ']' && depth == 0) break; } return result; } private Dictionary ParseObject(string obj) { var dict = new Dictionary(); int i = 0; while (i < obj.Length) { int kStart = obj.IndexOf('"', i); if (kStart < 0) break; int kEnd = obj.IndexOf('"', kStart + 1); if (kEnd < 0) break; string key = obj.Substring(kStart + 1, kEnd - kStart - 1); int colon = obj.IndexOf(':', kEnd); if (colon < 0) break; int vStart = colon + 1; while (vStart < obj.Length && (obj[vStart] == ' ' || obj[vStart] == '\t' || obj[vStart] == '\n' || obj[vStart] == '\r')) vStart++; if (vStart >= obj.Length) break; string val; int vEnd; if (obj[vStart] == '"') { vEnd = vStart + 1; while (vEnd < obj.Length && obj[vEnd] != '"') vEnd++; val = obj.Substring(vStart + 1, vEnd - vStart - 1); vEnd++; // past closing quote } else { vEnd = vStart; while (vEnd < obj.Length && obj[vEnd] != ',' && obj[vEnd] != '}' && obj[vEnd] != '\n' && obj[vEnd] != '\r') vEnd++; val = obj.Substring(vStart, vEnd - vStart).Trim(); if (val == "null") val = ""; } dict[key] = val; i = vEnd; int comma = obj.IndexOf(',', i); if (comma < 0) break; i = comma + 1; } return dict; } private string ParseString(Dictionary d, string k) { string v; return d.TryGetValue(k, out v) ? v ?? "" : ""; } private long ParseLong(Dictionary d, string k) { long n; if (d.TryGetValue(k, out string v) && long.TryParse(v, out n)) return n; return 0; } private double ParseDouble(Dictionary d, string k) { double n; if (d.TryGetValue(k, out string v) && double.TryParse(v, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out n)) return n; return 0.0; } // ────────────────────────────────────────────────────────────────── // Parameters (shown in NT's strategy UI) // ────────────────────────────────────────────────────────────────── [NinjaScriptProperty] [Display(Name = "Webhook URL", Order = 1, GroupName = "1) Connection", Description = "Your personal URL from the dashboard Auto-Trade tab")] public string WebhookUrl { get; set; } [NinjaScriptProperty] [Display(Name = "ATM Template", Order = 2, GroupName = "1) Connection", Description = "Name of an ATM template (Tools → ATM Strategies) that defines SL/TP/trailing")] public string AtmTemplate { get; set; } [NinjaScriptProperty] [Range(1, 30)] [Display(Name = "Poll seconds", Order = 3, GroupName = "1) Connection", Description = "How often to check for new signals")] public int PollSeconds { get; set; } [NinjaScriptProperty] [Display(Name = "Allow BUY", Order = 10, GroupName = "2) Filters", Description = "Accept long signals")] public bool AllowBuy { get; set; } [NinjaScriptProperty] [Display(Name = "Allow SELL", Order = 11, GroupName = "2) Filters", Description = "Accept short signals")] public bool AllowSell { get; set; } [NinjaScriptProperty] [Display(Name = "Require live mode", Order = 20, GroupName = "3) Safety", Description = "Only trade when NinjaTrader is Connected (not simulated)")] public bool RequireLiveMode { get; set; } } }