From e3b43e69bbc21ac280fc00a2307d80eaac2badb3 Mon Sep 17 00:00:00 2001 From: "AI M. Khalid" Date: Sun, 8 Mar 2026 15:44:41 -0700 Subject: [PATCH 1/4] fix(concurrency): A1-1/A2-1 -- stateLock wraps and null-abort rollbacks across all entry modules - FFMA, MOMO, OR, RMA (ExecuteRMAEntry/Custom/TrendSplit), Trend (ExecuteTRENDEntry/ManualEntry): deferred activePositions/entryOrders writes to AFTER null check on SubmitOrderUnmanaged; wrapped both assignments in lock(stateLock); added clean rollback + return on null submit. - Trailing.cs: wrapped three stopOrders[entryName] write sites in lock(stateLock). - Orders.Management.cs: wrapped two stopOrders[entryName] write sites in lock(stateLock). - Symmetry.cs: moved pos.EntryFilled = true inside existing lock(stateLock) block so EntryFilled and RemainingContracts are mutated atomically. Resolves Build 960 audit defects: Area 1 item 1 and Area 2 item 1. Co-Authored-By: Claude Sonnet 4.6 --- src/V12_002.Entries.FFMA.cs | 45 ++++++++++++++++++------ src/V12_002.Entries.MOMO.cs | 13 ++++--- src/V12_002.Entries.OR.cs | 15 ++++---- src/V12_002.Entries.RMA.cs | 59 ++++++++++++++++++++++---------- src/V12_002.Entries.Trend.cs | 25 +++++++++----- src/V12_002.Orders.Management.cs | 6 ++-- src/V12_002.Symmetry.cs | 3 +- src/V12_002.Trailing.cs | 11 +++--- 8 files changed, 119 insertions(+), 58 deletions(-) diff --git a/src/V12_002.Entries.FFMA.cs b/src/V12_002.Entries.FFMA.cs index 82f9af2d..8b90f14f 100644 --- a/src/V12_002.Entries.FFMA.cs +++ b/src/V12_002.Entries.FFMA.cs @@ -169,19 +169,26 @@ private void ExecuteFFMAEntry(MarketPosition direction) // Build 936 [FIX-2]: Deterministic OCO group ID for broker-native bracket protection. OcoGroupId = "V12_" + entryName.GetHashCode().ToString("X8") }; - activePositions[entryName] = pos; - // V12.13-D: Notify connected panel clients of position entry string syncMsg = string.Format("POSITION_ENTERED|FFMA|{0}", contracts); SendResponseToRemote(syncMsg); - // Submit MARKET order (immediate execution) Order entryOrder = direction == MarketPosition.Long ? SubmitOrderUnmanaged(0, OrderAction.Buy, OrderType.Market, contracts, 0, 0, "", entryName) : SubmitOrderUnmanaged(0, OrderAction.SellShort, OrderType.Market, contracts, 0, 0, "", entryName); - entryOrders[entryName] = entryOrder; + // A1-1/A2-1: Null-abort rollback + stateLock wrap (Build 960 audit fix) + if (entryOrder == null) + { + Print("[ENTRY_ABORT] FFMA SubmitOrderUnmanaged returned null for " + entryName + ". Rolling back."); + return; + } + lock (stateLock) + { + activePositions[entryName] = pos; + entryOrders[entryName] = entryOrder; + } Print(string.Format("FFMA MARKET ORDER: {0} {1}@MARKET | Stop: {2:F2} (candle {3})", signalName, contracts, stopPrice, direction == MarketPosition.Long ? "low" : "high")); @@ -304,13 +311,22 @@ private void ExecuteFFMALimitEntry(double manualPrice, MarketPosition direction) // Build 936 [FIX-2]: Deterministic OCO group ID for broker-native bracket protection. OcoGroupId = "V12_" + entryName.GetHashCode().ToString("X8") }; - activePositions[entryName] = pos; - // V12.27: Submit LIMIT order (not Market like standard FFMA) Order entryOrder = direction == MarketPosition.Long ? SubmitOrderUnmanaged(0, OrderAction.Buy, OrderType.Limit, contracts, entryPrice, 0, "", entryName) : SubmitOrderUnmanaged(0, OrderAction.SellShort, OrderType.Limit, contracts, entryPrice, 0, "", entryName); - entryOrders[entryName] = entryOrder; + + // A1-1/A2-1: Null-abort rollback + stateLock wrap (Build 960 audit fix) + if (entryOrder == null) + { + Print("[ENTRY_ABORT] FFMA_LIMIT SubmitOrderUnmanaged returned null for " + entryName + ". Rolling back."); + return; + } + lock (stateLock) + { + activePositions[entryName] = pos; + entryOrders[entryName] = entryOrder; + } Print(string.Format("V12.27 FFMA_LIMIT: {0} {1}@{2:F2} LIMIT | Stop: {3:F2} | ATR-based", direction, contracts, entryPrice, stopPrice)); @@ -443,13 +459,22 @@ private void ExecuteFFMAManualMarketEntry() // Build 936 [FIX-2]: Deterministic OCO group ID for broker-native bracket protection. OcoGroupId = "V12_" + entryName.GetHashCode().ToString("X8") }; - activePositions[entryName] = pos; - // Submit MARKET order (immediate execution) Order entryOrder = direction == MarketPosition.Long ? SubmitOrderUnmanaged(0, OrderAction.Buy, OrderType.Market, contracts, 0, 0, "", entryName) : SubmitOrderUnmanaged(0, OrderAction.SellShort, OrderType.Market, contracts, 0, 0, "", entryName); - entryOrders[entryName] = entryOrder; + + // A1-1/A2-1: Null-abort rollback + stateLock wrap (Build 960 audit fix) + if (entryOrder == null) + { + Print("[ENTRY_ABORT] FFMA_MANUAL_MARKET SubmitOrderUnmanaged returned null for " + entryName + ". Rolling back."); + return; + } + lock (stateLock) + { + activePositions[entryName] = pos; + entryOrders[entryName] = entryOrder; + } Print(string.Format("V12.27 FFMA_MANUAL_MARKET: {0} {1}@MARKET | Stop: {2:F2} (candle {3}) | Toward EMA9={4:F2}", direction, contracts, stopPrice, direction == MarketPosition.Long ? "low" : "high", ema9Value)); diff --git a/src/V12_002.Entries.MOMO.cs b/src/V12_002.Entries.MOMO.cs index 19244240..9c6947ae 100644 --- a/src/V12_002.Entries.MOMO.cs +++ b/src/V12_002.Entries.MOMO.cs @@ -143,8 +143,6 @@ private void ExecuteMOMOEntry(double clickPrice, int contracts) }; ApplyTargetLadderGuard(pos); - activePositions[entryName] = pos; - // Build 1102Y-V3 [MS-06]: Register Master expected BEFORE StopMarket entry. int masterDeltaMOMO = (direction == MarketPosition.Long) ? contracts : -contracts; AddExpectedPositionDeltaLocked(ExpKey(Account.Name), masterDeltaMOMO); @@ -154,13 +152,18 @@ private void ExecuteMOMOEntry(double clickPrice, int contracts) ? SubmitOrderUnmanaged(0, OrderAction.Buy, OrderType.StopMarket, contracts, 0, entryPrice, "", entryName) : SubmitOrderUnmanaged(0, OrderAction.SellShort, OrderType.StopMarket, contracts, 0, entryPrice, "", entryName); + // A1-1/A2-1: Null-abort rollback + stateLock wrap (Build 960 audit fix) if (entryOrder == null) { AddExpectedPositionDeltaLocked(ExpKey(Account.Name), -masterDeltaMOMO); - Print("[ERROR][1102Y-V3] MOMO SubmitOrderUnmanaged NULL for " + entryName + " -- rolled back."); + Print("[ENTRY_ABORT] MOMO SubmitOrderUnmanaged returned null for " + entryName + ". Rolling back."); + return; + } + lock (stateLock) + { + activePositions[entryName] = pos; + entryOrders[entryName] = entryOrder; } - - entryOrders[entryName] = entryOrder; Print(string.Format("MOMO ENTRY ORDER: {0} {1}@{2:F2} STOP MKT | Stop: {3:F2}pt", signalName, contracts, entryPrice, stopDistance)); Print(string.Format("MOMO TARGETS: T1:{0}@{1:F2}(+{2:F2}pt) | T2:{3}@{4:F2} | T3:{5}@{6:F2} | T4:{7}@{8:F2} | T5:{9}@{10:F2} (Runner targets trail-only)", diff --git a/src/V12_002.Entries.OR.cs b/src/V12_002.Entries.OR.cs index 99f37a24..75e526db 100644 --- a/src/V12_002.Entries.OR.cs +++ b/src/V12_002.Entries.OR.cs @@ -203,8 +203,6 @@ private void EnterORPosition(MarketPosition direction, double entryPrice, double }; ApplyTargetLadderGuard(pos); - activePositions[entryName] = pos; - // V12.13-D: Notify connected panel clients of position entry string syncMsg = string.Format("POSITION_ENTERED|OR|{0}", contracts); SendResponseToRemote(syncMsg); @@ -218,18 +216,19 @@ private void EnterORPosition(MarketPosition direction, double entryPrice, double ? SubmitOrderUnmanaged(0, OrderAction.Buy, OrderType.StopMarket, contracts, 0, entryPrice, "", entryName) : SubmitOrderUnmanaged(0, OrderAction.SellShort, OrderType.StopMarket, contracts, 0, entryPrice, "", entryName); + // A1-1/A2-1: Null-abort rollback + stateLock wrap (Build 960 audit fix) if (entryOrder == null) { // Build 1102Y-V3 [MS-03 ROLLBACK]: Submit failed -- undo Order Ledger reservation. AddExpectedPositionDeltaLocked(ExpKey(Account.Name), -masterDeltaOR); - Print("[ERROR][1102Y-V3] OR SubmitOrderUnmanaged returned NULL for " + entryName + " -- Master expected rolled back. Fleet dispatch aborted."); - // [FIX-OR-E]: Must abort here. Without an early return, ExecuteSmartDispatchEntry - // dispatches fleet followers for a master entry that does not exist -> phantom fleet entries. - activePositions.TryRemove(entryName, out _); + Print("[ENTRY_ABORT] OR SubmitOrderUnmanaged returned NULL for " + entryName + " -- Master expected rolled back. Fleet dispatch aborted."); return; } - - entryOrders[entryName] = entryOrder; + lock (stateLock) + { + activePositions[entryName] = pos; + entryOrders[entryName] = entryOrder; + } Print(string.Format("OR ENTRY ORDER: {0} {1}@{2:F2} | Stop: {3:F2} | OR Range: {4:F2}", signalName, contracts, entryPrice, stopPrice, sessionRange)); diff --git a/src/V12_002.Entries.RMA.cs b/src/V12_002.Entries.RMA.cs index 745c7a11..0de6a20e 100644 --- a/src/V12_002.Entries.RMA.cs +++ b/src/V12_002.Entries.RMA.cs @@ -90,21 +90,26 @@ private void ExecuteTrendSplitEntry() double stop1Price = Instrument.MasterInstrument.RoundToTickSize( direction == MarketPosition.Long ? e9 - stop9Dist : e9 + stop9Dist); PositionInfo pos1 = CreateTRENDPosition(entry1Name, direction, e9, stop1Price, qty9, true, trendGroupId, true); - activePositions[entry1Name] = pos1; List masterEntryNames = new List { entry1Name }; Order entryOrder1 = direction == MarketPosition.Long ? SubmitOrderUnmanaged(0, OrderAction.Buy, OrderType.Limit, qty9, e9, 0, "", entry1Name) : SubmitOrderUnmanaged(0, OrderAction.SellShort, OrderType.Limit, qty9, e9, 0, "", entry1Name); - entryOrders[entry1Name] = entryOrder1; + + // A1-1/A2-1: Null-abort + stateLock wrap for E1 (Build 960 audit fix) + if (entryOrder1 == null) + { + Print("[ENTRY_ABORT] TrendSplit E1 SubmitOrderUnmanaged returned null for " + entry1Name + ". Rolling back."); + return; + } + lock (stateLock) { activePositions[entry1Name] = pos1; entryOrders[entry1Name] = entryOrder1; } if (qty15 > 0) { double stop2Price = Instrument.MasterInstrument.RoundToTickSize( direction == MarketPosition.Long ? e15 - stop15Dist : e15 + stop15Dist); PositionInfo pos2 = CreateTRENDPosition(entry2Name, direction, e15, stop2Price, qty15, false, trendGroupId, true); - activePositions[entry2Name] = pos2; linkedTRENDEntries[entry1Name] = entry2Name; linkedTRENDEntries[entry2Name] = entry1Name; @@ -112,8 +117,18 @@ private void ExecuteTrendSplitEntry() Order entryOrder2 = direction == MarketPosition.Long ? SubmitOrderUnmanaged(0, OrderAction.Buy, OrderType.Limit, qty15, e15, 0, "", entry2Name) : SubmitOrderUnmanaged(0, OrderAction.SellShort, OrderType.Limit, qty15, e15, 0, "", entry2Name); - entryOrders[entry2Name] = entryOrder2; - masterEntryNames.Add(entry2Name); + + // A1-1/A2-1: Null-abort + stateLock wrap for E2 (Build 960 audit fix) + if (entryOrder2 == null) + { + Print("[ENTRY_ABORT] TrendSplit E2 SubmitOrderUnmanaged returned null for " + entry2Name + ". Rolling back."); + // E1 already submitted -- log but continue without E2 tracking + } + else + { + lock (stateLock) { activePositions[entry2Name] = pos2; entryOrders[entry2Name] = entryOrder2; } + masterEntryNames.Add(entry2Name); + } } double weightedEntryPrice = ((e9 * qty9) + (e15 * qty15)) / Math.Max(1, finalTotalQty); @@ -289,22 +304,23 @@ private void ExecuteRMAEntry(double clickPrice, MarketPosition? forcedDirection ? SubmitOrderUnmanaged(0, OrderAction.Buy, OrderType.Limit, contracts, entryPrice, 0, "", entryName) : SubmitOrderUnmanaged(0, OrderAction.SellShort, OrderType.Limit, contracts, entryPrice, 0, "", entryName); - if (entryOrder != null) - { - entryOrders[entryName] = entryOrder; - activePositions[entryName] = pos; // Only add to panel if order submitted - - // DEBUG: Visual Confirmation - Draw.Text(this, "Debug_" + entryName, "ORDER SUBMITTED", 0, entryPrice, Brushes.Yellow); - Draw.Line(this, "Line_" + entryName, 0, entryPrice, 10, entryPrice, Brushes.Yellow); - } - else + // A1-1/A2-1: Null-abort rollback + stateLock wrap (Build 960 audit fix) + if (entryOrder == null) { // Build 1102Y-V3 [MS-01 ROLLBACK]: Submit failed -- undo reservation to prevent ghost position. AddExpectedPositionDeltaLocked(ExpKey(Account.Name), -masterDeltaRMA); - Print("[ERROR][1102Y-V3] SubmitOrderUnmanaged returned NULL for " + entryName + " -- Master expected rolled back."); + Print("[ENTRY_ABORT] RMA SubmitOrderUnmanaged returned NULL for " + entryName + " -- Master expected rolled back."); Draw.Text(this, "Debug_Fail_" + entryName, "ORDER FAILED", 0, entryPrice, Brushes.Red); + return; + } + lock (stateLock) + { + activePositions[entryName] = pos; + entryOrders[entryName] = entryOrder; } + // DEBUG: Visual Confirmation + Draw.Text(this, "Debug_" + entryName, "ORDER SUBMITTED", 0, entryPrice, Brushes.Yellow); + Draw.Line(this, "Line_" + entryName, 0, entryPrice, 10, entryPrice, Brushes.Yellow); Print(string.Format("RMA ENTRY ORDER: {0} {1}@{2:F2} | ATR: {3:F2}", signalName, contracts, entryPrice, currentATR)); Print(string.Format("RMA TARGETS: T1:{0}@{1:F2}(+{2:F2}pt) | T2:{3}@{4:F2} | T3:{5}@{6:F2} | T4:{7}@{8:F2} | T5:{9}@{10:F2} (Runner targets trail-only)", @@ -397,8 +413,6 @@ private void ExecuteRMAEntryCustom(double price, MarketPosition direction) }; ApplyTargetLadderGuard(pos); - activePositions[entryName] = pos; - // Build 1102Y-V3 [MS-02]: Register Master's expected position in the Order Ledger BEFORE submit. int masterDeltaRMACustom = (direction == MarketPosition.Long) ? contracts : -contracts; AddExpectedPositionDeltaLocked(ExpKey(Account.Name), masterDeltaRMACustom); @@ -408,11 +422,18 @@ private void ExecuteRMAEntryCustom(double price, MarketPosition direction) ? SubmitOrderUnmanaged(0, OrderAction.Buy, OrderType.Market, contracts, 0, 0, "", entryName) : SubmitOrderUnmanaged(0, OrderAction.SellShort, OrderType.Market, contracts, 0, 0, "", entryName); + // A1-1/A2-1: Null-abort rollback + stateLock wrap (Build 960 audit fix) if (entryOrderCustom == null) { // Build 1102Y-V3 [MS-02 ROLLBACK]: Submit failed -- undo reservation. AddExpectedPositionDeltaLocked(ExpKey(Account.Name), -masterDeltaRMACustom); - Print("[ERROR][1102Y-V3] RMACustom SubmitOrderUnmanaged returned NULL for " + entryName + " -- Master expected rolled back."); + Print("[ENTRY_ABORT] RMACustom SubmitOrderUnmanaged returned NULL for " + entryName + " -- Master expected rolled back."); + return; + } + lock (stateLock) + { + activePositions[entryName] = pos; + entryOrders[entryName] = entryOrderCustom; } Print(string.Format("IPC EXEC: {0} {1} contracts at MKT (Ref: {2:F2})", direction, contracts, entryPrice)); diff --git a/src/V12_002.Entries.Trend.cs b/src/V12_002.Entries.Trend.cs index 8d9ed2b2..1ecf4a7a 100644 --- a/src/V12_002.Entries.Trend.cs +++ b/src/V12_002.Entries.Trend.cs @@ -204,14 +204,12 @@ private void ExecuteTRENDEntry(int contracts) entry1Qty, true, trendGroupId, isTrendRmaMode); // Build 1102Y-V3 [LG-01]: Enforce staircase rule on E1. ApplyTargetLadderGuard(pos1); - activePositions[entry1Name] = pos1; // Create position info for Entry 2 PositionInfo pos2 = CreateTRENDPosition(entry2Name, direction, entry2Price, stop2Price, entry2Qty, false, trendGroupId, isTrendRmaMode); // Build 1102Y-V3 [LG-01]: Enforce staircase rule on E2. ApplyTargetLadderGuard(pos2); - activePositions[entry2Name] = pos2; // Link the entries together linkedTRENDEntries[entry1Name] = entry2Name; @@ -226,12 +224,14 @@ private void ExecuteTRENDEntry(int contracts) ? SubmitOrderUnmanaged(0, OrderAction.Buy, OrderType.Limit, entry1Qty, entry1Price, 0, "", entry1Name) : SubmitOrderUnmanaged(0, OrderAction.SellShort, OrderType.Limit, entry1Qty, entry1Price, 0, "", entry1Name); + // A1-1/A2-1: Null-abort rollback + stateLock wrap for E1 (Build 960 audit fix) if (entryOrder1 == null) { AddExpectedPositionDeltaLocked(ExpKey(Account.Name), -masterDeltaE1); - Print("[ERROR][1102Y-V3] TREND E1 SubmitOrderUnmanaged NULL for " + entry1Name + " -- rolled back."); + Print("[ENTRY_ABORT] TREND E1 SubmitOrderUnmanaged NULL for " + entry1Name + " -- rolled back."); + return; } - entryOrders[entry1Name] = entryOrder1; + lock (stateLock) { activePositions[entry1Name] = pos1; entryOrders[entry1Name] = entryOrder1; } // Build 1102Y-V3 [MS-04b]: Register Master expected for E2 BEFORE submit. int masterDeltaE2 = (direction == MarketPosition.Long) ? entry2Qty : -entry2Qty; @@ -242,12 +242,14 @@ private void ExecuteTRENDEntry(int contracts) ? SubmitOrderUnmanaged(0, OrderAction.Buy, OrderType.Limit, entry2Qty, entry2Price, 0, "", entry2Name) : SubmitOrderUnmanaged(0, OrderAction.SellShort, OrderType.Limit, entry2Qty, entry2Price, 0, "", entry2Name); + // A1-1/A2-1: Null-abort rollback + stateLock wrap for E2 (Build 960 audit fix) if (entryOrder2 == null) { AddExpectedPositionDeltaLocked(ExpKey(Account.Name), -masterDeltaE2); - Print("[ERROR][1102Y-V3] TREND E2 SubmitOrderUnmanaged NULL for " + entry2Name + " -- rolled back."); + Print("[ENTRY_ABORT] TREND E2 SubmitOrderUnmanaged NULL for " + entry2Name + " -- rolled back."); + return; } - entryOrders[entry2Name] = entryOrder2; + lock (stateLock) { activePositions[entry2Name] = pos2; entryOrders[entry2Name] = entryOrder2; } Print(string.Format("TREND ORDERS PLACED: {0} Total={1} contracts", direction == MarketPosition.Long ? "LONG" : "SHORT", totalContracts)); @@ -397,7 +399,6 @@ private void ExecuteTRENDManualEntry(double manualPrice, MarketPosition directio // Build 1102Y-V3 [LG-01]: Enforce staircase rule. ApplyTargetLadderGuard(pos); - activePositions[entryName] = pos; // Build 1102Y-V3 [MS-05]: Register Master expected BEFORE submit. int masterDeltaTMNL = (direction == MarketPosition.Long) ? contracts : -contracts; @@ -408,12 +409,18 @@ private void ExecuteTRENDManualEntry(double manualPrice, MarketPosition directio ? SubmitOrderUnmanaged(0, OrderAction.Buy, OrderType.Limit, contracts, entryPrice, 0, "", entryName) : SubmitOrderUnmanaged(0, OrderAction.SellShort, OrderType.Limit, contracts, entryPrice, 0, "", entryName); + // A1-1/A2-1: Null-abort rollback + stateLock wrap (Build 960 audit fix) if (entryOrder == null) { AddExpectedPositionDeltaLocked(ExpKey(Account.Name), -masterDeltaTMNL); - Print("[ERROR][1102Y-V3] TRENDManual SubmitOrderUnmanaged NULL for " + entryName + " -- rolled back."); + Print("[ENTRY_ABORT] TRENDManual SubmitOrderUnmanaged NULL for " + entryName + " -- rolled back."); + return; + } + lock (stateLock) + { + activePositions[entryName] = pos; + entryOrders[entryName] = entryOrder; } - entryOrders[entryName] = entryOrder; Print(string.Format("V12.27 TREND_MANUAL: {0} {1}@{2:F2} LIMIT | Stop: {3:F2} | 100% Risk", direction, contracts, entryPrice, stopPrice)); diff --git a/src/V12_002.Orders.Management.cs b/src/V12_002.Orders.Management.cs index 249b4fd3..e8f9659a 100644 --- a/src/V12_002.Orders.Management.cs +++ b/src/V12_002.Orders.Management.cs @@ -100,7 +100,8 @@ private void SubmitBracketOrders(string entryName, PositionInfo pos) FlattenPositionByName(entryName); return; } - stopOrders[entryName] = stopOrder; + // A1-1: stopOrders mutation inside stateLock (Build 960 audit fix) + lock (stateLock) { stopOrders[entryName] = stopOrder; } int nonRunnerLimitQty = 0; int runnerQty = 0; @@ -553,7 +554,8 @@ private void CreateNewStopOrder(string entryName, int quantity, double stopPrice return; } - stopOrders[entryName] = newStop; + // A1-1: stopOrders mutation inside stateLock (Build 960 audit fix) + lock (stateLock) { stopOrders[entryName] = newStop; } // [LATENCY_AUDIT] Measure OCO turnaround: CreatedTime was stamped in UpdateStopQuantity() when // the target fill triggered the pending stop replacement. The delta = Target Fill -> Stop Cancel diff --git a/src/V12_002.Symmetry.cs b/src/V12_002.Symmetry.cs index 0a60b9c8..8b201dab 100644 --- a/src/V12_002.Symmetry.cs +++ b/src/V12_002.Symmetry.cs @@ -589,9 +589,10 @@ private void SymmetryGuardSkipFollower( "[SYMMETRY_GUARD] SKIP | {0} | {1} | FleetFill={2:F2} | Slip={3:F1} ticks (${4:F2}/ct)", fleetEntryName, reason, fleetFillPrice, slippageTicks, slippageUsdPerContract)); - pos.EntryFilled = true; + // A1-1: pos.EntryFilled must be inside stateLock to prevent torn read by REAPER (Build 960 audit fix) lock (stateLock) { + pos.EntryFilled = true; if (pos.RemainingContracts <= 0) pos.RemainingContracts = Math.Max(1, pos.TotalContracts); } diff --git a/src/V12_002.Trailing.cs b/src/V12_002.Trailing.cs index 80da1f47..7e3d06ac 100644 --- a/src/V12_002.Trailing.cs +++ b/src/V12_002.Trailing.cs @@ -631,10 +631,11 @@ private void UpdateStopOrder(string entryName, PositionInfo pos, double newStopP // No existing stop or not in a cancellable state - create directly if (pos.ExecutingAccount != null) { - newStop = pos.ExecutingAccount.CreateOrder(Instrument, pos.Direction == MarketPosition.Long ? OrderAction.Sell : OrderAction.BuyToCover, + newStop = pos.ExecutingAccount.CreateOrder(Instrument, pos.Direction == MarketPosition.Long ? OrderAction.Sell : OrderAction.BuyToCover, OrderType.StopMarket, TimeInForce.Gtc, pos.RemainingContracts, 0, validatedStopPrice, "Stop_" + entryName, "Stop_" + entryName, null); pos.ExecutingAccount.Submit(new[] { newStop }); - stopOrders[entryName] = newStop; + // A1-1: stopOrders mutation inside stateLock (Build 960 audit fix) + lock (stateLock) { stopOrders[entryName] = newStop; } } else { @@ -645,7 +646,8 @@ private void UpdateStopOrder(string entryName, PositionInfo pos, double newStopP OrderAction stopExitAction = pos.Direction == MarketPosition.Long ? OrderAction.Sell : OrderAction.BuyToCover; newStop = SubmitOrderUnmanaged(0, stopExitAction, OrderType.StopMarket, pos.RemainingContracts, 0, validatedStopPrice, "", stopSigName); - if (newStop != null) stopOrders[entryName] = newStop; + // A1-1: stopOrders mutation inside stateLock (Build 960 audit fix) + if (newStop != null) lock (stateLock) { stopOrders[entryName] = newStop; } } if (newStop == null) @@ -662,7 +664,8 @@ private void UpdateStopOrder(string entryName, PositionInfo pos, double newStopP return; } - stopOrders[entryName] = newStop; + // A1-1: stopOrders final write inside stateLock (Build 960 audit fix) + lock (stateLock) { stopOrders[entryName] = newStop; } pos.CurrentStopPrice = validatedStopPrice; pos.CurrentTrailLevel = newTrailLevel; From a38e7df550e7cc1dac5d27ca000855bfd78de789 Mon Sep 17 00:00:00 2001 From: "AI M. Khalid" Date: Sun, 8 Mar 2026 15:48:26 -0700 Subject: [PATCH 2/4] fix(fsm): A1-2/A1-3 -- FSM bypass remediation and TOCTOU snapshot lock A1-2: Added StampReaperMoveGrace() before Cancel in follower target replacements to open a 5-second REAPER suppression window, matching the established PropagateMasterTargetMove pattern: - Symmetry.cs SymmetryGuardReplaceExistingFollowerTarget: guard before Cancel, wrapped dict[fleetEntryName] write in lock(stateLock). - Trailing.cs MoveSpecificTarget: guard before follower target Cancel. A1-3: Wrapped the FSM PendingCancel->Submitting state transition snapshot in lock(stateLock) inside OnAccountOrderUpdate. This closes the TOCTOU window where PropagateFollowerEntryReplace (holding stateLock) could write to PendingQty/PendingPrice simultaneously with the callback thread reading them. Resolves Build 960 audit defects: Area 1 items 2 and 3. Co-Authored-By: Claude Sonnet 4.6 --- src/V12_002.Orders.Callbacks.cs | 25 ++++++++++++++++++------- src/V12_002.Symmetry.cs | 4 +++- src/V12_002.Trailing.cs | 2 ++ 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/V12_002.Orders.Callbacks.cs b/src/V12_002.Orders.Callbacks.cs index f540055e..b6003ffb 100644 --- a/src/V12_002.Orders.Callbacks.cs +++ b/src/V12_002.Orders.Callbacks.cs @@ -558,13 +558,24 @@ private void HandleMatchedFollowerOrder(string matchedEntry, PositionInfo matche return; } - // Snapshot latest spec values, transition to Submitting, schedule on strategy thread - int qty = fsm.PendingQty; - double price = fsm.PendingPrice; - string acctNameCapture = fsm.AccountName; - string sigName = fsm.SignalName; - FollowerReplaceSpec fsmCapture = fsm; - fsm.State = FollowerReplaceState.Submitting; + // A1-3: Snapshot qty/price and transition state atomically under stateLock to close TOCTOU window. + // PropagateFollowerEntryReplace can update PendingQty/PendingPrice inside lock(stateLock) + // while OnAccountOrderUpdate (background thread) reads them here. Without the lock, + // the snapshot and state transition can observe torn state. (Build 960 audit fix) + int qty; + double price; + string acctNameCapture; + string sigName; + FollowerReplaceSpec fsmCapture; + lock (stateLock) + { + qty = fsm.PendingQty; + price = fsm.PendingPrice; + acctNameCapture = fsm.AccountName; + sigName = fsm.SignalName; + fsmCapture = fsm; + fsm.State = FollowerReplaceState.Submitting; + } try { diff --git a/src/V12_002.Symmetry.cs b/src/V12_002.Symmetry.cs index 8b201dab..fbc0d4a3 100644 --- a/src/V12_002.Symmetry.cs +++ b/src/V12_002.Symmetry.cs @@ -552,6 +552,8 @@ private void SymmetryGuardReplaceExistingFollowerTarget( oldTarget.OrderState == OrderState.Submitted || oldTarget.OrderState == OrderState.ChangePending) { + // A1-2: Stamp REAPER grace window before cancel to suppress false desync during replace gap (Build 960 audit fix) + StampReaperMoveGrace(); pos.ExecutingAccount.Cancel(new[] { oldTarget }); } @@ -574,7 +576,7 @@ private void SymmetryGuardReplaceExistingFollowerTarget( null); pos.ExecutingAccount.Submit(new[] { replacement }); - dict[fleetEntryName] = replacement; + lock (stateLock) { dict[fleetEntryName] = replacement; } } private void SymmetryGuardSkipFollower( diff --git a/src/V12_002.Trailing.cs b/src/V12_002.Trailing.cs index 7e3d06ac..5f5c02dd 100644 --- a/src/V12_002.Trailing.cs +++ b/src/V12_002.Trailing.cs @@ -979,6 +979,8 @@ private void MoveSpecificTarget(int targetNum, double profitPoints) if (pos.IsFollower && pos.ExecutingAccount != null) { // [1102Z-F]: Fleet follower path -- cancel old limit, resubmit at new price + // A1-2: Stamp REAPER grace window before cancel to suppress false desync during replace gap (Build 960 audit fix) + StampReaperMoveGrace(); pos.ExecutingAccount.Cancel(new[] { targetOrder }); OrderAction exitAct = pos.Direction == MarketPosition.Long From 2043e370fee91415365646c7b188af4f511c5277 Mon Sep 17 00:00:00 2001 From: "AI M. Khalid" Date: Sun, 8 Mar 2026 15:52:08 -0700 Subject: [PATCH 3/4] fix(lifecycle): A2-2/A2-3 -- deferred metadata purge and confirmed-cancel expectedPositions rollback A2-2: Added PendingCleanup and FlattenAttemptCount fields to PositionInfo. Replaced immediate activePositions.TryRemove after RequestStopCancelLifecycleSafe in the final-target and trim-close flows with pos.PendingCleanup = true. Added deferred TryRemove logic to both HandleOrderCancelled (master stop) and OnAccountOrderUpdate (follower stop) -- purge fires only on broker-confirmed stop terminal state. A2-3: Removed DeltaExpectedPositionLocked from SymmetryGuardCascadeFollowerCleanup (was firing immediately on cancel request). Moved the confirmed-cancel delta rollback into the non-FSM follower entry cancelled branch in OnAccountOrderUpdate -- rollback now fires only after OrderState.Cancelled confirmed by the broker, eliminating the microsecond fill-race desync between the master cancel request and broker confirmation. Resolves Build 960 audit defects: Area 2 items 2 and 3. Co-Authored-By: Claude Sonnet 4.6 --- src/V12_002.Orders.Callbacks.cs | 69 +++++++++++++++++++++++++++++++-- src/V12_002.Symmetry.cs | 8 +--- src/V12_002.cs | 9 +++++ 3 files changed, 76 insertions(+), 10 deletions(-) diff --git a/src/V12_002.Orders.Callbacks.cs b/src/V12_002.Orders.Callbacks.cs index b6003ffb..d0f038b5 100644 --- a/src/V12_002.Orders.Callbacks.cs +++ b/src/V12_002.Orders.Callbacks.cs @@ -388,6 +388,28 @@ private bool HandleOrderCancelled(Order order) break; } } + + // A2-2: Deferred PendingCleanup purge -- master stop terminal (Build 960 audit fix). + // If no pendingStopReplacement matched, check if this stop cancel completes a + // final-target/trim close where activePositions was intentionally kept alive. + if (!handled) + { + foreach (var kvp in stopOrders.ToArray()) + { + if (kvp.Value == order) + { + PositionInfo cleanupPos; + if (activePositions.TryGetValue(kvp.Key, out cleanupPos) && cleanupPos != null + && cleanupPos.PendingCleanup && cleanupPos.RemainingContracts <= 0) + { + lock (stateLock) { activePositions.TryRemove(kvp.Key, out _); } + SymmetryGuardForgetEntry(kvp.Key); + Print("[A2-2] Deferred PendingCleanup purge (master stop cancel): " + kvp.Key); + } + break; + } + } + } } if (!handled && entryOrders.Values.Contains(order)) @@ -596,6 +618,17 @@ private void HandleMatchedFollowerOrder(string matchedEntry, PositionInfo matche return; // FSM-controlled cancel -- not a real desync } + // A2-3: Direction-aware delta rollback on CONFIRMED cancel -- deferred from SymmetryGuardCascadeFollowerCleanup + // to prevent REAPER desync on microsecond fill race (Build 960 audit fix). + PositionInfo cancelledFollowerPos; + if (activePositions.TryGetValue(matchedEntry, out cancelledFollowerPos) && cancelledFollowerPos != null) + { + string cancelAcctKey = cancelledFollowerPos.ExecutingAccount != null + ? cancelledFollowerPos.ExecutingAccount.Name : Account.Name; + int cancelDelta = (cancelledFollowerPos.Direction == MarketPosition.Long) + ? -cancelledFollowerPos.TotalContracts : cancelledFollowerPos.TotalContracts; + DeltaExpectedPositionLocked(ExpKey(cancelAcctKey), cancelDelta); + } Print(string.Format("[SIMA] Follower entry cancelled: {0} on {1}. Reaper monitoring.", matchedEntry, acctName)); Draw.TextFixed(this, "SIMA_DESYNC_" + acctName, "(!) FOLLOWER DESYNC: " + acctName, TextPosition.TopLeft, Brushes.Red, new SimpleFont("Arial", 11), Brushes.Transparent, Brushes.Transparent, 50); } @@ -634,6 +667,26 @@ private void HandleMatchedFollowerOrder(string matchedEntry, PositionInfo matche } } } + // A2-2: Deferred PendingCleanup purge -- follower stop terminal (Build 960 audit fix). + if (order.Name.StartsWith("Stop_") || order.Name.StartsWith("S_")) + { + foreach (var _sc in stopOrders.ToArray()) + { + if (_sc.Value == order) + { + PositionInfo _scPos; + if (activePositions.TryGetValue(_sc.Key, out _scPos) && _scPos != null + && _scPos.PendingCleanup && _scPos.RemainingContracts <= 0) + { + lock (stateLock) { activePositions.TryRemove(_sc.Key, out _); } + SymmetryGuardForgetEntry(_sc.Key); + Print("[A2-2] Deferred PendingCleanup purge (follower stop terminal): " + _sc.Key); + } + break; + } + } + } + Print(string.Format("[SIMA] Follower order terminal: {0} on {1} ({2}) | Id={3}", order.Name, acctName, reason, order.OrderId)); RemoveGhostOrderRef(order, reason); } @@ -1028,9 +1081,13 @@ protected override void OnExecutionUpdate(Execution execution, string executionI else { // Position fully closed, cancel stop + // A2-2: Defer activePositions.TryRemove to broker-confirmed stop terminal state (Build 960) RequestStopCancelLifecycleSafe(entryName); - activePositions.TryRemove(entryName, out _); - SymmetryGuardForgetEntry(entryName); + PositionInfo closedPos; + if (activePositions.TryGetValue(entryName, out closedPos) && closedPos != null) + closedPos.PendingCleanup = true; + else + SymmetryGuardForgetEntry(entryName); // already gone -- clean up now } // V12.1101E [F-07]: Clear target ref only after broker confirms Filled. @@ -1078,6 +1135,7 @@ protected override void OnExecutionUpdate(Execution execution, string executionI { // Position fully closed by trim, cancel stop Print(string.Format("TRIM FLATTEN: Position {0} fully closed. Cancelling stop.", entryName)); + // A2-2: Defer activePositions.TryRemove to broker-confirmed stop terminal state (Build 960) RequestStopCancelLifecycleSafe(entryName); // Also clean up any pending replacements @@ -1086,8 +1144,11 @@ protected override void OnExecutionUpdate(Execution execution, string executionI Interlocked.Decrement(ref pendingReplacementCount); } - activePositions.TryRemove(entryName, out _); - SymmetryGuardForgetEntry(entryName); + PositionInfo trimPos; + if (activePositions.TryGetValue(entryName, out trimPos) && trimPos != null) + trimPos.PendingCleanup = true; + else + SymmetryGuardForgetEntry(entryName); // already gone -- clean up now } } } diff --git a/src/V12_002.Symmetry.cs b/src/V12_002.Symmetry.cs index fbc0d4a3..0c40e6b2 100644 --- a/src/V12_002.Symmetry.cs +++ b/src/V12_002.Symmetry.cs @@ -694,12 +694,8 @@ private void SymmetryGuardCascadeFollowerCleanup(string masterEntryName) pos.ExecutingAccount.Cancel(new[] { order }); else CancelOrder(order); - - // Build 930.1 P1: Direction-aware delta rollback. - // expectedPositions is signed (Long=+, Short=-). Cancelling a Short must add back. - string acctKey = pos.ExecutingAccount != null ? pos.ExecutingAccount.Name : Account.Name; - int delta = (pos.Direction == MarketPosition.Long) ? -pos.TotalContracts : pos.TotalContracts; - DeltaExpectedPositionLocked(ExpKey(acctKey), delta); + // A2-3: DeltaExpectedPositionLocked deferred to OnAccountOrderUpdate confirmed-cancel + // to prevent REAPER desync if the follower was microseconds from filling (Build 960 audit fix). } } } diff --git a/src/V12_002.cs b/src/V12_002.cs index cd1a420b..833fb71f 100644 --- a/src/V12_002.cs +++ b/src/V12_002.cs @@ -358,6 +358,15 @@ private class PositionInfo // Broker (Rithmic/Apex/Tradovate) uses this to cancel remaining orders when one fills, // protecting the position natively even during NT8 restarts. public string OcoGroupId; + + // Build 960 [A2-2]: Deferred metadata purge -- set true when stop cancel is requested on + // final-target/trim close. Actual activePositions.TryRemove deferred to OnAccountOrderUpdate + // or HandleOrderCancelled when broker confirms stop terminal state. + public bool PendingCleanup; + + // Build 960 [A3-3]: Circuit breaker counter for emergency flatten attempts from null stop submit. + // Incremented each call to FlattenPositionByName triggered by null stop. Halts after 3 failures. + public int FlattenAttemptCount; } private TargetMode GetTargetMode(int targetNumber) From 67e94d0872440f2f7dd1fd92bfb4bfb4d89af5a6 Mon Sep 17 00:00:00 2001 From: "AI M. Khalid" Date: Sun, 8 Mar 2026 15:57:06 -0700 Subject: [PATCH 4/4] fix(core): cycle 4 - queue drain, REAPER guards, flatten circuit breaker A3-1 (V12_002.SIMA.cs): - PumpFleetDispatch: hard guard drains queue if SIMA disabled or flatten running - ApplySimaState disable path: drain _pendingFleetDispatches after Reaper stop A3-2 (V12_002.REAPER.cs): - ExecuteReaperRepair: isFlattenRunning guard as first statement - Background REAPER thread: _repairInFlight.Add moved before TriggerCustomEvent to prevent double-enqueue in next audit cycle A3-3 (V12_002.Trailing.cs): - UpdateStopOrder null-stop path: FlattenAttemptCount circuit breaker (cap at 3) - UpdateStopOrder catch path: same circuit breaker guards emergency flatten - Reset FlattenAttemptCount to 0 on successful stop submission Build 960 Phase 2 Omni-Audit -- Area 3 complete. Co-Authored-By: Claude Sonnet 4.6 --- src/V12_002.REAPER.cs | 13 +++++++++--- src/V12_002.SIMA.cs | 15 ++++++++++++++ src/V12_002.Trailing.cs | 44 +++++++++++++++++++++++++++++++++++------ 3 files changed, 63 insertions(+), 9 deletions(-) diff --git a/src/V12_002.REAPER.cs b/src/V12_002.REAPER.cs index 3978398e..65ab4071 100644 --- a/src/V12_002.REAPER.cs +++ b/src/V12_002.REAPER.cs @@ -285,6 +285,8 @@ private bool AuditSingleFleetAccount(Account acct, bool shouldLog) if (!hasWorkingEntry) { if (shouldLog) Print($"[REAPER] * REPAIR CANDIDATE: {acct.Name} is Flat, expected={expectedQty}. Enqueuing repair."); + // A3-2: Mark in-flight BEFORE TriggerCustomEvent to block double-enqueue in next audit cycle (Build 960 audit fix) + lock (stateLock) { _repairInFlight.Add(repairKey); } _reaperRepairQueue.Enqueue(acct.Name); try { TriggerCustomEvent(o => ProcessReaperRepairQueue(), null); } catch { } } @@ -527,6 +529,13 @@ private void ProcessReaperRepairQueue() // Threading: runs on strategy thread (via TriggerCustomEvent). All stateLock usages unchanged. private void ExecuteReaperRepair(string accountName) { + // A3-2: Abort immediately if a flatten is in progress (Build 960 audit fix) + if (isFlattenRunning) + { + Print("[REAPER REPAIR] Aborted -- flatten in progress."); + return; + } + string repairKey = accountName + "_" + Instrument.FullName; try { @@ -632,9 +641,7 @@ private void ExecuteReaperRepair(string accountName) return; } - // 4. Mark in-flight to prevent double-repair - lock (stateLock) { _repairInFlight.Add(repairKey); } - + // 4. In-flight was already set on the background thread before TriggerCustomEvent (A3-2) try // Build 940 [FIX-2]: Inner try/finally guarantees _repairInFlight cleanup on all exit paths. { // 5. Re-issue entry order using the SIMA acct.CreateOrder + acct.Submit pattern diff --git a/src/V12_002.SIMA.cs b/src/V12_002.SIMA.cs index 61239ce3..5bbe1700 100644 --- a/src/V12_002.SIMA.cs +++ b/src/V12_002.SIMA.cs @@ -637,6 +637,15 @@ private void ExecuteSmartDispatchEntry(string tradeType, OrderAction action, int /// private void PumpFleetDispatch() { + // A3-1: Abort and drain queue if SIMA is disabled or flatten is running (Build 960 audit fix) + if (isFlattenRunning || !EnableSIMA) + { + FleetDispatchRequest stale; + while (_pendingFleetDispatches.TryDequeue(out stale)) { } + Print("[PUMP] Abort: SIMA inactive or flatten running. Queue drained."); + return; + } + if (!_pendingFleetDispatches.TryDequeue(out var req)) return; @@ -817,6 +826,12 @@ private void ApplySimaState(bool enabled) CancelAllV12GtcOrders(false); // [BUILD 948] GTC sweep before teardown -- skip accounts with open positions StopReaperAudit(); UnsubscribeFromFleetAccounts(); + // A3-1: Drain ghost dispatch queue on SIMA disable (Build 960 audit fix) + { + FleetDispatchRequest ignored; + while (_pendingFleetDispatches.TryDequeue(out ignored)) { } + Print("[SIMA] Dispatch queue cleared on shutdown."); + } Print("[SIMA LIFECYCLE] SIMA DISABLED -- Reaper stopped, handlers unsubscribed"); } EnableSIMA = enabled; diff --git a/src/V12_002.Trailing.cs b/src/V12_002.Trailing.cs index 5f5c02dd..5cb0e989 100644 --- a/src/V12_002.Trailing.cs +++ b/src/V12_002.Trailing.cs @@ -659,11 +659,29 @@ private void UpdateStopOrder(string entryName, PositionInfo pos, double newStopP pos.EntryPrice)); Print(string.Format("(!) Attempted stop price: {0:F2} | Current price: {1:F2}", validatedStopPrice, Close[0])); + // A3-3: Circuit breaker -- cap consecutive flatten attempts to 3 (Build 960 audit fix) + PositionInfo cbPos; + if (activePositions.TryGetValue(entryName, out cbPos) && cbPos != null) + { + cbPos.FlattenAttemptCount++; + if (cbPos.FlattenAttemptCount > 3) + { + Print(string.Format("[CIRCUIT BREAKER] Emergency flatten halted after 3 consecutive failures for {0}. Manual intervention required.", entryName)); + return; + } + } Print(string.Format("(!) Attempting emergency flatten for {0}...", entryName)); FlattenPositionByName(entryName); return; } + // A3-3: Reset circuit breaker counter on successful stop submission + { + PositionInfo cbReset; + if (activePositions.TryGetValue(entryName, out cbReset) && cbReset != null) + cbReset.FlattenAttemptCount = 0; + } + // A1-1: stopOrders final write inside stateLock (Build 960 audit fix) lock (stateLock) { stopOrders[entryName] = newStop; } pos.CurrentStopPrice = validatedStopPrice; @@ -678,15 +696,29 @@ private void UpdateStopOrder(string entryName, PositionInfo pos, double newStopP Print(string.Format("(!) ERROR UpdateStopOrder for {0}: {1}", entryName, ex.Message)); Print(string.Format("(!) POSITION MAY BE UNPROTECTED: {0} contracts", pos.RemainingContracts)); - // Attempt emergency flatten - try + // A3-3: Circuit breaker -- cap consecutive flatten attempts to 3 (Build 960 audit fix) + PositionInfo exCbPos; + bool flattenBlocked = false; + if (activePositions.TryGetValue(entryName, out exCbPos) && exCbPos != null) { - Print(string.Format("(!) Attempting emergency flatten for {0}...", entryName)); - FlattenPositionByName(entryName); + exCbPos.FlattenAttemptCount++; + if (exCbPos.FlattenAttemptCount > 3) + { + Print(string.Format("[CIRCUIT BREAKER] Emergency flatten halted after 3 consecutive failures for {0}. Manual intervention required.", entryName)); + flattenBlocked = true; + } } - catch (Exception flattenEx) + if (!flattenBlocked) { - Print(string.Format("(!)(!) EMERGENCY FLATTEN FAILED: {0}", flattenEx.Message)); + try + { + Print(string.Format("(!) Attempting emergency flatten for {0}...", entryName)); + FlattenPositionByName(entryName); + } + catch (Exception flattenEx) + { + Print(string.Format("(!)(!) EMERGENCY FLATTEN FAILED: {0}", flattenEx.Message)); + } } } }