Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions FORENSIC_CASE_STUDY_923B.md
Original file line number Diff line number Diff line change
@@ -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.
52 changes: 52 additions & 0 deletions INFRASTRUCTURE_PROTOCOL.md
Original file line number Diff line number Diff line change
@@ -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:** `925`
* **Audit Step:** On strategy start, verify the NinjaTrader Output window shows:
`🛡 BMad HARDENED DEPLOYMENT PROTOCOL ACTIVE | Build: 925`

## 🔎 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."
12 changes: 10 additions & 2 deletions src/UniversalORStrategyV12_002_Dev.Entries.OR.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
Expand Down
89 changes: 81 additions & 8 deletions src/UniversalORStrategyV12_002_Dev.Orders.Callbacks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ private void ApplyTargetFill(

// V12.1101E [F-07]: Request stop cancellation without dropping dictionary state early.
// We only remove references after broker-confirmed terminal states.
// [BUILD 925 - P1 Fix]: Route follower stop cancels through pos.ExecutingAccount.Cancel()
// instead of the master-local CancelOrder() API. CancelOrder() is a NinjaScript-managed
// call that only works for orders submitted via SubmitOrderUnmanaged(). Fleet follower
// stops are submitted via acct.Submit(), so they require the broker-level Account.Cancel()
// API — identical to the pattern already proven correct in CleanupPosition() [BUG-2a].
private void RequestStopCancelLifecycleSafe(string entryName)
{
if (string.IsNullOrEmpty(entryName)) return;
Expand All @@ -98,7 +103,22 @@ private void RequestStopCancelLifecycleSafe(string entryName)
if (stopOrder.OrderState == OrderState.Working || stopOrder.OrderState == OrderState.Accepted
|| stopOrder.OrderState == OrderState.ChangePending || stopOrder.OrderState == OrderState.ChangeSubmitted)
{
CancelOrder(stopOrder);
// [BUILD 925 - P1 Fix]: Check if this is a fleet follower — use its account context.
bool isFollowerStop = activePositions.TryGetValue(entryName, out var posRef)
&& posRef != null && posRef.IsFollower && posRef.ExecutingAccount != null;

if (isFollowerStop)
{
// Fleet follower stop: must use Account API — CancelOrder() targets master account only.
Print(string.Format("[925-P1] Follower stop cancel routed via ExecutingAccount.Cancel() for {0} on {1}",
entryName, posRef.ExecutingAccount.Name));
posRef.ExecutingAccount.Cancel(new[] { stopOrder });
}
else
{
// Master/local stop: use the standard NinjaScript managed cancel.
CancelOrder(stopOrder);
}
return;
}

Expand Down Expand Up @@ -1199,6 +1219,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;
Expand Down Expand Up @@ -1254,6 +1279,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";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prioritize RETEST before RMA in master type detection

In PropagateMasterPriceMove, masterTradeType is derived by checking IsRMATrade before IsRetestTrade, which misclassifies RETEST trades running in RMA mode as RMA. RETEST positions are created with both flags true in Entries.Retest.cs (IsRMATrade can be true while IsRetestTrade is always true), so when symmetry dispatch context is missing, fallback propagation will target RMA followers and skip RETEST followers, causing missed price-sync updates and cross-strategy order moves in that recovery path.

Useful? React with 👍 / 👎.

else if (masterPosForType.IsMOMOTrade) masterTradeType = "MOMO";
else if (masterPosForType.IsFFMATrade) masterTradeType = "FFMA";
else if (masterPosForType.IsRetestTrade) masterTradeType = "RETEST";
else masterTradeType = "OR";
}

IEnumerable<string> followerEntryNames;
if (symmetryMasterEntryToDispatch.TryGetValue(masterEntryName, out string dispatchId) &&
symmetryDispatchById.TryGetValue(dispatchId, out var ctx))
Expand All @@ -1264,11 +1302,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<string>();
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Match fallback follower type without IsRMATrade overloading

In PropagateMasterPriceMove, the new fallback classifier uses IsRMATrade to identify RMA followers, but followers created by ExecuteSmartDispatchEntry are stamped with IsRMATrade = true even for non-RMA trades, so this filter misroutes updates whenever the symmetry dispatch mapping is unavailable (the exact path handled by this fallback). In that scenario, OR followers fail the OR predicate and can be incorrectly included in RMA propagation, causing missed OR price-sync updates and cross-strategy order modifications.

Useful? React with 👍 / 👎.

|| (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;
}

Expand All @@ -1292,6 +1345,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;
}
}

/// <summary>
Expand Down Expand Up @@ -1346,7 +1405,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 });
Expand Down Expand Up @@ -1383,9 +1443,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;
Expand Down Expand Up @@ -1420,7 +1491,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 });
Expand Down
Loading