Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 14 additions & 0 deletions .agent/standards_manifesto.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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]
Expand Down
43 changes: 43 additions & 0 deletions docs/brain/walkthrough.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Comment on lines +79 to +86
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

This section claims BUILD_TAG = "950" in the listed file changes, but the strategy BUILD_TAG was updated to 955 in this PR. Please update the walkthrough to reflect the correct build tag (or make the note build-agnostic) to avoid future confusion during verification/audits.

Suggested change
- 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"
- src/V12_002.cs -- TargetSnapshot class, PendingStopReplacement extended, BUILD_TAG = "955"
- 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 "[B955] Target T1 restored", "[B955] Target T2 restored"

Copilot uses AI. Check for mistakes.
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
77 changes: 15 additions & 62 deletions src/V12_001.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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.
}
Comment on lines 2999 to 3003
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

ConnectToStrategy() is now a no-op, but SendCommand() still spins a Task.Run and calls ConnectToStrategy() whenever isConnected == false. With IPC deprecated, this means every UI action that calls SendCommand() will continue to enqueue a no-op background task (threadpool churn) and isConnected will never become true. Consider short-circuiting SendCommand() (or ConnectToStrategy()) behind an explicit ipcEnabled flag and returning synchronously without Task.Run when IPC is disabled.

Copilot uses AI. Check for mistakes.

private void DisconnectFromStrategy()
Expand Down
1 change: 1 addition & 0 deletions src/V12_002.Entries.Retest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
49 changes: 47 additions & 2 deletions src/V12_002.Orders.Callbacks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

The follower stop-cancel match uses reference equality only (OldOrder == order). Elsewhere in this file (e.g., TryFindOrderInPosition) you already fall back to OrderId comparison to handle cases where the callback provides a different Order instance for the same broker order. Consider matching pending stop replacements by OrderId as well to avoid missing the replacement/restore path.

Suggested change
if (_psr.Value.OldOrder == order)
var _oldOrder = _psr.Value.OldOrder;
// Match by reference when possible, but fall back to OrderId equality to handle
// cases where the callback supplies a different Order instance for the same broker order.
if (_oldOrder == order
|| (_oldOrder != null
&& order != null
&& !string.IsNullOrEmpty(_oldOrder.OrderId)
&& string.Equals(_oldOrder.OrderId, order.OrderId, StringComparison.Ordinal)))

Copilot uses AI. Check for mistakes.
{
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;
}
}
Comment on lines +601 to +624
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

TryRemove is unconditional on activePositions.TryGetValue — asymmetric with master path

In the master path (HandleOrderCancelled, line 370), the pending record is only removed when activePositions.TryGetValue succeeds (the TryRemove is inside the combined if condition). Here in the follower path the TryRemove at line 620 is outside the activePositions.TryGetValue block, so the pending record is always cleaned up even when the position key is missing from activePositions.

If a position is cleaned up from activePositions (e.g., by the emergency flatten) just before this callback fires, the pending stop replacement is silently dropped without a new stop being issued and without any log entry. The V8.30 timeout that would normally catch a stale pending in the master path is bypassed because the pending no longer exists.

Adding a diagnostic log when the position is missing improves observability:

if (activePositions.TryGetValue(_psr.Key, out _rPos))
{
    // ... qty check and CreateNewStopOrder ...
}
else
{
    Print(string.Format("[B955] WARN: Follower stop cancel matched pending '{0}' but position not in activePositions -- pending cleaned up without replacement.", _psr.Key));
}
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);
}
Expand Down
Loading
Loading