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] 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_001.cs b/src/V12_001.cs index 3fb09247..60402735 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,67 +2998,8 @@ private void SyncAll_Click(object sender, RoutedEventArgs e) private void ConnectToStrategy() { - 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.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.Orders.Callbacks.cs b/src/V12_002.Orders.Callbacks.cs index 29525d6e..f540055e 100644 --- a/src/V12_002.Orders.Callbacks.cs +++ b/src/V12_002.Orders.Callbacks.cs @@ -369,8 +369,20 @@ 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 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, _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) + { + 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 +590,39 @@ 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; + // 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; } + if (_rQty > 0) + { + 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; + } + } + } 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..80da1f47 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); + } } } } @@ -507,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 @@ -534,6 +553,21 @@ 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; + } } pos.CurrentStopPrice = validatedStopPrice; @@ -544,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 diff --git a/src/V12_002.cs b/src/V12_002.cs index b6ab9b8b..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 = "948"; // V12.948: Freeze Prevention (GTC Sweep + Order Adoption + Burst Cap) + 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 @@ -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