diff --git a/FORENSIC_CASE_STUDY_923B.md b/FORENSIC_CASE_STUDY_923B.md new file mode 100644 index 00000000..cb918dea --- /dev/null +++ b/FORENSIC_CASE_STUDY_923B.md @@ -0,0 +1,31 @@ +# Forensic Case Study: Build 923B (Ghost Entry Fix) + +## πŸ•΅οΈ The Bug: Spontaneous Cancellations & Hallucinated REAPER Repairs +During live testing, the system exhibited "Ghost" behavior where follower accounts would fire orders that the Master account did not authorize, or would fail to cancel orders that were terminated by the Master. + +### 1. The Critical Failure: Identity Chain +The system suffered from a **Race Condition** in the `PropagateMasterEntryMove` and `PropagateMasterCancellation` logic. +- **Symptom:** The REAPER (Audit System) would detect a discrepancy between the broker state and the internal `expectedPositions`. +- **Root Cause:** If the Master order was cancelled *before* the Follower order was fully established in the internal dictionary, the cancellation would fail (Identity Chain break), leaving the Follower order active. +- **Ghost Repair:** The REAPER, seeing an active order in a Follower account that wasn't in its `expectedPositions` map, would attempt to "repair" it by either killing it incorrectly or, in some cases, re-issuing it if it thought a fill was missing. + +### 2. The REAPER Race Condition +The REAPER audit was firing while orders were still in a "Pending" state on the broker side. +- **Hallucination:** Because the broker confirmation lags the internal state update, the REAPER would occasionally "hallucinate" that a follower fill was missing and fire a market order to "sync" the accounts, resulting in double fills or unmanaged positions. + +## πŸ›‘οΈ The Fix: Build 923B (Forensic Ghost Repair) + +### Implementation 1: Create-before-Cancel +We hardened the `PropagateMasterEntryMove` logic. The system now ensures that any identity move or adjustment is fully registered in the fleet telemetry *before* any cancellation signal is sent. This prevents the "Orphaned Order" state. + +### Implementation 2: CascadeFleetFollowerCleanup +A new surgical method, `CascadeFleetFollowerCleanup`, was added. +- **Action:** When a Master order is cancelled, the strategy now performs a recursive sweep of the entire fleet to kill any "hallucinated" or orphaned follower entries that might be lingering due to network latency. + +### Implementation 3: Enhanced REAPER Grace +We implemented the `ReaperFillGraceTicks` (5-second window). +- **Hardening:** The REAPER is now strictly forbidden from performing any "Autosync" or "Repair" within 5 seconds of a fresh Master entry. This allows the broker's data-cycle to catch up and prevents the "Race to Market" that caused duplicate orders. + +## 🏁 Observation & Results +- **Status:** FIXED. +- **Verification:** Log audits confirm that `expectedPositions` remains stable during high-volatility price moves, and the "Ghost" orders have been eliminated. diff --git a/INFRASTRUCTURE_PROTOCOL.md b/INFRASTRUCTURE_PROTOCOL.md new file mode 100644 index 00000000..9440c94e --- /dev/null +++ b/INFRASTRUCTURE_PROTOCOL.md @@ -0,0 +1,52 @@ +# BMad Hardened Infrastructure Protocol + +**Purpose:** To eliminate the "Old Code" failure mode by ensuring a single, verifiable source of truth between the development environment and NinjaTrader 8. + +--- + +## πŸ›‘οΈ 1. The "One Source of Truth" (HardLink) +All strategy files in the NinjaTrader directory MUST be **HardLinks** to the files in `C:\WSGTA\universal-or-strategy\src`. +* **Verification Command (Run by AI at start of session):** + `fsutil hardlink list "C:\Users\Mohammed Khalid\Documents\NinjaTrader 8\bin\Custom\Strategies\UniversalORStrategyV12_002_Dev.cs"` +* **Success Criteria:** The command MUST return both the NinjaTrader path and the `C:\WSGTA` path. If it only returns one path, the link is broken. + +## πŸ› οΈ 2. Deployment Protocol +If the link is broken or files need refreshing, run the deployment script: +* **Script:** `C:\WSGTA\universal-or-strategy\deploy-sync.ps1` +* **Policy:** Never manually copy/paste code into the NinjaTrader editor. Always edit in the Repo and let the HardLink propagate. + +## 🏷️ 3. Version Traceability (Build Tags) +Every forensic or architectural change MUST be accompanied by an increment to the `BUILD_TAG` in `UniversalORStrategyV12_002_Dev.cs`. +* **Current Build:** `923B` +* **Audit Step:** On strategy start, verify the NinjaTrader Output window shows: + `πŸ›‘ BMad HARDENED DEPLOYMENT PROTOCOL ACTIVE | Build: 923B` + +## πŸ”Ž 4. Zero-Trust Policy +Never assume the code on GitHub is the code running in NinjaTrader. +1. **Always** verify the local Build Tag. +2. **Always** verify HardLinks using `fsutil` at the start of every session. +3. **Mandatory Script:** Use `deploy-sync.ps1` to restore or establish links. Manual copy-pasting is a violation of the Security Shield. + +## πŸ› οΈ 5. Live Repair Protocol (Emergency) +If a bug is detected during active trading: +1. **Repair Location:** Edit files ONLY in `C:\WSGTA\universal-or-strategy\src`. +2. **Build Increment:** Immediately increment the `BUILD_TAG` (e.g., `923B` -> `923C`). +3. **Propagation:** Save the file; the HardLink will update NinjaTrader's file instantly. +4. **User Action:** Instruct the user: "HardLink updated. Please press F5 in NinjaTrader to compile." +5. **Audit:** Verify the NinjaTrader Output window shows the NEW Build Tag. +6. **Commit:** Commit the repair to the current branch immediately, but **Hold Merge** until the session reset. + +## 🏁 6. Git & Pull Request Workflow +To maintain code integrity, all changes must be committed to a branch and merged via Pull Request using the GitHub CLI (`gh`). +1. **Branching:** Always work on a descriptive feature branch (e.g., `audit/full-codebase-review`). +2. **Commit Policy:** Every commit must reference the `BUILD_TAG`. +3. **PR Action:** When a build is verified, the AI must automatically prepare the PR: + * `git push origin [branch-name]` + * `gh pr create --title "Build [BUILD_TAG]: [Brief Description]" --body "Forensic Audit & Repairs for Build [BUILD_TAG]."` +4. **Merge Rule:** Do not merge into `main` during live trading. Merges should happen after the market close/reset. + +## 🏁 7. Handoff Requirements +When closing a coding session, the AI must confirm: +1. **Sync Status:** "All files HardLinked and verified via fsutil." +2. **Build Tag:** "Current code signature is [BUILD_TAG]." +3. **Compile Status:** "User advised to press F5 in NinjaTrader." diff --git a/src/UniversalORStrategyV12_002_Dev.Entries.OR.cs b/src/UniversalORStrategyV12_002_Dev.Entries.OR.cs index a72a579b..69403657 100644 --- a/src/UniversalORStrategyV12_002_Dev.Entries.OR.cs +++ b/src/UniversalORStrategyV12_002_Dev.Entries.OR.cs @@ -220,7 +220,11 @@ private void EnterORPosition(MarketPosition direction, double entryPrice, double { // 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."); + 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 _); + return; } entryOrders[entryName] = entryOrder; @@ -233,7 +237,11 @@ private void EnterORPosition(MarketPosition direction, double entryPrice, double // V12 SIMA: Dispatch to fleet (replaces legacy slave broadcast) if (EnableSIMA) { - ExecuteSmartDispatchEntry("OR", direction == MarketPosition.Long ? OrderAction.Buy : OrderAction.SellShort, contracts, entryPrice, OrderType.Limit); + // [923A-P0-OR]: StopMarket prevents immediate "marketable limit" fill. + // OR Long entry price is ABOVE current market; a Limit order there is immediately + // marketable on Apex/Tradovate (fills at current ask). StopMarket activates only + // when price actually reaches/breaks the OR High/Low β€” matching master behavior. + ExecuteSmartDispatchEntry("OR", direction == MarketPosition.Long ? OrderAction.Buy : OrderAction.SellShort, contracts, entryPrice, OrderType.StopMarket); } } catch (Exception ex) diff --git a/src/UniversalORStrategyV12_002_Dev.Orders.Callbacks.cs b/src/UniversalORStrategyV12_002_Dev.Orders.Callbacks.cs index 28801e44..1cc5020e 100644 --- a/src/UniversalORStrategyV12_002_Dev.Orders.Callbacks.cs +++ b/src/UniversalORStrategyV12_002_Dev.Orders.Callbacks.cs @@ -1199,6 +1199,11 @@ private void PropagateMasterPriceMove(Order masterOrder, double newLimit, double { if (!EnableSIMA || masterOrder == null || masterOrder.Account != this.Account) return; + // [BUILD 924 – Fix C] Raise propagation flag before dispatch; finally block clears it. + _propagationActive = true; + try + { + // --- Step 1: Identify master position and move type via object identity --- string masterEntryName = null; bool isEntryMove = false; @@ -1254,6 +1259,19 @@ private void PropagateMasterPriceMove(Order masterOrder, double newLimit, double if (masterEntryName == null) return; // Not a tracked master order // --- Step 2: Resolve follower entry names via Symmetry dispatch context --- + + // [BUILD 924 – Fix A] Derive master TradeType for type-strict fallback filter. + string masterTradeType = null; + if (activePositions.TryGetValue(masterEntryName, out var masterPosForType)) + { + if (masterPosForType.IsTRENDTrade) masterTradeType = "TREND"; + else if (masterPosForType.IsRMATrade) masterTradeType = "RMA"; + else if (masterPosForType.IsMOMOTrade) masterTradeType = "MOMO"; + else if (masterPosForType.IsFFMATrade) masterTradeType = "FFMA"; + else if (masterPosForType.IsRetestTrade) masterTradeType = "RETEST"; + else masterTradeType = "OR"; + } + IEnumerable followerEntryNames; if (symmetryMasterEntryToDispatch.TryGetValue(masterEntryName, out string dispatchId) && symmetryDispatchById.TryGetValue(dispatchId, out var ctx)) @@ -1264,11 +1282,26 @@ private void PropagateMasterPriceMove(Order masterOrder, double newLimit, double } else { - // Fallback: scan all active followers (no dispatch context available) + // [BUILD 924 – Fix A] Fallback now filters by TradeType β€” prevents TREND/RMA/OR + // price moves from broadcasting to non-related follower accounts. var fallback = new List(); foreach (var kvp in activePositions) - if (kvp.Value.IsFollower && kvp.Value.ExecutingAccount != null) + { + if (!kvp.Value.IsFollower || kvp.Value.ExecutingAccount == null) continue; + bool typeMatch = (masterTradeType == null) + || (masterTradeType == "TREND" && kvp.Value.IsTRENDTrade) + || (masterTradeType == "RMA" && kvp.Value.IsRMATrade) + || (masterTradeType == "MOMO" && kvp.Value.IsMOMOTrade) + || (masterTradeType == "FFMA" && kvp.Value.IsFFMATrade) + || (masterTradeType == "RETEST" && kvp.Value.IsRetestTrade) + || (masterTradeType == "OR" && !kvp.Value.IsTRENDTrade + && !kvp.Value.IsRMATrade + && !kvp.Value.IsMOMOTrade + && !kvp.Value.IsFFMATrade + && !kvp.Value.IsRetestTrade); + if (typeMatch) fallback.Add(kvp.Key); + } followerEntryNames = fallback; } @@ -1292,6 +1325,12 @@ private void PropagateMasterPriceMove(Order masterOrder, double newLimit, double else if (isTargetMove) PropagateMasterTargetMove(fleetEntryName, pos, masterTargetNum, newLimit); } + } // end try + finally + { + // [BUILD 924 – Fix C] Always clear propagation flag, even on exception. + _propagationActive = false; + } } /// @@ -1346,7 +1385,8 @@ private void PropagateMasterTargetMove(string fleetEntryName, PositionInfo pos, Order replacement = pos.ExecutingAccount.CreateOrder( Instrument, exitAction, OrderType.Limit, TimeInForce.Gtc, qty, roundedLimit, 0, - "MGT_" + DateTime.UtcNow.Ticks.ToString(), + // [923A-P1b-GUID]: 8-char GUID fragment replaces Ticks β€” eliminates collision risk at high resubmit frequency + "MGT_" + Guid.NewGuid().ToString("N").Substring(0, 8), signalName, null); pos.ExecutingAccount.Submit(new[] { replacement }); @@ -1383,9 +1423,20 @@ private void PropagateMasterEntryMove(string fleetEntryName, PositionInfo pos, d // [QTY-SYNC]: Scale master quantity for this follower. // Fallback to fEntry.Quantity if no quantity signal (pure price-change callback, or qty=0 noise). - int scaledQty = (newMasterQty > 0 && FleetParityMultiplier > 0) - ? (int)Math.Max(1L, (long)newMasterQty * FleetParityMultiplier) // [922Z-OVF]: long cast prevents int overflow before clamp - : fEntry.Quantity; + // [923A-P2a-OVF]: checked{} forces explicit OverflowException vs silent int truncation on extreme parity ratios + // (e.g., 1 NQ β†’ 10 MES with very high master qty). Clamps to maxContracts on overflow. + int scaledQty; + try + { + scaledQty = (newMasterQty > 0 && FleetParityMultiplier > 0) + ? checked((int)Math.Max(1L, (long)newMasterQty * FleetParityMultiplier)) // [922Z-OVF+923A]: long cast + checked int + : fEntry.Quantity; + } + catch (OverflowException) + { + Print(string.Format("[923A-OVF] Parity scalar overflow for {0} β€” clamping to maxContracts ({1})", fleetEntryName, maxContracts)); + scaledQty = maxContracts; + } bool priceChanged = Math.Abs(fEffectivePrice - roundedLimit) > tickSize / 2; bool quantityChanged = scaledQty != fEntry.Quantity; @@ -1420,7 +1471,9 @@ private void PropagateMasterEntryMove(string fleetEntryName, PositionInfo pos, d Order newEntry = pos.ExecutingAccount.CreateOrder( Instrument, entryAction, fEntry.OrderType, TimeInForce.Gtc, scaledQty, limitPx, stopPx, - "MGE_" + DateTime.UtcNow.Ticks.ToString(), + // [923A-P1-GUID]: 8-char hex GUID fragment replaces Ticks β€” eliminates collision risk + // at extreme resubmit frequency. ocoId only; signalName = fleetEntryName unchanged (GHOST-FIX-1). + "MGE_" + Guid.NewGuid().ToString("N").Substring(0, 8), signalName, null); pos.ExecutingAccount.Submit(new[] { newEntry }); diff --git a/src/UniversalORStrategyV12_002_Dev.Orders.Management.cs b/src/UniversalORStrategyV12_002_Dev.Orders.Management.cs index fe7763e5..ede2a483 100644 --- a/src/UniversalORStrategyV12_002_Dev.Orders.Management.cs +++ b/src/UniversalORStrategyV12_002_Dev.Orders.Management.cs @@ -43,10 +43,36 @@ private void SubmitBracketOrders(string entryName, PositionInfo pos) // Validate stop price double validatedStopPrice = ValidateStopPrice(pos.Direction, pos.InitialStopPrice); + // [BUILD 924 - Fix B] Route bracket submission to follower account when applicable. + bool isFollowerSubmit = pos.IsFollower && pos.ExecutingAccount != null; + OrderAction bracketExitAction = pos.Direction == MarketPosition.Long + ? OrderAction.Sell : OrderAction.BuyToCover; + // Submit initial stop for all contracts - Order stopOrder = pos.Direction == MarketPosition.Long - ? SubmitOrderUnmanaged(0, OrderAction.Sell, OrderType.StopMarket, pos.TotalContracts, 0, validatedStopPrice, "", "Stop_" + entryName) - : SubmitOrderUnmanaged(0, OrderAction.BuyToCover, OrderType.StopMarket, pos.TotalContracts, 0, validatedStopPrice, "", "Stop_" + entryName); + Order stopOrder; + if (isFollowerSubmit) + { + // [BUILD 924 - Fix B] Follower stop: use ExecutingAccount API (not SubmitOrderUnmanaged which is master-local) + string stopSig = SymmetryTrim("Stop_" + entryName, 40); + Order sOrd = pos.ExecutingAccount.CreateOrder( + Instrument, bracketExitAction, OrderType.StopMarket, TimeInForce.Gtc, + pos.TotalContracts, 0, validatedStopPrice, "", stopSig, null); + // [BUILD 924 - Fix B / Director's Note] Null-guard after CreateOrder matches S-001 pattern. + if (sOrd == null) + { + Print(string.Format("[BRACKET_FATAL] Follower stop CreateOrder returned null for {0}. Flattening.", entryName)); + FlattenPositionByName(entryName); + return; + } + pos.ExecutingAccount.Submit(new[] { sOrd }); + stopOrder = sOrd; + } + else + { + stopOrder = pos.Direction == MarketPosition.Long + ? SubmitOrderUnmanaged(0, OrderAction.Sell, OrderType.StopMarket, pos.TotalContracts, 0, validatedStopPrice, "", "Stop_" + entryName) + : SubmitOrderUnmanaged(0, OrderAction.BuyToCover, OrderType.StopMarket, pos.TotalContracts, 0, validatedStopPrice, "", "Stop_" + entryName); + } // V12.Audit [S-001]: Null-guard stop submission result. If broker rejects or drops // the stop, flatten immediately β€” never leave a position with a false "protected" state. @@ -89,9 +115,27 @@ private void SubmitBracketOrders(string entryName, PositionInfo pos) Print(string.Format("[FORENSIC] T{0} {1}: qty={2} price={3:F2} submitting limit", targetNum, entryName, targetQty, targetPrice)); - Order limitOrder = pos.Direction == MarketPosition.Long - ? SubmitOrderUnmanaged(0, OrderAction.Sell, OrderType.Limit, targetQty, targetPrice, 0, "", "T" + targetNum + "_" + entryName) - : SubmitOrderUnmanaged(0, OrderAction.BuyToCover, OrderType.Limit, targetQty, targetPrice, 0, "", "T" + targetNum + "_" + entryName); + Order limitOrder; + if (isFollowerSubmit) + { + // [BUILD 924 - Fix B] Follower target: use ExecutingAccount API + string targetSig = SymmetryTrim("T" + targetNum + "_" + entryName, 40); + Order tOrd = pos.ExecutingAccount.CreateOrder( + Instrument, bracketExitAction, OrderType.Limit, TimeInForce.Gtc, + targetQty, targetPrice, 0, "", targetSig, null); + // [BUILD 924 - Fix B / Director's Note] Null-guard after CreateOrder matches S-015 pattern. + if (tOrd != null) + pos.ExecutingAccount.Submit(new[] { tOrd }); + else + Print(string.Format("[TARGET_WARN] Follower target T{0} CreateOrder returned null for {1}.", targetNum, entryName)); + limitOrder = tOrd; + } + else + { + limitOrder = pos.Direction == MarketPosition.Long + ? SubmitOrderUnmanaged(0, OrderAction.Sell, OrderType.Limit, targetQty, targetPrice, 0, "", "T" + targetNum + "_" + entryName) + : SubmitOrderUnmanaged(0, OrderAction.BuyToCover, OrderType.Limit, targetQty, targetPrice, 0, "", "T" + targetNum + "_" + entryName); + } var targetDict = GetTargetOrdersDictionary(targetNum); // V12.Audit [S-015]: Only store non-null target orders. A null result means @@ -614,6 +658,14 @@ private void ManageCIT() if (activePositions.Count == 0 && entryOrders.Count == 0) return; if (string.IsNullOrEmpty(ChaseIfTouchPoints) || ChaseIfTouchPoints == "0") return; + // [BUILD 924 – Fix C] Suppress CIT during price-move propagation to prevent + // race-fire on freshly resubmitted follower limit orders before sync cycle completes. + if (_propagationActive) + { + Print("[CIT] Suppressed during price-move propagation (Build 924 Fix C)"); + return; + } + double citOffset = 0; if (!double.TryParse(ChaseIfTouchPoints, out citOffset)) return; diff --git a/src/UniversalORStrategyV12_002_Dev.SIMA.cs b/src/UniversalORStrategyV12_002_Dev.SIMA.cs index 207006ad..43e0e550 100644 --- a/src/UniversalORStrategyV12_002_Dev.SIMA.cs +++ b/src/UniversalORStrategyV12_002_Dev.SIMA.cs @@ -369,7 +369,17 @@ private void ExecuteSmartDispatchEntry(string tradeType, OrderAction action, int stopPrice = Instrument.MasterInstrument.RoundToTickSize(stopPrice); // V1102Q [PARITY-01]: Scale quantity for Micro accounts (e.g. ES->MES 10x parity) - int followerQty = Math.Max(1, quantity * FleetParityMultiplier); + // [923A-P2c-OVF]: checked{} prevents silent int overflow on parity multiply (cf. Callbacks.cs same pattern) + int followerQty; + try + { + followerQty = checked((int)Math.Max(1L, (long)quantity * FleetParityMultiplier)); + } + catch (OverflowException) + { + Print(string.Format("[923A-OVF] SIMA parity overflow qty={0} x mult={1} β€” clamping to maxContracts ({2})", quantity, FleetParityMultiplier, maxContracts)); + followerQty = maxContracts; + } // V12.40 FLEET PARITY: Use same distribution as Master (applied to scaled quantity) // FIX-B [Build 1102Z]: Pass dispatchTargetCount snapshot so all fleet accounts use the same @@ -947,6 +957,16 @@ private void ExecuteRMAEntryV2(double price, MarketPosition direction, int contr return; } + // [923B-FIX-A]: Zero-price guard β€” a Limit order at price=0 is treated as a Market order + // by Apex/Tradovate, causing an immediate fill without price ever touching the RMA level. + // Root cause: IPC path (UI.IPC.cs) can pass currentPrice=0 if lastKnownPrice<=0 AND + // Close[0] is not yet initialized (strategy just loaded, pre-session bars not formed). + if (price <= 0) + { + Print(string.Format("[RMA V2] ABORT: price={0:F2} is zero or negative. Refusing to submit Limit @ 0 β€” would fill as Market. Ensure lastKnownPrice is valid before dispatching.", price)); + return; + } + try { // Calculate stop and 5 targets using RMA profile. @@ -1066,9 +1086,10 @@ private void ExecuteRMAEntryV2(double price, MarketPosition direction, int contr } } + // [923B-FIX-B]: fleetKey declared outside try so catch can access it for dict rollback. + string fleetKey = acct.Name + "_RMA_" + baseSignal; try { - string fleetKey = acct.Name + "_RMA_" + baseSignal; SymmetryGuardRegisterFollower(symmetryDispatchId, fleetKey); string ocoId = fleetKey; @@ -1085,16 +1106,19 @@ private void ExecuteRMAEntryV2(double price, MarketPosition direction, int contr continue; } - // V12.Phase7 [C-02]: Reserve expectedPositions BEFORE Submit to eliminate Reaper race. - int delta = (direction == MarketPosition.Long) ? qty : -qty; - AddExpectedPositionDeltaLocked(ExpKey(acct.Name), delta); - - acct.Submit(new[] { fEntry }); - - // Register in unified dictionaries so CIT + trailing works for this account - entryOrders[fleetKey] = fEntry; + // [923B-FIX-B]: Phantom-Fix FIX-1 backport β€” register tracking dicts BEFORE + // updating expectedPositions. Mirrors the fix already applied to ExecuteSmartDispatchEntry + // (SIMA.cs Phantom-Fix comment at ~line 554). + // + // OLD (broken) order: expectedPositions FIRST β†’ Submit β†’ entryOrders/activePositions LAST. + // Race: REAPER background thread fires between steps 1 and 3, observes non-zero + // expectedPositions with no entry in entryOrders β†’ hasWorkingEntry=false + // β†’ phantom repair queued β†’ second Limit order submitted at same price + // β†’ original entry orphaned β†’ double fill or naked position on price touch. + // + // FIXED order: build PositionInfo β†’ register dicts atomically (stateLock) FIRST + // β†’ expectedPositions SECOND β†’ Submit LAST. // V12.1101E: Full 5-target distribution mirrors Master exactly. - // Build 1102X [T3]: Named local variable so role stamps can be applied after initializer. PositionInfo fleetFollowerPos = new PositionInfo { SignalName = fleetKey, @@ -1123,16 +1147,28 @@ private void ExecuteRMAEntryV2(double price, MarketPosition direction, int contr ExtremePriceSinceEntry = price, CurrentTrailLevel = 0 }; - activePositions[fleetKey] = fleetFollowerPos; + lock (stateLock) + { + activePositions[fleetKey] = fleetFollowerPos; // FIRST: dicts registered atomically + entryOrders[fleetKey] = fEntry; // REAPER hasWorkingEntry check reads these + } + + int delta = (direction == MarketPosition.Long) ? qty : -qty; + AddExpectedPositionDeltaLocked(ExpKey(acct.Name), delta); // SECOND: expectedPositions + + acct.Submit(new[] { fEntry }); // LAST β€” stateLock not held here // stopOrders/target1..target5 are set by follower bracket submission on fill fleetOk++; } catch (Exception ex) { - // V12.Phase7 [C-02]: Undo expectedPositions reservation if submission failed. + // [923B-FIX-B]: Full rollback β€” dicts were registered before expectedPositions, + // so both must be cleaned up on Submit failure (mirrors ExecuteSmartDispatchEntry catch). int rollbackDelta = (direction == MarketPosition.Long) ? -qty : qty; AddOrUpdateExpectedPositionLocked(ExpKey(acct.Name), rollbackDelta, v => Math.Max(0, v + rollbackDelta)); + activePositions.TryRemove(fleetKey, out _); + entryOrders.TryRemove(fleetKey, out _); Print($"[SIMA RMA V2] FAIL {acct.Name}: {ex.Message}"); } } diff --git a/src/UniversalORStrategyV12_002_Dev.UI.IPC.cs b/src/UniversalORStrategyV12_002_Dev.UI.IPC.cs index 9152d207..a99f04f8 100644 --- a/src/UniversalORStrategyV12_002_Dev.UI.IPC.cs +++ b/src/UniversalORStrategyV12_002_Dev.UI.IPC.cs @@ -794,6 +794,15 @@ private void ProcessIpcCommands() // Original single-account logic MarketPosition direction = action == "LONG" ? MarketPosition.Long : MarketPosition.Short; double currentPrice = lastKnownPrice > 0 ? lastKnownPrice : Close[0]; + // [923B-FIX-C]: Guard against zero price β€” Close[0] returns 0 if the strategy + // has just loaded and bars have not yet been initialized (pre-session or fresh attach). + // Passing currentPrice=0 to ExecuteRMAEntryV2 would submit a Limit @ 0, which + // Apex/Tradovate treats as a Market order β†’ instant fill without price touching level. + if (currentPrice <= 0) + { + Print("[IPC] ABORT RMA dispatch: currentPrice=0 β€” lastKnownPrice and Close[0] both invalid. Wait for first bar before sending RMA commands."); + break; + } double stopDist = CalculateATRStopDistance(RMAStopATRMultiplier); int contracts = CalculatePositionSize(stopDist); ExecuteRMAEntryV2(currentPrice, direction, contracts); diff --git a/src/UniversalORStrategyV12_002_Dev.UI.Sizing.cs b/src/UniversalORStrategyV12_002_Dev.UI.Sizing.cs index 052f2471..38cbdc3b 100644 --- a/src/UniversalORStrategyV12_002_Dev.UI.Sizing.cs +++ b/src/UniversalORStrategyV12_002_Dev.UI.Sizing.cs @@ -101,7 +101,15 @@ private int CalculatePositionSize(double stopDistanceRaw) double effectiveRisk = riskToUse - slippageCushionDollars; // STEP 2: FLOOR the quantity (never exceed $MaxRisk after slippage reserve) - int contracts = (int)Math.Floor(effectiveRisk / stopDollars); + // [923A-P2b-OVF]: checked{} guards against astronomically low stopDollars (near-zero ATR) + // producing a doubleβ†’int overflow. Clamps to maxContracts on overflow rather than silent wrap. + int contracts; + try { contracts = checked((int)Math.Floor(effectiveRisk / stopDollars)); } + catch (OverflowException) + { + Print($"[923A-OVF] Sizing overflow β€” stop={stopDollars:F4} effectiveRisk={effectiveRisk:F0} β€” clamping to maxContracts ({maxContracts})"); + contracts = maxContracts; + } // V12.Phase8.3: Diagnostic warning when ATR/Risk math produces 0 β€” makes risk-floor fallbacks visible if (contracts == 0) diff --git a/src/UniversalORStrategyV12_002_Dev.cs b/src/UniversalORStrategyV12_002_Dev.cs index c7800633..c04f1715 100644 --- a/src/UniversalORStrategyV12_002_Dev.cs +++ b/src/UniversalORStrategyV12_002_Dev.cs @@ -1,4 +1,4 @@ -// V12.12 FLEET SYMMETRY & SAFETY HARDENING - Single-Instance Multi-Account Copy Trading Engine +ο»Ώ// V12.12 FLEET SYMMETRY & SAFETY HARDENING - Single-Instance Multi-Account Copy Trading Engine // Based on UniversalORStrategyV10_3.cs (BUILD 1702) // SIMA Architecture: One strategy instance on Master account broadcasts to all Apex accounts // @@ -41,6 +41,8 @@ namespace NinjaTrader.NinjaScript.Strategies { public partial class UniversalORStrategyV12_002_Dev : Strategy { + public const string BUILD_TAG = "924"; // V12.924: Shield Hardening – Cross-Trade Price Move Repair + #region Variables // OR tracking @@ -234,6 +236,11 @@ private DateTime circuitBreakerActivatedTime private DateTime lastDailySummaryCheck = DateTime.MinValue; private readonly object dailySummaryLock = new object(); + // [BUILD 924 - Fix C] CIT suppression flag: set true during PropagateMasterPriceMove, + // cleared in finally block. Prevents CIT from market-firing freshly resubmitted follower + // limit entries before the propagation sync cycle completes. + private volatile bool _propagationActive = false; + // CIT (Chase If Touch) Ò€” uses ChaseIfTouchPoints property (NinjaScriptProperty) #endregion @@ -554,7 +561,7 @@ protected override void OnStateChange() { if (State == State.SetDefaults) { - Description = "Universal OR Strategy V12.12 - Build 1102K"; + Description = "Universal OR Strategy V12.12 - Build " + BUILD_TAG; Name = "UniversalORStrategyV12_002"; Calculate = Calculate.OnPriceChange; // CRITICAL FIX: Updates on every price tick for real-time trailing EntriesPerDirection = 10; @@ -781,6 +788,11 @@ protected override void OnStateChange() } else if (State == State.Realtime) { + Print("╔══════════════════════════════════════════════════════════════╗"); + Print("β•‘ πŸ›‘ BMad HARDENED DEPLOYMENT PROTOCOL ACTIVE β•‘"); + Print(string.Format("β•‘ Build: {0,-10} | Sync: ONE SOURCE OF TRUTH β•‘", BUILD_TAG)); + Print("β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•"); + // V12.2 HEADLESS SAFETY: Start core services even if ChartControl is null (for background execution) // EMERGENCY SAFE MODE (V12.32): Disabling background services to allow platform login StartIpcServer();