From 1a5692611fde69cba0f4ab2021f2e32d5db2cab8 Mon Sep 17 00:00:00 2001 From: mkalhitti-cloud Date: Wed, 4 Mar 2026 20:44:41 -0800 Subject: [PATCH 1/4] Build 950: OCO Cascade Fix -- Resilient Bracket Replacement FSM - Add TargetSnapshot class for bracket state capture - Extend PendingStopReplacement with CapturedTargets - Fix follower stop-cancel black hole in HandleMatchedFollowerOrder - Add RestoreCascadedTargets via TriggerCustomEvent - Fix CreateNewStopOrder to re-link OcoGroupId - Add bracket restore to V8.30 timeout fallback path Co-Authored-By: Claude Sonnet 4.6 --- docs/brain/walkthrough.md | 43 ++++++++++++++ src/V12_002.Orders.Callbacks.cs | 38 +++++++++++++ src/V12_002.Orders.Management.cs | 96 +++++++++++++++++++++++++++++++- src/V12_002.Trailing.cs | 52 +++++++++++++++++ src/V12_002.cs | 14 ++++- 5 files changed, 240 insertions(+), 3 deletions(-) diff --git a/docs/brain/walkthrough.md b/docs/brain/walkthrough.md index ec0d5c1f..7267e471 100644 --- a/docs/brain/walkthrough.md +++ b/docs/brain/walkthrough.md @@ -45,3 +45,46 @@ The 1,806-line `Entries.cs` has been surgically partitioned into 6 mode-specific 1. **Compile**: Press F5 in NinjaTrader to verify Phase 7 partial class structure compiles. 2. **Live Deployment**: Deploy to a single PA account for "Live Smoke Test." 3. **Performance Audit**: Begin tracking P/L symmetry across the 20-account fleet. + +## Build 950: OCO Cascade Fix -- Resilient Bracket Replacement FSM + +### Problem +When UpdateStopOrder cancelled a follower stop for BE/trail replacement, broker-native OCO +(OcoGroupId shared across stop + all targets) auto-cancelled T1/T2/T3. Simultaneously, +follower stop-cancel events arrive via OnAccountOrderUpdate -- which only checked +_followerReplaceSpecs (entry FSM), NOT pendingStopReplacements. Result: no new stop for +followers, no targets either. V8.30 5-second timeout eventually fired an emergency stop but +with no OCO group and no targets -- naked bracket. + +### Fix: Two-Part Resilient Bracket Replacement FSM + +**Part 1 -- Follower stop black hole (HandleMatchedFollowerOrder):** +Added pendingStopReplacements lookup in HandleMatchedFollowerOrder (Orders.Callbacks.cs). +When a follower stop cancel matches OldOrder, CreateNewStopOrder is called immediately -- +same logic as HandleOrderCancelled does for master accounts. + +**Part 2 -- OCO cascade target restoration (RestoreCascadedTargets):** +Extended PendingStopReplacement with CapturedTargets[] (TargetSnapshot array: TargetNum, +Price, Qty, Order ref). Populated in UpdateStopOrder before cancel is issued. +After new stop is created (on any path: normal callback, follower callback, V8.30 timeout), +RestoreCascadedTargets() is scheduled via TriggerCustomEvent. It checks each captured +Order.OrderState -- if Cancelled, the target was OCO-cascade-killed and is re-submitted +with the same OcoGroupId and same price/qty. + +**Part 3 -- CreateNewStopOrder OcoGroupId fix:** +New stop now includes pos.OcoGroupId so it re-enters the broker OCO bracket. Restored +targets also use OcoGroupId -- full bracket linkage is restored. + +### Files Changed +- src/V12_002.cs -- TargetSnapshot class, PendingStopReplacement extended, BUILD_TAG = "950" +- src/V12_002.Trailing.cs -- target snapshot in UpdateStopOrder, restore in V8.30 timeout +- src/V12_002.Orders.Callbacks.cs -- follower stop handler + master bracket restore +- src/V12_002.Orders.Management.cs -- RestoreCascadedTargets(), CreateNewStopOrder OcoId fix + +### Verification +1. Sim session: Enter 4-contract position, verify bracket (Stop + T1/T2/T3) all Working +2. Send BE_CUSTOM via IPC -- confirm logs show "[B950] Target T1 restored", "[B950] Target T2 restored" +3. Confirm new stop in stopOrders[entryName], new targets in target1Orders/target2Orders +4. REAPER must NOT fire emergency stop (no naked position) +5. Let T1 fill -- confirm stop reduces to 3 contracts, T2/T3 still Working +6. Let stop fill -- confirm remaining targets cancelled by existing manual OCO loop diff --git a/src/V12_002.Orders.Callbacks.cs b/src/V12_002.Orders.Callbacks.cs index 29525d6e..d76699ff 100644 --- a/src/V12_002.Orders.Callbacks.cs +++ b/src/V12_002.Orders.Callbacks.cs @@ -370,7 +370,16 @@ private bool HandleOrderCancelled(Order order) if (kvp.Value.OldOrder == order && activePositions.TryGetValue(kvp.Key, out var pos)) { if (pos.RemainingContracts > 0) + { CreateNewStopOrder(kvp.Key, pos.RemainingContracts, kvp.Value.StopPrice, kvp.Value.Direction); + // Build 950: Restore OCO-cascade-cancelled targets after stop replacement. + if (kvp.Value.BracketRestorationNeeded && kvp.Value.CapturedTargets != null) + { + TargetSnapshot[] _mSnap = kvp.Value.CapturedTargets; + string _mKey = kvp.Key; + TriggerCustomEvent(o => RestoreCascadedTargets(_mKey, _mSnap), null); + } + } if (pendingStopReplacements.TryRemove(kvp.Key, out _)) Interlocked.Decrement(ref pendingReplacementCount); handled = true; break; @@ -578,6 +587,35 @@ private void HandleMatchedFollowerOrder(string matchedEntry, PositionInfo matche } else { + // Build 950: Follower stop replacement -- mirrors HandleOrderCancelled master path. + // Follower stop cancels arrive via OnAccountOrderUpdate (not OnOrderUpdate), so + // HandleOrderCancelled never fires for them. Match pendingStopReplacements here. + // This block is in the else branch because stop orders are not in entryOrders. + if (order.Name.StartsWith("Stop_") || order.Name.StartsWith("S_")) + { + foreach (var _psr in pendingStopReplacements.ToArray()) + { + if (_psr.Value.OldOrder == order) + { + PositionInfo _rPos; + if (activePositions.TryGetValue(_psr.Key, out _rPos) && _rPos.RemainingContracts > 0) + { + int _rQty; + lock (stateLock) { _rQty = _rPos.RemainingContracts; } + CreateNewStopOrder(_psr.Key, _rQty, _psr.Value.StopPrice, _psr.Value.Direction); + if (_psr.Value.BracketRestorationNeeded && _psr.Value.CapturedTargets != null) + { + TargetSnapshot[] _snap = _psr.Value.CapturedTargets; + string _rKey = _psr.Key; + TriggerCustomEvent(o => RestoreCascadedTargets(_rKey, _snap), null); + } + } + if (pendingStopReplacements.TryRemove(_psr.Key, out _)) + Interlocked.Decrement(ref pendingReplacementCount); + return; + } + } + } Print(string.Format("[SIMA] Follower order terminal: {0} on {1} ({2}) | Id={3}", order.Name, acctName, reason, order.OrderId)); RemoveGhostOrderRef(order, reason); } diff --git a/src/V12_002.Orders.Management.cs b/src/V12_002.Orders.Management.cs index 79f4bf55..249b4fd3 100644 --- a/src/V12_002.Orders.Management.cs +++ b/src/V12_002.Orders.Management.cs @@ -519,20 +519,26 @@ private void CreateNewStopOrder(string entryName, int quantity, double stopPrice // V12.3: Route to correct account (fleet follower vs local) if (activePositions.TryGetValue(entryName, out var pos) && pos.IsFollower && pos.ExecutingAccount != null) { + // Build 950: Re-link replacement stop to broker OCO bracket. + string _b950OcoId; + lock (stateLock) { _b950OcoId = pos.OcoGroupId ?? string.Empty; } // Fleet follower: use Account API string sigName = "S_" + entryName; if (sigName.Length > 50) sigName = sigName.Substring(0, 50); newStop = pos.ExecutingAccount.CreateOrder(Instrument, exitAction, - OrderType.StopMarket, TimeInForce.Gtc, quantity, 0, stopPrice, sigName, sigName, null); + OrderType.StopMarket, TimeInForce.Gtc, quantity, 0, stopPrice, _b950OcoId, sigName, null); pos.ExecutingAccount.Submit(new[] { newStop }); } else { + // Build 950: Re-link replacement stop to broker OCO bracket. + string _b950OcoId; + lock (stateLock) { _b950OcoId = pos != null ? (pos.OcoGroupId ?? string.Empty) : string.Empty; } // Local: use SubmitOrderUnmanaged with truncated signal name string suffix = (DateTime.Now.Ticks % 100000000).ToString(); string sigName = "S_" + entryName + "_" + suffix; if (sigName.Length > 50) sigName = sigName.Substring(0, 50); - newStop = SubmitOrderUnmanaged(0, exitAction, OrderType.StopMarket, quantity, 0, stopPrice, "", sigName); + newStop = SubmitOrderUnmanaged(0, exitAction, OrderType.StopMarket, quantity, 0, stopPrice, _b950OcoId, sigName); } if (newStop == null) @@ -568,6 +574,92 @@ private void CreateNewStopOrder(string entryName, int quantity, double stopPrice } } + // Build 950: Re-submit profit targets that were OCO-cascade-cancelled during stop replacement. + // Runs on strategy thread via TriggerCustomEvent. Checks Order.OrderState directly on the + // captured Order object -- avoids dict-timing races with RemoveGhostOrderRef. + private void RestoreCascadedTargets(string entryName, TargetSnapshot[] capturedTargets) + { + if (capturedTargets == null || capturedTargets.Length == 0) return; + + PositionInfo pos; + if (!activePositions.TryGetValue(entryName, out pos)) return; + + bool entryFilled; + int remainingContracts; + MarketPosition direction; + bool isFollower; + Account executingAccount; + string ocoGroupId; + + lock (stateLock) + { + entryFilled = pos.EntryFilled; + remainingContracts = pos.RemainingContracts; + direction = pos.Direction; + isFollower = pos.IsFollower; + executingAccount = pos.ExecutingAccount; + ocoGroupId = pos.OcoGroupId; + } + + if (!entryFilled || remainingContracts <= 0) return; + + OrderAction exitAction = direction == MarketPosition.Long + ? OrderAction.Sell : OrderAction.BuyToCover; + string bracketOcoId = ocoGroupId ?? string.Empty; + + foreach (TargetSnapshot snap in capturedTargets) + { + if (snap == null || snap.CapturedOrder == null) continue; + + // Only restore targets the broker OCO cascade-cancelled. + // Filled targets have OrderState.Filled -- skip them. + if (snap.CapturedOrder.OrderState != OrderState.Cancelled + && snap.CapturedOrder.OrderState != OrderState.Rejected) + continue; + + double restoredPrice = Instrument.MasterInstrument.RoundToTickSize(snap.Price); + Order newTarget = null; + + if (isFollower && executingAccount != null) + { + string tSig = SymmetryTrim("T" + snap.TargetNum + "_" + entryName, 40); + Order tOrd = executingAccount.CreateOrder( + Instrument, exitAction, OrderType.Limit, TimeInForce.Gtc, + snap.Qty, restoredPrice, 0, bracketOcoId, tSig, null); + if (tOrd != null) + { + executingAccount.Submit(new[] { tOrd }); + newTarget = tOrd; + } + } + else + { + string tSig = "T" + snap.TargetNum + "_" + entryName; + newTarget = direction == MarketPosition.Long + ? SubmitOrderUnmanaged(0, OrderAction.Sell, OrderType.Limit, + snap.Qty, restoredPrice, 0, bracketOcoId, tSig) + : SubmitOrderUnmanaged(0, OrderAction.BuyToCover, OrderType.Limit, + snap.Qty, restoredPrice, 0, bracketOcoId, tSig); + } + + var tDict = GetTargetOrdersDictionary(snap.TargetNum); + if (tDict != null) + { + if (newTarget != null) + { + tDict[entryName] = newTarget; + Print(string.Format("[B950] Target T{0} restored for {1} @ {2:F2} qty={3}", + snap.TargetNum, entryName, restoredPrice, snap.Qty)); + } + else + { + Print(string.Format("[B950] WARN: Target T{0} restore NULL for {1}", + snap.TargetNum, entryName)); + } + } + } + } + private double ValidateStopPrice(MarketPosition direction, double desiredStopPrice, int level = 0, double entryPrice = 0) { // V12.41: Use real-time price instead of stale bar Close[0] diff --git a/src/V12_002.Trailing.cs b/src/V12_002.Trailing.cs index 671cff46..c539b696 100644 --- a/src/V12_002.Trailing.cs +++ b/src/V12_002.Trailing.cs @@ -471,6 +471,13 @@ private void CleanupStalePendingReplacements() replacementQty = pos.RemainingContracts; } CreateNewStopOrder(kvp.Key, replacementQty, pending.StopPrice, pending.Direction); + // Build 950: Also restore bracket targets after V8.30 emergency stop. + if (pending.BracketRestorationNeeded && pending.CapturedTargets != null) + { + TargetSnapshot[] _tSnap = pending.CapturedTargets; + string _tKey = kvp.Key; + TriggerCustomEvent(o => RestoreCascadedTargets(_tKey, _tSnap), null); + } } } } @@ -534,6 +541,36 @@ private void UpdateStopOrder(string entryName, PositionInfo pos, double newStopP { // Just update the pending price pending.StopPrice = validatedStopPrice; + // Build 950: Refresh CapturedTargets on the live pending record if not yet populated. + if (!pending.BracketRestorationNeeded) + { + var _b950Refresh = new System.Collections.Generic.List(); + for (int _t2 = 1; _t2 <= 5; _t2++) + { + var _tD2 = GetTargetOrdersDictionary(_t2); + Order _tO2; + if (_tD2 != null && _tD2.TryGetValue(entryName, out _tO2) && _tO2 != null + && (_tO2.OrderState == OrderState.Working || _tO2.OrderState == OrderState.Accepted)) + _b950Refresh.Add(new TargetSnapshot { TargetNum = _t2, Price = _tO2.LimitPrice, Qty = _tO2.Quantity, CapturedOrder = _tO2 }); + } + pending.CapturedTargets = _b950Refresh.Count > 0 ? _b950Refresh.ToArray() : null; + pending.BracketRestorationNeeded = _b950Refresh.Count > 0; + } + } + + // Build 950: Snapshot Working/Accepted targets before cancel for OCO cascade restoration. + { + var _b950Targets = new System.Collections.Generic.List(); + for (int _t = 1; _t <= 5; _t++) + { + var _tD = GetTargetOrdersDictionary(_t); + Order _tO; + if (_tD != null && _tD.TryGetValue(entryName, out _tO) && _tO != null + && (_tO.OrderState == OrderState.Working || _tO.OrderState == OrderState.Accepted)) + _b950Targets.Add(new TargetSnapshot { TargetNum = _t, Price = _tO.LimitPrice, Qty = _tO.Quantity, CapturedOrder = _tO }); + } + newPending.CapturedTargets = _b950Targets.Count > 0 ? _b950Targets.ToArray() : null; + newPending.BracketRestorationNeeded = _b950Targets.Count > 0; } pos.CurrentStopPrice = validatedStopPrice; @@ -566,6 +603,21 @@ private void UpdateStopOrder(string entryName, PositionInfo pos, double newStopP } } + // Build 950: Snapshot Working/Accepted targets before cancel for OCO cascade restoration. + { + var _b950Targets = new System.Collections.Generic.List(); + for (int _t = 1; _t <= 5; _t++) + { + var _tD = GetTargetOrdersDictionary(_t); + Order _tO; + if (_tD != null && _tD.TryGetValue(entryName, out _tO) && _tO != null + && (_tO.OrderState == OrderState.Working || _tO.OrderState == OrderState.Accepted)) + _b950Targets.Add(new TargetSnapshot { TargetNum = _t, Price = _tO.LimitPrice, Qty = _tO.Quantity, CapturedOrder = _tO }); + } + newPending.CapturedTargets = _b950Targets.Count > 0 ? _b950Targets.ToArray() : null; + newPending.BracketRestorationNeeded = _b950Targets.Count > 0; + } + if (pos.ExecutingAccount != null) { pos.ExecutingAccount.Cancel(new[] { currentStop }); diff --git a/src/V12_002.cs b/src/V12_002.cs index b6ab9b8b..f78fdc7c 100644 --- a/src/V12_002.cs +++ b/src/V12_002.cs @@ -41,7 +41,7 @@ namespace NinjaTrader.NinjaScript.Strategies { public partial class V12_002 : Strategy { - public const string BUILD_TAG = "948"; // V12.948: Freeze Prevention (GTC Sweep + Order Adoption + Burst Cap) + public const string BUILD_TAG = "950"; // V12.950: OCO Cascade Fix (bracket restore on V8.30 timeout path) #region Variables @@ -279,6 +279,15 @@ private readonly ConcurrentDictionary private readonly ConcurrentDictionary _citNudgedKeys = new ConcurrentDictionary(); + // Build 950: Target snapshot for OCO cascade detection during stop replacement. + private class TargetSnapshot + { + public int TargetNum; // 1-5 + public double Price; // LimitPrice at snapshot time + public int Qty; // Quantity at snapshot time + public Order CapturedOrder; // Order ref -- check .OrderState for cascade detection + } + #endregion #region Position Info Class @@ -560,6 +569,9 @@ private class PendingStopReplacement public MarketPosition Direction; public Order OldOrder; // Track the old order being cancelled public DateTime CreatedTime; // V8.30: Timeout support - clean up stale replacements + // Build 950: Bracket restoration -- populated before stop cancel is sent. + public TargetSnapshot[] CapturedTargets; // null if no Working targets at cancel time + public bool BracketRestorationNeeded; // true when CapturedTargets is non-null } // V8.22: Thread-Safe UI Snapshot Struct From 7fc6b4ba922005f87eac522b3718d943c353c719 Mon Sep 17 00:00:00 2001 From: "AI M. Khalid" Date: Fri, 6 Mar 2026 13:09:45 -0800 Subject: [PATCH 2/4] fix(954): IPC ghost conn stub, RETEST null state leak, deprecated mode fallback Fix 1 (V12_001.cs): Stub ConnectToStrategy() with early return -- strategy no longer hosts IPC server after Phase 6 pruning. Panel runs in standalone UI mode; SendCommand calls safely no-op via tcpStream null guard. Fix 2 (V12_002.Entries.Retest.cs): Insert return; inside null-order guard after SubmitOrderUnmanaged fails. Prevents retestFiredThisSession latch and SIMA dispatch from arming on a failed order submission. Fix 3 (V12_001.cs): Add else branch in LoadConfig() to normalize both activeMode and selectedConfigMode to RMA when a deprecated/pruned mode is found in saved config. Prevents GetSettings() from loading garbage data from a deleted slot. Build tag incremented to 954. Co-Authored-By: Claude Sonnet 4.6 --- src/V12_001.cs | 17 ++++++++++++++++- src/V12_002.Entries.Retest.cs | 1 + src/V12_002.cs | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/V12_001.cs b/src/V12_001.cs index 3fb09247..2fb036c7 100644 --- a/src/V12_001.cs +++ b/src/V12_001.cs @@ -785,7 +785,19 @@ private void LoadConfig() selectedTargetCount = activeCount; Button modeBtn = GetModeButton(activeMode); - if (modeBtn != null) HighlightModeButton(modeBtn); + if (modeBtn != null) + { + HighlightModeButton(modeBtn); + } + else + { + // [Build 954]: Saved mode is deprecated/unrecognized -- normalize both vars to RMA baseline. + Print("[WARN][954] Unrecognized saved mode '" + activeMode + "' -- falling back to RMA."); + activeMode = "RMA"; + selectedConfigMode = "RMA"; + modeBtn = GetModeButton("RMA"); + if (modeBtn != null) HighlightModeButton(modeBtn); + } // Apply active mode+count settings to UI ApplySettings(fullConfig.GetSettings(activeMode, activeCount)); @@ -2986,6 +2998,9 @@ private void SyncAll_Click(object sender, RoutedEventArgs e) private void ConnectToStrategy() { + // [Build 954]: IPC deprecated -- strategy no longer hosts IPC server (Phase 6 pruning). + // Panel operates in standalone UI mode. All SendCommand calls are safely no-oped via tcpStream null guard. + return; try { lock (tcpLock) diff --git a/src/V12_002.Entries.Retest.cs b/src/V12_002.Entries.Retest.cs index e14f9d31..2d4e6646 100644 --- a/src/V12_002.Entries.Retest.cs +++ b/src/V12_002.Entries.Retest.cs @@ -186,6 +186,7 @@ private void ExecuteRetestEntry(int contracts) { AddExpectedPositionDeltaLocked(ExpKey(Account.Name), -masterDeltaRetest); Print("[ERROR][1102Y-V3] RETEST SubmitOrderUnmanaged NULL for " + entryName + " -- rolled back."); + return; // [Build 954]: Do not latch session or dispatch SIMA for a failed order. } entryOrders[entryName] = entryOrder; diff --git a/src/V12_002.cs b/src/V12_002.cs index f78fdc7c..e9ecbd84 100644 --- a/src/V12_002.cs +++ b/src/V12_002.cs @@ -41,7 +41,7 @@ namespace NinjaTrader.NinjaScript.Strategies { public partial class V12_002 : Strategy { - public const string BUILD_TAG = "950"; // V12.950: OCO Cascade Fix (bracket restore on V8.30 timeout path) + public const string BUILD_TAG = "954"; // V12.954: Bot Audit Fixes (IPC ghost conn, RETEST null state, deprecated mode fallback) #region Variables From 2c337a732fcd474125f332fe643bd9cb4f968b4b Mon Sep 17 00:00:00 2001 From: "AI M. Khalid" Date: Fri, 6 Mar 2026 14:18:46 -0800 Subject: [PATCH 3/4] fix(955): Race condition & TOCTOU remediation -- PR #24 audit fixes FINDING 1 (CRITICAL) -- V12_002.Trailing.cs: - Snapshot CapturedTargets/BracketRestorationNeeded BEFORE pendingStopReplacements.TryAdd in both TryAdd paths (CancelPending/Submitted and Working/Accepted state handlers). - Eliminated race window where stop-cancel callback could read a partially-constructed PendingStopReplacement with BracketRestorationNeeded=false and skip RestoreCascadedTargets. - Removed now-redundant post-TryAdd snapshot blocks (were dead/duplicate after fix). FINDING 2 (HIGH) -- V12_002.Orders.Callbacks.cs: - HandleOrderCancelled (master path): snapshot RemainingContracts under stateLock once; use same snapshot for guard check and CreateNewStopOrder call. Eliminates 0-qty stop risk. - OnAccountOrderUpdate (follower path): move RemainingContracts guard inside stateLock so check and use are atomic. Eliminates TOCTOU on follower stop replacement qty. FINDING 3 (LOW) -- V12_001.cs: - Remove unreachable try/catch body after intentional return stub in ConnectToStrategy(). SendCommand() safely no-ops via tcpStream null guard -- no behavior change. Bump BUILD_TAG -> 955. ASCII gate: PASS. Co-Authored-By: Claude Sonnet 4.6 --- src/V12_001.cs | 66 +--------------------------- src/V12_002.Orders.Callbacks.cs | 27 +++++++----- src/V12_002.Trailing.cs | 78 +++++++++++++++------------------ src/V12_002.cs | 2 +- 4 files changed, 56 insertions(+), 117 deletions(-) diff --git a/src/V12_001.cs b/src/V12_001.cs index 2fb036c7..60402735 100644 --- a/src/V12_001.cs +++ b/src/V12_001.cs @@ -2998,70 +2998,8 @@ private void SyncAll_Click(object sender, RoutedEventArgs e) private void ConnectToStrategy() { - // [Build 954]: IPC deprecated -- strategy no longer hosts IPC server (Phase 6 pruning). - // Panel operates in standalone UI mode. All SendCommand calls are safely no-oped via tcpStream null guard. - return; - try - { - lock (tcpLock) - { - if (isConnected) return; - - tcpClient = new TcpClient(); - tcpClient.Connect("127.0.0.1", IpcPort); - tcpStream = tcpClient.GetStream(); - isConnected = true; - // [Build 934]: Reset retry counters on successful connect - _ipcRetryCount = 0; - _lastRetryLogTime = DateTime.MinValue; - - Print($"V12 Panel: Strategy connected on port {IpcPort} ?"); - - if (ChartControl != null) - { - ChartControl.Dispatcher.BeginInvoke(new Action(() => - { - if (hubStatusLed != null) - { - hubStatusLed.Background = GreenFg; - hubStatusLed.ToolTip = "IPC Connected"; - } - })); - } - - receiveThread = new Thread(ReceiveLoop) { IsBackground = true, Name = "V12_Std_Receive" }; - receiveThread.Start(); - - SendCommand("GET_LAYOUT"); - } - } - catch (Exception) - { - isConnected = false; - _ipcRetryCount++; - - // [Build 934]: Log only on first failure and then at most once per 60 seconds - if (_ipcRetryCount == 1 || (DateTime.Now - _lastRetryLogTime).TotalSeconds >= 60) - { - Print($"V12 Panel: Strategy offline -- retrying in background (attempt #{_ipcRetryCount})"); - _lastRetryLogTime = DateTime.Now; - } - - if (ChartControl != null) - { - ChartControl.Dispatcher.BeginInvoke(new Action(() => - { - if (hubStatusLed != null) - { - hubStatusLed.Background = TextMuted; - hubStatusLed.ToolTip = "Waiting for Strategy (retrying...)"; - } - })); - } - - // [Build 933]: Start retry loop on initial failure (market closed / Strategy not yet live). - ScheduleReconnect(); - } + // [Build 954]: IPC deprecated. Strategy no longer hosts IPC server (Phase 6 pruning). + // [Build 955]: Dead code removed. SendCommand() is safely no-oped via tcpStream null guard. } private void DisconnectFromStrategy() diff --git a/src/V12_002.Orders.Callbacks.cs b/src/V12_002.Orders.Callbacks.cs index d76699ff..f540055e 100644 --- a/src/V12_002.Orders.Callbacks.cs +++ b/src/V12_002.Orders.Callbacks.cs @@ -369,9 +369,12 @@ private bool HandleOrderCancelled(Order order) { if (kvp.Value.OldOrder == order && activePositions.TryGetValue(kvp.Key, out var pos)) { - if (pos.RemainingContracts > 0) + // Build 955: Snapshot qty under stateLock -- single atomic read for both check and use. + int _stopQty; + lock (stateLock) { _stopQty = pos.RemainingContracts; } + if (_stopQty > 0) { - CreateNewStopOrder(kvp.Key, pos.RemainingContracts, kvp.Value.StopPrice, kvp.Value.Direction); + CreateNewStopOrder(kvp.Key, _stopQty, kvp.Value.StopPrice, kvp.Value.Direction); // Build 950: Restore OCO-cascade-cancelled targets after stop replacement. if (kvp.Value.BracketRestorationNeeded && kvp.Value.CapturedTargets != null) { @@ -598,18 +601,22 @@ private void HandleMatchedFollowerOrder(string matchedEntry, PositionInfo matche if (_psr.Value.OldOrder == order) { PositionInfo _rPos; - if (activePositions.TryGetValue(_psr.Key, out _rPos) && _rPos.RemainingContracts > 0) + // Build 955: Move guard inside lock -- check and use same atomic snapshot. + if (activePositions.TryGetValue(_psr.Key, out _rPos)) { int _rQty; lock (stateLock) { _rQty = _rPos.RemainingContracts; } - CreateNewStopOrder(_psr.Key, _rQty, _psr.Value.StopPrice, _psr.Value.Direction); - if (_psr.Value.BracketRestorationNeeded && _psr.Value.CapturedTargets != null) + if (_rQty > 0) { - TargetSnapshot[] _snap = _psr.Value.CapturedTargets; - string _rKey = _psr.Key; - TriggerCustomEvent(o => RestoreCascadedTargets(_rKey, _snap), null); - } - } + CreateNewStopOrder(_psr.Key, _rQty, _psr.Value.StopPrice, _psr.Value.Direction); + if (_psr.Value.BracketRestorationNeeded && _psr.Value.CapturedTargets != null) + { + TargetSnapshot[] _snap = _psr.Value.CapturedTargets; + string _rKey = _psr.Key; + TriggerCustomEvent(o => RestoreCascadedTargets(_rKey, _snap), null); + } + } // if (_rQty > 0) + } // if (activePositions.TryGetValue) if (pendingStopReplacements.TryRemove(_psr.Key, out _)) Interlocked.Decrement(ref pendingReplacementCount); return; diff --git a/src/V12_002.Trailing.cs b/src/V12_002.Trailing.cs index c539b696..80da1f47 100644 --- a/src/V12_002.Trailing.cs +++ b/src/V12_002.Trailing.cs @@ -514,14 +514,26 @@ private void UpdateStopOrder(string entryName, PositionInfo pos, double newStopP if (currentStop != null && (currentStop.OrderState == OrderState.CancelPending || currentStop.OrderState == OrderState.Submitted)) { // Order is already being cancelled or submitted - queue the new stop price + // Build 955: Snapshot targets BEFORE TryAdd so any callback sees a fully-initialized record. + var _b955TargetsA = new System.Collections.Generic.List(); + for (int _tA = 1; _tA <= 5; _tA++) + { + var _tDA = GetTargetOrdersDictionary(_tA); + Order _tOA; + if (_tDA != null && _tDA.TryGetValue(entryName, out _tOA) && _tOA != null + && (_tOA.OrderState == OrderState.Working || _tOA.OrderState == OrderState.Accepted)) + _b955TargetsA.Add(new TargetSnapshot { TargetNum = _tA, Price = _tOA.LimitPrice, Qty = _tOA.Quantity, CapturedOrder = _tOA }); + } var newPending = new PendingStopReplacement { - EntryName = entryName, - Quantity = pos.RemainingContracts, - StopPrice = validatedStopPrice, - Direction = pos.Direction, - OldOrder = currentStop, - CreatedTime = DateTime.Now // V8.30: Timeout support + EntryName = entryName, + Quantity = pos.RemainingContracts, + StopPrice = validatedStopPrice, + Direction = pos.Direction, + OldOrder = currentStop, + CreatedTime = DateTime.Now, // V8.30: Timeout support + CapturedTargets = _b955TargetsA.Count > 0 ? _b955TargetsA.ToArray() : null, + BracketRestorationNeeded = _b955TargetsA.Count > 0 }; // V8.30: Thread-safe add or update @@ -558,21 +570,6 @@ private void UpdateStopOrder(string entryName, PositionInfo pos, double newStopP } } - // Build 950: Snapshot Working/Accepted targets before cancel for OCO cascade restoration. - { - var _b950Targets = new System.Collections.Generic.List(); - for (int _t = 1; _t <= 5; _t++) - { - var _tD = GetTargetOrdersDictionary(_t); - Order _tO; - if (_tD != null && _tD.TryGetValue(entryName, out _tO) && _tO != null - && (_tO.OrderState == OrderState.Working || _tO.OrderState == OrderState.Accepted)) - _b950Targets.Add(new TargetSnapshot { TargetNum = _t, Price = _tO.LimitPrice, Qty = _tO.Quantity, CapturedOrder = _tO }); - } - newPending.CapturedTargets = _b950Targets.Count > 0 ? _b950Targets.ToArray() : null; - newPending.BracketRestorationNeeded = _b950Targets.Count > 0; - } - pos.CurrentStopPrice = validatedStopPrice; pos.CurrentTrailLevel = newTrailLevel; Print(string.Format("V8.12: Stop update queued for {0} (current state: {1})", entryName, currentStop.OrderState)); @@ -581,14 +578,26 @@ private void UpdateStopOrder(string entryName, PositionInfo pos, double newStopP if (currentStop != null && (currentStop.OrderState == OrderState.Working || currentStop.OrderState == OrderState.Accepted)) { + // Build 955: Snapshot targets BEFORE TryAdd so any callback sees a fully-initialized record. + var _b955TargetsB = new System.Collections.Generic.List(); + for (int _tB = 1; _tB <= 5; _tB++) + { + var _tDB = GetTargetOrdersDictionary(_tB); + Order _tOB; + if (_tDB != null && _tDB.TryGetValue(entryName, out _tOB) && _tOB != null + && (_tOB.OrderState == OrderState.Working || _tOB.OrderState == OrderState.Accepted)) + _b955TargetsB.Add(new TargetSnapshot { TargetNum = _tB, Price = _tOB.LimitPrice, Qty = _tOB.Quantity, CapturedOrder = _tOB }); + } var newPending = new PendingStopReplacement { - EntryName = entryName, - Quantity = pos.RemainingContracts, - StopPrice = validatedStopPrice, - Direction = pos.Direction, - OldOrder = currentStop, - CreatedTime = DateTime.Now // V8.30: Timeout support + EntryName = entryName, + Quantity = pos.RemainingContracts, + StopPrice = validatedStopPrice, + Direction = pos.Direction, + OldOrder = currentStop, + CreatedTime = DateTime.Now, // V8.30: Timeout support + CapturedTargets = _b955TargetsB.Count > 0 ? _b955TargetsB.ToArray() : null, + BracketRestorationNeeded = _b955TargetsB.Count > 0 }; // V8.30: Thread-safe add @@ -603,21 +612,6 @@ private void UpdateStopOrder(string entryName, PositionInfo pos, double newStopP } } - // Build 950: Snapshot Working/Accepted targets before cancel for OCO cascade restoration. - { - var _b950Targets = new System.Collections.Generic.List(); - for (int _t = 1; _t <= 5; _t++) - { - var _tD = GetTargetOrdersDictionary(_t); - Order _tO; - if (_tD != null && _tD.TryGetValue(entryName, out _tO) && _tO != null - && (_tO.OrderState == OrderState.Working || _tO.OrderState == OrderState.Accepted)) - _b950Targets.Add(new TargetSnapshot { TargetNum = _t, Price = _tO.LimitPrice, Qty = _tO.Quantity, CapturedOrder = _tO }); - } - newPending.CapturedTargets = _b950Targets.Count > 0 ? _b950Targets.ToArray() : null; - newPending.BracketRestorationNeeded = _b950Targets.Count > 0; - } - if (pos.ExecutingAccount != null) { pos.ExecutingAccount.Cancel(new[] { currentStop }); diff --git a/src/V12_002.cs b/src/V12_002.cs index e9ecbd84..4171b70d 100644 --- a/src/V12_002.cs +++ b/src/V12_002.cs @@ -41,7 +41,7 @@ namespace NinjaTrader.NinjaScript.Strategies { public partial class V12_002 : Strategy { - public const string BUILD_TAG = "954"; // V12.954: Bot Audit Fixes (IPC ghost conn, RETEST null state, deprecated mode fallback) + public const string BUILD_TAG = "955"; // V12.955: Race condition fix -- snapshot-before-TryAdd; TOCTOU lock-guard on RemainingContracts; IPC dead code pruned #region Variables From e35028fa975ab66f79ff55ce14ebee914bb70211 Mon Sep 17 00:00:00 2001 From: "AI M. Khalid" Date: Fri, 6 Mar 2026 15:35:12 -0800 Subject: [PATCH 4/4] docs(955): Add Fresh PR Rule and Claude Agent Operation Protocol to Standards Manifesto - Section 6: Add Autonomous Pull Request Handover rule mandating fresh branches for bot audit submissions to ensure full-file audit sweeps - Section 12: Add Claude Agent Operation Protocol with Do-Not-Interrupt, .NET 4.8 Hardening Hook, Missing Brief Failsafe, and Autonomy Rule Co-Authored-By: Claude Sonnet 4.6 --- .agent/standards_manifesto.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.agent/standards_manifesto.md b/.agent/standards_manifesto.md index 4fa90c5e..ca0266dc 100644 --- a/.agent/standards_manifesto.md +++ b/.agent/standards_manifesto.md @@ -44,6 +44,11 @@ This document provides the immutable technical standards for all AI agents (Anth ## 6. Clean-Slate Repo Hygiene (The "Hygiene Rule") - **Zero-Delta Mandate**: Every new Mission (initialized via `$MISSION`) MUST start with a 0-delta `main` branch. If "Big Numbers" (large uncommitted/unmerged diffs > 100 lines) exist, the agent MUST recommend a cleanup/merge before starting new work. +- **Autonomous Pull Request Handover (The Fresh PR Rule)**: When submitting code for bot audit or human review, agents MUST NEVER push to an existing open Pull Request (e.g., updating a dirty branch). Instead, agents MUST: + 1. Checkout a completely new semantic branch (e.g., `build/955-audit-remediation`). + 2. Push the new branch and open a BRAND NEW Pull Request targeting `main`. + 3. Close any superseded or legacy PRs via the GitHub CLI, explicitly leaving a comment referencing the new clean PR. + _Why? Incrementally updating existing PRs can cause automated audit bots (Codex, Greptile, DeepSource) to miss context. A fresh PR triggers a 100% clean, full-file audit sweep._ - **Atomic Missions**: Every bug fix or feature MUST be its own branch and MUST be merged into `main` immediately upon verification (e.g. F5 compile in NT8). No "stacking" unrelated fixes in long-lived branches. - **Binary & Log Purge**: Never commit `.exe`, `.log`, `.bak`, or legacy backup folders to source control. They should be stashed, deleted, or added to `.gitignore`. - **Dashboard Cleanup**: Before ending a session, the agent MUST ensure all work is either Committed or the user has been guided to Merge. The goal is a +0/-0 dashboard between missions. @@ -156,6 +161,15 @@ OnLineInfo ... status=open <- live untracked GTC order at broker **Full discovery steps:** See `.agent/workflows/live-bug-triage.md` Section 0. +## 12. Claude Agent Operation Protocol (Usage Insights) + +**Based on historical friction data, all agents MUST adhere to these execution constraints:** + +- **The "Do Not Interrupt" Protocol:** Agents operating in standard execution mode should complete their logical batches and commit _autonomously_. Do not pause mid-task to ask for user check-ins unless explicitly blocked by a missing file or a hard compilation failure. +- **.NET 4.8 Hardening Hook:** Target framework is .NET 4.8. Do NOT use C# features unavailable in .NET 4.8 (e.g., range operators `[..]`, `Index`/`Range` types, default interface implementations). Always use `CultureInfo.InvariantCulture` for numeric parsing. This must be checked before every commit. +- **The "Missing Brief" Failsafe:** Before any phase starts, the Agent MUST verify that the referenced `implementation_plan.md` or `$MISSION` artifact exists on disk. If it does not, the Agent MUST halt and ask the user for the brief, rather than attempting to guess or reverse-engineer the plan via codebase searches. +- **Autonomy Rule (Default to Action):** Agents are empowered and EXPECTED to execute the full end-to-end lifecycle of a task autonomously. This includes branch creation, surgical implementation, local verification (compile/ASCII), git committing, pushing, and opening/updating PRs. Do not wait for manual approval to move from "Code Change" to "Git Push" if local verification (`deploy-sync.ps1`) passes. + --- > [!NOTE]