// 01DTE Gobble Gobble SIM bridge for NinjaTrader 8. // Import this as a NinjaScript Strategy, attach it to the matching futures chart, // and run it on a Sim account only. #region Using declarations using System; using System.Collections.Generic; using System.IO; using System.Net.WebSockets; using System.Reflection; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using NinjaTrader.Cbi; using NinjaTrader.NinjaScript; using NinjaTrader.NinjaScript.Strategies; #endregion namespace NinjaTrader.NinjaScript.Strategies { public class GobbleGobbleSimBridgeStrategy : Strategy { private const string StrategyVersion = "v2026.07.20.3"; private const string TokenCacheFileName = "01dte-gobble-bridge-token.dat"; private static readonly object InstanceRegistryLock = new object(); private static readonly Dictionary ActiveInstancesByAsset = new Dictionary(StringComparer.OrdinalIgnoreCase); private ClientWebSocket socket; private CancellationTokenSource cancelSource; private readonly HashSet seenSignalIds = new HashSet(); private string currentEntrySignal = ""; private string currentSide = ""; private readonly object armedLock = new object(); private bool bridgeArmed; private Grid armGrid; private Button armButton; private TextBlock statusText; private MarketPosition lastReportedMarketPosition = MarketPosition.Flat; private int lastReportedQuantity = 0; private readonly SemaphoreSlim sendLock = new SemaphoreSlim(1, 1); private double activeStopPrice = 0; private Order activeEntryOrder = null; private DateTime activeEntrySubmittedAtUtc = DateTime.MinValue; private double activeEntryLimitPrice = 0; private int activeEntryLimitTtlSeconds = 0; private bool activeEntryChaseLimit = false; private bool activeEntryIsShort = false; private double activeEntrySignalPrice = 0; private double activeEntryMaxChasePoints = 0; private double activeInitialStopDistancePoints = 0; private bool activeInitialStopAnchoredToFill = false; private bool unsupportedAssetBlocked = false; private bool duplicateInstanceBlocked = false; private bool chartTraderBlocked = false; private bool bridgeConnected = false; private bool bridgeAuthorized = false; private string bridgeBlockedReason = ""; private string instanceAssetKey = ""; [NinjaScriptProperty] public string WebSocketUrl { get; set; } [NinjaScriptProperty] public string UserBridgeToken { get; set; } [NinjaScriptProperty] public string ExpectedAsset { get; set; } [NinjaScriptProperty] public int DefaultQuantity { get; set; } [NinjaScriptProperty] public bool SubmitOrders { get; set; } [NinjaScriptProperty] public bool SimOnly { get; set; } [NinjaScriptProperty] public bool ArmOnStart { get; set; } [NinjaScriptProperty] public bool BlockWhenChartTraderVisible { get; set; } protected override void OnStateChange() { if (State == State.SetDefaults) { Name = "GobbleGobbleSimBridgeStrategy"; Description = "Receives 01DTE Gobble SIM signals over WebSocket and submits SIM-only managed orders."; Calculate = Calculate.OnEachTick; EntriesPerDirection = 1; EntryHandling = EntryHandling.AllEntries; IsExitOnSessionCloseStrategy = true; ExitOnSessionCloseSeconds = 30; IncludeCommission = true; IsInstantiatedOnEachOptimizationIteration = false; WebSocketUrl = "wss://01dte.com/ninja-gobble-sim"; UserBridgeToken = ""; ExpectedAsset = "AUTO"; DefaultQuantity = 1; SubmitOrders = true; SimOnly = true; ArmOnStart = false; BlockWhenChartTraderVisible = false; } else if (State == State.DataLoaded) { RestoreRememberedBridgeToken(); string resolvedAsset = ResolveAsset(); BlockWhenChartTraderVisible = false; unsupportedAssetBlocked = !IsSupportedAsset(resolvedAsset); duplicateInstanceBlocked = !unsupportedAssetBlocked && !TryClaimInstance(resolvedAsset); bridgeArmed = ArmOnStart && !unsupportedAssetBlocked && !duplicateInstanceBlocked && HasBridgeToken(); if (unsupportedAssetBlocked) { Print("[GobbleSim] blocked: Gobble bridge only supports ES, MES, NQ, and MNQ charts."); } else if (duplicateInstanceBlocked) { Print("[GobbleSim] blocked: another Gobble bridge already owns " + resolvedAsset + ". Close the other " + resolvedAsset + " Gobble strategy first."); } else if (!HasBridgeToken()) { Print("[GobbleSim] bridge not started: token required. Paste bridge token or URL in UserBridgeToken."); } else { SaveRememberedBridgeToken(); StartBridge(); } } else if (State == State.Historical) { AddArmButton(); } else if (State == State.Terminated) { RemoveArmButton(); StopBridge(); ReleaseInstanceClaim(); } } protected override void OnBarUpdate() { CancelExpiredEntryLimit(); ChaseWorkingEntryLimit(); ReportPosition("ON_BAR_UPDATE", false); } private void StartBridge() { if (!IsSupportedAsset(ResolveAsset())) { unsupportedAssetBlocked = true; bridgeArmed = false; Print("[GobbleSim] bridge not started: unsupported asset " + ResolveAsset() + ". ES, MES, NQ, or MNQ only."); UpdateArmButton(); return; } if (duplicateInstanceBlocked) { bridgeArmed = false; Print("[GobbleSim] bridge not started: duplicate " + ResolveAsset() + " instance is blocked."); UpdateArmButton(); return; } if (cancelSource != null) return; cancelSource = new CancellationTokenSource(); Task.Run(() => ReceiveLoop(cancelSource.Token)); Print("[GobbleSim] bridge starting: " + WebSocketUrl); } private bool TryClaimInstance(string asset) { if (!IsSupportedAsset(asset)) return false; lock (InstanceRegistryLock) { GobbleGobbleSimBridgeStrategy existing; if (ActiveInstancesByAsset.TryGetValue(asset, out existing) && existing != null && !ReferenceEquals(existing, this)) return false; ActiveInstancesByAsset[asset] = this; instanceAssetKey = asset; return true; } } private void ReleaseInstanceClaim() { lock (InstanceRegistryLock) { GobbleGobbleSimBridgeStrategy existing; if (!string.IsNullOrWhiteSpace(instanceAssetKey) && ActiveInstancesByAsset.TryGetValue(instanceAssetKey, out existing) && ReferenceEquals(existing, this)) { ActiveInstancesByAsset.Remove(instanceAssetKey); } instanceAssetKey = ""; } } private void StopBridge() { try { cancelSource?.Cancel(); } catch { } try { socket?.Abort(); socket?.Dispose(); } catch { } cancelSource = null; socket = null; } private async Task ReceiveLoop(CancellationToken token) { while (!token.IsCancellationRequested) { try { socket = new ClientWebSocket(); await socket.ConnectAsync(new Uri(BuildBridgeUrl()), token); TriggerCustomEvent(o => { bridgeConnected = true; bridgeAuthorized = false; bridgeBlockedReason = "WAITING FOR LOGIN"; UpdateArmButton(); Print("[GobbleSim] socket connected"); }, null); await SendAckAsync("CONNECTED", "OK", "WebSocket connected", null); var buffer = new byte[16 * 1024]; while (socket.State == WebSocketState.Open && !token.IsCancellationRequested) { var result = await socket.ReceiveAsync(new ArraySegment(buffer), token); if (result.MessageType == WebSocketMessageType.Close) { string closeText = "[GobbleSim] socket closed by server"; if (socket.CloseStatus.HasValue) closeText += " code=" + socket.CloseStatus.Value; if (!string.IsNullOrWhiteSpace(socket.CloseStatusDescription)) closeText += " reason=" + socket.CloseStatusDescription; string closeReason = socket.CloseStatusDescription; TriggerCustomEvent(o => { bridgeConnected = false; bridgeAuthorized = false; bridgeBlockedReason = FriendlyBridgeCloseReason(closeReason); lock (armedLock) bridgeArmed = false; UpdateArmButton(); Print(closeText); }, null); break; } var text = Encoding.UTF8.GetString(buffer, 0, result.Count); TriggerCustomEvent(o => ProcessServerMessage(text), null); } } catch (Exception ex) { TriggerCustomEvent(o => { bridgeConnected = false; bridgeAuthorized = false; bridgeBlockedReason = "NOT CONNECTED"; lock (armedLock) bridgeArmed = false; UpdateArmButton(); Print("[GobbleSim] socket error: " + ex.Message); }, null); } try { socket?.Dispose(); } catch { } socket = null; if (!token.IsCancellationRequested) { TriggerCustomEvent(o => Print("[GobbleSim] socket reconnecting in 3 seconds"), null); await Task.Delay(3000, token); } } } private string BuildBridgeUrl() { string url = string.IsNullOrWhiteSpace(WebSocketUrl) ? "wss://01dte.com/ninja-gobble-sim" : WebSocketUrl.Trim(); string token = (UserBridgeToken ?? "").Trim(); if (LooksLikeBridgeUrl(token)) return token; if (url.IndexOf("bridgeToken=", StringComparison.OrdinalIgnoreCase) >= 0) return url; token = ExtractBridgeToken(token); if (string.IsNullOrWhiteSpace(token)) return url; return url + (url.Contains("?") ? "&" : "?") + "bridgeToken=" + Uri.EscapeDataString(token); } private bool HasBridgeToken() { string token = ExtractBridgeToken(UserBridgeToken); if (!string.IsNullOrWhiteSpace(token)) return true; string url = (WebSocketUrl ?? "").Trim(); return url.IndexOf("bridgeToken=", StringComparison.OrdinalIgnoreCase) >= 0 && !string.IsNullOrWhiteSpace(ExtractBridgeToken(url)); } private string EffectiveBridgeToken() { string token = ExtractBridgeToken(UserBridgeToken); if (!string.IsNullOrWhiteSpace(token)) return token; return ExtractBridgeToken(WebSocketUrl); } private string TokenCachePath() { string documents = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); string root = string.IsNullOrWhiteSpace(documents) ? Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) : documents; string dir = Path.Combine(root, "NinjaTrader 8", "templates", "01DTE"); Directory.CreateDirectory(dir); return Path.Combine(dir, TokenCacheFileName); } private void RestoreRememberedBridgeToken() { try { if (HasBridgeToken()) return; string file = TokenCachePath(); if (!File.Exists(file)) return; string encoded = File.ReadAllText(file).Trim(); string token = UnprotectText(encoded); if (string.IsNullOrWhiteSpace(token)) return; UserBridgeToken = token.Trim(); Print("[GobbleSim] restored remembered bridge token for this Windows user."); } catch (Exception ex) { Print("[GobbleSim] token restore skipped: " + ex.Message); } } private void SaveRememberedBridgeToken() { try { string token = EffectiveBridgeToken(); if (string.IsNullOrWhiteSpace(token)) return; File.WriteAllText(TokenCachePath(), ProtectText(token.Trim())); } catch (Exception ex) { Print("[GobbleSim] token remember skipped: " + ex.Message); } } private static string ProtectText(string text) { byte[] raw = Encoding.UTF8.GetBytes(text ?? ""); try { Type protectedData = FindType( "System.Security.Cryptography.ProtectedData", "System.Security.Cryptography.ProtectedData, System.Security", "System.Security.Cryptography.ProtectedData, System.Security.Cryptography.ProtectedData"); Type scopeType = FindType( "System.Security.Cryptography.DataProtectionScope", "System.Security.Cryptography.DataProtectionScope, System.Security", "System.Security.Cryptography.DataProtectionScope, System.Security.Cryptography.ProtectedData"); if (protectedData != null && scopeType != null) { object scope = Enum.Parse(scopeType, "CurrentUser"); MethodInfo protect = protectedData.GetMethod("Protect", new[] { typeof(byte[]), typeof(byte[]), scopeType }); if (protect != null) { byte[] encrypted = (byte[])protect.Invoke(null, new object[] { raw, null, scope }); return "dpapi:" + Convert.ToBase64String(encrypted); } } } catch { } return "plain:" + Convert.ToBase64String(raw); } private static string UnprotectText(string encoded) { string text = (encoded ?? "").Trim(); if (text.StartsWith("dpapi:", StringComparison.OrdinalIgnoreCase)) { try { Type protectedData = FindType( "System.Security.Cryptography.ProtectedData", "System.Security.Cryptography.ProtectedData, System.Security", "System.Security.Cryptography.ProtectedData, System.Security.Cryptography.ProtectedData"); Type scopeType = FindType( "System.Security.Cryptography.DataProtectionScope", "System.Security.Cryptography.DataProtectionScope, System.Security", "System.Security.Cryptography.DataProtectionScope, System.Security.Cryptography.ProtectedData"); if (protectedData != null && scopeType != null) { object scope = Enum.Parse(scopeType, "CurrentUser"); MethodInfo unprotect = protectedData.GetMethod("Unprotect", new[] { typeof(byte[]), typeof(byte[]), scopeType }); if (unprotect != null) { byte[] encrypted = Convert.FromBase64String(text.Substring("dpapi:".Length)); byte[] raw = (byte[])unprotect.Invoke(null, new object[] { encrypted, null, scope }); return Encoding.UTF8.GetString(raw); } } } catch { } return ""; } if (text.StartsWith("plain:", StringComparison.OrdinalIgnoreCase)) return Encoding.UTF8.GetString(Convert.FromBase64String(text.Substring("plain:".Length))); return ""; } private static Type FindType(params string[] names) { foreach (string name in names) { Type type = Type.GetType(name); if (type != null) return type; } return null; } private static bool LooksLikeBridgeUrl(string value) { string text = (value ?? "").Trim(); return text.StartsWith("ws://", StringComparison.OrdinalIgnoreCase) || text.StartsWith("wss://", StringComparison.OrdinalIgnoreCase); } private static string ExtractBridgeToken(string value) { string text = (value ?? "").Trim(); if (string.IsNullOrWhiteSpace(text)) return ""; int marker = text.IndexOf("bridgeToken=", StringComparison.OrdinalIgnoreCase); if (marker >= 0) { string token = text.Substring(marker + "bridgeToken=".Length); int amp = token.IndexOf('&'); if (amp >= 0) token = token.Substring(0, amp); try { token = Uri.UnescapeDataString(token); } catch { } return token.Trim(); } return text; } private void ProcessServerMessage(string json) { if (!IsSupportedAsset(ResolveAsset())) { unsupportedAssetBlocked = true; bridgeArmed = false; UpdateArmButton(); return; } if (duplicateInstanceBlocked) { bridgeArmed = false; UpdateArmButton(); return; } if (!HasBridgeToken()) { bridgeArmed = false; UpdateArmButton(); return; } string type = JsonString(json, "type"); if (type == "GOBBLE_SIM_HELLO") { bridgeConnected = true; bridgeAuthorized = true; bridgeBlockedReason = ""; UpdateArmButton(); Print("[GobbleSim] server hello"); _ = SendAckAsync("HELLO", "OK", "HELLO received", null); return; } if (type == "GOBBLE_SIM_HEARTBEAT") { string heartbeatId = JsonString(json, "id"); _ = SendAckAsync(string.IsNullOrWhiteSpace(heartbeatId) ? "HEARTBEAT" : heartbeatId, "OK", "heartbeat", null); return; } if (type != "GOBBLE_SIM_SIGNAL") return; GobbleCommand cmd = GobbleCommand.FromJson(json); string localAsset = ResolveAsset(); if (!string.Equals(cmd.Asset, localAsset, StringComparison.OrdinalIgnoreCase)) { _ = SendAckAsync(cmd.Id, "IGNORED", "asset mismatch", cmd); return; } if (seenSignalIds.Contains(cmd.Id)) { _ = SendAckAsync(cmd.Id, "DUPLICATE", "duplicate signal id ignored", cmd); return; } seenSignalIds.Add(cmd.Id); if (seenSignalIds.Count > 500) seenSignalIds.Clear(); Print(string.Format("[GobbleSim] {0} {1} {2} qty={3} stop={4} dryRun={5}", cmd.Id, cmd.Action, cmd.Side, cmd.Qty, cmd.StopPrice, cmd.DryRun)); if (cmd.Action == "PING") { _ = SendAckAsync(cmd.Id, "OK", "PING received", cmd); return; } if (cmd.Action == "STATUS" || cmd.Action == "POSITION") { ReportPosition("COMMAND_" + cmd.Action, true); _ = SendAckAsync(cmd.Id, "OK", "position snapshot requested", cmd); return; } if (!IsBridgeArmed()) { _ = SendAckAsync(cmd.Id, HasBridgeToken() ? "DISARMED" : "TOKEN_REQUIRED", HasBridgeToken() ? "Gobble button is OFF" : "Gobble bridge token is required", cmd); return; } if (cmd.DryRun || !SubmitOrders) { _ = SendAckAsync(cmd.Id, "DRY_RUN", "orders disabled by strategy setting", cmd); return; } if (SimOnly && !IsSimAccount()) { _ = SendAckAsync(cmd.Id, "REJECTED", "strategy is SIM_ONLY and account is not Sim/Demo", cmd); return; } if (cmd.Action == "ENTER") { string entryBlock = EntryBlockReason(cmd, localAsset); if (!string.IsNullOrWhiteSpace(entryBlock)) { _ = SendAckAsync(cmd.Id, entryBlock.StartsWith("ENTRY_EXPIRED") ? "ENTRY_EXPIRED" : "DO_NOT_CHASE", entryBlock, cmd); return; } } try { ExecuteCommand(cmd); ReportPosition("COMMAND_" + cmd.Action, true); _ = SendAckAsync(cmd.Id, "ACCEPTED", "command submitted to Ninja strategy", cmd); } catch (Exception ex) { Print("[GobbleSim] command failed: " + ex.Message); _ = SendAckAsync(cmd.Id, "ERROR", ex.Message, cmd); } } private bool IsBridgeArmed() { lock (armedLock) return bridgeArmed && HasBridgeToken() && bridgeAuthorized && !unsupportedAssetBlocked && !duplicateInstanceBlocked; } private void SetBridgeArmed(bool value) { if (value && !IsSupportedAsset(ResolveAsset())) { unsupportedAssetBlocked = true; value = false; Print("[GobbleSim] cannot arm: unsupported asset. ES, MES, NQ, or MNQ only."); } if (value && duplicateInstanceBlocked) { value = false; Print("[GobbleSim] cannot arm: duplicate " + ResolveAsset() + " Gobble bridge instance is blocked."); } if (value && !HasBridgeToken()) { value = false; Print("[GobbleSim] cannot arm: bridge token required."); } if (value && !bridgeAuthorized) { value = false; Print("[GobbleSim] cannot arm: " + BridgeAccessLabel().ToLowerInvariant() + "."); } lock (armedLock) bridgeArmed = value; UpdateArmButton(); Print("[GobbleSim] " + (value ? "GOBBLE ON" : "GOBBLE OFF")); _ = SendAckAsync("GOBBLE_ARM", value ? "ARMED" : (!HasBridgeToken() ? "TOKEN_REQUIRED" : (!bridgeAuthorized ? "LOGIN_REQUIRED" : "DISARMED")), value ? "Gobble button ON" : (!HasBridgeToken() ? "Bridge token required" : (!bridgeAuthorized ? BridgeAccessLabel() : "Gobble button OFF")), null); } private void AddArmButton() { if (ChartControl == null) return; ChartControl.Dispatcher.InvokeAsync(() => { if (armGrid != null) return; armGrid = new Grid { HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Top, Margin = new Thickness(8, 64, 0, 0) }; armGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); armGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); armButton = new Button { MinWidth = 132, Height = 34, Padding = new Thickness(10, 4, 10, 4), FontWeight = FontWeights.Bold, BorderBrush = Brushes.DimGray, BorderThickness = new Thickness(1), ToolTip = "Toggle Gobble SIM command execution. OFF still receives PING/connection checks but rejects trade commands." }; armButton.Click += ArmButton_Click; Grid.SetRow(armButton, 0); armGrid.Children.Add(armButton); statusText = new TextBlock { MinWidth = 220, Margin = new Thickness(0, 4, 0, 0), Padding = new Thickness(8, 5, 8, 5), FontWeight = FontWeights.Bold, FontSize = 13, Foreground = Brushes.White, Background = Brushes.DimGray, Text = "GOBBLE SIM LOADING", ToolTip = "Gobble SIM bridge position status. This is a chart overlay only; Ninja's account/position window remains the execution source of truth." }; Grid.SetRow(statusText, 1); armGrid.Children.Add(statusText); UserControlCollection.Add(armGrid); UpdateArmButtonOnUi(); }); } private void RemoveArmButton() { if (ChartControl == null) return; ChartControl.Dispatcher.InvokeAsync(() => { if (armButton != null) armButton.Click -= ArmButton_Click; if (armGrid != null && UserControlCollection.Contains(armGrid)) UserControlCollection.Remove(armGrid); armButton = null; statusText = null; armGrid = null; }); } private void ArmButton_Click(object sender, RoutedEventArgs e) { SetBridgeArmed(!IsBridgeArmed()); } private void UpdateArmButton() { if (ChartControl == null) return; ChartControl.Dispatcher.InvokeAsync(UpdateArmButtonOnUi); } private void UpdateArmButtonOnUi() { if (armButton == null) return; unsupportedAssetBlocked = !IsSupportedAsset(ResolveAsset()); chartTraderBlocked = false; bool tokenMissing = !HasBridgeToken(); bool accessBlocked = !tokenMissing && !bridgeAuthorized; bool armed = IsBridgeArmed(); if (unsupportedAssetBlocked || duplicateInstanceBlocked || chartTraderBlocked || tokenMissing || accessBlocked) armed = false; armButton.Content = armed ? "GOBBLE ON" : "GOBBLE OFF"; if (unsupportedAssetBlocked) armButton.Content = "GOBBLE ES MES NQ MNQ"; else if (duplicateInstanceBlocked) armButton.Content = "GOBBLE DUPLICATE"; else if (chartTraderBlocked) armButton.Content = "GOBBLE CHART TRADER"; else if (tokenMissing) armButton.Content = "TOKEN REQUIRED"; else if (accessBlocked) armButton.Content = BridgeAccessLabel(); armButton.Background = (unsupportedAssetBlocked || duplicateInstanceBlocked || chartTraderBlocked || tokenMissing || accessBlocked) ? Brushes.DimGray : (armed ? Brushes.DarkGreen : Brushes.DarkRed); armButton.Foreground = Brushes.White; armButton.BorderBrush = (unsupportedAssetBlocked || duplicateInstanceBlocked || chartTraderBlocked || tokenMissing || accessBlocked) ? Brushes.Gray : (armed ? Brushes.LimeGreen : Brushes.IndianRed); if (statusText != null) { statusText.Text = BuildOverlayStatus(); statusText.Background = Position == null || Position.MarketPosition == MarketPosition.Flat ? ((unsupportedAssetBlocked || duplicateInstanceBlocked || chartTraderBlocked || tokenMissing || accessBlocked) ? Brushes.DarkRed : (armed ? Brushes.DarkSlateGray : Brushes.DimGray)) : (Position.MarketPosition == MarketPosition.Long ? Brushes.DarkGreen : Brushes.DarkRed); } } private string FriendlyBridgeCloseReason(string reason) { string text = (reason ?? "").Trim().ToUpperInvariant(); if (text == "MEMBERSHIP_REQUIRED") return "MEMBERSHIP REQUIRED"; if (text == "GOBBLE_NOT_APPROVED") return "GOBBLE NOT APPROVED"; if (text == "BAD_GOBBLE_USER_TOKEN") return "LOGIN REQUIRED"; if (text == "GOBBLE_TOKEN_AUTH_REQUIRED") return "LOGIN REQUIRED"; if (text == "GOBBLE_TOKEN_RATE_LIMIT") return "TOKEN RATE LIMITED"; if (text == "DUPLICATE_GOBBLE_TOKEN_ASSET") return "DUPLICATE"; return "NOT CONNECTED"; } private string BridgeAccessLabel() { if (!HasBridgeToken()) return "TOKEN REQUIRED"; if (!bridgeConnected && !string.IsNullOrWhiteSpace(bridgeBlockedReason)) return bridgeBlockedReason; if (!bridgeConnected) return "NOT CONNECTED"; if (!bridgeAuthorized && !string.IsNullOrWhiteSpace(bridgeBlockedReason)) return bridgeBlockedReason; if (!bridgeAuthorized) return "LOGIN REQUIRED"; return "CONNECTED"; } private string BuildOverlayStatus() { string asset = ResolveAsset(); string accountName = Account == null ? "" : Account.Name; if (!IsSupportedAsset(asset)) return string.Format("{0} BLOCKED | ES MES NQ MNQ ONLY | {1}", string.IsNullOrWhiteSpace(asset) ? "UNKNOWN" : asset, StrategyVersion); if (duplicateInstanceBlocked) return string.Format("{0} BLOCKED | DUPLICATE | {1}", asset, StrategyVersion); if (!HasBridgeToken()) return string.Format("{0} TOKEN REQUIRED | {1} | {2}", asset, accountName, StrategyVersion); if (!bridgeAuthorized) return string.Format("{0} {1} | {2} | {3}", asset, BridgeAccessLabel(), accountName, StrategyVersion); string armedText = IsBridgeArmed() ? "ARMED" : "DISARMED"; if (Position == null || Position.MarketPosition == MarketPosition.Flat) return string.Format("{0} FLAT | {1} | {2} | {3}", asset, accountName, armedText, StrategyVersion); string side = Position.MarketPosition == MarketPosition.Long ? "LONG" : "SHORT"; string stopText = activeStopPrice > 0 ? " | STOP " + JsonNumber(activeStopPrice) : ""; return string.Format("{0} {1} {2} @ {3}{4} | {5}", asset, side, Position.Quantity, JsonNumber(Position.AveragePrice), stopText, accountName + " | " + StrategyVersion); } private bool IsSimAccount() { string accountName = Account == null ? "" : Account.Name; return accountName.StartsWith("Sim", StringComparison.OrdinalIgnoreCase) || accountName.StartsWith("DEMO", StringComparison.OrdinalIgnoreCase); } private bool IsChartTraderBlocking() { chartTraderBlocked = false; return false; } private double ValidatedStopMovePrice(double requestedStopPrice) { if (Position == null || Position.MarketPosition == MarketPosition.Flat) throw new InvalidOperationException("MOVE_STOP rejected: no open position"); double market = CurrentNinjaMarketPrice(); double tick = NinjaTickSize(); if (market > 0 && tick > 0) { if (Position.MarketPosition == MarketPosition.Long && requestedStopPrice >= market - tick) throw new InvalidOperationException("MOVE_STOP rejected: long stop would trigger now requested=" + JsonNumber(requestedStopPrice) + " market=" + JsonNumber(market)); if (Position.MarketPosition == MarketPosition.Short && requestedStopPrice <= market + tick) throw new InvalidOperationException("MOVE_STOP rejected: short stop would trigger now requested=" + JsonNumber(requestedStopPrice) + " market=" + JsonNumber(market)); } return requestedStopPrice; } private double CurrentNinjaMarketPrice() { try { double bid = GetCurrentBid(); double ask = GetCurrentAsk(); double close = Close != null && CurrentBar >= 0 ? Close[0] : 0; if (Position != null && Position.MarketPosition == MarketPosition.Long && bid > 0) return bid; if (Position != null && Position.MarketPosition == MarketPosition.Short && ask > 0) return ask; if (close > 0) return close; if (bid > 0 && ask > 0) return (bid + ask) / 2.0; if (bid > 0) return bid; if (ask > 0) return ask; } catch { } return 0; } private double NinjaTickSize() { try { if (Instrument != null && Instrument.MasterInstrument != null && Instrument.MasterInstrument.TickSize > 0) return Instrument.MasterInstrument.TickSize; } catch { } return 0.25; } private double RoundToNinjaTick(double price) { double tick = NinjaTickSize(); if (tick <= 0) return price; return Math.Round(price / tick, MidpointRounding.AwayFromZero) * tick; } private bool DetectChartTraderVisibleOnUi() { try { DependencyObject root = Window.GetWindow(ChartControl) as DependencyObject; if (root == null) root = ChartControl; return ContainsVisibleChartTrader(root); } catch { return false; } } private bool ContainsVisibleChartTrader(DependencyObject node) { if (node == null) return false; FrameworkElement element = node as FrameworkElement; if (IsVisibleChartTraderPanel(element)) return true; int count = 0; try { count = VisualTreeHelper.GetChildrenCount(node); } catch { return false; } for (int i = 0; i < count; i++) { DependencyObject child = null; try { child = VisualTreeHelper.GetChild(node, i); } catch { child = null; } if (child != null && ContainsVisibleChartTrader(child)) return true; } return false; } private bool IsVisibleChartTraderPanel(FrameworkElement element) { if (element == null) return false; if (!element.IsVisible || element.Visibility != Visibility.Visible) return false; if (element.ActualWidth < 120 || element.ActualHeight < 160) return false; string signature = ChartTraderElementSignature(element); if (!(signature.Contains("CHARTTRADER") || signature.Contains("CHART TRADER"))) return false; return ContainsVisibleChartTraderOrderControls(element); } private bool ContainsVisibleChartTraderOrderControls(DependencyObject node) { return CountVisibleChartTraderOrderControls(node) >= 3; } private int CountVisibleChartTraderOrderControls(DependencyObject node) { if (node == null) return 0; int signals = 0; FrameworkElement element = node as FrameworkElement; if (element != null && element.IsVisible && element.Visibility == Visibility.Visible && element.ActualWidth > 8 && element.ActualHeight > 8) { string name = ChartTraderElementSignature(element); if (name.Contains("BUY MKT")) signals++; if (name.Contains("SELL MKT")) signals++; if (name.Contains("BUY ASK")) signals++; if (name.Contains("SELL ASK")) signals++; if (name.Contains("BUY BID")) signals++; if (name.Contains("SELL BID")) signals++; if (name.Contains("ORDER QTY")) signals++; if (name.Contains("ATM STRATEGY")) signals++; } int count = 0; try { count = VisualTreeHelper.GetChildrenCount(node); } catch { return signals; } for (int i = 0; i < count; i++) { DependencyObject child = null; try { child = VisualTreeHelper.GetChild(node, i); } catch { child = null; } if (child != null) signals += CountVisibleChartTraderOrderControls(child); if (signals >= 3) return signals; } return signals; } private string ChartTraderElementSignature(FrameworkElement element) { string text = ""; ContentControl contentControl = element as ContentControl; if (contentControl != null && contentControl.Content != null) text += " " + contentControl.Content.ToString(); TextBlock textBlock = element as TextBlock; if (textBlock != null) text += " " + textBlock.Text; return ((element.Name ?? "") + " " + element.GetType().FullName + text).ToUpperInvariant(); } private void ExecuteCommand(GobbleCommand cmd) { if (cmd.Action == "ENTER") { if (Position.MarketPosition != MarketPosition.Flat) throw new InvalidOperationException("position is not flat"); int qty = cmd.Qty > 0 ? cmd.Qty : Math.Max(1, DefaultQuantity); bool isShort = cmd.Side == "SHORT" || cmd.Side == "SELL"; currentSide = isShort ? "SHORT" : "LONG"; currentEntrySignal = isShort ? "GobbleShort" : "GobbleLong"; activeInitialStopDistancePoints = 0; activeInitialStopAnchoredToFill = false; if (cmd.StopPrice > 0) { activeStopPrice = cmd.StopPrice; if (cmd.SignalPrice > 0) activeInitialStopDistancePoints = Math.Abs(cmd.StopPrice - cmd.SignalPrice); SetStopLoss(currentEntrySignal, CalculationMode.Price, cmd.StopPrice, false); } if (cmd.EntryType == "LIMIT") { double limitPrice = ResolveEntryLimitPrice(cmd, isShort); if (limitPrice <= 0) throw new InvalidOperationException("LIMIT entry requires limitPrice or signalPrice/limitOffsetPoints"); activeEntrySubmittedAtUtc = DateTime.UtcNow; activeEntryLimitPrice = limitPrice; activeEntryLimitTtlSeconds = cmd.LimitTtlSeconds > 0 ? cmd.LimitTtlSeconds : 30; activeEntryChaseLimit = cmd.ChaseLimit; activeEntryIsShort = isShort; activeEntrySignalPrice = cmd.SignalPrice > 0 ? cmd.SignalPrice : limitPrice; activeEntryMaxChasePoints = cmd.MaxChasePoints > 0 ? cmd.MaxChasePoints : DefaultMaxChasePoints(ResolveAsset()); if (isShort) EnterShortLimit(qty, limitPrice, currentEntrySignal); else EnterLongLimit(qty, limitPrice, currentEntrySignal); } else if (isShort) EnterShort(qty, currentEntrySignal); else EnterLong(qty, currentEntrySignal); return; } if (cmd.Action == "MOVE_STOP" || cmd.Action == "TRAIL_STOP") { if (cmd.StopPrice <= 0) throw new InvalidOperationException("MOVE_STOP requires stopPrice"); string fromEntry = string.IsNullOrWhiteSpace(currentEntrySignal) ? (Position.MarketPosition == MarketPosition.Short ? "GobbleShort" : "GobbleLong") : currentEntrySignal; double safeStopPrice = ValidatedStopMovePrice(cmd.StopPrice); activeStopPrice = safeStopPrice; SetStopLoss(fromEntry, CalculationMode.Price, safeStopPrice, false); UpdateArmButton(); return; } if (cmd.Action == "EXIT") { int qty = cmd.Qty > 0 ? cmd.Qty : Math.Max(1, DefaultQuantity); if (Position.MarketPosition == MarketPosition.Long) ExitLong(qty, "GobbleExit", string.IsNullOrWhiteSpace(currentEntrySignal) ? "GobbleLong" : currentEntrySignal); else if (Position.MarketPosition == MarketPosition.Short) ExitShort(qty, "GobbleExit", string.IsNullOrWhiteSpace(currentEntrySignal) ? "GobbleShort" : currentEntrySignal); activeStopPrice = 0; activeInitialStopDistancePoints = 0; activeInitialStopAnchoredToFill = false; UpdateArmButton(); return; } if (cmd.Action == "SCALE_OUT") { int qty = cmd.Qty > 0 ? cmd.Qty : 1; if (Position.MarketPosition == MarketPosition.Long) ExitLong(qty, "GobbleScaleOut", string.IsNullOrWhiteSpace(currentEntrySignal) ? "GobbleLong" : currentEntrySignal); else if (Position.MarketPosition == MarketPosition.Short) ExitShort(qty, "GobbleScaleOut", string.IsNullOrWhiteSpace(currentEntrySignal) ? "GobbleShort" : currentEntrySignal); UpdateArmButton(); return; } throw new InvalidOperationException("unsupported action: " + cmd.Action); } private string EntryBlockReason(GobbleCommand cmd, string asset) { double maxAgeMs = cmd.MaxEntryAgeMs > 0 ? cmd.MaxEntryAgeMs : DefaultEntryAgeMs(asset); if (maxAgeMs > 0 && !string.IsNullOrWhiteSpace(cmd.ServerTime)) { DateTime serverTime; if (DateTime.TryParse(cmd.ServerTime, null, System.Globalization.DateTimeStyles.RoundtripKind, out serverTime)) { double ageMs = Math.Abs((DateTime.UtcNow - serverTime.ToUniversalTime()).TotalMilliseconds); if (ageMs > maxAgeMs) return "ENTRY_EXPIRED ageMs=" + Math.Round(ageMs) + " maxAgeMs=" + Math.Round(maxAgeMs); } } if (cmd.SignalPrice <= 0) return ""; bool isShort = cmd.Side == "SHORT" || cmd.Side == "SELL"; double current = EntryReferencePrice(isShort); if (current <= 0) return ""; double maxChase = cmd.MaxChasePoints > 0 ? cmd.MaxChasePoints : DefaultMaxChasePoints(asset); if (maxChase <= 0) return ""; double chase = isShort ? cmd.SignalPrice - current : current - cmd.SignalPrice; if (chase > maxChase) return "DO_NOT_CHASE side=" + (isShort ? "SHORT" : "LONG") + " signalPrice=" + JsonNumber(cmd.SignalPrice) + " currentPrice=" + JsonNumber(current) + " chasePoints=" + JsonNumber(chase) + " maxChasePoints=" + JsonNumber(maxChase); return ""; } private double EntryReferencePrice(bool isShort) { double price = 0; try { price = isShort ? GetCurrentBid() : GetCurrentAsk(); } catch { } if (price > 0) return price; try { price = Close[0]; } catch { } return price; } private double ResolveEntryLimitPrice(GobbleCommand cmd, bool isShort) { if (cmd.LimitPrice > 0) return cmd.LimitPrice; double reference = cmd.SignalPrice > 0 ? cmd.SignalPrice : EntryReferencePrice(isShort); if (reference <= 0) return 0; double offset = cmd.LimitOffsetPoints; return reference + offset; } private void CancelExpiredEntryLimit() { try { if (activeEntryOrder == null || activeEntryLimitTtlSeconds <= 0 || activeEntrySubmittedAtUtc == DateTime.MinValue) return; if (activeEntryOrder.OrderState == OrderState.Filled || activeEntryOrder.OrderState == OrderState.Cancelled || activeEntryOrder.OrderState == OrderState.Rejected) { activeEntryOrder = null; activeEntrySubmittedAtUtc = DateTime.MinValue; return; } if ((DateTime.UtcNow - activeEntrySubmittedAtUtc).TotalSeconds >= activeEntryLimitTtlSeconds) { CancelOrder(activeEntryOrder); _ = SendAckAsync(currentEntrySignal, "LIMIT_TTL_CANCEL", "entry limit TTL expired", null); activeEntryOrder = null; activeEntrySubmittedAtUtc = DateTime.MinValue; } } catch (Exception ex) { Print("[GobbleSim] limit TTL cancel failed: " + ex.Message); } } private void ChaseWorkingEntryLimit() { try { if (!activeEntryChaseLimit || activeEntryOrder == null || activeEntrySignalPrice <= 0 || activeEntryMaxChasePoints <= 0) return; if (activeEntryOrder.OrderState != OrderState.Working && activeEntryOrder.OrderState != OrderState.Accepted && activeEntryOrder.OrderState != OrderState.Submitted) return; double current = EntryReferencePrice(activeEntryIsShort); if (current <= 0) return; double allowed = activeEntryIsShort ? activeEntrySignalPrice - activeEntryMaxChasePoints : activeEntrySignalPrice + activeEntryMaxChasePoints; double desired = activeEntryIsShort ? Math.Max(current, allowed) : Math.Min(current, allowed); if (desired <= 0) return; bool improves = activeEntryIsShort ? desired < activeEntryLimitPrice : desired > activeEntryLimitPrice; if (!improves) return; ChangeOrder(activeEntryOrder, activeEntryOrder.Quantity, desired, 0); activeEntryLimitPrice = desired; _ = SendAckAsync(currentEntrySignal, "LIMIT_CHASE", "entry limit chased to " + JsonNumber(desired), null); } catch (Exception ex) { Print("[GobbleSim] limit chase failed: " + ex.Message); } } private static double DefaultEntryAgeMs(string asset) { return asset == "NQ" || asset == "MNQ" ? 2000 : asset == "ES" || asset == "MES" ? 2500 : 2500; } private static double DefaultMaxChasePoints(string asset) { return asset == "NQ" || asset == "MNQ" ? 5.0 : asset == "ES" || asset == "MES" ? 2.0 : asset == "RTY" ? 1.0 : 5.0; } private async Task SendAckAsync(string id, string status, string message, GobbleCommand cmd) { try { if (socket == null || socket.State != WebSocketState.Open) return; string json = "{" + "\"type\":\"GOBBLE_SIM_ACK\"," + "\"id\":\"" + Escape(id) + "\"," + "\"status\":\"" + Escape(status) + "\"," + "\"message\":\"" + Escape(message) + "\"," + "\"asset\":\"" + Escape(ResolveAsset()) + "\"," + "\"instrument\":\"" + Escape(ResolveInstrumentName()) + "\"," + "\"pointValue\":" + JsonNumber(ResolvePointValue()) + "," + "\"commandAsset\":\"" + Escape(cmd == null ? "" : cmd.Asset) + "\"," + "\"account\":\"" + Escape(Account == null ? "" : Account.Name) + "\"," + "\"strategy\":\"" + Escape(Name) + "\"," + "\"strategyVersion\":\"" + Escape(StrategyVersion) + "\"," + "\"bridgeFeature\":\"TOKEN_REQUIRED_VERSIONED_STATUS_V1\"," + "\"armed\":" + (IsBridgeArmed() ? "true" : "false") + "," + "\"marketPosition\":\"" + Escape(Position == null ? "" : Position.MarketPosition.ToString()) + "\"," + "\"positionQuantity\":" + (Position == null ? 0 : Position.Quantity) + "," + "\"positionAveragePrice\":" + JsonNumber(Position == null ? 0 : Position.AveragePrice) + "," + "\"time\":\"" + DateTime.UtcNow.ToString("o") + "\"" + "}"; await SendRawAsync(json); } catch { } } protected override void OnOrderUpdate(Order order, double limitPrice, double stopPrice, int quantity, int filled, double averageFillPrice, OrderState orderState, DateTime time, ErrorCode error, string nativeError) { try { string json = "{" + "\"type\":\"GOBBLE_SIM_ORDER\"," + "\"orderId\":\"" + Escape(order == null ? "" : order.OrderId) + "\"," + "\"name\":\"" + Escape(order == null ? "" : order.Name) + "\"," + "\"fromEntrySignal\":\"" + Escape(order == null ? "" : order.FromEntrySignal) + "\"," + "\"orderState\":\"" + Escape(orderState.ToString()) + "\"," + "\"orderAction\":\"" + Escape(order == null ? "" : order.OrderAction.ToString()) + "\"," + "\"orderType\":\"" + Escape(order == null ? "" : order.OrderType.ToString()) + "\"," + "\"quantity\":" + quantity + "," + "\"filled\":" + filled + "," + "\"limitPrice\":" + JsonNumber(limitPrice) + "," + "\"stopPrice\":" + JsonNumber(stopPrice) + "," + "\"averageFillPrice\":" + JsonNumber(averageFillPrice) + "," + "\"asset\":\"" + Escape(ResolveAsset()) + "\"," + "\"instrument\":\"" + Escape(ResolveInstrumentName()) + "\"," + "\"pointValue\":" + JsonNumber(ResolvePointValue()) + "," + "\"account\":\"" + Escape(Account == null ? "" : Account.Name) + "\"," + "\"error\":\"" + Escape(error.ToString()) + "\"," + "\"nativeError\":\"" + Escape(nativeError) + "\"," + "\"time\":\"" + time.ToUniversalTime().ToString("o") + "\"" + "}"; _ = SendRawAsync(json); if (order != null && (order.Name == "GobbleLong" || order.Name == "GobbleShort") && order.OrderType == OrderType.Limit) { if (orderState == OrderState.Working || orderState == OrderState.Accepted || orderState == OrderState.Submitted) { activeEntryOrder = order; if (activeEntrySubmittedAtUtc == DateTime.MinValue) activeEntrySubmittedAtUtc = DateTime.UtcNow; } else if (orderState == OrderState.Filled || orderState == OrderState.Cancelled || orderState == OrderState.Rejected) { activeEntryOrder = null; activeEntrySubmittedAtUtc = DateTime.MinValue; activeEntryLimitPrice = 0; activeEntryLimitTtlSeconds = 0; activeEntryChaseLimit = false; activeEntryIsShort = false; activeEntrySignalPrice = 0; activeEntryMaxChasePoints = 0; } } if (order != null && order.OrderType == OrderType.StopMarket && stopPrice > 0) activeStopPrice = stopPrice; ReportPosition("ON_ORDER_UPDATE_" + orderState.ToString(), true); } catch { } } protected override void OnExecutionUpdate(Execution execution, string executionId, double price, int quantity, MarketPosition marketPosition, string orderId, DateTime time) { try { string json = "{" + "\"type\":\"GOBBLE_SIM_EXECUTION\"," + "\"executionId\":\"" + Escape(executionId) + "\"," + "\"orderId\":\"" + Escape(orderId) + "\"," + "\"name\":\"" + Escape(execution?.Order == null ? "" : execution.Order.Name) + "\"," + "\"fromEntrySignal\":\"" + Escape(execution?.Order == null ? "" : execution.Order.FromEntrySignal) + "\"," + "\"orderAction\":\"" + Escape(execution?.Order == null ? "" : execution.Order.OrderAction.ToString()) + "\"," + "\"price\":" + JsonNumber(price) + "," + "\"quantity\":" + quantity + "," + "\"marketPosition\":\"" + Escape(marketPosition.ToString()) + "\"," + "\"asset\":\"" + Escape(ResolveAsset()) + "\"," + "\"instrument\":\"" + Escape(ResolveInstrumentName()) + "\"," + "\"pointValue\":" + JsonNumber(ResolvePointValue()) + "," + "\"account\":\"" + Escape(Account == null ? "" : Account.Name) + "\"," + "\"time\":\"" + time.ToUniversalTime().ToString("o") + "\"" + "}"; _ = SendRawAsync(json); string orderName = execution?.Order == null ? "" : execution.Order.Name; if (!activeInitialStopAnchoredToFill && activeInitialStopDistancePoints > 0 && price > 0 && (orderName == "GobbleLong" || orderName == "GobbleShort")) { double fillPrice = Position != null && Position.AveragePrice > 0 ? Position.AveragePrice : price; bool isShortEntry = orderName == "GobbleShort"; double anchoredStop = isShortEntry ? fillPrice + activeInitialStopDistancePoints : fillPrice - activeInitialStopDistancePoints; anchoredStop = RoundToNinjaTick(anchoredStop); string fromEntry = string.IsNullOrWhiteSpace(currentEntrySignal) ? orderName : currentEntrySignal; activeStopPrice = anchoredStop; activeInitialStopAnchoredToFill = true; SetStopLoss(fromEntry, CalculationMode.Price, anchoredStop, false); } ReportPosition("ON_EXECUTION_UPDATE", true); } catch { } } private void ReportPosition(string reason, bool force) { try { if (Position == null) return; if (!force && Position.MarketPosition == lastReportedMarketPosition && Position.Quantity == lastReportedQuantity) return; lastReportedMarketPosition = Position.MarketPosition; lastReportedQuantity = Position.Quantity; if (Position.MarketPosition == MarketPosition.Flat) { activeStopPrice = 0; activeInitialStopDistancePoints = 0; activeInitialStopAnchoredToFill = false; } string json = "{" + "\"type\":\"GOBBLE_SIM_POSITION\"," + "\"reason\":\"" + Escape(reason) + "\"," + "\"marketPosition\":\"" + Escape(Position.MarketPosition.ToString()) + "\"," + "\"quantity\":" + Position.Quantity + "," + "\"averagePrice\":" + JsonNumber(Position.AveragePrice) + "," + "\"currentPrice\":" + JsonNumber(CurrentNinjaMarketPrice()) + "," + "\"asset\":\"" + Escape(ResolveAsset()) + "\"," + "\"instrument\":\"" + Escape(ResolveInstrumentName()) + "\"," + "\"pointValue\":" + JsonNumber(ResolvePointValue()) + "," + "\"account\":\"" + Escape(Account == null ? "" : Account.Name) + "\"," + "\"time\":\"" + DateTime.UtcNow.ToString("o") + "\"" + "}"; _ = SendRawAsync(json); UpdateArmButton(); } catch { } } private async Task SendRawAsync(string json) { try { await sendLock.WaitAsync(); if (socket == null || socket.State != WebSocketState.Open) return; byte[] bytes = Encoding.UTF8.GetBytes(json); await socket.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, CancellationToken.None); } catch { } finally { try { sendLock.Release(); } catch { } } } private string ResolveAsset() { string fromInstrument = CleanAsset( (Instrument == null ? "" : Instrument.FullName) + " " + (Instrument == null || Instrument.MasterInstrument == null ? "" : Instrument.MasterInstrument.Name) ); if (!string.IsNullOrWhiteSpace(fromInstrument)) return fromInstrument; return CleanAsset(ExpectedAsset); } private string ResolveInstrumentName() { try { if (Instrument != null && !string.IsNullOrWhiteSpace(Instrument.FullName)) return Instrument.FullName; if (Instrument != null && Instrument.MasterInstrument != null) return Instrument.MasterInstrument.Name; } catch { } return ""; } private double ResolvePointValue() { try { if (Instrument != null && Instrument.MasterInstrument != null) return Instrument.MasterInstrument.PointValue; } catch { } return 0; } private static string CleanAsset(string value) { string raw = (value ?? "").ToUpperInvariant(); if (raw.Contains("MNQ")) return "MNQ"; if (raw.Contains("MES")) return "MES"; if (raw.Contains("NQ")) return "NQ"; if (raw.Contains("ES")) return "ES"; if (raw.Contains("M2K") || raw.Contains("MRTY") || raw.Contains("RTY")) return "RTY"; if (raw.Contains("MYM") || raw.Contains("YM")) return "YM"; return ""; } private static bool IsSupportedAsset(string asset) { return string.Equals(asset, "ES", StringComparison.OrdinalIgnoreCase) || string.Equals(asset, "MES", StringComparison.OrdinalIgnoreCase) || string.Equals(asset, "MNQ", StringComparison.OrdinalIgnoreCase) || string.Equals(asset, "NQ", StringComparison.OrdinalIgnoreCase); } private static string Escape(string value) { return (value ?? "").Replace("\\", "\\\\").Replace("\"", "\\\""); } private static string JsonNumber(double value) { if (double.IsNaN(value) || double.IsInfinity(value)) return "0"; return value.ToString(System.Globalization.CultureInfo.InvariantCulture); } private static string JsonString(string json, string key) { var match = Regex.Match(json ?? "", "\"" + Regex.Escape(key) + "\"\\s*:\\s*\"([^\"]*)\"", RegexOptions.IgnoreCase); return match.Success ? match.Groups[1].Value : ""; } private static double JsonDouble(string json, string key) { var match = Regex.Match(json ?? "", "\"" + Regex.Escape(key) + "\"\\s*:\\s*(-?\\d+(?:\\.\\d+)?)", RegexOptions.IgnoreCase); double value; return match.Success && double.TryParse(match.Groups[1].Value, out value) ? value : 0; } private static bool JsonBool(string json, string key) { var match = Regex.Match(json ?? "", "\"" + Regex.Escape(key) + "\"\\s*:\\s*(true|false)", RegexOptions.IgnoreCase); return match.Success && string.Equals(match.Groups[1].Value, "true", StringComparison.OrdinalIgnoreCase); } private class GobbleCommand { public string Id; public string Asset; public string Action; public string Side; public string EntryType; public int Qty; public double StopPrice; public double LimitPrice; public double LimitOffsetPoints; public int LimitTtlSeconds; public bool ChaseLimit; public double SignalPrice; public double MaxChasePoints; public double MaxEntryAgeMs; public string ServerTime; public bool DryRun; public static GobbleCommand FromJson(string json) { return new GobbleCommand { Id = JsonString(json, "id"), Asset = JsonString(json, "asset"), Action = JsonString(json, "action").ToUpperInvariant(), Side = JsonString(json, "side").ToUpperInvariant(), EntryType = JsonString(json, "entryType").ToUpperInvariant() == "LIMIT" ? "LIMIT" : "MARKET", Qty = (int)Math.Max(0, JsonDouble(json, "qty")), StopPrice = JsonDouble(json, "stopPrice"), LimitPrice = JsonDouble(json, "limitPrice"), LimitOffsetPoints = JsonDouble(json, "limitOffsetPoints"), LimitTtlSeconds = (int)Math.Max(0, JsonDouble(json, "limitTtlSeconds")), ChaseLimit = JsonBool(json, "chaseLimit"), SignalPrice = JsonDouble(json, "signalPrice"), MaxChasePoints = JsonDouble(json, "maxChasePoints"), MaxEntryAgeMs = JsonDouble(json, "maxEntryAgeMs"), ServerTime = JsonString(json, "serverTime"), DryRun = JsonBool(json, "dryRun") }; } } } }