diff --git a/docs/brain/implementation_plan.md b/docs/brain/implementation_plan.md index bea5ae92..4178b2d3 100644 --- a/docs/brain/implementation_plan.md +++ b/docs/brain/implementation_plan.md @@ -1,44 +1,410 @@ -# MISSION: B985 Phase 5 Distributed Pipeline Forensic Repair -**REPO:** universal-or-strategy -**BUILD TAG:** B985-CI-REPAIR -**BRANCH:** Current +# Implementation Plan: Phase 5 God Function Extraction Repairs -## 1. STRATEGIC ANALYSIS & PROOF OF FAILURE -**Qwen & GLM (OpenCode) Failure:** -The `anomalyco/opencode` action expects a rigid custom provider configuration structure in `opencode.json`. Furthermore, local testing confirms that the provided "free tier" API keys for DashScope (Qwen) and Zhipu (GLM) are returning HTTP 401 (Invalid Key) and HTTP 429 (Insufficient Balance) errors. Per Director's orders, since these models fail to operate on the free tier, their respective workflows will be purged to clean the CI pipeline. +**MISSION**: Phase 5 God Function Extraction Repairs +**BUILD_TAG**: 1111.006-phase-5-part-2 +**REPO**: universal-or-strategy +**BRANCH**: phase-5-part-2 -**Jules AI (Sovereign Auditor) Failure:** -The script in `.github/workflows/jules-pr-review.yml` has a hardcoded timeout of ~20 minutes (`maxAttempts = 40` at `30000`ms intervals). Jules forensic audits, particularly those traversing entire branches via `githubRepoContext`, can exceed this window and are being prematurely terminated. Since the Jules account is Pro-tier and active, repairing this is the primary focus. +## 1. STRATEGIC ANALYSIS & OBJECTIVE -## 2. STRUCTURAL REPAIR PLAN +The Phase 5 god-function extraction PR introduced six static-analysis +regressions (Codacy / DeepSource + Arena AI supplemental audit). All are +extraction artifacts: dead code, missing dedupe in extracted enqueue +helpers (Fleet AND Master), dropped validation in a switch-style handler, +mixed timezone usage, one redundant LINQ query (cache required), systemic +brace omissions, plus 3 verbatim Print logs dropped during the +`ExecuteTRENDEntry` extraction. -### Phase 1: Purge Broken "Free" Workflows -We will delete the workflows and configurations for the AI models that are failing authentication/billing checks. -1. **Remove `opencode.json`**: Delete the file from the repository root. -2. **Remove `.github/workflows/qwen-review.yml`**: Delete the Qwen workflow. -3. **Remove `.github/workflows/glm-review.yml`**: Delete the GLM workflow. +This plan executes surgical repairs ONLY -- no speculative refactor, +no new public surface, no whitespace mutation beyond the explicit +brace insertions in T6. -### Phase 2: Jules AI Timeout Mitigation -1. **Modify `.github/workflows/jules-pr-review.yml`**: - - Change the polling interval from `30000`ms to `60000`ms (60 seconds). - - Increase `maxAttempts` from `40` to `60` (providing a 60-minute timeout window at 60 seconds per poll). - - This prevents the premature termination of deep architectural audits while reducing API spam. +## 2. FORENSIC VERIFICATION -## 3. BMad V12 DNA & ASCII COMPLIANCE -- All edits to the YAML files will strictly adhere to the ASCII-only string requirement. -- There will be no `lock()` statements or non-ASCII characters introduced in any of the workflow scripts. +| ID | File | Line(s) | Evidence | +|---|------|---------|----------| +| F-01a | `src/V12_002.Entries.Trend.cs` | 71-75 vs 79-83 | Outer `CurrentBar < 20` guard returns; inner duplicate inside `try` is dead code | +| F-01b | `src/V12_002.Entries.Trend.cs` | 269, 620, 623 | `DateTime.Now` used; rest of codebase (`REAPER.Audit.cs` 18, 45, 122, 306) uses `DateTime.UtcNow` | +| F-02 | `src/V12_002.REAPER.Audit.cs` | 262-266 (Fleet), 449-453 (Master) | BOTH `EnqueueReaperFlattenCandidate` AND `EnqueueReaperMasterFlatten` unconditionally return `true`; callers (lines 141, 370) are guarded by `if` expecting dedupe. Master path MUST receive the same `_reaperFlattenInFlight` guard as Fleet -- no asymmetry permitted | +| F-03 | `src/V12_002.UI.IPC.Commands.Config.cs` | 138 | T1 writes `Target1Value = v` directly; T2-T5 (lines 141-175) gate via `ValidateIpcMultiplier` | +| F-04 | All four touched files | various | Single-line `if () return;` without braces flagged by Codacy | +| F-05 | `src/V12_002.REAPER.Audit.cs` | 53 vs 189 | `acct.Positions.FirstOrDefault(p => p.Instrument.FullName == Instrument.FullName)` called twice per audit loop -- result MUST be cached on first call and reused (single LINQ scan per tick) | +| F-06 | `src/V12_002.Entries.Trend.cs` | post-SubmitLeg2 in `ExecuteTRENDEntry` | 3 verbatim Print logs ("TREND ORDERS PLACED ..." + E1 details + E2 details) dropped during god-function extraction -- must be restored exactly as the pre-extraction implementation emitted them (operations greps + SOVEREIGN replay harness depend on these strings) | -## 4. DIRECTOR'S HANDOFF BLOCK (For P5 ENGINEER) +## 3. REPAIR PROTOCOL (V12 PLATINUM STANDARD) + +- Lock-free: `ConcurrentDictionary` + `TryAdd`/`TryRemove` only. NO `lock()`. +- Actor compliance: All mutations on existing strategy/marshal threads. No new `Enqueue` paths required. +- ASCII-only literals. +- Zero-allocation bias: dedupe via existing `_repairInFlight` byte-dict pattern; no new collection types. +- Whitespace mutation BANNED outside the explicit brace insertions in T6. +- Diff budget: stay comfortably under 150 KB per AGENTS.md. + +## 4. TICKET BACKLOG + +--- + +### T1 -- Eliminate dead `CurrentBar < 20` guard inside `try` + +**File**: `src/V12_002.Entries.Trend.cs` +**Method**: `ExecuteTRENDEntry` +**Action**: DELETE the inner duplicate guard at lines 79-83 (the +`if (CurrentBar < 20) { Print(...); return; }` block immediately +inside `try {`). The outer guard at lines 71-75 already exits +before `try` is entered. + +**Verify**: +- `grep -cn "CurrentBar < 20" src/V12_002.Entries.Trend.cs` == 1 +- Compiles clean. No new `lock(`. No public surface change. + +--- + +### T2 -- Replace `DateTime.Now` with `DateTime.UtcNow` in TREND ID generation + +**File**: `src/V12_002.Entries.Trend.cs` +**Edits**: +- Line 269 (`ExecuteTREND_CalculateLegs`): + `string timestamp = DateTime.Now.ToString("HHmmssffff");` -> + use `DateTime.UtcNow.ToString("HHmmssffff", System.Globalization.CultureInfo.InvariantCulture)`. +- Line 620 (`ExecuteTRENDManual_BuildPosition`): + `entryName = signalName + "_" + DateTime.Now.ToString("HHmmssffff");` -> + same UTC + invariant culture. +- Line 623 (`ExecuteTRENDManual_BuildPosition`): + `"TMNL_" + DateTime.Now.Ticks` -> `"TMNL_" + DateTime.UtcNow.Ticks`. + +Note: `using System.Globalization;` already present (line 11) -- no new using directive needed. + +**Verify**: +- `grep -n "DateTime.Now" src/V12_002.Entries.Trend.cs` == 0 hits. + +--- + +### T2b -- Restore 3 verbatim Print logs dropped during `ExecuteTRENDEntry` extraction (F-06) + +**File**: `src/V12_002.Entries.Trend.cs` +**Method**: `ExecuteTRENDEntry` +**Insertion point**: Immediately AFTER the successful +`ExecuteTREND_SubmitLeg2(...)` return-true path (currently between the +SubmitLeg2 call at line 121 and the `ExecuteTREND_DispatchSima(...)` call +at line 129), inside the existing `try` block. + +**Action**: Restore the 3 Print statements VERBATIM as the pre-extraction +god-function emitted them. They depend on locals already in scope after +`ExecuteTREND_CalculateLegs` returns via its `out` parameters +(`direction`, `totalContracts`, `entry1Qty`, `ema9Value`, `stop1Price`, +`TRENDEntry1ATRMultiplier`, `entry2Qty`, `ema15Value`, `stop2Price`, +`TRENDEntry2ATRMultiplier`): + +1. `Print(string.Format("TREND ORDERS PLACED: {0} Total={1} contracts", direction == MarketPosition.Long ? "LONG" : "SHORT", totalContracts));` +2. `Print(string.Format(" E1: {0}@{1:F2} (EMA9) | Stop: {2:F2} ({3}xATR from EMA9)", entry1Qty, ema9Value, stop1Price, TRENDEntry1ATRMultiplier));` +3. `Print(string.Format(" E2: {0}@{1:F2} (EMA15) | Stop: {2:F2} ({3}xATR trail)", entry2Qty, ema15Value, stop2Price, TRENDEntry2ATRMultiplier));` + +**Constraints**: +- Do NOT relocate these into `ExecuteTREND_CalculateLegs` -- the logs + must fire ONLY when both legs successfully submit (i.e., AFTER + SubmitLeg2 returns true), preserving the pre-extraction emission order + and semantic meaning ("orders placed" = both legs accepted by broker). +- Do NOT mutate any string literal (ASCII compliance + Arena AI verbatim + comparison gate -- byte-identical to the original god-function output). +- Two-space indentation prefix on lines 2 and 3 (" E1:" / " E2:") is + load-bearing for log post-processors and MUST be preserved verbatim. + +**Rationale (Arena AI F-06)**: The pre-extraction `ExecuteTRENDEntry` +emitted these 3 diagnostic lines after successful order placement. +Operations / Forensics greps (`grep "TREND ORDERS PLACED" logs/`) and +the SOVEREIGN replay harness depend on their presence and exact format. +Arena AI flagged them as missing in the Phase 5 PR -- a verbatim-fidelity +violation of the extraction protocol. + +**Verify**: +- `grep -cn "TREND ORDERS PLACED" src/V12_002.Entries.Trend.cs` == 1 +- `grep -cn "(EMA9) | Stop:" src/V12_002.Entries.Trend.cs` == 1 +- `grep -cn "(EMA15) | Stop:" src/V12_002.Entries.Trend.cs` == 1 +- All 3 Prints reside inside `ExecuteTRENDEntry` between the SubmitLeg2 + success return and `ExecuteTREND_DispatchSima` call (NOT inside any + `ExecuteTREND_*` sub-handler). + +--- + +### T3 -- Restore deduplication in flatten enqueue helpers (F-02: Fleet + Master parity) + +**Pattern reference**: `_repairInFlight` (`src/V12_002.REAPER.cs` line 28) ++ `EnqueueReaperRepairCandidate` (`src/V12_002.REAPER.Audit.cs` lines 236-260) ++ cleanup-in-finally (`src/V12_002.REAPER.Repair.cs` lines 222-225). + +**F-02 SCOPE NOTE (Arena AI emphasis)**: The dedupe guard MUST be applied +SYMMETRICALLY to BOTH enqueue helpers -- `EnqueueReaperFlattenCandidate` +(Fleet, step 3b) AND `EnqueueReaperMasterFlatten` (Master, step 3c). +Asymmetry here re-introduces the unbounded master-flatten re-enqueue +regression Arena AI flagged. Steps 3d/3e symmetrically clear the guard +for both Fleet and Master code paths -- no Master-side shortcut is +permitted. + +**Step 3a -- Add the in-flight guard field** +File: `src/V12_002.REAPER.cs` +Insertion point: immediately after the `_repairInFlight` declaration (line 28), +inside the same `#region V12 REAPER Audit Logic`. +Add a `private readonly ConcurrentDictionary _reaperFlattenInFlight` +initialized to a new empty dictionary, with comment +`// [Phase 5 Repair] Mirrors _repairInFlight to dedupe flatten enqueues across audit cycles.` + +**Step 3b -- Dedupe in `EnqueueReaperFlattenCandidate`** +File: `src/V12_002.REAPER.Audit.cs` (lines 262-266) +Replace body: +1. Compute `flattenKey = acct.Name + "_" + Instrument.FullName;`. +2. If `_reaperFlattenInFlight.TryAdd(flattenKey, 0)` returns `false`, + `return false;` (already in-flight; skip enqueue and skip caller's + `TriggerCustomEvent`). +3. Else `_reaperFlattenQueue.Enqueue(acct.Name); return true;`. + +**Step 3c -- Dedupe in `EnqueueReaperMasterFlatten`** +File: `src/V12_002.REAPER.Audit.cs` (lines 449-453) +Same body shape as 3b but using `Account.Name + "_" + Instrument.FullName` +and `_reaperFlattenQueue.Enqueue(Account.Name);`. + +**Step 3d -- Replace fragile `TryDequeue` rollback in caller catch handlers** +File: `src/V12_002.REAPER.Audit.cs` + +Caller 1 -- inside `AuditSingleFleetAccount`, the catch block at lines 144-151 +(reached when `TriggerCustomEvent` for fleet flatten throws). Remove the +`string _discarded; _reaperFlattenQueue.TryDequeue(out _discarded);` lines +and replace with +`_reaperFlattenInFlight.TryRemove(acct.Name + "_" + Instrument.FullName, out _);`. +Keep the existing Print message verbatim except change the trailing +`-- dequeued, will re-detect next cycle` to `-- in-flight cleared, will re-detect next cycle`. + +Caller 2 -- inside `AuditMasterAccountIfNeeded`, the catch block at lines 373-380. +Remove `string _mDiscarded; _reaperFlattenQueue.TryDequeue(out _mDiscarded);` +and replace with +`_reaperFlattenInFlight.TryRemove(Account.Name + "_" + Instrument.FullName, out _);`. +Apply the same Print-message tail change. + +**Step 3e -- Clear in-flight after the marshaled flatten completes** +File: `src/V12_002.REAPER.Audit.cs` +Method: `ProcessReaperFlattenQueue` (lines 479-505). +Inside the per-iteration `try { ... } catch { ... }` block, add a +`finally { _reaperFlattenInFlight.TryRemove(accountName + "_" + Instrument.FullName, out _); }` +clause so the guard is released on BOTH success and failure paths +(mirrors Repair.cs lines 222-225). + +**Rationale**: Without dedupe, every Reaper audit tick (subsecond cadence) +re-enqueues the same account, growing `_reaperFlattenQueue` without bound +and repeatedly issuing market-close orders. The `if` wrapping at lines 141 +and 370 was load-bearing, not stylistic. + +**Verify**: +- `grep -n "_reaperFlattenInFlight" src/` returns exactly 5 hits + (1 declaration in REAPER.cs + 2 `TryAdd` + 2 `TryRemove` + 1 `TryRemove` in finally = 6). + Adjust expected count to 6 if finally added. +- `grep -n "_reaperFlattenQueue.TryDequeue" src/V12_002.REAPER.Audit.cs` + returns ONLY the legitimate dequeue inside `ProcessReaperFlattenQueue` + (line ~483). Both caller-catch dequeues are gone. +- `grep -n "lock(" src/V12_002.REAPER*.cs` == 0 hits. + +--- + +### T4 -- Apply `ValidateIpcMultiplier` to T1 branch + +**File**: `src/V12_002.UI.IPC.Commands.Config.cs` +**Method**: `TryApplyConfigTarget_Value` (line 136) +**Action**: Rewrite the T1 branch (line 138) to mirror the T2 branch +shape (lines 140-148): `double.TryParse`, then call +`ValidateIpcMultiplier(v, out vmReason)`. On failure +`Print($"[IPC REJECT] T1 value {v} rejected: {vmReason}");`. +On success `Target1Value = v;`. Return `true` to preserve +the dispatch-table semantics. + +**Rationale**: T1 currently bypasses the domain guard, allowing +zero/negative multipliers to invert target prices (per +`ValidateIpcMultiplier` comment, `src/V12_002.UI.IPC.cs` lines 102-105). + +**Verify**: +- T1 branch now mirrors T2-T5 structure. +- `grep -cn "ValidateIpcMultiplier" src/V12_002.UI.IPC.Commands.Config.cs` >= 5 + (one per T1-T5 + STR). + +--- + +### T5 -- Cache `acct.Positions` lookup result in REAPER audit loop (F-05) + +**File**: `src/V12_002.REAPER.Audit.cs` +**Methods**: +- `AuditSingleFleetAccount` (line 51) -- queries + `acct.Positions.FirstOrDefault(p => p.Instrument.FullName == Instrument.FullName)` + at line 53. +- `AuditFleet_CalculateExpectedActual` (line 183) -- queries the SAME + predicate against the SAME `acct.Positions` enumerable at line 189. + +**Action (cache pattern -- single LINQ scan per audit tick)**: +1. Add `out Position pos` to the END of the + `AuditFleet_CalculateExpectedActual` parameter list (lines 183-188). + Inside, the existing line 189 `FirstOrDefault` becomes the SINGLE + source of truth -- assign its result into the new `out pos` parameter. +2. In `AuditSingleFleetAccount`, DELETE the line 53 query. Declare a + local `Position pos;` (cache slot) and pass it BY OUT to + `AuditFleet_CalculateExpectedActual` as the new last argument. +3. The downstream usage at line 166 + (`EnqueueReaperNakedStopCandidate(acct, pos, actualQty, expectedKey, shouldLog)`) + now reads from the cached `pos` -- no second `FirstOrDefault` + traversal of `acct.Positions`. + +**Rationale (Arena AI F-05)**: `acct.Positions` is a NinjaTrader broker +collection; iterating it twice per audit tick (subsecond cadence) doubles +the broker-side enumeration cost and adds GC pressure on the per-call +predicate delegate allocation. Caching the result honors the V12 +zero-allocation bias and matches the established cache pattern already +used in `AuditFleet_CheckWorkingStop` (line 271, `var orders = acct.Orders.ToArray();`). + +**Scope clarification**: F-05 covers ONLY the duplicate scan within the +fleet audit loop. `AuditMasterAccountIfNeeded` already performs a single +`Account.Positions.FirstOrDefault(...)` call at line 347 -- no caching +change required there. + +**Verify**: +- `grep -cn "acct.Positions.FirstOrDefault" src/V12_002.REAPER.Audit.cs` == 1 + (cached, single call site inside `AuditFleet_CalculateExpectedActual`). +- `grep -cn "Account.Positions.FirstOrDefault" src/V12_002.REAPER.Audit.cs` == 1 + (Master path, unchanged). +- Cached `pos` reaches `EnqueueReaperNakedStopCandidate` unchanged; audit + semantics are byte-identical to the pre-cache double-scan version. + +--- + +### T6 -- Brace standardization for single-line control structures + +**Scope**: ONLY the four files modified above. Do NOT touch other files. + +**Files**: +- `src/V12_002.Entries.Trend.cs` +- `src/V12_002.REAPER.cs` +- `src/V12_002.REAPER.Audit.cs` +- `src/V12_002.UI.IPC.Commands.Config.cs` + +**Action**: For every `if`, `else`, `else if`, `for`, `foreach`, `while`, +`do`, `using` statement whose body is a single statement WITHOUT braces, +wrap the body in `{ }` using the file's existing K&R Allman convention +(open brace on next line). Apply ONLY to violations Codacy already flags. + +**Codacy hot zones to fix** (non-exhaustive checklist): +- `Entries.Trend.cs`: 60, 67, 89, 120, 121, 140, 142, 528, 555, 572, 574. +- `REAPER.Audit.cs`: 27, 36, 80, 138, 140, 156, 232, 257, 365, 369, 416, + 434, 444, plus any new single-line returns introduced in T3. +- `UI.IPC.Commands.Config.cs`: 104, 111, 113, 116, 117, 192, 227, 228. + +**Diff hygiene constraint (AGENTS.md)**: +- Touch ONLY the lines that gain braces. Do NOT reflow indentation of + unaffected lines. Do NOT change line endings. +- Keep total PR diff under 150 KB. If brace insertion alone approaches + the limit, split into a follow-up PR (`phase-5-part-3-braces`) + and report immediately. + +**Verify**: +- Codacy "Always use braces" rule: 0 hits in the four files. +- `git diff --stat HEAD` consistent with brace-only adds (no whitespace + mutation in unrelated lines). + +--- + +## 5. VERIFICATION SEQUENCE (after ALL tickets) ```text -@ENGINEER (Codex/Jules) - P5 Surgical Execution +1. ASCII gate: + python check_ascii.py src/V12_002.Entries.Trend.cs ` + src/V12_002.REAPER.cs ` + src/V12_002.REAPER.Audit.cs ` + src/V12_002.UI.IPC.Commands.Config.cs + +2. Lock-free gate: + grep -rn "lock(" src/ -- must be zero hits in modified files + +3. Dead-code / timezone gate (T1, T2 -- F-01a / F-01b): + grep -cn "CurrentBar < 20" src/V12_002.Entries.Trend.cs -- 1 + grep -cn "DateTime.Now" src/V12_002.Entries.Trend.cs -- 0 + +3b. Verbatim log restoration gate (T2b -- F-06): + grep -cn "TREND ORDERS PLACED" src/V12_002.Entries.Trend.cs -- 1 + grep -cn "(EMA9) | Stop:" src/V12_002.Entries.Trend.cs -- 1 + grep -cn "(EMA15) | Stop:" src/V12_002.Entries.Trend.cs -- 1 + +4. Flatten dedupe gate (T3 -- F-02 -- Fleet AND Master): + grep -n "_reaperFlattenInFlight" src/ -- decl + Fleet TryAdd + Master TryAdd + 2 catch TryRemove + finally TryRemove + grep -cn "_reaperFlattenQueue.TryDequeue" src/V12_002.REAPER.Audit.cs -- 1 + (i.e., the only remaining TryDequeue is the legitimate one inside ProcessReaperFlattenQueue; + both caller-catch dequeues are gone and replaced with TryRemove on _reaperFlattenInFlight) + +5. Validation gate (T4 -- F-03): + grep -cn "ValidateIpcMultiplier" src/V12_002.UI.IPC.Commands.Config.cs -- >= 5 + +6. LINQ cache gate (T5 -- F-05): + grep -cn "acct.Positions.FirstOrDefault" src/V12_002.REAPER.Audit.cs -- 1 (Fleet, cached single source) + grep -cn "Account.Positions.FirstOrDefault" src/V12_002.REAPER.Audit.cs -- 1 (Master, unchanged) + +7. Hard-link sync (mandatory per AGENTS.md): + powershell -File .\deploy-sync.ps1 -- must EXIT 0 + +8. Build: + dotnet build .\Linting.csproj -- zero new errors / warnings + +9. Tests: + dotnet test .\Testing.csproj -- all green + +10. Lint pillar: + powershell -File .\scripts\lint.ps1 + Re-run Codacy / DeepSource locally if available; verify all five + regression categories close. +``` + +## 6. DIRECTOR'S HANDOFF BLOCK (For P5 ENGINEER -- Codex / Jules) + +```text +@ENGINEER (Codex / Jules) - P5 Surgical Execution +TASK: Phase 5 God Function Extraction Repairs +BUILD: 1111.006-phase-5-part-2 +BRANCH: phase-5-part-2 + +Execute tickets T1, T2, T2b, T3, T4, T5, T6 IN ORDER. Each ticket has a +Verify gate; do NOT proceed to the next ticket until the current Verify +gate passes. + +Arena AI emphasis (NON-NEGOTIABLE): + - T2b restores 3 verbatim Print logs in ExecuteTRENDEntry (F-06). + Strings must be byte-identical to the originals listed in the ticket. + - T3 applies the _reaperFlattenInFlight dedupe guard SYMMETRICALLY to + BOTH EnqueueReaperFlattenCandidate (Fleet) AND EnqueueReaperMasterFlatten + (Master) (F-02). No Master-side shortcut is permitted. + - T5 establishes a single-source cache for acct.Positions lookup -- + one FirstOrDefault per audit tick, plumbed via out Position pos (F-05). + +Touch ONLY: + src/V12_002.Entries.Trend.cs + src/V12_002.REAPER.cs + src/V12_002.REAPER.Audit.cs + src/V12_002.UI.IPC.Commands.Config.cs + +V12 Platinum constraints (NON-NEGOTIABLE): + - NO lock() additions. Use the existing _repairInFlight + ConcurrentDictionary pattern as the template. + - ASCII-only string literals (no Unicode, no curly quotes, no emoji). + - NO new public methods. NO new fields outside the single + _reaperFlattenInFlight added in T3a. + - NO whitespace mutation outside the explicit brace insertions + enumerated in T6. + - Diff under 150 KB total. If T6 alone overflows, split into a + follow-up PR (phase-5-part-3-braces) and report. -Please execute the following structural repairs: + 1. Re-run ALL Section 5 verification gates in order + (including new gate 3b for F-06 verbatim log restoration and the + Fleet/Master split in gates 4 and 6). + 2. powershell -File .\deploy-sync.ps1 -- hard-link sync (mandatory). + 3. powershell -File .\scripts\lint.ps1 -- Codacy / DeepSource close-out. + 4. Push to phase-5-part-2 and request CI re-run; confirm Arena AI + re-audit closes F-01a, F-01b, F-02 (Fleet+Master), F-03, F-04, F-05, + and F-06. -1. Delete the following files from the repository to purge the broken CI pipelines: - - `opencode.json` - - `.github/workflows/qwen-review.yml` - - `.github/workflows/glm-review.yml` -2. In `.github/workflows/jules-pr-review.yml`, update the `maxAttempts` variable in the polling loop to `60` and change the `setTimeout` interval from `30000` to `60000` (60 seconds) to allow for a 60-minute timeout window with less aggressive polling. -3. Once edits are complete, run `powershell -File .\deploy-sync.ps1` and verify the ASCII gate passes. -``` \ No newline at end of file +Report back: + - Per-ticket Verify gate output (one line per gate). + - Final grep counts for the six gates in Section 5. + - deploy-sync.ps1 exit code. + - dotnet build / test summary. + - Codacy / DeepSource issue delta vs prior CI run. +``` diff --git a/docs/brain/master_roadmap.md b/docs/brain/master_roadmap.md index bad03278..607e509b 100644 --- a/docs/brain/master_roadmap.md +++ b/docs/brain/master_roadmap.md @@ -2,8 +2,8 @@ ## Build-984-SourceHardening | 12 Repairs CONFIRMED LIVE -- COMPLIANCE PASS -**Last Synced**: 2026-05-06T01:12:00Z -**Protocol**: V14 Alpha | **Current Build**: 1111.005-v28.0-b984 +**Last Synced**: 2026-05-07T23:40:00Z +**Protocol**: V14 Alpha | **Current Build**: 1111.006-v28.0-b984-complete **Status**: 🟢 **READY FOR MERGE** (StyleCop & ASCII Gates PASS) **Active Branch**: `build-984-source-hardening` | **Last Stable PR**: #76 @@ -43,7 +43,8 @@ | **Phase 1** | Foundation (Monolith Partition -- 20+ partial files) | ✅ DONE | | **Phase 2** | Command Routing (IPC TCP + FSM + OCO Fix) | ✅ DONE | | **Phase 3** | Strategy Patterns (RAII + Resource Leak Remediation) | ✅ DONE | -| **Phase 4** | Event Lifecycle Dispatcher (ADR-020) | ✅ DONE -- Extraction confirmed live (2026-05-05) | +| **Phase 4** | Event Lifecycle Dispatcher (ADR-020) | ✅ DONE | +| **Phase 5** | Modularization (StickyState + Trend + UI/Photon IO Subgraphs) | ✅ DONE | --- @@ -151,11 +152,13 @@ | Signal | Status | | :--- | :--- | -| **Compilation** | [OK] `1111.005-v28.0-b984` -- CLEAN (NinjaTrader live confirmed 2026-05-05) | +| **Compilation** | [OK] `1111.006-v28.0-b984-complete` -- CLEAN (NinjaTrader live confirmed 2026-05-07, three sessions) | | **ASCII Gate** | [PASS] Zero non-ASCII violations | -| **Lock Audit** | [PASS] Zero `lock()` in `src/*.cs` (hardened regex) | -| **B984 Hardening** | [DONE] 11 repairs live (F-01 to F-12, F-09 waived), commit 159fb9a | -| **Phase 4 Extraction** | [DONE] 5 handlers live in `V12_002.Lifecycle.cs` (confirmed 2026-05-05) | +| **Lock Audit** | [PASS] Zero executable `lock()` in `src/*.cs` (hardened regex) | +| **StickyState Refactor** | [DONE] K0-K4 extractions live in `V12_002.StickyState.cs` (2026-05-07) | +| **Trend Refactor (T1-T3)** | [DONE] T1/T2/T3 extractions live in `V12_002.Entries.Trend.cs` (2026-05-07) | +| **UI/Photon IO Refactor (U1-U15)** | [DONE] U1-U15 extractions live across 7 UI/IPC files (2026-05-07) | +| **Phase 5 Status** | [COMPLETE] All three subgraphs done. God-function extraction mission closed. | | **RAII Leak Fix** | [DONE] `ClearDispatchSyncPending` injected (2 occurrences) | | **Hard Links** | [SYNCED] `deploy-sync.ps1` EXIT 0 | | **Risk Audit** | [PASS] Cases 1-7 pass, 8-9 idle (no live positions) | diff --git a/docs/brain/nexus_a2a.json b/docs/brain/nexus_a2a.json index 0325b02e..fe11e219 100644 --- a/docs/brain/nexus_a2a.json +++ b/docs/brain/nexus_a2a.json @@ -1,17 +1,17 @@ { - "mission": "Build-984 Source Hardening -- 12 Deferred Arena Findings (V12_002.Lifecycle.cs)", + "mission": "B984-COMPLETE Phase 5 - Session 5 (SIMA Subgraph Extraction)", "mission_status": "ACTIVE", "milestone": "M3", - "build_tag": "1111.004-v28.0-pr75-repairs", - "branch": "build-984-source-hardening", - "pr": "#76", + "build_tag": "1111.006-v28.0-b984-complete", + "branch": "main", + "pr": "#94", "plan_path": "docs/brain/implementation_plan.md", - "last_updated": "2026-05-05T18:13:00Z", + "last_updated": "2026-05-07T23:40:00Z", "morpheus_mode": true, "agent_readiness_target": "LEVEL_5", - "phase": "P3", - "current_phase": "B984_P3_WORKFLOW_HARDENING", - "status": "PR_INTELLIGENCE_SUITE_EXTENDED_QWEN_GLM_PRAGENT", + "phase": "P5", + "current_phase": "P5_SESSION_5_SIMA_SUBGRAPH", + "status": "SIMA_ENGINE_MODULARIZATION_GOD_FUNCTION_PARTITIONING", "agents": { "P1_orchestrator": "Antigravity", "P2_forensics": "Codex", @@ -25,132 +25,50 @@ { "phase": "P1_INTAKE", "status": "COMPLETE", - "timestamp": "2026-05-02T16:30:00Z" + "timestamp": "2026-05-06T18:00:00Z" }, { - "phase": "P2_FORENSICS", + "phase": "P5_SESSION_1_CI_REPAIR", "status": "COMPLETE", - "timestamp": "2026-05-02T16:35:00Z" + "timestamp": "2026-05-07T22:00:00Z" }, { - "phase": "P3_ARCHITECT_V1", - "status": "FAILED", - "timestamp": "2026-05-01T18:00:00Z", - "reason": "Null Fix -- empty try/finally, no actual cleanup" - }, - { - "phase": "P4_ADJUDICATION", - "status": "FAILED", - "timestamp": "2026-05-02T23:50:00Z", - "reason": "Type 2 Resource Leaks & Institutional Gaps" - }, - { - "phase": "ARENA_RETRO_AUDIT", - "status": "COMPLETE", - "timestamp": "2026-05-03T00:02:00Z", - "reason": "Null Fix confirmed 2/2 FAIL -- plan rejected" - }, - { - "phase": "P3_ARCHITECT_V2_RAII", + "phase": "P5_SESSION_4_STICKYSTATE_KERNEL", "status": "COMPLETE", - "timestamp": "2026-05-03T02:00:00Z" + "timestamp": "2026-05-07T23:30:00Z" }, { - "phase": "P5_ENGINEER_RAII", + "phase": "P5_SESSION_5_TREND_T1_T3", "status": "COMPLETE", - "timestamp": "2026-05-04T10:00:00Z" - }, - { - "phase": "P6_VALIDATOR_RAII", - "status": "PASS", - "timestamp": "2026-05-04T10:37:00Z", - "agent": "Gemini CLI" + "timestamp": "2026-05-08T00:26:00Z" }, { - "phase": "P5_PR76_REPAIRS", + "phase": "P5_SESSION_6_UI_PHOTON_IO_U1_U15", "status": "COMPLETE", - "timestamp": "2026-05-05T00:00:00Z", - "details": "D1/D2/D3/D6 confirmed live. BUILD_TAG bumped to pr75-repairs." + "timestamp": "2026-05-08T01:09:00Z" }, { - "phase": "P4_PHASE4_ARENA_AUDIT", - "status": "PASS", - "timestamp": "2026-05-04T17:30:00Z", - "details": "12 findings triaged as pre-existing source defects. Path A approved. All 7 extraction checklist items PASS." - }, - { - "phase": "P5_PHASE4_EXTRACTION", - "status": "CONFIRMED_LIVE", - "timestamp": "2026-05-05T18:10:00Z", - "details": "ProcessOnStateChange extracted into 5 handlers. Verified in live src/V12_002.Lifecycle.cs lines 93/220/302/404/451." - }, - { - "phase": "B984_P1_DECLARATION", - "status": "COMPLETE", - "timestamp": "2026-05-05T18:12:00Z", - "details": "Phase 4 declared complete by Director. Build-984 Source Hardening opened. 12 deferred findings scope confirmed." - }, - { - "phase": "B984_P3_WORKFLOW_HARDENING", - "status": "COMPLETE", - "timestamp": "2026-05-06T03:30:00Z", - "details": "Installed and configured 6-pillar CI suite: Dependency Review, OSV-Scanner, Codecov, MLC, Stale, Release Drafter. Secrets configured." - }, - { - "phase": "B984_P3_PR_INTELLIGENCE_SUITE", - "status": "COMPLETE", - "timestamp": "2026-05-06T17:38:00Z", - "details": "Installed Qwen Code Review, GLM OpenCode Review, and CodiumAI PR-Agent with V12 DNA (.pr_agent.toml)." - }, - { - "phase": "B984_P7_SENTINEL_MERGE", - "status": "COMPLETE", - "timestamp": "2026-05-06T17:53:00Z", - "details": "PR #80 Admin Merged into main. Redundant Jules PRs closed. Branch cleanup complete." + "phase": "PHASE_5_GOD_FUNCTION_EXTRACTION", + "status": "CLOSED", + "timestamp": "2026-05-08T01:09:00Z" } ], "current_blockers": [], "open_findings": { - "source": "Phase 4 Arena Audit (Codex 5.3, 2026-05-04)", - "count": 12, - "triage": "ALL pre-existing source defects in V12_002.Lifecycle.cs", - "known_confirmed": [ - { - "id": "F-01", - "severity": "MEDIUM", - "location": "V12_002.Lifecycle.cs:263-268 (OnStateChangeConfigure)", - "description": "FleetDispatchSlot layout invariant -- throws InvalidOperationException cold on Configure if struct padding changes" - }, - { - "id": "F-02", - "severity": "HIGH", - "location": "V12_002.Lifecycle.cs:345 (OnStateChangeDataLoaded)", - "description": "BarsArray[1] accessed without guard -- NullReferenceException if AddDataSeries did not complete in Configure" - }, - { - "id": "F-03", - "severity": "LOW", - "location": "V12_002.Lifecycle.cs:295-297 (OnStateChangeConfigure)", - "description": "AddDataSeries ordering concern -- called after other state setup, NT8 doc recommends SetDefaults" - }, - { - "id": "F-04", - "severity": "LOW", - "location": "V12_002.Lifecycle.cs:327-342 (OnStateChangeDataLoaded)", - "description": "Silent target count override -- ConfiguredTargetCount mutated in backward-compat path without logging" - }, - { - "id": "F-05 to F-12", - "severity": "TBD", - "location": "V12_002.Lifecycle.cs (various handlers)", - "description": "8 additional pre-existing defects identified by Arena auditor. To be formally catalogued by Claude (P3 Architect)." - } + "source": "Phase 5 Refactor Audit (Antigravity, 2026-05-07)", + "count": 5, + "triage": "SIMA Engine God Functions requiring partition", + "targets": [ + "ExecuteSmartDispatchEntry (CC=471)", + "HydrateWorkingOrdersFromBroker", + "HydrateFSMsFromWorkingOrders", + "EnumerateApexAccounts", + "HydrateExpectedPositionsFromBroker" ] }, "previous_missions": { - "Build-982-Phase2-RAII": "COMPLETE", - "Build-983-Phase3-PR75-Repairs": "COMPLETE", - "Build-983-Phase4-Dispatcher": "COMPLETE" + "Build-984-Source-Hardening": "COMPLETE", + "Phase-1-Monolith-Partition": "COMPLETE" }, - "nexus_relay_timestamp": "2026-05-05T18:13:00Z" + "nexus_relay_timestamp": "2026-05-07T22:19:00Z" } diff --git a/src/V12_002.Entries.Trend.cs b/src/V12_002.Entries.Trend.cs index 6642366f..744110f7 100644 --- a/src/V12_002.Entries.Trend.cs +++ b/src/V12_002.Entries.Trend.cs @@ -56,16 +56,106 @@ private double CalculateTRENDStopDistance() /// Entry 2 (2/3) at 15 EMA with 1.1x ATR trailing stop off EMA15 /// private void ExecuteTRENDEntry(int contracts) + { + if (!ExecuteTREND_Preflight(contracts)) + { + return; + } + + // V11: Trend RMA (9/15 Split) Mode + if (isTrendRmaMode) + { + Print(string.Format("V12.20: TREND Multiplier -> Mode=RMA (9/15 Split) ATR={0:F2}", currentATR)); + ExecuteTrendSplitEntry(contracts); + return; + } + + // V8.2: Ensure we have enough bars for EMA calculation + if (CurrentBar < 20) + { + Print("Cannot execute TREND entry - not enough bars (CurrentBar=" + CurrentBar + ")"); + return; + } + try + { + MarketPosition direction; + double currentPrice; + double ema9Value; + double ema15Value; + if (!ExecuteTREND_ResolveDirection(out currentPrice, out ema9Value, out ema15Value, out direction)) + { + return; + } + + int totalContracts; + int entry1Qty; + int entry2Qty; + string entry1Name; + string entry2Name; + double entry1Price; + double entry2Price; + double stop1Price; + double stop2Price; + PositionInfo pos1; + PositionInfo pos2; + ExecuteTREND_CalculateLegs( + contracts, + direction, + ema9Value, + ema15Value, + out totalContracts, + out entry1Qty, + out entry2Qty, + out entry1Name, + out entry2Name, + out entry1Price, + out entry2Price, + out stop1Price, + out stop2Price, + out pos1, + out pos2); + + Order entryOrder1; + if (!ExecuteTREND_SubmitLeg1(direction, entry1Qty, entry1Price, entry1Name, pos1, out entryOrder1)) + { + return; + } + if (!ExecuteTREND_SubmitLeg2(direction, entry2Qty, entry2Price, entry1Name, entry2Name, pos2, entryOrder1)) + { + return; + } + + Print(string.Format("TREND ORDERS PLACED: {0} Total={1} contracts", + direction == MarketPosition.Long ? "LONG" : "SHORT", totalContracts)); + Print(string.Format(" E1: {0}@{1:F2} (EMA9) | Stop: {2:F2} ({3}xATR from EMA9)", + entry1Qty, ema9Value, stop1Price, TRENDEntry1ATRMultiplier)); + Print(string.Format(" E2: {0}@{1:F2} (EMA15) | Stop: {2:F2} ({3}xATR trail)", + entry2Qty, ema15Value, stop2Price, TRENDEntry2ATRMultiplier)); + ExecuteTREND_DispatchSima(direction, totalContracts, currentPrice, entry1Name, entry2Name); + } + catch (Exception ex) + { + Print("ERROR ExecuteTRENDEntry: " + ex.Message); + } + } + + private bool ExecuteTREND_Preflight(int contracts) { // V12.Phase7 [C-09]: Compliance enforcement gate. - if (!IsOrderAllowed()) return; + if (!IsOrderAllowed()) + { + return false; + } // V12.Phase6 [FLATTEN-GUARD]: Prevent order submission during active flatten - if (isFlattenRunning) return; + if (isFlattenRunning) + { + return false; + } if (contracts <= 0) { Print(string.Format("[TREND] ExecuteTRENDEntry received invalid contracts={0}. Aborting entry.", contracts)); - return; + return false; } // V8.2 FIX: Only execute when on primary series (BarsInProgress=0) @@ -74,7 +164,7 @@ private void ExecuteTRENDEntry(int contracts) { pendingTRENDEntry = true; Print("TREND entry deferred to next primary bar update (BarsInProgress=" + BarsInProgress + ")"); - return; + return false; } // Clear pending flag since we're executing now @@ -83,227 +173,313 @@ private void ExecuteTRENDEntry(int contracts) if (!TRENDEnabled) { Print("TREND mode is disabled"); - return; + return false; } if (currentATR <= 0 || ema9 == null || ema15 == null) { Print("Cannot execute TREND entry - indicators not ready"); - return; + return false; } - // V11: Trend RMA (9/15 Split) Mode - if (isTrendRmaMode) - { - Print(string.Format("V12.20: TREND Multiplier -> Mode=RMA (9/15 Split) ATR={0:F2}", currentATR)); - ExecuteTrendSplitEntry(contracts); - return; - } + return true; + } - // V8.2: Ensure we have enough bars for EMA calculation - if (CurrentBar < 20) + private bool ExecuteTREND_ResolveDirection(out double currentPrice, out double ema9Value, out double ema15Value, out MarketPosition direction) + { + // Get current tick price for direction determination + currentPrice = lastKnownPrice > 0 ? lastKnownPrice : Close[0]; + + // V8.2: Use stored EMA instances (now guaranteed BarsInProgress=0) + if (ema9 == null || ema15 == null) { - Print("Cannot execute TREND entry - not enough bars (CurrentBar=" + CurrentBar + ")"); - return; + Print("Cannot execute TREND entry - EMA indicators not initialized"); + ema9Value = 0; + ema15Value = 0; + direction = MarketPosition.Flat; + return false; } - try - { - // V8.2: Simple check for enough bars - if (CurrentBar < 20) - { - Print("Cannot execute TREND entry - not enough bars (CurrentBar=" + CurrentBar + ")"); - return; - } - // Get current tick price for direction determination - double currentPrice = lastKnownPrice > 0 ? lastKnownPrice : Close[0]; + // V8.10: Use [0] (live tick) for real-time EMA values since Calculate.OnPriceChange updates EMAs on every tick + ema9Value = ema9[0]; + ema15Value = ema15[0]; - // V8.2: Use stored EMA instances (now guaranteed BarsInProgress=0) - if (ema9 == null || ema15 == null) - { - Print("Cannot execute TREND entry - EMA indicators not initialized"); - return; - } + // V8.10 DEBUG + Print(string.Format("TREND DEBUG: ema9[0]={0:F2} ema15[0]={1:F2} Price={2:F2}", ema9Value, ema15Value, currentPrice)); + Print(string.Format("TREND DEBUG: Close[0]={0:F2} CurrentBar={1} BarsInProgress={2}", + Close[0], CurrentBar, BarsInProgress)); - // V8.10: Use [0] (live tick) for real-time EMA values since Calculate.OnPriceChange updates EMAs on every tick - double ema9Value = ema9[0]; - double ema15Value = ema15[0]; + // Sanity check: EMAs should be different + if (Math.Abs(ema9Value - ema15Value) < tickSize * 2) + { + Print(string.Format("WARNING: EMAs very close ({0:F2} vs {1:F2})", ema9Value, ema15Value)); + } - // V8.10 DEBUG - Print(string.Format("TREND DEBUG: ema9[0]={0:F2} ema15[0]={1:F2} Price={2:F2}", ema9Value, ema15Value, currentPrice)); - Print(string.Format("TREND DEBUG: Close[0]={0:F2} CurrentBar={1} BarsInProgress={2}", - Close[0], CurrentBar, BarsInProgress)); + // Direction: EMA below price = LONG (buying pullback), EMA above = SHORT + if (ema9Value < currentPrice) + { + direction = MarketPosition.Long; + Print(string.Format("TREND: EMA9 below price ({0:F2} < {1:F2}) = LONG setup", ema9Value, currentPrice)); + } + else + { + direction = MarketPosition.Short; + Print(string.Format("TREND: EMA9 above price ({0:F2} > {1:F2}) = SHORT setup", ema9Value, currentPrice)); + } - // Sanity check: EMAs should be different - if (Math.Abs(ema9Value - ema15Value) < tickSize * 2) - { - Print(string.Format("WARNING: EMAs very close ({0:F2} vs {1:F2})", ema9Value, ema15Value)); - } + return true; + } - // Direction: EMA below price = LONG (buying pullback), EMA above = SHORT - MarketPosition direction; - if (ema9Value < currentPrice) - { - direction = MarketPosition.Long; - Print(string.Format("TREND: EMA9 below price ({0:F2} < {1:F2}) = LONG setup", ema9Value, currentPrice)); - } - else - { - direction = MarketPosition.Short; - Print(string.Format("TREND: EMA9 above price ({0:F2} > {1:F2}) = SHORT setup", ema9Value, currentPrice)); - } + private void ExecuteTREND_CalculateLegs( + int contracts, + MarketPosition direction, + double ema9Value, + double ema15Value, + out int totalContracts, + out int entry1Qty, + out int entry2Qty, + out string entry1Name, + out string entry2Name, + out double entry1Price, + out double entry2Price, + out double stop1Price, + out double stop2Price, + out PositionInfo pos1, + out PositionInfo pos2) + { + // V8.31: Both E1 and E2 now use ATR-based stops from live EMAs + double e1MultTrend = isTrendRmaMode ? RMAStopATRMultiplier : TRENDEntry1ATRMultiplier; + double e2MultTrend = isTrendRmaMode ? RMAStopATRMultiplier : TRENDEntry2ATRMultiplier; + Print(string.Format("V12.20: TREND Multiplier -> Mode={0} E1={1:F2}x E2={2:F2}x", + isTrendRmaMode ? "RMA" : "STD", e1MultTrend, e2MultTrend)); + + double e1StopDist = CalculateATRStopDistance(e1MultTrend); // V12.30: Ceiling-rounded + double e2StopDist = CalculateATRStopDistance(e2MultTrend); // V12.30: Ceiling-rounded + + // Weighted average stop distance for the group (used for logging only; sizing comes from caller) + double weightedStopDist = (e1StopDist * (1.0/3.0)) + (e2StopDist * (2.0/3.0)); + + totalContracts = contracts; + + // TREND-SPLIT-FIX: Strict floor -- E1 (EMA9) gets ?Total/3?, E2 (EMA15) gets remainder. + // Prevents risk budget overrun when Math.Ceiling pushes E1 past 1/3 of total contracts. + entry1Qty = Math.Max(1, totalContracts / 3); + entry2Qty = Math.Max(1, totalContracts - entry1Qty); + + // Final validation: totalContracts = sum of entries + totalContracts = entry1Qty + entry2Qty; + + Print(string.Format("TREND RISK: Risk=${0} | E1Stop={1:F2} | E2Stop={2:F2} | WeightedDist={3:F2} | TotalQty={4}", + MaxRiskAmount, e1StopDist, e2StopDist, weightedStopDist, totalContracts)); + Print(string.Format("TREND SPLIT: E1Qty={0} (1/3) | E2Qty={1} (2/3)", entry1Qty, entry2Qty)); + + string timestamp = DateTime.UtcNow.ToString("HHmmssffff", CultureInfo.InvariantCulture); + string trendGroupId = "TREND_" + timestamp; + entry1Name = trendGroupId + "_E1"; + entry2Name = trendGroupId + "_E2"; + + // V8.31: ENTRY 1: 1/3 at 9 EMA with ATR-based stop from live EMA9 + // V12.Phase6 [TICK-01]: Round EMA to valid tick increment before broker submission + entry1Price = Instrument.MasterInstrument.RoundToTickSize(ema9Value); + double e1AtrStop = CalculateATRStopDistance(e1MultTrend); // V12.30: Ceiling-rounded + stop1Price = Instrument.MasterInstrument.RoundToTickSize(direction == MarketPosition.Long + ? entry1Price - e1AtrStop // V8.31: Stop is 1.1x ATR below live EMA9 + : entry1Price + e1AtrStop); // V8.31: Stop is 1.1x ATR above live EMA9 + + // ENTRY 2: 2/3 at 15 EMA with ATR trailing stop + // V12.Phase6 [TICK-01]: Round EMA to valid tick increment before broker submission + entry2Price = Instrument.MasterInstrument.RoundToTickSize(ema15Value); + stop2Price = Instrument.MasterInstrument.RoundToTickSize(direction == MarketPosition.Long + ? entry2Price - CalculateATRStopDistance(e2MultTrend) + : entry2Price + CalculateATRStopDistance(e2MultTrend)); + + // Create position info for Entry 1 + pos1 = CreateTRENDPosition(entry1Name, direction, entry1Price, stop1Price, + entry1Qty, true, trendGroupId, isTrendRmaMode); + // Build 1102Y-V3 [LG-01]: Enforce staircase rule on E1. + ApplyTargetLadderGuard(pos1); + + // Create position info for Entry 2 + pos2 = CreateTRENDPosition(entry2Name, direction, entry2Price, stop2Price, + entry2Qty, false, trendGroupId, isTrendRmaMode); + // Build 1102Y-V3 [LG-01]: Enforce staircase rule on E2. + ApplyTargetLadderGuard(pos2); + } - // V8.31: Both E1 and E2 now use ATR-based stops from live EMAs - double e1MultTrend = isTrendRmaMode ? RMAStopATRMultiplier : TRENDEntry1ATRMultiplier; - double e2MultTrend = isTrendRmaMode ? RMAStopATRMultiplier : TRENDEntry2ATRMultiplier; - Print(string.Format("V12.20: TREND Multiplier -> Mode={0} E1={1:F2}x E2={2:F2}x", - isTrendRmaMode ? "RMA" : "STD", e1MultTrend, e2MultTrend)); - - double e1StopDist = CalculateATRStopDistance(e1MultTrend); // V12.30: Ceiling-rounded - double e2StopDist = CalculateATRStopDistance(e2MultTrend); // V12.30: Ceiling-rounded - - // Weighted average stop distance for the group (used for logging only; sizing comes from caller) - double weightedStopDist = (e1StopDist * (1.0/3.0)) + (e2StopDist * (2.0/3.0)); - - int totalContracts = contracts; - - // TREND-SPLIT-FIX: Strict floor -- E1 (EMA9) gets ?Total/3?, E2 (EMA15) gets remainder. - // Prevents risk budget overrun when Math.Ceiling pushes E1 past 1/3 of total contracts. - int entry1Qty = Math.Max(1, totalContracts / 3); - int entry2Qty = Math.Max(1, totalContracts - entry1Qty); - - // Final validation: totalContracts = sum of entries - totalContracts = entry1Qty + entry2Qty; - - Print(string.Format("TREND RISK: Risk=${0} | E1Stop={1:F2} | E2Stop={2:F2} | WeightedDist={3:F2} | TotalQty={4}", - MaxRiskAmount, e1StopDist, e2StopDist, weightedStopDist, totalContracts)); - Print(string.Format("TREND SPLIT: E1Qty={0} (1/3) | E2Qty={1} (2/3)", entry1Qty, entry2Qty)); - - string timestamp = DateTime.Now.ToString("HHmmssffff"); - string trendGroupId = "TREND_" + timestamp; - string entry1Name = trendGroupId + "_E1"; - string entry2Name = trendGroupId + "_E2"; - - // V8.31: ENTRY 1: 1/3 at 9 EMA with ATR-based stop from live EMA9 - // V12.Phase6 [TICK-01]: Round EMA to valid tick increment before broker submission - double entry1Price = Instrument.MasterInstrument.RoundToTickSize(ema9Value); - double e1AtrStop = CalculateATRStopDistance(e1MultTrend); // V12.30: Ceiling-rounded - double stop1Price = Instrument.MasterInstrument.RoundToTickSize(direction == MarketPosition.Long - ? entry1Price - e1AtrStop // V8.31: Stop is 1.1x ATR below live EMA9 - : entry1Price + e1AtrStop); // V8.31: Stop is 1.1x ATR above live EMA9 - - // ENTRY 2: 2/3 at 15 EMA with ATR trailing stop - // V12.Phase6 [TICK-01]: Round EMA to valid tick increment before broker submission - double entry2Price = Instrument.MasterInstrument.RoundToTickSize(ema15Value); - double stop2Price = Instrument.MasterInstrument.RoundToTickSize(direction == MarketPosition.Long - ? entry2Price - CalculateATRStopDistance(e2MultTrend) - : entry2Price + CalculateATRStopDistance(e2MultTrend)); - - // Create position info for Entry 1 - PositionInfo pos1 = CreateTRENDPosition(entry1Name, direction, entry1Price, stop1Price, - entry1Qty, true, trendGroupId, isTrendRmaMode); - // Build 1102Y-V3 [LG-01]: Enforce staircase rule on E1. - ApplyTargetLadderGuard(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); - - // Build 1102Y-V3 [MS-04a]: Register Master expected for E1 BEFORE submit. - int masterDeltaE1 = (direction == MarketPosition.Long) ? entry1Qty : -entry1Qty; - { var _aek966 = ExpKey(Account.Name); var _aed966 = (masterDeltaE1); Enqueue(ctx => ctx.AddExpectedPositionDeltaLocked(_aek966, _aed966)); } - - // Submit Entry 1 limit order - Order entryOrder1 = direction == MarketPosition.Long - ? 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) - { - { var _aek966 = ExpKey(Account.Name); var _aed966 = (-masterDeltaE1); Enqueue(ctx => ctx.AddExpectedPositionDeltaLocked(_aek966, _aed966)); } - Print("[ENTRY_ABORT] TREND E1 SubmitOrderUnmanaged NULL for " + entry1Name + " -- rolled back."); - return; - } - { var _en966 = entry1Name; var _p966 = pos1; var _eo966 = entryOrder1; - Enqueue(ctx => { ctx.activePositions[_en966] = _p966; ctx.entryOrders[_en966] = _eo966; }); } + private bool ExecuteTREND_SubmitLeg1(MarketPosition direction, int entry1Qty, double entry1Price, string entry1Name, PositionInfo pos1, out Order entryOrder1) + { + // Build 1102Y-V3 [MS-04a]: Register Master expected for E1 BEFORE submit. + int masterDeltaE1 = (direction == MarketPosition.Long) ? entry1Qty : -entry1Qty; + { var _aek966 = ExpKey(Account.Name); var _aed966 = (masterDeltaE1); Enqueue(ctx => ctx.AddExpectedPositionDeltaLocked(_aek966, _aed966)); } - // Only link the two legs after E1 is confirmed to have a live order handle. - linkedTRENDEntries[entry1Name] = entry2Name; - linkedTRENDEntries[entry2Name] = entry1Name; + // Submit Entry 1 limit order + entryOrder1 = direction == MarketPosition.Long + ? SubmitOrderUnmanaged(0, OrderAction.Buy, OrderType.Limit, entry1Qty, entry1Price, 0, "", entry1Name) + : SubmitOrderUnmanaged(0, OrderAction.SellShort, OrderType.Limit, entry1Qty, entry1Price, 0, "", entry1Name); - // Build 1102Y-V3 [MS-04b]: Register Master expected for E2 BEFORE submit. - int masterDeltaE2 = (direction == MarketPosition.Long) ? entry2Qty : -entry2Qty; - { var _aek966 = ExpKey(Account.Name); var _aed966 = (masterDeltaE2); Enqueue(ctx => ctx.AddExpectedPositionDeltaLocked(_aek966, _aed966)); } + // A1-1/A2-1: Null-abort rollback + stateLock wrap for E1 (Build 960 audit fix) + if (entryOrder1 == null) + { + { var _aek966 = ExpKey(Account.Name); var _aed966 = (-masterDeltaE1); Enqueue(ctx => ctx.AddExpectedPositionDeltaLocked(_aek966, _aed966)); } + Print("[ENTRY_ABORT] TREND E1 SubmitOrderUnmanaged NULL for " + entry1Name + " -- rolled back."); + return false; + } + { var _en966 = entry1Name; var _p966 = pos1; var _eo966 = entryOrder1; + Enqueue(ctx => { ctx.activePositions[_en966] = _p966; ctx.entryOrders[_en966] = _eo966; }); } - // Submit Entry 2 limit order - Order entryOrder2 = direction == MarketPosition.Long - ? SubmitOrderUnmanaged(0, OrderAction.Buy, OrderType.Limit, entry2Qty, entry2Price, 0, "", entry2Name) - : SubmitOrderUnmanaged(0, OrderAction.SellShort, OrderType.Limit, entry2Qty, entry2Price, 0, "", entry2Name); + return true; + } - // A1-1/A2-1: Null-abort rollback + stateLock wrap for E2 (Build 960 audit fix) - if (entryOrder2 == null) - { - { var _aek966 = ExpKey(Account.Name); var _aed966 = (-masterDeltaE2); Enqueue(ctx => ctx.AddExpectedPositionDeltaLocked(_aek966, _aed966)); } - // Remove partnership references; HandleOrderCancelled will teardown E1 state naturally. - string removedPartner; - linkedTRENDEntries.TryRemove(entry1Name, out removedPartner); - linkedTRENDEntries.TryRemove(entry2Name, out removedPartner); - if (entryOrder1 != null && !IsOrderTerminal(entryOrder1.OrderState)) CancelOrderSafe(entryOrder1, null); - Print("[ENTRY_ABORT] TREND E2 NULL -- E1 cancel issued for " + entry1Name + "; teardown deferred to cancel callback."); - return; - } - { var _en966 = entry2Name; var _p966 = pos2; var _eo966 = entryOrder2; - Enqueue(ctx => { ctx.activePositions[_en966] = _p966; ctx.entryOrders[_en966] = _eo966; }); } + private bool ExecuteTREND_SubmitLeg2(MarketPosition direction, int entry2Qty, double entry2Price, string entry1Name, string entry2Name, PositionInfo pos2, Order entryOrder1) + { + // Only link the two legs after E1 is confirmed to have a live order handle. + linkedTRENDEntries[entry1Name] = entry2Name; + linkedTRENDEntries[entry2Name] = entry1Name; - Print(string.Format("TREND ORDERS PLACED: {0} Total={1} contracts", - direction == MarketPosition.Long ? "LONG" : "SHORT", totalContracts)); - Print(string.Format(" E1: {0}@{1:F2} (EMA9) | Stop: {2:F2} ({3}xATR from EMA9)", - entry1Qty, ema9Value, stop1Price, TRENDEntry1ATRMultiplier)); - Print(string.Format(" E2: {0}@{1:F2} (EMA15) | Stop: {2:F2} ({3}xATR trail)", - entry2Qty, ema15Value, stop2Price, TRENDEntry2ATRMultiplier)); + // Build 1102Y-V3 [MS-04b]: Register Master expected for E2 BEFORE submit. + int masterDeltaE2 = (direction == MarketPosition.Long) ? entry2Qty : -entry2Qty; + { var _aek966 = ExpKey(Account.Name); var _aed966 = (masterDeltaE2); Enqueue(ctx => ctx.AddExpectedPositionDeltaLocked(_aek966, _aed966)); } - // V12.1: Smart Dispatch to SIMA Fleet - if (EnableSIMA) - { - // For Trend trades, followers get the full totalContracts qty split by the dispatcher - ExecuteSmartDispatchEntry( - "TREND", - direction == MarketPosition.Long ? OrderAction.Buy : OrderAction.SellShort, - totalContracts, - currentPrice, - OrderType.Limit, // 1102Z-A F1: followers use Limit to match leader pullback price - entry1Name, - entry2Name); - } + // Submit Entry 2 limit order + Order entryOrder2 = direction == MarketPosition.Long + ? SubmitOrderUnmanaged(0, OrderAction.Buy, OrderType.Limit, entry2Qty, entry2Price, 0, "", entry2Name) + : SubmitOrderUnmanaged(0, OrderAction.SellShort, OrderType.Limit, entry2Qty, entry2Price, 0, "", entry2Name); - // Deactivate TREND mode after placing orders - DeactivateTRENDMode(); + // A1-1/A2-1: Null-abort rollback + stateLock wrap for E2 (Build 960 audit fix) + if (entryOrder2 == null) + { + { var _aek966 = ExpKey(Account.Name); var _aed966 = (-masterDeltaE2); Enqueue(ctx => ctx.AddExpectedPositionDeltaLocked(_aek966, _aed966)); } + // Remove partnership references; HandleOrderCancelled will teardown E1 state naturally. + string removedPartner; + linkedTRENDEntries.TryRemove(entry1Name, out removedPartner); + linkedTRENDEntries.TryRemove(entry2Name, out removedPartner); + if (entryOrder1 != null && !IsOrderTerminal(entryOrder1.OrderState)) CancelOrderSafe(entryOrder1, null); + Print("[ENTRY_ABORT] TREND E2 NULL -- E1 cancel issued for " + entry1Name + "; teardown deferred to cancel callback."); + return false; } - catch (Exception ex) + { var _en966 = entry2Name; var _p966 = pos2; var _eo966 = entryOrder2; + Enqueue(ctx => { ctx.activePositions[_en966] = _p966; ctx.entryOrders[_en966] = _eo966; }); } + + return true; + } + + private void ExecuteTREND_DispatchSima(MarketPosition direction, int totalContracts, double currentPrice, string entry1Name, string entry2Name) + { + // V12.1: Smart Dispatch to SIMA Fleet + if (EnableSIMA) { - Print("ERROR ExecuteTRENDEntry: " + ex.Message); + // For Trend trades, followers get the full totalContracts qty split by the dispatcher + ExecuteSmartDispatchEntry( + "TREND", + direction == MarketPosition.Long ? OrderAction.Buy : OrderAction.SellShort, + totalContracts, + currentPrice, + OrderType.Limit, // 1102Z-A F1: followers use Limit to match leader pullback price + entry1Name, + entry2Name); } + + // Deactivate TREND mode after placing orders + DeactivateTRENDMode(); } private PositionInfo CreateTRENDPosition(string entryName, MarketPosition direction, double entryPrice, double stopPrice, int contracts, bool isEntry1, string groupId, bool isRma) + { + double target1Price; + double target2Price; + double target3Price; + double target4Price; + double target5Price; + int t1Qty; + int t2Qty; + int t3Qty; + int t4Qty; + int t5Qty; + CreateTRENDPosition_CalculateTargets( + direction, + entryPrice, + contracts, + out target1Price, + out target2Price, + out target3Price, + out target4Price, + out target5Price, + out t1Qty, + out t2Qty, + out t3Qty, + out t4Qty, + out t5Qty); + + return CreateTRENDPosition_BuildInfo( + entryName, + direction, + entryPrice, + stopPrice, + contracts, + isEntry1, + groupId, + isRma, + target1Price, + target2Price, + target3Price, + target4Price, + target5Price, + t1Qty, + t2Qty, + t3Qty, + t4Qty, + t5Qty); + } + + private void CreateTRENDPosition_CalculateTargets( + MarketPosition direction, + double entryPrice, + int contracts, + out double target1Price, + out double target2Price, + out double target3Price, + out double target4Price, + out double target5Price, + out int t1Qty, + out int t2Qty, + out int t3Qty, + out int t4Qty, + out int t5Qty) { // Universal Ladder: T(n)Type dropdown drives all target pricing. - double target1Price = CalculateTargetPrice(direction, entryPrice, 1); - double target2Price = CalculateTargetPrice(direction, entryPrice, 2); - double target3Price = CalculateTargetPrice(direction, entryPrice, 3); - double target4Price = CalculateTargetPrice(direction, entryPrice, 4); - double target5Price = CalculateTargetPrice(direction, entryPrice, 5); + target1Price = CalculateTargetPrice(direction, entryPrice, 1); + target2Price = CalculateTargetPrice(direction, entryPrice, 2); + target3Price = CalculateTargetPrice(direction, entryPrice, 3); + target4Price = CalculateTargetPrice(direction, entryPrice, 4); + target5Price = CalculateTargetPrice(direction, entryPrice, 5); - int t1Qty, t2Qty, t3Qty, t4Qty, t5Qty; GetTargetDistribution(contracts, out t1Qty, out t2Qty, out t3Qty, out t4Qty, out t5Qty); Print(string.Format("TREND POSITION: {0} contracts -> T1:{1} T2:{2} T3:{3} T4:{4} T5:{5}", contracts, t1Qty, t2Qty, t3Qty, t4Qty, t5Qty)); + } + private PositionInfo CreateTRENDPosition_BuildInfo( + string entryName, + MarketPosition direction, + double entryPrice, + double stopPrice, + int contracts, + bool isEntry1, + string groupId, + bool isRma, + double target1Price, + double target2Price, + double target3Price, + double target4Price, + double target5Price, + int t1Qty, + int t2Qty, + int t3Qty, + int t4Qty, + int t5Qty) + { var tPos = new PositionInfo { SignalName = entryName, @@ -359,88 +535,154 @@ private void DeactivateTRENDMode() /// Submits a single limit order at the manual price. /// private void ExecuteTRENDManualEntry(double manualPrice, MarketPosition direction, int contracts) + { + if (!ExecuteTRENDManual_Preflight(contracts)) + { + return; + } + + try + { + double entryPrice; + double stopPrice; + int t1Qty; + int t2Qty; + int t3Qty; + int t4Qty; + int t5Qty; + string entryName; + PositionInfo pos; + ExecuteTRENDManual_BuildPosition( + manualPrice, + direction, + contracts, + out entryPrice, + out stopPrice, + out t1Qty, + out t2Qty, + out t3Qty, + out t4Qty, + out t5Qty, + out entryName, + out pos); + + if (!ExecuteTRENDManual_SubmitEntry(direction, contracts, entryPrice, entryName, pos)) + { + return; + } + + Print(string.Format("V12.27 TREND_MANUAL: {0} {1}@{2:F2} LIMIT | Stop: {3:F2} | 100% Risk", + direction, contracts, entryPrice, stopPrice)); + Print(string.Format("V12.27 TREND_MANUAL TARGETS: T1:{0}@{1:F2} | T2:{2}@{3:F2} | T3:{4}@{5:F2} | T4:{6}@{7:F2} | T5:{8}@{9:F2}", + t1Qty, pos.Target1Price, t2Qty, pos.Target2Price, t3Qty, pos.Target3Price, t4Qty, pos.Target4Price, t5Qty, pos.Target5Price)); + ExecuteTRENDManual_DispatchSima(direction, contracts, entryPrice, entryName); + } + catch (Exception ex) + { + Print("ERROR ExecuteTRENDManualEntry: " + ex.Message); + } + } + + private bool ExecuteTRENDManual_Preflight(int contracts) { // V12.Phase7 [C-09]: Compliance enforcement gate. - if (!IsOrderAllowed()) return; + if (!IsOrderAllowed()) + { + return false; + } // V12.Phase6 [FLATTEN-GUARD]: Prevent order submission during active flatten - if (isFlattenRunning) return; + if (isFlattenRunning) + { + return false; + } if (contracts <= 0) { Print(string.Format("[TREND] ExecuteTRENDManualEntry received invalid contracts={0}. Aborting entry.", contracts)); - return; + return false; } if (currentATR <= 0) { Print("V12.27 TREND_MANUAL: Ignored - ATR not available"); - return; + return false; } - try - { - double entryPrice = Instrument.MasterInstrument.RoundToTickSize(manualPrice); + return true; + } - // V12.27: 100% risk allocation - single position at manual price - // Stop uses RMA multiplier (Trend RMA Mode forced) - double stopDistance = CalculateATRStopDistance(RMAStopATRMultiplier); // V12.30: Ceiling-rounded - // V12.Phase6 [TICK-01]: All prices rounded to valid tick increments - double stopPrice = Instrument.MasterInstrument.RoundToTickSize(direction == MarketPosition.Long - ? entryPrice - stopDistance - : entryPrice + stopDistance); + private void ExecuteTRENDManual_BuildPosition( + double manualPrice, + MarketPosition direction, + int contracts, + out double entryPrice, + out double stopPrice, + out int t1Qty, + out int t2Qty, + out int t3Qty, + out int t4Qty, + out int t5Qty, + out string entryName, + out PositionInfo pos) + { + entryPrice = Instrument.MasterInstrument.RoundToTickSize(manualPrice); - // V12.27: 100% risk - full position size supplied by caller (no split) - int t1Qty, t2Qty, t3Qty, t4Qty, t5Qty; - GetTargetDistribution(contracts, out t1Qty, out t2Qty, out t3Qty, out t4Qty, out t5Qty); + // V12.27: 100% risk allocation - single position at manual price + // Stop uses RMA multiplier (Trend RMA Mode forced) + double stopDistance = CalculateATRStopDistance(RMAStopATRMultiplier); // V12.30: Ceiling-rounded + // V12.Phase6 [TICK-01]: All prices rounded to valid tick increments + stopPrice = Instrument.MasterInstrument.RoundToTickSize(direction == MarketPosition.Long + ? entryPrice - stopDistance + : entryPrice + stopDistance); - string signalName = direction == MarketPosition.Long ? "TrendMnlLong" : "TrendMnlShort"; - string entryName = signalName + "_" + DateTime.Now.ToString("HHmmssffff"); + // V12.27: 100% risk - full position size supplied by caller (no split) + GetTargetDistribution(contracts, out t1Qty, out t2Qty, out t3Qty, out t4Qty, out t5Qty); - PositionInfo pos = CreateTRENDPosition(entryName, direction, entryPrice, stopPrice, - contracts, true, "TMNL_" + DateTime.Now.Ticks, true); + string signalName = direction == MarketPosition.Long ? "TrendMnlLong" : "TrendMnlShort"; + entryName = signalName + "_" + DateTime.UtcNow.ToString("HHmmssffff", CultureInfo.InvariantCulture); - // Build 1102Y-V3 [LG-01]: Enforce staircase rule. - ApplyTargetLadderGuard(pos); + pos = CreateTRENDPosition(entryName, direction, entryPrice, stopPrice, + contracts, true, "TMNL_" + DateTime.UtcNow.Ticks, true); - // Build 1102Y-V3 [MS-05]: Register Master expected BEFORE submit. - int masterDeltaTMNL = (direction == MarketPosition.Long) ? contracts : -contracts; - { var _aek966 = ExpKey(Account.Name); var _aed966 = (masterDeltaTMNL); Enqueue(ctx => ctx.AddExpectedPositionDeltaLocked(_aek966, _aed966)); } + // Build 1102Y-V3 [LG-01]: Enforce staircase rule. + ApplyTargetLadderGuard(pos); + } - // Submit LIMIT order at manual price - 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); + private bool ExecuteTRENDManual_SubmitEntry(MarketPosition direction, int contracts, double entryPrice, string entryName, PositionInfo pos) + { + // Build 1102Y-V3 [MS-05]: Register Master expected BEFORE submit. + int masterDeltaTMNL = (direction == MarketPosition.Long) ? contracts : -contracts; + { var _aek966 = ExpKey(Account.Name); var _aed966 = (masterDeltaTMNL); Enqueue(ctx => ctx.AddExpectedPositionDeltaLocked(_aek966, _aed966)); } - // A1-1/A2-1: Null-abort rollback + stateLock wrap (Build 960 audit fix) - if (entryOrder == null) - { - { var _aek966 = ExpKey(Account.Name); var _aed966 = (-masterDeltaTMNL); Enqueue(ctx => ctx.AddExpectedPositionDeltaLocked(_aek966, _aed966)); } - Print("[ENTRY_ABORT] TRENDManual SubmitOrderUnmanaged NULL for " + entryName + " -- rolled back."); - return; - } - { var _en966ap = entryName; var _p966ap = pos; Enqueue(ctx => { ctx.activePositions[_en966ap] = _p966ap; }); } - { var _en966 = entryName; var _eo966 = entryOrder; Enqueue(ctx => { ctx.entryOrders[_en966] = _eo966; }); } + // Submit LIMIT order at manual price + 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); - Print(string.Format("V12.27 TREND_MANUAL: {0} {1}@{2:F2} LIMIT | Stop: {3:F2} | 100% Risk", - direction, contracts, entryPrice, stopPrice)); - Print(string.Format("V12.27 TREND_MANUAL TARGETS: T1:{0}@{1:F2} | T2:{2}@{3:F2} | T3:{4}@{5:F2} | T4:{6}@{7:F2} | T5:{8}@{9:F2}", - t1Qty, pos.Target1Price, t2Qty, pos.Target2Price, t3Qty, pos.Target3Price, t4Qty, pos.Target4Price, t5Qty, pos.Target5Price)); + // A1-1/A2-1: Null-abort rollback + stateLock wrap (Build 960 audit fix) + if (entryOrder == null) + { + { var _aek966 = ExpKey(Account.Name); var _aed966 = (-masterDeltaTMNL); Enqueue(ctx => ctx.AddExpectedPositionDeltaLocked(_aek966, _aed966)); } + Print("[ENTRY_ABORT] TRENDManual SubmitOrderUnmanaged NULL for " + entryName + " -- rolled back."); + return false; + } + { var _en966ap = entryName; var _p966ap = pos; Enqueue(ctx => { ctx.activePositions[_en966ap] = _p966ap; }); } + { var _en966 = entryName; var _eo966 = entryOrder; Enqueue(ctx => { ctx.entryOrders[_en966] = _eo966; }); } - if (EnableSIMA) - { - ExecuteSmartDispatchEntry( - "TREND_MNL", - direction == MarketPosition.Long ? OrderAction.Buy : OrderAction.SellShort, - contracts, - entryPrice, - OrderType.Limit, - entryName); - } + return true; + } - } - catch (Exception ex) + private void ExecuteTRENDManual_DispatchSima(MarketPosition direction, int contracts, double entryPrice, string entryName) + { + if (EnableSIMA) { - Print("ERROR ExecuteTRENDManualEntry: " + ex.Message); + ExecuteSmartDispatchEntry( + "TREND_MNL", + direction == MarketPosition.Long ? OrderAction.Buy : OrderAction.SellShort, + contracts, + entryPrice, + OrderType.Limit, + entryName); } } diff --git a/src/V12_002.REAPER.Audit.cs b/src/V12_002.REAPER.Audit.cs index f69949e2..1b05c531 100644 --- a/src/V12_002.REAPER.Audit.cs +++ b/src/V12_002.REAPER.Audit.cs @@ -24,7 +24,10 @@ private void AuditApexPositions() if (IsFleetAccount(acct)) { auditedCount++; - if (AuditSingleFleetAccount(acct, shouldLog)) activeCount++; + if (AuditSingleFleetAccount(acct, shouldLog)) + { + activeCount++; + } } } @@ -33,15 +36,22 @@ private void AuditApexPositions() if (!masterAudited) { auditedCount++; - if (AuditMasterAccountIfNeeded(shouldLog)) activeCount++; + if (AuditMasterAccountIfNeeded(shouldLog)) + { + activeCount++; + } } if (shouldLog) { if (activeCount == 0) + { Print($"[REAPER] Heartbeat: All {auditedCount} accounts flat."); + } else + { Print($"[REAPER] Heartbeat: {activeCount}/{auditedCount} accounts with positions."); + } lastReaperLog = DateTime.UtcNow; } } @@ -50,51 +60,26 @@ private void AuditApexPositions() // Returns true if the account has non-zero state (for heartbeat counter). private bool AuditSingleFleetAccount(Account acct, bool shouldLog) { - Position pos = acct.Positions.FirstOrDefault(p => p.Instrument.FullName == Instrument.FullName); - int actualQty = 0; - if (pos != null && pos.MarketPosition != MarketPosition.Flat) - actualQty = pos.MarketPosition == MarketPosition.Long ? pos.Quantity : -pos.Quantity; - - // Build 1105: FSM is the SOLE authority for follower expected position. - var accountFsms = _followerBrackets.Values.Where(f => f.AccountName == acct.Name).ToList(); - int fsmExpectedQty = GetFsmExpectedPosition(acct.Name); - - // Handle hydrated Active FSMs with no order reference (restart edge case) - foreach (var f in accountFsms) - { - if (f.State == FollowerBracketState.Active && f.EntryOrder == null) - { - if (actualQty != 0) - { - fsmExpectedQty += actualQty; - } - else - { - FollowerBracketFSM staleFsm; - if (TryTerminateFollowerBracket(f.EntryName, out staleFsm)) - { - Print(string.Format("[REAPER-C7] Stale Active FSM for {0} on {1} (broker flat) -- auto-terminating", - f.EntryName, acct.Name)); - } - } - } - } - - // Build 999: If Position Pass failed on reconnect but FSM has since been created (replace cycle completed), clear grace. - if (fsmExpectedQty != 0) - _positionPassFailedFirstSeen.TryRemove(acct.Name, out _); - - // AUTHORITY: Use FSM state from now on - string expectedKey = ExpKey(acct.Name); - int expectedQty = fsmExpectedQty; - - bool syncPending = _dispatchSyncPendingExpKeys.ContainsKey(expectedKey); // [B967-FIX-02] - // Build 935 [REAPER-B935-002]: Per-account grace prevents Account A fill blocking Account B repair. - bool inFillGrace = IsReaperFillGraceActive(expectedKey); - - bool hasState = expectedQty != 0 || actualQty != 0; - if (shouldLog && hasState) - Print($"[REAPER] {acct.Name}: Expected={expectedQty}, Actual={actualQty}"); + Position pos; + int actualQty; + int expectedQty; + string expectedKey; + bool syncPending; + bool inFillGrace; + bool hasState; + List accountFsms; + + AuditFleet_CalculateExpectedActual( + acct, + shouldLog, + out actualQty, + out expectedQty, + out expectedKey, + out syncPending, + out inFillGrace, + out hasState, + out accountFsms, + out pos); if (expectedQty != actualQty) { @@ -103,7 +88,10 @@ private bool AuditSingleFleetAccount(Account acct, bool shouldLog) // GHOST-FIX-3: Skip repair for Master -- it uses no FollowerBracketFSM -- repair path not applicable. if (acct.Name == Account.Name) { - if (shouldLog) Print($"[REAPER] {acct.Name} is the Master account -- skipping follower repair."); + if (shouldLog) + { + Print($"[REAPER] {acct.Name} is the Master account -- skipping follower repair."); + } return hasState; } @@ -117,32 +105,17 @@ private bool AuditSingleFleetAccount(Account acct, bool shouldLog) return hasState; } - string repairKey = acct.Name + "_" + Instrument.FullName; - bool alreadyInFlight; - alreadyInFlight = _repairInFlight.ContainsKey(repairKey); // [Build 968] - - if (!alreadyInFlight) + string repairKey; + if (EnqueueReaperRepairCandidate(acct, shouldLog, expectedQty, accountFsms, out repairKey)) { - // Phase 4: Use FSM to identify working entry - bool hasWorkingEntry = accountFsms.Any(f => f.State == FollowerBracketState.Submitted || f.State == FollowerBracketState.Accepted); - - if (!hasWorkingEntry) + // B957/E1: Clear in-flight guard if TriggerCustomEvent fails, preventing permanent lockout. + try { TriggerCustomEvent(o => ProcessReaperRepairQueue(), null); } + catch (Exception repairTriggerEx) { - 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) - _repairInFlight.TryAdd(repairKey, 0); // [Build 968] - _reaperRepairQueue.Enqueue(acct.Name); - // B957/E1: Clear in-flight guard if TriggerCustomEvent fails, preventing permanent lockout. - try { TriggerCustomEvent(o => ProcessReaperRepairQueue(), null); } - catch (Exception repairTriggerEx) - { - _repairInFlight.TryRemove(repairKey, out _); // [Build 968] - Print("[REAPER] TriggerCustomEvent failed for " + repairKey + ": " + repairTriggerEx.Message + " -- in-flight cleared."); - } + _repairInFlight.TryRemove(repairKey, out _); // [Build 968] + Print("[REAPER] TriggerCustomEvent failed for " + repairKey + ": " + repairTriggerEx.Message + " -- in-flight cleared."); } } - else if (shouldLog) - Print($"[REAPER] {acct.Name} repair already in-flight -- skipping."); return hasState; } @@ -164,8 +137,10 @@ private bool AuditSingleFleetAccount(Account acct, bool shouldLog) if (graceElapsed < 10.0) { if (shouldLog) + { Print(string.Format("[REAPER] {0}: Position Pass grace ({1:F1}s/10s) -- deferring critical desync. Stop replace in progress.", acct.Name, graceElapsed)); + } return hasState; // Defer -- check again next audit cycle } // Grace expired -- clear entry and fall through to critical desync @@ -175,93 +150,220 @@ private bool AuditSingleFleetAccount(Account acct, bool shouldLog) } } - if (shouldLog) Print($"[REAPER] * CRITICAL DESYNC on {acct.Name}: Expected={expectedQty}, Actual={actualQty}"); + if (shouldLog) + { + Print($"[REAPER] * CRITICAL DESYNC on {acct.Name}: Expected={expectedQty}, Actual={actualQty}"); + } if (AutoFlattenDesync) { - if (shouldLog) Print($"[REAPER] * QUEUING FLATTEN for {acct.Name} - Emergency Re-sync!"); - _reaperFlattenQueue.Enqueue(acct.Name); - try { TriggerCustomEvent(o => ProcessReaperFlattenQueue(), null); } - catch (Exception _flatTriggerEx) + if (shouldLog) { - string _discarded; - _reaperFlattenQueue.TryDequeue(out _discarded); - Print("[REAPER] TriggerCustomEvent failed for flatten of " - + acct.Name + ": " + _flatTriggerEx.Message - + " -- dequeued, will re-detect next cycle"); + Print($"[REAPER] * QUEUING FLATTEN for {acct.Name} - Emergency Re-sync!"); + } + if (EnqueueReaperFlattenCandidate(acct)) + { + try { TriggerCustomEvent(o => ProcessReaperFlattenQueue(), null); } + catch (Exception _flatTriggerEx) + { + _reaperFlattenInFlight.TryRemove(acct.Name + "_" + Instrument.FullName, out _); + Print("[REAPER] TriggerCustomEvent failed for flatten of " + + acct.Name + ": " + _flatTriggerEx.Message + + " -- in-flight cleared, will re-detect next cycle"); + } } } } else if (shouldLog) + { Print($"[REAPER] Minor Desync on {acct.Name}: Expected={expectedQty}, Actual={actualQty}"); + } } // --- NAKED POSITION AUDIT (Build 1102R) --------------------------------- if (actualQty != 0) { - // Build 1108.003 [D3]: Snapshot broker orders before iteration. orderSnapshot - var orders = acct.Orders.ToArray(); - bool hasWorkingStop = orders.Any(o => - o.Instrument?.FullName == Instrument?.FullName && - (o.OrderState == OrderState.Working || o.OrderState == OrderState.Accepted) && - (o.OrderType == OrderType.StopMarket || o.OrderType == OrderType.StopLimit) && - (o.OrderAction == OrderAction.Sell || o.OrderAction == OrderAction.BuyToCover)); + bool hasWorkingStop = AuditFleet_CheckWorkingStop(acct); if (!hasWorkingStop) { - bool hasPendingStopReplace = false; - foreach (var psr in pendingStopReplacements.Values) + if (EnqueueReaperNakedStopCandidate(acct, pos, actualQty, expectedKey, shouldLog)) { - PositionInfo psrPos; - if (activePositions.TryGetValue(psr.EntryName, out psrPos) - && psrPos != null && psrPos.ExecutingAccount != null - && psrPos.ExecutingAccount.Name == acct.Name) + try { TriggerCustomEvent(e => ProcessReaperNakedStopQueue(), null); } + catch (Exception tcEx) { - hasPendingStopReplace = true; - break; + _reaperNakedStopInFlight.TryRemove(expectedKey, out _); // [Build 969] + Print(string.Format("[REAPER][NAKED_STOP] TriggerCustomEvent failed for {0}: {1} -- in-flight cleared.", acct.Name, tcEx.Message)); } } + } + else + { + _nakedPositionFirstSeen.TryRemove(acct.Name, out _); + } + } + + return hasState; + } + + private void AuditFleet_CalculateExpectedActual( + Account acct, bool shouldLog, + out int actualQty, out int expectedQty, out string expectedKey, + out bool syncPending, out bool inFillGrace, out bool hasState, + out List accountFsms, out Position pos) + { + pos = acct.Positions.FirstOrDefault(p => p.Instrument.FullName == Instrument.FullName); + actualQty = 0; + if (pos != null && pos.MarketPosition != MarketPosition.Flat) + { + actualQty = pos.MarketPosition == MarketPosition.Long ? pos.Quantity : -pos.Quantity; + } - if (hasPendingStopReplace) + // Build 1105: FSM is the SOLE authority for follower expected position. + accountFsms = _followerBrackets.Values.Where(f => f.AccountName == acct.Name).ToList(); + int fsmExpectedQty = GetFsmExpectedPosition(acct.Name); + + // Handle hydrated Active FSMs with no order reference (restart edge case) + foreach (var f in accountFsms) + { + if (f.State == FollowerBracketState.Active && f.EntryOrder == null) + { + if (actualQty != 0) { - _nakedPositionFirstSeen.TryRemove(acct.Name, out _); - if (shouldLog) - Print(string.Format("[REAPER] {0}: Stop replace in flight -- suppressing naked audit.", acct.Name)); + fsmExpectedQty += actualQty; } else { - DateTime firstSeen; - int graceSeconds = (NakedPositionGraceSec >= 5) ? NakedPositionGraceSec : 5; - if (!_nakedPositionFirstSeen.TryGetValue(acct.Name, out firstSeen)) - { - _nakedPositionFirstSeen[acct.Name] = DateTime.UtcNow; - Print(string.Format("[REAPER][NAKED_POSITION] {0}: {1}ct naked -- starting {2}s grace window.", - acct.Name, actualQty, graceSeconds)); - } - else if ((DateTime.UtcNow - firstSeen).TotalSeconds >= graceSeconds) + FollowerBracketFSM staleFsm; + if (TryTerminateFollowerBracket(f.EntryName, out staleFsm)) { - bool alreadyNakedInFlight; - alreadyNakedInFlight = _reaperNakedStopInFlight.ContainsKey(ExpKey(acct.Name)); // [Build 968] - if (!alreadyNakedInFlight) - { - _reaperNakedStopInFlight.TryAdd(ExpKey(acct.Name), 0); // [Build 968] - Print(string.Format("[REAPER][NAKED_POSITION] {0}: {1}ct CONFIRMED naked after {2:F1}s grace. Queuing emergency hard stop.", - acct.Name, actualQty, (DateTime.UtcNow - firstSeen).TotalSeconds)); - _reaperNakedStopQueue.Enqueue((acct.Name, pos.MarketPosition, Math.Abs(actualQty))); - try { TriggerCustomEvent(e => ProcessReaperNakedStopQueue(), null); } - catch (Exception tcEx) - { - _reaperNakedStopInFlight.TryRemove(ExpKey(acct.Name), out _); // [Build 969] - Print(string.Format("[REAPER][NAKED_STOP] TriggerCustomEvent failed for {0}: {1} -- in-flight cleared.", acct.Name, tcEx.Message)); - } - } + Print(string.Format("[REAPER-C7] Stale Active FSM for {0} on {1} (broker flat) -- auto-terminating", + f.EntryName, acct.Name)); } } } - else - _nakedPositionFirstSeen.TryRemove(acct.Name, out _); } - return hasState; + // Build 999: If Position Pass failed on reconnect but FSM has since been created (replace cycle completed), clear grace. + if (fsmExpectedQty != 0) + { + _positionPassFailedFirstSeen.TryRemove(acct.Name, out _); + } + + // AUTHORITY: Use FSM state from now on + expectedKey = ExpKey(acct.Name); + expectedQty = fsmExpectedQty; + + syncPending = _dispatchSyncPendingExpKeys.ContainsKey(expectedKey); // [B967-FIX-02] + // Build 935 [REAPER-B935-002]: Per-account grace prevents Account A fill blocking Account B repair. + inFillGrace = IsReaperFillGraceActive(expectedKey); + + hasState = expectedQty != 0 || actualQty != 0; + if (shouldLog && hasState) + { + Print($"[REAPER] {acct.Name}: Expected={expectedQty}, Actual={actualQty}"); + } + } + + private bool EnqueueReaperRepairCandidate(Account acct, bool shouldLog, int expectedQty, List accountFsms, out string repairKey) + { + repairKey = acct.Name + "_" + Instrument.FullName; + bool alreadyInFlight; + alreadyInFlight = _repairInFlight.ContainsKey(repairKey); // [Build 968] + + if (!alreadyInFlight) + { + // Phase 4: Use FSM to identify working entry + bool hasWorkingEntry = accountFsms.Any(f => f.State == FollowerBracketState.Submitted || f.State == FollowerBracketState.Accepted); + + 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) + _repairInFlight.TryAdd(repairKey, 0); // [Build 968] + _reaperRepairQueue.Enqueue(acct.Name); + return true; + } + } + else if (shouldLog) + { + Print($"[REAPER] {acct.Name} repair already in-flight -- skipping."); + } + + return false; + } + + private bool EnqueueReaperFlattenCandidate(Account acct) + { + string flattenKey = acct.Name + "_" + Instrument.FullName; + if (!_reaperFlattenInFlight.TryAdd(flattenKey, 0)) + { + return false; + } + _reaperFlattenQueue.Enqueue(acct.Name); + return true; + } + + private bool AuditFleet_CheckWorkingStop(Account acct) + { + // Build 1108.003 [D3]: Snapshot broker orders before iteration. orderSnapshot + var orders = acct.Orders.ToArray(); + return orders.Any(o => + o.Instrument?.FullName == Instrument?.FullName && + (o.OrderState == OrderState.Working || o.OrderState == OrderState.Accepted) && + (o.OrderType == OrderType.StopMarket || o.OrderType == OrderType.StopLimit) && + (o.OrderAction == OrderAction.Sell || o.OrderAction == OrderAction.BuyToCover)); + } + + private bool EnqueueReaperNakedStopCandidate(Account acct, Position pos, int actualQty, string expectedKey, bool shouldLog) + { + bool hasPendingStopReplace = false; + foreach (var psr in pendingStopReplacements.Values) + { + PositionInfo psrPos; + if (activePositions.TryGetValue(psr.EntryName, out psrPos) + && psrPos != null && psrPos.ExecutingAccount != null + && psrPos.ExecutingAccount.Name == acct.Name) + { + hasPendingStopReplace = true; + break; + } + } + + if (hasPendingStopReplace) + { + _nakedPositionFirstSeen.TryRemove(acct.Name, out _); + if (shouldLog) + Print(string.Format("[REAPER] {0}: Stop replace in flight -- suppressing naked audit.", acct.Name)); + } + else + { + DateTime firstSeen; + int graceSeconds = (NakedPositionGraceSec >= 5) ? NakedPositionGraceSec : 5; + if (!_nakedPositionFirstSeen.TryGetValue(acct.Name, out firstSeen)) + { + _nakedPositionFirstSeen[acct.Name] = DateTime.UtcNow; + Print(string.Format("[REAPER][NAKED_POSITION] {0}: {1}ct naked -- starting {2}s grace window.", + acct.Name, actualQty, graceSeconds)); + } + else if ((DateTime.UtcNow - firstSeen).TotalSeconds >= graceSeconds) + { + bool alreadyNakedInFlight; + alreadyNakedInFlight = _reaperNakedStopInFlight.ContainsKey(expectedKey); // [Build 968] + if (!alreadyNakedInFlight) + { + _reaperNakedStopInFlight.TryAdd(expectedKey, 0); // [Build 968] + Print(string.Format("[REAPER][NAKED_POSITION] {0}: {1}ct CONFIRMED naked after {2:F1}s grace. Queuing emergency hard stop.", + acct.Name, actualQty, (DateTime.UtcNow - firstSeen).TotalSeconds)); + _reaperNakedStopQueue.Enqueue((acct.Name, pos.MarketPosition, Math.Abs(actualQty))); + return true; + } + } + } + + return false; } private void TerminateFsmsForAccount(string accountName) @@ -269,7 +371,10 @@ private void TerminateFsmsForAccount(string accountName) foreach (var kvp in _followerBrackets.ToArray()) { FollowerBracketFSM fsm = kvp.Value; - if (fsm == null || fsm.AccountName != accountName) continue; + if (fsm == null || fsm.AccountName != accountName) + { + continue; + } FollowerBracketFSM removedFsm; if (TryTerminateFollowerBracket(kvp.Key, out removedFsm)) @@ -286,56 +391,46 @@ private bool AuditMasterAccountIfNeeded(bool shouldLog) Position masterPos = Account.Positions.FirstOrDefault(p => p.Instrument.FullName == Instrument.FullName); int masterActualQty = 0; if (masterPos != null && masterPos.MarketPosition != MarketPosition.Flat) + { masterActualQty = masterPos.MarketPosition == MarketPosition.Long ? masterPos.Quantity : -masterPos.Quantity; + } int masterExpectedQty = 0; + string masterExpectedKey = ExpKey(Account.Name); // Build 1102U [BUG-1]: Composite key + stateLock guard. - expectedPositions.TryGetValue(ExpKey(Account.Name), out masterExpectedQty); + expectedPositions.TryGetValue(masterExpectedKey, out masterExpectedQty); bool hasState = masterExpectedQty != 0 || masterActualQty != 0; if (shouldLog && hasState) + { Print($"[REAPER] {Account.Name} (Master): Expected={masterExpectedQty}, Actual={masterActualQty}"); + } if (masterExpectedQty != masterActualQty) { if (masterActualQty == 0 && masterExpectedQty != 0) { - if (shouldLog) Print($"[REAPER] {Account.Name} (Master) is Flat (Target/Stop hit). Expected was {masterExpectedQty}."); + if (shouldLog) + { + Print($"[REAPER] {Account.Name} (Master) is Flat (Target/Stop hit). Expected was {masterExpectedQty}."); + } } - else + else if (AuditMaster_CheckExpectedActual(shouldLog, masterActualQty, masterExpectedQty)) { - // REAP-01: Suppress critical-desync within ReaperFillGraceTicks of a fresh reservation. - long stampTicks = Interlocked.Read(ref _lastExpectedPositionSetTicks); - bool inFillGrace = stampTicks > 0 && - (DateTime.UtcNow.Ticks - stampTicks) < ReaperFillGraceTicks; - - bool isCriticalDesync = !inFillGrace && - ((masterActualQty != 0 && masterExpectedQty == 0) || - (Math.Sign(masterActualQty) != Math.Sign(masterExpectedQty) && masterExpectedQty != 0)); - - if (inFillGrace && shouldLog) - Print($"[REAPER] {Account.Name} (Master): Fill grace active -- desync check suppressed."); - - if (isCriticalDesync) + if (shouldLog) { - if (shouldLog) - Print($"[REAPER] CRITICAL DESYNC on {Account.Name} (Master): Expected={masterExpectedQty}, Actual={masterActualQty}"); - if (AutoFlattenDesync) + Print($"[REAPER] QUEUING FLATTEN for {Account.Name} (Master) - Emergency Re-sync!"); + } + if (EnqueueReaperMasterFlatten()) + { + try { TriggerCustomEvent(o => ProcessReaperFlattenQueue(), null); } + catch (Exception _mFlatTriggerEx) { - if (shouldLog) Print($"[REAPER] QUEUING FLATTEN for {Account.Name} (Master) - Emergency Re-sync!"); - _reaperFlattenQueue.Enqueue(Account.Name); - try { TriggerCustomEvent(o => ProcessReaperFlattenQueue(), null); } - catch (Exception _mFlatTriggerEx) - { - string _mDiscarded; - _reaperFlattenQueue.TryDequeue(out _mDiscarded); - Print("[REAPER] TriggerCustomEvent failed for master flatten: " - + _mFlatTriggerEx.Message + " -- dequeued, will re-detect next cycle"); - } + _reaperFlattenInFlight.TryRemove(Account.Name + "_" + Instrument.FullName, out _); + Print("[REAPER] TriggerCustomEvent failed for master flatten: " + + _mFlatTriggerEx.Message + " -- in-flight cleared, will re-detect next cycle"); } } - else if (shouldLog) - Print($"[REAPER] Minor Desync on {Account.Name} (Master): Expected={masterExpectedQty}, Actual={masterActualQty}"); } } @@ -359,33 +454,89 @@ private bool AuditMasterAccountIfNeeded(bool shouldLog) Print(string.Format("[REAPER][NAKED_POSITION] {0} (Master): {1}ct naked -- starting {2}s grace window.", Account.Name, masterActualQty, graceSeconds)); } - else if ((DateTime.UtcNow - masterFirstSeen).TotalSeconds >= graceSeconds) + else if (EnqueueReaperMasterNakedStop(masterPos, masterActualQty, masterExpectedKey, masterFirstSeen)) { - bool alreadyNakedInFlight; - alreadyNakedInFlight = _reaperNakedStopInFlight.ContainsKey(ExpKey(Account.Name)); - if (!alreadyNakedInFlight) + try { TriggerCustomEvent(e => ProcessReaperNakedStopQueue(), null); } + catch (Exception tcEx) { - _reaperNakedStopInFlight.TryAdd(ExpKey(Account.Name), 0); - Print(string.Format("[REAPER][NAKED_POSITION] {0} (Master): {1}ct CONFIRMED naked after {2:F1}s grace. Queuing emergency hard stop.", - Account.Name, masterActualQty, (DateTime.UtcNow - masterFirstSeen).TotalSeconds)); - _reaperNakedStopQueue.Enqueue((Account.Name, masterPos.MarketPosition, Math.Abs(masterActualQty))); - try { TriggerCustomEvent(e => ProcessReaperNakedStopQueue(), null); } - catch (Exception tcEx) - { - _reaperNakedStopInFlight.TryRemove(ExpKey(Account.Name), out _); - Print(string.Format("[REAPER][NAKED_STOP] TriggerCustomEvent failed for {0} (Master): {1} -- in-flight cleared.", - Account.Name, tcEx.Message)); - } + _reaperNakedStopInFlight.TryRemove(masterExpectedKey, out _); + Print(string.Format("[REAPER][NAKED_STOP] TriggerCustomEvent failed for {0} (Master): {1} -- in-flight cleared.", + Account.Name, tcEx.Message)); } } } else + { _nakedPositionFirstSeen.TryRemove(Account.Name, out _); + } } return hasState; } + private bool AuditMaster_CheckExpectedActual(bool shouldLog, int masterActualQty, int masterExpectedQty) + { + // REAP-01: Suppress critical-desync within ReaperFillGraceTicks of a fresh reservation. + long stampTicks = Interlocked.Read(ref _lastExpectedPositionSetTicks); + bool inFillGrace = stampTicks > 0 && + (DateTime.UtcNow.Ticks - stampTicks) < ReaperFillGraceTicks; + + bool isCriticalDesync = !inFillGrace && + ((masterActualQty != 0 && masterExpectedQty == 0) || + (Math.Sign(masterActualQty) != Math.Sign(masterExpectedQty) && masterExpectedQty != 0)); + + if (inFillGrace && shouldLog) + { + Print($"[REAPER] {Account.Name} (Master): Fill grace active -- desync check suppressed."); + } + + if (isCriticalDesync) + { + if (shouldLog) + Print($"[REAPER] CRITICAL DESYNC on {Account.Name} (Master): Expected={masterExpectedQty}, Actual={masterActualQty}"); + if (AutoFlattenDesync) + { + return true; + } + } + else if (shouldLog) + { + Print($"[REAPER] Minor Desync on {Account.Name} (Master): Expected={masterExpectedQty}, Actual={masterActualQty}"); + } + + return false; + } + + private bool EnqueueReaperMasterFlatten() + { + string flattenKey = Account.Name + "_" + Instrument.FullName; + if (!_reaperFlattenInFlight.TryAdd(flattenKey, 0)) + { + return false; + } + _reaperFlattenQueue.Enqueue(Account.Name); + return true; + } + + private bool EnqueueReaperMasterNakedStop(Position masterPos, int masterActualQty, string masterExpectedKey, DateTime masterFirstSeen) + { + if ((DateTime.UtcNow - masterFirstSeen).TotalSeconds >= ((NakedPositionGraceSec >= 5) ? NakedPositionGraceSec : 5)) + { + bool alreadyNakedInFlight; + alreadyNakedInFlight = _reaperNakedStopInFlight.ContainsKey(masterExpectedKey); + if (!alreadyNakedInFlight) + { + _reaperNakedStopInFlight.TryAdd(masterExpectedKey, 0); + Print(string.Format("[REAPER][NAKED_POSITION] {0} (Master): {1}ct CONFIRMED naked after {2:F1}s grace. Queuing emergency hard stop.", + Account.Name, masterActualQty, (DateTime.UtcNow - masterFirstSeen).TotalSeconds)); + _reaperNakedStopQueue.Enqueue((Account.Name, masterPos.MarketPosition, Math.Abs(masterActualQty))); + return true; + } + } + + return false; + } + /// /// V12.17 FIX: Processes queued flatten requests on the strategy thread. /// Called via TriggerCustomEvent from the Reaper background thread. @@ -398,71 +549,13 @@ private void ProcessReaperFlattenQueue() { try { - // Find the account by name - Account targetAcct = null; - foreach (Account acct in Account.All) - { - if (acct.Name == accountName) - { - targetAcct = acct; - break; - } - } - - // Also check if it's the Master account - if (targetAcct == null && Account.Name == accountName) - targetAcct = Account; + Account targetAcct = ProcessReaperFlatten_FindAccount(accountName); if (targetAcct != null) { - // [V12.Phase9] REAPER FIX: Use manual unmanaged close instead of broken targetAcct.Flatten(). - // 1. Cancel all working orders for this instrument - List ordersToCancel = new List(); - foreach (Order order in targetAcct.Orders) - { - if (order != null && order.Instrument.FullName == Instrument.FullName && - (order.OrderState == OrderState.Working || order.OrderState == OrderState.Submitted || - order.OrderState == OrderState.Accepted || order.OrderState == OrderState.ChangePending)) - { - ordersToCancel.Add(order); - } - } - if (ordersToCancel.Count > 0) - { - foreach (Order orderToCancel in ordersToCancel) - CancelOrderOnAccount(orderToCancel, targetAcct); - Print($"[REAPER] Emergency Cancel: {ordersToCancel.Count} orders on {accountName}"); - } - - // 2. Proactively close positions via unmanaged market orders - foreach (Position position in targetAcct.Positions) - { - if (position.Instrument.FullName != Instrument.FullName || position.MarketPosition == MarketPosition.Flat) continue; - - int qty = position.Quantity; - string signalName = "ReaperFlatten_" + position.MarketPosition.ToString(); - - if (targetAcct == this.Account) - { - // Master Account - if (position.MarketPosition == MarketPosition.Long) - SubmitOrderUnmanaged(0, OrderAction.Sell, OrderType.Market, qty, 0, 0, "", signalName); - else - SubmitOrderUnmanaged(0, OrderAction.BuyToCover, OrderType.Market, qty, 0, 0, "", signalName); - } - else - { - // Fleet Account - OrderAction closeAction = position.MarketPosition == MarketPosition.Long ? OrderAction.Sell : OrderAction.BuyToCover; - Order closeOrder = targetAcct.CreateOrder(Instrument, closeAction, OrderType.Market, TimeInForce.Gtc, qty, 0, 0, "", signalName, null); - targetAcct.Submit(new[] { closeOrder }); - } - Print($"[REAPER] ? Emergency Market Close: {qty} contracts on {accountName}"); - } - - // Build 1004: SetExpectedPositionLocked(0) removed -- FSM termination is the sole teardown. - // expectedPositions write is vestigial once FSM is the authority source. - TerminateFsmsForAccount(accountName); + ProcessReaperFlatten_CancelWorkingOrders(targetAcct, accountName); + ProcessReaperFlatten_ClosePositions(targetAcct, accountName); + ProcessReaperFlatten_TerminateFsms(accountName); Print($"[REAPER] ? MARSHAL-FLATTEN (Unmanaged) executed on strategy thread for {accountName}"); } else @@ -474,9 +567,100 @@ private void ProcessReaperFlattenQueue() { Print($"[REAPER] [X] MARSHAL-FLATTEN FAILED for {accountName}: {ex.Message}"); } + finally + { + _reaperFlattenInFlight.TryRemove(accountName + "_" + Instrument.FullName, out _); + } } } + private Account ProcessReaperFlatten_FindAccount(string accountName) + { + // Find the account by name + Account targetAcct = null; + foreach (Account acct in Account.All) + { + if (acct.Name == accountName) + { + targetAcct = acct; + break; + } + } + + // Also check if it's the Master account + if (targetAcct == null && Account.Name == accountName) + targetAcct = Account; + + return targetAcct; + } + + private void ProcessReaperFlatten_CancelWorkingOrders(Account targetAcct, string accountName) + { + // [V12.Phase9] REAPER FIX: Use manual unmanaged close instead of broken targetAcct.Flatten(). + // 1. Cancel all working orders for this instrument + List ordersToCancel = new List(); + foreach (Order order in targetAcct.Orders) + { + if (order != null && order.Instrument.FullName == Instrument.FullName && + (order.OrderState == OrderState.Working || order.OrderState == OrderState.Submitted || + order.OrderState == OrderState.Accepted || order.OrderState == OrderState.ChangePending)) + { + ordersToCancel.Add(order); + } + } + if (ordersToCancel.Count > 0) + { + foreach (Order orderToCancel in ordersToCancel) + { + CancelOrderOnAccount(orderToCancel, targetAcct); + } + Print($"[REAPER] Emergency Cancel: {ordersToCancel.Count} orders on {accountName}"); + } + } + + private void ProcessReaperFlatten_ClosePositions(Account targetAcct, string accountName) + { + // 2. Proactively close positions via unmanaged market orders + foreach (Position position in targetAcct.Positions) + { + if (position.Instrument.FullName != Instrument.FullName || position.MarketPosition == MarketPosition.Flat) + { + continue; + } + + int qty = position.Quantity; + string signalName = "ReaperFlatten_" + position.MarketPosition.ToString(); + + if (targetAcct == this.Account) + { + // Master Account + if (position.MarketPosition == MarketPosition.Long) + { + SubmitOrderUnmanaged(0, OrderAction.Sell, OrderType.Market, qty, 0, 0, "", signalName); + } + else + { + SubmitOrderUnmanaged(0, OrderAction.BuyToCover, OrderType.Market, qty, 0, 0, "", signalName); + } + } + else + { + // Fleet Account + OrderAction closeAction = position.MarketPosition == MarketPosition.Long ? OrderAction.Sell : OrderAction.BuyToCover; + Order closeOrder = targetAcct.CreateOrder(Instrument, closeAction, OrderType.Market, TimeInForce.Gtc, qty, 0, 0, "", signalName, null); + targetAcct.Submit(new[] { closeOrder }); + } + Print($"[REAPER] ? Emergency Market Close: {qty} contracts on {accountName}"); + } + } + + private void ProcessReaperFlatten_TerminateFsms(string accountName) + { + // Build 1004: SetExpectedPositionLocked(0) removed -- FSM termination is the sole teardown. + // expectedPositions write is vestigial once FSM is the authority source. + TerminateFsmsForAccount(accountName); + } + #endregion } } diff --git a/src/V12_002.REAPER.cs b/src/V12_002.REAPER.cs index 0bed6c13..d0dee61b 100644 --- a/src/V12_002.REAPER.cs +++ b/src/V12_002.REAPER.cs @@ -25,6 +25,8 @@ public partial class V12_002 : Strategy private ConcurrentQueue _reaperRepairQueue = new ConcurrentQueue(); // V12.Phase8.2: Prevents double-repair for the same account while an order is in-flight private readonly ConcurrentDictionary _repairInFlight = new ConcurrentDictionary(); // [Build 968] + // [Phase 5 Repair] Mirrors _repairInFlight to dedupe flatten enqueues across audit cycles. + private readonly ConcurrentDictionary _reaperFlattenInFlight = new ConcurrentDictionary(); // Build 1102R: Queue for naked-position emergency stop requests (background -> strategy thread) private ConcurrentQueue<(string AccountName, MarketPosition Direction, int Qty)> _reaperNakedStopQueue @@ -67,7 +69,9 @@ private void StampAccountFillGrace(string expKey) private bool IsReaperFillGraceActive(string expKey) { if (_accountFillGraceTicks.TryGetValue(expKey, out long stampTicks)) + { return stampTicks > 0 && (DateTime.UtcNow.Ticks - stampTicks) < ReaperFillGraceTicks; + } // Fallback: check legacy global stamp (covers master account path) long globalStamp = Interlocked.Read(ref _lastExpectedPositionSetTicks); return globalStamp > 0 && (DateTime.UtcNow.Ticks - globalStamp) < ReaperFillGraceTicks; @@ -79,7 +83,9 @@ private bool TryGetRepairDistanceLimitPoints(out double limitPoints) limitPoints = 0; double atrLimit = CalculateATRStopDistance(RMAStopATRMultiplier); if (atrLimit <= 0) + { atrLimit = MinimumStop; + } double fenceLimit = (RepairTickFence > 0 && tickSize > 0) ? RepairTickFence * tickSize @@ -110,7 +116,10 @@ private void StartReaperAudit() /// private void StopReaperAudit() { - if (_reaperTimer == null) return; + if (_reaperTimer == null) + { + return; + } _reaperTimer.Stop(); _reaperTimer.Elapsed -= OnReaperTimerElapsed; @@ -126,8 +135,10 @@ private void StopReaperAudit() private void OnReaperTimerElapsed(object sender, System.Timers.ElapsedEventArgs e) { // V12.Phase8 [F-05]: Skip auditing while a flatten is actively running - if (isFlattenRunning || !_orderAdoptionComplete || State != State.Realtime) + if (isFlattenRunning || !_orderAdoptionComplete || State != State.Realtime) + { return; + } try { diff --git a/src/V12_002.StickyState.cs b/src/V12_002.StickyState.cs index f78f2a03..5ed2057d 100644 --- a/src/V12_002.StickyState.cs +++ b/src/V12_002.StickyState.cs @@ -68,7 +68,15 @@ private void MarkStickyDirty() private string SerializeStickyState() { var sb = new StringBuilder(1024); + SerializeSticky_WriteHeaderConfig(sb); + SerializeSticky_WriteFleetAnchor(sb); + SerializeSticky_WriteModeProfiles(sb); + SerializeSticky_WritePositions(sb); + return sb.ToString(); + } + private void SerializeSticky_WriteHeaderConfig(StringBuilder sb) + { // Header sb.AppendLine("# V12 StickyState v1"); sb.AppendLine("# Symbol: " + (Instrument != null ? Instrument.FullName : "unknown")); @@ -103,7 +111,10 @@ private string SerializeStickyState() sb.AppendLine("TRMA=" + (isTrendRmaMode ? "1" : "0")); sb.AppendLine("RRMA=" + (isRetestRmaMode ? "1" : "0")); sb.AppendLine(); + } + private void SerializeSticky_WriteFleetAnchor(StringBuilder sb) + { // [FLEET] sb.AppendLine("[FLEET]"); sb.AppendLine("LEADER=" + (_stickyLeaderAccount ?? "")); @@ -119,7 +130,10 @@ private string SerializeStickyState() sb.AppendLine("TYPE=" + AnchorTypeToString(currentRmaAnchor)); sb.AppendLine(string.Format(CultureInfo.InvariantCulture, "MNL_PRICE={0}", cachedMnlPrice)); sb.AppendLine(); + } + private void SerializeSticky_WriteModeProfiles(StringBuilder sb) + { // Build 1106: [CONFIG_*] -- per-mode profile snapshots string activeMode = "OR"; if (isRMAModeActive) activeMode = "RMA"; @@ -149,7 +163,10 @@ private string SerializeStickyState() sb.AppendLine(string.Format(CultureInfo.InvariantCulture, "MAX={0}", p.MaxRisk)); sb.AppendLine(); } + } + private void SerializeSticky_WritePositions(StringBuilder sb) + { // [POSITIONS] -- trailing stop state for active positions sb.AppendLine("[POSITIONS]"); sb.AppendLine("# key|extremePrice|trailLevel|beArmed|beTriggered|initialTargetCount"); @@ -169,8 +186,6 @@ private string SerializeStickyState() pi.InitialTargetCount)); } } - - return sb.ToString(); } // Build 1106: Captures current global config into a mode-specific profile. @@ -267,7 +282,7 @@ private bool LoadStickyState() try { - string[] lines = System.IO.File.ReadAllLines(_stickyStatePath, Encoding.UTF8); + string[] lines = LoadStickyState_ReadLines(); string section = ""; int appliedCount = 0; @@ -284,30 +299,10 @@ private bool LoadStickyState() continue; } - if (section == "CONFIG") - { - appliedCount += ApplyStickyConfig(line) ? 1 : 0; - } - else if (section.StartsWith("CONFIG_") && section.Length > 7) - { - // Build 1106: Per-mode profile section (e.g., CONFIG_OR, CONFIG_RMA) - string profileMode = section.Substring(7); - appliedCount += ApplyStickyModeProfile(profileMode, line) ? 1 : 0; - } - else if (section == "FLEET") - { - appliedCount += ApplyStickyFleet(line) ? 1 : 0; - } - else if (section == "ANCHOR") - { - appliedCount += ApplyStickyAnchor(line) ? 1 : 0; - } - // [POSITIONS] deferred to EnrichTrailStateFromSticky() + appliedCount += LoadStickyState_DispatchSection(section, line); } - Print(string.Format("[STICKY] Loaded {0} settings from {1}", appliedCount, - System.IO.Path.GetFileName(_stickyStatePath))); - return appliedCount > 0; + return LoadStickyState_LogOutcome(appliedCount); } catch (Exception ex) { @@ -316,13 +311,58 @@ private bool LoadStickyState() } } + private string[] LoadStickyState_ReadLines() + { + return System.IO.File.ReadAllLines(_stickyStatePath, Encoding.UTF8); + } + + private int LoadStickyState_DispatchSection(string section, string line) + { + if (section == "CONFIG") + { + return ApplyStickyConfig(line) ? 1 : 0; + } + else if (section.StartsWith("CONFIG_") && section.Length > 7) + { + // Build 1106: Per-mode profile section (e.g., CONFIG_OR, CONFIG_RMA) + string profileMode = section.Substring(7); + return ApplyStickyModeProfile(profileMode, line) ? 1 : 0; + } + else if (section == "FLEET") + { + return ApplyStickyFleet(line) ? 1 : 0; + } + else if (section == "ANCHOR") + { + return ApplyStickyAnchor(line) ? 1 : 0; + } + + // [POSITIONS] deferred to EnrichTrailStateFromSticky() + return 0; + } + + private bool LoadStickyState_LogOutcome(int appliedCount) + { + Print(string.Format("[STICKY] Loaded {0} settings from {1}", appliedCount, + System.IO.Path.GetFileName(_stickyStatePath))); + return appliedCount > 0; + } + private bool ApplyStickyConfig(string line) { int eq = line.IndexOf('='); if (eq < 1) return false; string key = line.Substring(0, eq).ToUpperInvariant(); string val = line.Substring(eq + 1); + if (ApplyStickyConfig_ModeSafetyGate(key, val)) return true; + if (ApplyStickyConfig_TargetValues(key, val)) return true; + if (ApplyStickyConfig_TargetTypes(key, val)) return true; + if (ApplyStickyConfig_RiskAndFlags(key, val)) return true; + return false; + } + private bool ApplyStickyConfig_ModeSafetyGate(string key, string val) + { switch (key) { case "MODE": @@ -334,6 +374,14 @@ private bool ApplyStickyConfig(string line) Print(string.Format("[STICKY] MODE on disk was {0} -- forced to OR (safety gate)", val)); return true; + default: return false; + } + } + + private bool ApplyStickyConfig_TargetValues(string key, string val) + { + switch (key) + { case "COUNT": if (int.TryParse(val, out int cnt)) activeTargetCount = Math.Max(1, Math.Min(5, cnt)); @@ -360,12 +408,28 @@ private bool ApplyStickyConfig(string line) Target5Value = t5; return true; + default: return false; + } + } + + private bool ApplyStickyConfig_TargetTypes(string key, string val) + { + switch (key) + { case "T1TYPE": T1Type = ParseTargetMode(val); return true; case "T2TYPE": T2Type = ParseTargetMode(val); return true; case "T3TYPE": T3Type = ParseTargetMode(val); return true; case "T4TYPE": T4Type = ParseTargetMode(val); return true; case "T5TYPE": T5Type = ParseTargetMode(val); return true; + default: return false; + } + } + + private bool ApplyStickyConfig_RiskAndFlags(string key, string val) + { + switch (key) + { case "STR": if (double.TryParse(val, NumberStyles.Float, CultureInfo.InvariantCulture, out double str)) { @@ -408,6 +472,14 @@ private bool ApplyStickyModeProfile(string mode, string line) _modeProfiles[mode] = profile; } + if (ApplyStickyModeProfile_TargetValues(key, val, profile)) return true; + if (ApplyStickyModeProfile_TargetTypes(key, val, profile)) return true; + if (ApplyStickyModeProfile_Risk(key, val, profile)) return true; + return false; + } + + private bool ApplyStickyModeProfile_TargetValues(string key, string val, ModeConfigProfile profile) + { switch (key) { case "COUNT": @@ -434,11 +506,29 @@ private bool ApplyStickyModeProfile(string mode, string line) if (double.TryParse(val, NumberStyles.Float, CultureInfo.InvariantCulture, out double t5)) profile.T5 = t5; return true; + + default: return false; + } + } + + private bool ApplyStickyModeProfile_TargetTypes(string key, string val, ModeConfigProfile profile) + { + switch (key) + { case "T1TYPE": profile.T1Type = ParseTargetMode(val); return true; case "T2TYPE": profile.T2Type = ParseTargetMode(val); return true; case "T3TYPE": profile.T3Type = ParseTargetMode(val); return true; case "T4TYPE": profile.T4Type = ParseTargetMode(val); return true; case "T5TYPE": profile.T5Type = ParseTargetMode(val); return true; + + default: return false; + } + } + + private bool ApplyStickyModeProfile_Risk(string key, string val, ModeConfigProfile profile) + { + switch (key) + { case "STR": if (double.TryParse(val, NumberStyles.Float, CultureInfo.InvariantCulture, out double str)) profile.StopMult = str; @@ -447,6 +537,7 @@ private bool ApplyStickyModeProfile(string mode, string line) if (double.TryParse(val, NumberStyles.Float, CultureInfo.InvariantCulture, out double max)) profile.MaxRisk = max; return true; + default: return false; } } diff --git a/src/V12_002.UI.Callbacks.cs b/src/V12_002.UI.Callbacks.cs index 933bd7e0..6e008689 100644 --- a/src/V12_002.UI.Callbacks.cs +++ b/src/V12_002.UI.Callbacks.cs @@ -208,99 +208,130 @@ private void ClearClickTraderBorderIfInactive() /// private void OnChartClick(object sender, MouseButtonEventArgs e) { - // Check if Shift is held OR RMA/MOMO button mode is active - bool shiftHeld = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift); - bool rmaActive = (RMAEnabled && (shiftHeld || isRMAModeActive)); - bool momoActive = (MOMOEnabled && isMOMOModeActive); - - if (!rmaActive && !momoActive) return; + if (!HandleChartClick_ValidateMode(out bool rmaActive, out bool momoActive)) return; try { if (ChartControl == null || ChartPanel == null) return; double currentPrice = lastKnownPrice > 0 ? lastKnownPrice : Close[0]; + if (!HandleChartClick_ConvertPrice(e, momoActive, currentPrice, out double clickPrice)) + return; - // ################################################################### - // V12.4: ChartPanel-based price conversion (PROVEN WORKING) - // ChartPanel.H includes time axis - effective price area is ~67% of height - // ################################################################### - Point mouseInPanel = e.GetPosition(ChartPanel as System.Windows.IInputElement); - - // Build 1102Z: UI Safety Fence -- Ignore clicks outside the actual price plotting area - // This prevents trades from triggering when clicking on the side panel, price axis, or scrollbars. - if (mouseInPanel.X < 0 || mouseInPanel.X > ChartPanel.W || mouseInPanel.Y < 0 || mouseInPanel.Y > ChartPanel.H) + if (momoActive) { - return; + HandleChartClick_ExecuteMomo(clickPrice); + } + else + { + HandleChartClick_ExecuteRma(clickPrice, currentPrice); } - double panelHeight = ChartPanel.H; - double maxPrice = ChartPanel.MaxValue; - double minPrice = ChartPanel.MinValue; - double priceRange = maxPrice - minPrice; + e.Handled = true; + } + catch (Exception ex) + { + Print("ERROR OnChartClick: " + ex.Message); + } + } - // CRITICAL: ChartPanel.H includes time axis at bottom - // The actual price plotting area is approximately 67% of total panel height - double effectivePriceHeight = panelHeight * 0.667; + private bool HandleChartClick_ValidateMode(out bool rmaActive, out bool momoActive) + { + // Check if Shift is held OR RMA/MOMO button mode is active + bool shiftHeld = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift); + rmaActive = (RMAEnabled && (shiftHeld || isRMAModeActive)); + momoActive = (MOMOEnabled && isMOMOModeActive); - // Clamp Y to valid range - double yInPanel = mouseInPanel.Y; - if (yInPanel < 0) yInPanel = 0; - if (yInPanel > effectivePriceHeight) yInPanel = effectivePriceHeight; + return rmaActive || momoActive; + } - // Convert: Y=0 is top (maxPrice), Y=effectivePriceHeight is bottom (minPrice) - double yRatio = yInPanel / effectivePriceHeight; - double clickPrice = maxPrice - (yRatio * priceRange); + private bool HandleChartClick_ConvertPrice( + MouseButtonEventArgs e, + bool momoActive, + double currentPrice, + out double clickPrice) + { + clickPrice = 0; - string modeLabel = momoActive ? "MOMO" : "RMA"; - Print(string.Format("{0} v12.4 CLICK: x={1:F1}, y={2:F1}, w={3:F1}, h={4:F1}, ratio={5:F3}, price={6:F2} (Market={7:F2})", - modeLabel, mouseInPanel.X, mouseInPanel.Y, ChartPanel.W, panelHeight, yRatio, clickPrice, currentPrice)); + // ################################################################### + // V12.4: ChartPanel-based price conversion (PROVEN WORKING) + // ChartPanel.H includes time axis - effective price area is ~67% of height + // ################################################################### + Point mouseInPanel = e.GetPosition(ChartPanel as System.Windows.IInputElement); - // Round to tick size - clickPrice = Instrument.MasterInstrument.RoundToTickSize(clickPrice); + // Build 1102Z: UI Safety Fence -- Ignore clicks outside the actual price plotting area + // This prevents trades from triggering when clicking on the side panel, price axis, or scrollbars. + if (mouseInPanel.X < 0 || mouseInPanel.X > ChartPanel.W || mouseInPanel.Y < 0 || mouseInPanel.Y > ChartPanel.H) + { + return false; + } - // Validate price is within chart range - if (clickPrice < minPrice - priceRange || clickPrice > maxPrice + priceRange) - { - Print(string.Format("{0}: Click price {1:F2} outside valid range [{2:F2} - {3:F2}]", - modeLabel, clickPrice, minPrice, maxPrice)); - return; - } + double panelHeight = ChartPanel.H; + double maxPrice = ChartPanel.MaxValue; + double minPrice = ChartPanel.MinValue; + double priceRange = maxPrice - minPrice; - if (momoActive) - { - // MOMO uses a fixed-points stop: Math.Min(MOMOStopPoints, MaximumStop) - double momoStopDist = Math.Min(MOMOStopPoints, MaximumStop); - int momoContracts = CalculatePositionSize(momoStopDist); - double capturedMomoPrice = clickPrice; int capturedMomoContracts = momoContracts; - Enqueue(ctx => ctx.ExecuteMOMOEntry(capturedMomoPrice, capturedMomoContracts)); - } - else - { - MarketPosition direction = (clickPrice > currentPrice) ? MarketPosition.Short : MarketPosition.Long; - double rmaStopDist = CalculateATRStopDistance(RMAStopATRMultiplier); - int rmaContracts = CalculatePositionSize(rmaStopDist); - double capturedRmaPrice = clickPrice; MarketPosition capturedDir = direction; int capturedRmaContracts = rmaContracts; - Enqueue(ctx => ctx.ExecuteRMAEntryV2(capturedRmaPrice, capturedDir, capturedRmaContracts)); + // CRITICAL: ChartPanel.H includes time axis at bottom + // The actual price plotting area is approximately 67% of total panel height + double effectivePriceHeight = panelHeight * 0.667; - if (isRMAButtonClicked) - { - isRMAButtonClicked = false; - isRMAModeActive = false; - ClearClickTraderBorderIfInactive(); + // Clamp Y to valid range + double yInPanel = mouseInPanel.Y; + if (yInPanel < 0) yInPanel = 0; + if (yInPanel > effectivePriceHeight) yInPanel = effectivePriceHeight; - // V12.43: Lightweight deactivation -- only signal mode change, don't clobber config - SendResponseToRemote("SET_RMA_MODE|OFF"); - Print("V12.43: RMA auto-deactivated after entry (lightweight signal, no CONFIG clobber)"); - } - } + // Convert: Y=0 is top (maxPrice), Y=effectivePriceHeight is bottom (minPrice) + double yRatio = yInPanel / effectivePriceHeight; + clickPrice = maxPrice - (yRatio * priceRange); - e.Handled = true; - } - catch (Exception ex) + string modeLabel = momoActive ? "MOMO" : "RMA"; + Print(string.Format("{0} v12.4 CLICK: x={1:F1}, y={2:F1}, w={3:F1}, h={4:F1}, ratio={5:F3}, price={6:F2} (Market={7:F2})", + modeLabel, mouseInPanel.X, mouseInPanel.Y, ChartPanel.W, panelHeight, yRatio, clickPrice, currentPrice)); + + // Round to tick size + clickPrice = Instrument.MasterInstrument.RoundToTickSize(clickPrice); + + // Validate price is within chart range + if (clickPrice < minPrice - priceRange || clickPrice > maxPrice + priceRange) { - Print("ERROR OnChartClick: " + ex.Message); + Print(string.Format("{0}: Click price {1:F2} outside valid range [{2:F2} - {3:F2}]", + modeLabel, clickPrice, minPrice, maxPrice)); + return false; } + + return true; + } + + private void HandleChartClick_ExecuteMomo(double clickPrice) + { + // MOMO uses a fixed-points stop: Math.Min(MOMOStopPoints, MaximumStop) + double momoStopDist = Math.Min(MOMOStopPoints, MaximumStop); + int momoContracts = CalculatePositionSize(momoStopDist); + double capturedMomoPrice = clickPrice; int capturedMomoContracts = momoContracts; + Enqueue(ctx => ctx.ExecuteMOMOEntry(capturedMomoPrice, capturedMomoContracts)); + } + + private void HandleChartClick_ExecuteRma(double clickPrice, double currentPrice) + { + MarketPosition direction = (clickPrice > currentPrice) ? MarketPosition.Short : MarketPosition.Long; + double rmaStopDist = CalculateATRStopDistance(RMAStopATRMultiplier); + int rmaContracts = CalculatePositionSize(rmaStopDist); + double capturedRmaPrice = clickPrice; MarketPosition capturedDir = direction; int capturedRmaContracts = rmaContracts; + Enqueue(ctx => ctx.ExecuteRMAEntryV2(capturedRmaPrice, capturedDir, capturedRmaContracts)); + + if (isRMAButtonClicked) + HandleChartClick_DeactivateRma(); + } + + private void HandleChartClick_DeactivateRma() + { + isRMAButtonClicked = false; + isRMAModeActive = false; + ClearClickTraderBorderIfInactive(); + + // V12.43: Lightweight deactivation -- only signal mode change, don't clobber config + SendResponseToRemote("SET_RMA_MODE|OFF"); + Print("V12.43: RMA auto-deactivated after entry (lightweight signal, no CONFIG clobber)"); } private void OnKeyDown(object sender, KeyEventArgs e) @@ -377,17 +408,8 @@ private void ExecuteTargetAction(string targetType, string action) continue; } - if (!TryResolveTargetContext(pos, targetType, out int targetNumber, out var targetOrders, out int targetContracts, out bool targetFilled)) - { - Print(string.Format("{0} ACTION: Invalid target identifier", targetType)); + if (!ExecuteTarget_ValidateContext(pos, entryName, targetType, out int targetNumber, out var targetOrders, out int targetContracts)) continue; - } - - if (targetContracts <= 0) - { - Print(string.Format("{0} ACTION: No contracts assigned for {1}", targetType, entryName)); - continue; - } if (IsRunnerTarget(targetNumber) && action != "market" && action != "cancel") { @@ -396,88 +418,32 @@ private void ExecuteTargetAction(string targetType, string action) continue; } - if (targetFilled) - { - Print(string.Format("{0} ACTION: {1} already filled for {2}", targetType, targetType, entryName)); - continue; - } - double currentPrice = lastKnownPrice > 0 ? lastKnownPrice : Close[0]; switch (action) { case "market": - // Fill target at market NOW - // V8.30: Thread-safe removal - if (targetOrders.TryGetValue(entryName, out var existingOrder)) - { - if (existingOrder != null && !IsOrderTerminal(existingOrder.OrderState)) - CancelOrderSafe(existingOrder, pos); - else - targetOrders.TryRemove(entryName, out _); - } - - Order marketOrder = SubmitExitOrderForPosition( - pos, targetContracts, OrderType.Market, 0, targetType + "_Market_" + entryName); - - if (marketOrder != null) - Print(string.Format("? {0} MARKET FILL: {1} - Closing {2} contracts at market", targetType, entryName, targetContracts)); - else - Print(string.Format("ERROR {0} MARKET FILL FAILED: {1} - Could not close {2} contracts", targetType, entryName, targetContracts)); + ExecuteTarget_Market(entryName, pos, targetType, targetOrders, targetContracts); break; case "1point": - // V8.18: Absolute profit target (Entry + 1 point) - double newPrice1pt = pos.Direction == MarketPosition.Long - ? pos.EntryPrice + 1.0 - : pos.EntryPrice - 1.0; - newPrice1pt = Instrument.MasterInstrument.RoundToTickSize(newPrice1pt); - - Print(string.Format("? {0} -> 1 POINT PROFIT: {1} - New target @ {2:F2} (Entry was {3:F2})", - targetType, entryName, newPrice1pt, pos.EntryPrice)); - - MoveTargetOrder(entryName, pos, targetType, newPrice1pt, targetContracts); + ExecuteTarget_OnePoint(entryName, pos, targetType, targetContracts); break; case "2point": - // V8.18: Absolute profit target (Entry + 2 points) - double newPrice2pt = pos.Direction == MarketPosition.Long - ? pos.EntryPrice + 2.0 - : pos.EntryPrice - 2.0; - newPrice2pt = Instrument.MasterInstrument.RoundToTickSize(newPrice2pt); - - Print(string.Format("? {0} -> 2 POINTS PROFIT: {1} - New target @ {2:F2} (Entry was {3:F2})", - targetType, entryName, newPrice2pt, pos.EntryPrice)); - - MoveTargetOrder(entryName, pos, targetType, newPrice2pt, targetContracts); + ExecuteTarget_TwoPoint(entryName, pos, targetType, targetContracts); break; case "marketprice": - // Move target to current market price (instant fill) - double marketPrice = Instrument.MasterInstrument.RoundToTickSize(currentPrice); - MoveTargetOrder(entryName, pos, targetType, marketPrice, targetContracts); - Print(string.Format("? {0} -> MARKET PRICE: {1} - New target @ {2:F2}", targetType, entryName, marketPrice)); + ExecuteTarget_MarketPrice(entryName, pos, targetType, targetContracts, currentPrice); break; case "breakeven": - // Move target to breakeven (entry price) - MoveTargetOrder(entryName, pos, targetType, pos.EntryPrice, targetContracts); - Print(string.Format("? {0} -> BREAKEVEN: {1} - New target @ {2:F2}", targetType, entryName, pos.EntryPrice)); + ExecuteTarget_Breakeven(entryName, pos, targetType, targetContracts); break; case "cancel": - // Cancel target order - let contracts run - // V8.30: Thread-safe removal - if (targetOrders.TryGetValue(entryName, out var cancelOrder)) - { - if (cancelOrder != null && !IsOrderTerminal(cancelOrder.OrderState)) - { - CancelOrderSafe(cancelOrder, pos); - Print(string.Format("? {0} CANCELLED: {1} - {2} contracts will run with stop", targetType, entryName, targetContracts)); - } - else - targetOrders.TryRemove(entryName, out _); - } + ExecuteTarget_Cancel(entryName, pos, targetType, targetOrders, targetContracts); break; } } @@ -488,58 +454,235 @@ private void ExecuteTargetAction(string targetType, string action) } } + private bool ExecuteTarget_ValidateContext( + PositionInfo pos, + string entryName, + string targetType, + out int targetNumber, + out ConcurrentDictionary targetOrders, + out int targetContracts) + { + targetNumber = 0; + targetOrders = null; + targetContracts = 0; + + if (!TryResolveTargetContext(pos, targetType, out targetNumber, out targetOrders, out targetContracts, out bool targetFilled)) + { + Print(string.Format("{0} ACTION: Invalid target identifier", targetType)); + return false; + } + + if (targetContracts <= 0) + { + Print(string.Format("{0} ACTION: No contracts assigned for {1}", targetType, entryName)); + return false; + } + + if (targetFilled) + { + Print(string.Format("{0} ACTION: {1} already filled for {2}", targetType, targetType, entryName)); + return false; + } + + return true; + } + + private void ExecuteTarget_Market( + string entryName, + PositionInfo pos, + string targetType, + ConcurrentDictionary targetOrders, + int targetContracts) + { + // Fill target at market NOW + // V8.30: Thread-safe removal + if (targetOrders.TryGetValue(entryName, out var existingOrder)) + { + if (existingOrder != null && !IsOrderTerminal(existingOrder.OrderState)) + CancelOrderSafe(existingOrder, pos); + else + targetOrders.TryRemove(entryName, out _); + } + + Order marketOrder = SubmitExitOrderForPosition( + pos, targetContracts, OrderType.Market, 0, targetType + "_Market_" + entryName); + + if (marketOrder != null) + Print(string.Format("? {0} MARKET FILL: {1} - Closing {2} contracts at market", targetType, entryName, targetContracts)); + else + Print(string.Format("ERROR {0} MARKET FILL FAILED: {1} - Could not close {2} contracts", targetType, entryName, targetContracts)); + } + + private void ExecuteTarget_OnePoint(string entryName, PositionInfo pos, string targetType, int targetContracts) + { + // V8.18: Absolute profit target (Entry + 1 point) + double newPrice1pt = pos.Direction == MarketPosition.Long + ? pos.EntryPrice + 1.0 + : pos.EntryPrice - 1.0; + newPrice1pt = Instrument.MasterInstrument.RoundToTickSize(newPrice1pt); + + Print(string.Format("? {0} -> 1 POINT PROFIT: {1} - New target @ {2:F2} (Entry was {3:F2})", + targetType, entryName, newPrice1pt, pos.EntryPrice)); + + MoveTargetOrder(entryName, pos, targetType, newPrice1pt, targetContracts); + } + + private void ExecuteTarget_TwoPoint(string entryName, PositionInfo pos, string targetType, int targetContracts) + { + // V8.18: Absolute profit target (Entry + 2 points) + double newPrice2pt = pos.Direction == MarketPosition.Long + ? pos.EntryPrice + 2.0 + : pos.EntryPrice - 2.0; + newPrice2pt = Instrument.MasterInstrument.RoundToTickSize(newPrice2pt); + + Print(string.Format("? {0} -> 2 POINTS PROFIT: {1} - New target @ {2:F2} (Entry was {3:F2})", + targetType, entryName, newPrice2pt, pos.EntryPrice)); + + MoveTargetOrder(entryName, pos, targetType, newPrice2pt, targetContracts); + } + + private void ExecuteTarget_MarketPrice( + string entryName, + PositionInfo pos, + string targetType, + int targetContracts, + double currentPrice) + { + // Move target to current market price (instant fill) + double marketPrice = Instrument.MasterInstrument.RoundToTickSize(currentPrice); + MoveTargetOrder(entryName, pos, targetType, marketPrice, targetContracts); + Print(string.Format("? {0} -> MARKET PRICE: {1} - New target @ {2:F2}", targetType, entryName, marketPrice)); + } + + private void ExecuteTarget_Breakeven(string entryName, PositionInfo pos, string targetType, int targetContracts) + { + // Move target to breakeven (entry price) + MoveTargetOrder(entryName, pos, targetType, pos.EntryPrice, targetContracts); + Print(string.Format("? {0} -> BREAKEVEN: {1} - New target @ {2:F2}", targetType, entryName, pos.EntryPrice)); + } + + private void ExecuteTarget_Cancel( + string entryName, + PositionInfo pos, + string targetType, + ConcurrentDictionary targetOrders, + int targetContracts) + { + // Cancel target order - let contracts run + // V8.30: Thread-safe removal + if (targetOrders.TryGetValue(entryName, out var cancelOrder)) + { + if (cancelOrder != null && !IsOrderTerminal(cancelOrder.OrderState)) + { + CancelOrderSafe(cancelOrder, pos); + Print(string.Format("? {0} CANCELLED: {1} - {2} contracts will run with stop", targetType, entryName, targetContracts)); + } + else + targetOrders.TryRemove(entryName, out _); + } + } + private void MoveTargetOrder(string entryName, PositionInfo pos, string targetType, double newPrice, int quantity) { - if (!TryParseTargetNumber(targetType, out int targetNumber)) + if (!MoveTargetOrder_Validate(targetType, quantity, out int targetNumber, out ConcurrentDictionary targetOrders)) return; + Order existingTarget; + if (targetOrders.TryGetValue(entryName, out existingTarget) && existingTarget != null) + { + if (MoveTargetOrder_PrepareFollowerReplace(entryName, pos, targetNumber, newPrice, quantity, existingTarget)) + return; + + MoveTargetOrder_CancelExisting(entryName, pos, targetOrders, existingTarget); + } + + MoveTargetOrder_SubmitReplacement(entryName, pos, targetType, newPrice, quantity, targetOrders); + } + + private bool MoveTargetOrder_Validate( + string targetType, + int quantity, + out int targetNumber, + out ConcurrentDictionary targetOrders) + { + targetNumber = 0; + targetOrders = null; + + if (!TryParseTargetNumber(targetType, out targetNumber)) + return false; + // Runner targets are trail-only: do not submit limit orders. if (IsRunnerTarget(targetNumber)) { Print(string.Format("MoveTargetOrder SKIPPED: {0} is configured as Runner (trail-only)", targetType)); - return; + return false; } - if (quantity <= 0) return; + if (quantity <= 0) return false; - ConcurrentDictionary targetOrders = GetTargetOrdersDictionary(targetNumber); - if (targetOrders == null) return; + targetOrders = GetTargetOrdersDictionary(targetNumber); + return targetOrders != null; + } - Order existingTarget; - if (targetOrders.TryGetValue(entryName, out existingTarget) && existingTarget != null) + private bool MoveTargetOrder_PrepareFollowerReplace( + string entryName, + PositionInfo pos, + int targetNumber, + double newPrice, + int quantity, + Order existingTarget) + { + if (IsOrderTerminal(existingTarget.OrderState)) + return false; + + if (pos == null || !pos.IsFollower || pos.ExecutingAccount == null) + return false; + + OrderAction exitAct = pos.Direction == MarketPosition.Long + ? OrderAction.Sell : OrderAction.BuyToCover; + string targetOrderName = "T" + targetNumber + "_" + entryName; + var tSpec = new FollowerTargetReplaceSpec { - if (IsOrderTerminal(existingTarget.OrderState)) - { - targetOrders.TryRemove(entryName, out _); - } - else if (pos != null && pos.IsFollower && pos.ExecutingAccount != null) - { - OrderAction exitAct = pos.Direction == MarketPosition.Long - ? OrderAction.Sell : OrderAction.BuyToCover; - string targetOrderName = "T" + targetNumber + "_" + entryName; - var tSpec = new FollowerTargetReplaceSpec - { - EntryName = entryName, - TargetNum = targetNumber, - NewTargetPrice = newPrice, - Quantity = quantity, - ExitAction = exitAct, - TargetAccount = pos.ExecutingAccount, - CancellingOrderId = existingTarget.OrderId - }; - _followerTargetReplaceSpecs[targetOrderName] = tSpec; - StampReaperMoveGrace(); - pos.ExecutingAccount.Cancel(new[] { existingTarget }); - Print(string.Format("[UI_TGT] Follower target replace queued: T{0} {1} on {2} -> {3:F2}", - targetNumber, entryName, pos.ExecutingAccount.Name, newPrice)); - return; - } - else if (targetOrders.TryRemove(entryName, out existingTarget)) - { - CancelOrderSafe(existingTarget, pos); - } + EntryName = entryName, + TargetNum = targetNumber, + NewTargetPrice = newPrice, + Quantity = quantity, + ExitAction = exitAct, + TargetAccount = pos.ExecutingAccount, + CancellingOrderId = existingTarget.OrderId + }; + _followerTargetReplaceSpecs[targetOrderName] = tSpec; + StampReaperMoveGrace(); + pos.ExecutingAccount.Cancel(new[] { existingTarget }); + Print(string.Format("[UI_TGT] Follower target replace queued: T{0} {1} on {2} -> {3:F2}", + targetNumber, entryName, pos.ExecutingAccount.Name, newPrice)); + return true; + } + + private void MoveTargetOrder_CancelExisting( + string entryName, + PositionInfo pos, + ConcurrentDictionary targetOrders, + Order existingTarget) + { + if (IsOrderTerminal(existingTarget.OrderState)) + { + targetOrders.TryRemove(entryName, out _); } + else if (targetOrders.TryRemove(entryName, out existingTarget)) + { + CancelOrderSafe(existingTarget, pos); + } + } + private void MoveTargetOrder_SubmitReplacement( + string entryName, + PositionInfo pos, + string targetType, + double newPrice, + int quantity, + ConcurrentDictionary targetOrders) + { // Submit new target order at new price Order newTargetOrder = SubmitExitOrderForPosition(pos, quantity, OrderType.Limit, newPrice, targetType + "_" + entryName); @@ -656,81 +799,27 @@ private void ExecuteRunnerAction(string action) switch (action) { case "market": - // Close runner at market - Order runnerMarketOrder = SubmitExitOrderForPosition( - pos, runnerContracts, OrderType.Market, 0, "Runner_Market_" + entryName); - - if (runnerMarketOrder != null) - Print(string.Format("? RUNNER MARKET CLOSE: {0} - Closing {1} contracts at market", entryName, runnerContracts)); - else - Print(string.Format("ERROR RUNNER MARKET CLOSE FAILED: {0} - Could not close {1} contracts", entryName, runnerContracts)); + ExecuteRunner_Market(entryName, pos, runnerContracts); break; case "stop1pt": - // V8.19: Absolute profit lock (Entry + 1 point) - double newStop1pt = pos.Direction == MarketPosition.Long - ? pos.EntryPrice + 1.0 - : pos.EntryPrice - 1.0; - newStop1pt = Instrument.MasterInstrument.RoundToTickSize(newStop1pt); - - // Safety: Only move if it's better than current stop or entry-relative profit-lock - UpdateStopOrder(entryName, pos, newStop1pt, pos.CurrentTrailLevel); - Print(string.Format("? RUNNER STOP -> 1 PT PROFIT LOCK: {0} - Stop @ {1:F2} (Entry was {2:F2})", entryName, newStop1pt, pos.EntryPrice)); + ExecuteRunner_StopOnePoint(entryName, pos); break; case "stop2pt": - // V8.19: Absolute profit lock (Entry + 2 points) - double newStop2pt = pos.Direction == MarketPosition.Long - ? pos.EntryPrice + 2.0 - : pos.EntryPrice - 2.0; - newStop2pt = Instrument.MasterInstrument.RoundToTickSize(newStop2pt); - - UpdateStopOrder(entryName, pos, newStop2pt, pos.CurrentTrailLevel); - Print(string.Format("? RUNNER STOP -> 2 PT PROFIT LOCK: {0} - Stop @ {1:F2} (Entry was {2:F2})", entryName, newStop2pt, pos.EntryPrice)); + ExecuteRunner_StopTwoPoint(entryName, pos); break; case "stopbe": - // [Build 1102I] Use correct BE stop formula: EntryPrice +/- BreakEvenOffsetTicks. - // Guard checks vs full beStopTarget, not raw entry, to prevent partial-offset execution. - double beStopTarget = pos.Direction == MarketPosition.Long - ? pos.EntryPrice + (BreakEvenOffsetTicks * Instrument.MasterInstrument.TickSize) - : pos.EntryPrice - (BreakEvenOffsetTicks * Instrument.MasterInstrument.TickSize); - beStopTarget = Instrument.MasterInstrument.RoundToTickSize(beStopTarget); - bool beViable = pos.Direction == MarketPosition.Long - ? currentPrice >= beStopTarget - : currentPrice <= beStopTarget; - if (!beViable) - { - pos.ManualBreakevenArmed = true; - pos.ManualBreakevenTriggered = false; - Print(string.Format("? BE SHIELD: {0} price {1:F2} not at BE level {2:F2} -- armed for auto-trigger", - entryName, currentPrice, beStopTarget)); - break; - } - UpdateStopOrder(entryName, pos, beStopTarget, 1); - // [Build 1102K] Mark triggered so ManageTrailingStops armed path does not re-fire. - pos.ManualBreakevenTriggered = true; - Print(string.Format("? RUNNER STOP -> BREAKEVEN: {0} - Stop @ {1:F2} (Entry +/- {2} ticks)", - entryName, beStopTarget, BreakEvenOffsetTicks)); + ExecuteRunner_Breakeven(entryName, pos, currentPrice); break; case "lock50": - // Lock 50% of current profit - double unrealizedProfit = pos.Direction == MarketPosition.Long - ? currentPrice - pos.EntryPrice - : pos.EntryPrice - currentPrice; - double lock50Stop = pos.Direction == MarketPosition.Long - ? pos.EntryPrice + (unrealizedProfit * 0.5) - : pos.EntryPrice - (unrealizedProfit * 0.5); - lock50Stop = Instrument.MasterInstrument.RoundToTickSize(lock50Stop); - UpdateStopOrder(entryName, pos, lock50Stop, pos.CurrentTrailLevel); - Print(string.Format("? RUNNER LOCK 50%: {0} - Stop @ {1:F2} (profit: {2:F2})", entryName, lock50Stop, unrealizedProfit)); + ExecuteRunner_Lock50(entryName, pos, currentPrice); break; case "disabletrail": - // Disable trailing - keep stop where it is - pos.CurrentTrailLevel = 999; // Set to high number to prevent further trailing - Print(string.Format("? RUNNER TRAILING DISABLED: {0} - Stop fixed @ {1:F2}", entryName, pos.CurrentStopPrice)); + ExecuteRunner_DisableTrail(entryName, pos); break; } } @@ -740,6 +829,90 @@ private void ExecuteRunnerAction(string action) Print(string.Format("ERROR ExecuteRunnerAction ({0}): {1}", action, ex.Message)); } } + + private void ExecuteRunner_Market(string entryName, PositionInfo pos, int runnerContracts) + { + // Close runner at market + Order runnerMarketOrder = SubmitExitOrderForPosition( + pos, runnerContracts, OrderType.Market, 0, "Runner_Market_" + entryName); + + if (runnerMarketOrder != null) + Print(string.Format("? RUNNER MARKET CLOSE: {0} - Closing {1} contracts at market", entryName, runnerContracts)); + else + Print(string.Format("ERROR RUNNER MARKET CLOSE FAILED: {0} - Could not close {1} contracts", entryName, runnerContracts)); + } + + private void ExecuteRunner_StopOnePoint(string entryName, PositionInfo pos) + { + // V8.19: Absolute profit lock (Entry + 1 point) + double newStop1pt = pos.Direction == MarketPosition.Long + ? pos.EntryPrice + 1.0 + : pos.EntryPrice - 1.0; + newStop1pt = Instrument.MasterInstrument.RoundToTickSize(newStop1pt); + + // Safety: Only move if it's better than current stop or entry-relative profit-lock + UpdateStopOrder(entryName, pos, newStop1pt, pos.CurrentTrailLevel); + Print(string.Format("? RUNNER STOP -> 1 PT PROFIT LOCK: {0} - Stop @ {1:F2} (Entry was {2:F2})", entryName, newStop1pt, pos.EntryPrice)); + } + + private void ExecuteRunner_StopTwoPoint(string entryName, PositionInfo pos) + { + // V8.19: Absolute profit lock (Entry + 2 points) + double newStop2pt = pos.Direction == MarketPosition.Long + ? pos.EntryPrice + 2.0 + : pos.EntryPrice - 2.0; + newStop2pt = Instrument.MasterInstrument.RoundToTickSize(newStop2pt); + + UpdateStopOrder(entryName, pos, newStop2pt, pos.CurrentTrailLevel); + Print(string.Format("? RUNNER STOP -> 2 PT PROFIT LOCK: {0} - Stop @ {1:F2} (Entry was {2:F2})", entryName, newStop2pt, pos.EntryPrice)); + } + + private void ExecuteRunner_Breakeven(string entryName, PositionInfo pos, double currentPrice) + { + // [Build 1102I] Use correct BE stop formula: EntryPrice +/- BreakEvenOffsetTicks. + // Guard checks vs full beStopTarget, not raw entry, to prevent partial-offset execution. + double beStopTarget = pos.Direction == MarketPosition.Long + ? pos.EntryPrice + (BreakEvenOffsetTicks * Instrument.MasterInstrument.TickSize) + : pos.EntryPrice - (BreakEvenOffsetTicks * Instrument.MasterInstrument.TickSize); + beStopTarget = Instrument.MasterInstrument.RoundToTickSize(beStopTarget); + bool beViable = pos.Direction == MarketPosition.Long + ? currentPrice >= beStopTarget + : currentPrice <= beStopTarget; + if (!beViable) + { + pos.ManualBreakevenArmed = true; + pos.ManualBreakevenTriggered = false; + Print(string.Format("? BE SHIELD: {0} price {1:F2} not at BE level {2:F2} -- armed for auto-trigger", + entryName, currentPrice, beStopTarget)); + return; + } + UpdateStopOrder(entryName, pos, beStopTarget, 1); + // [Build 1102K] Mark triggered so ManageTrailingStops armed path does not re-fire. + pos.ManualBreakevenTriggered = true; + Print(string.Format("? RUNNER STOP -> BREAKEVEN: {0} - Stop @ {1:F2} (Entry +/- {2} ticks)", + entryName, beStopTarget, BreakEvenOffsetTicks)); + } + + private void ExecuteRunner_Lock50(string entryName, PositionInfo pos, double currentPrice) + { + // Lock 50% of current profit + double unrealizedProfit = pos.Direction == MarketPosition.Long + ? currentPrice - pos.EntryPrice + : pos.EntryPrice - currentPrice; + double lock50Stop = pos.Direction == MarketPosition.Long + ? pos.EntryPrice + (unrealizedProfit * 0.5) + : pos.EntryPrice - (unrealizedProfit * 0.5); + lock50Stop = Instrument.MasterInstrument.RoundToTickSize(lock50Stop); + UpdateStopOrder(entryName, pos, lock50Stop, pos.CurrentTrailLevel); + Print(string.Format("? RUNNER LOCK 50%: {0} - Stop @ {1:F2} (profit: {2:F2})", entryName, lock50Stop, unrealizedProfit)); + } + + private void ExecuteRunner_DisableTrail(string entryName, PositionInfo pos) + { + // Disable trailing - keep stop where it is + pos.CurrentTrailLevel = 999; // Set to high number to prevent further trailing + Print(string.Format("? RUNNER TRAILING DISABLED: {0} - Stop fixed @ {1:F2}", entryName, pos.CurrentStopPrice)); + } #endregion #endregion diff --git a/src/V12_002.UI.IPC.Commands.Config.cs b/src/V12_002.UI.IPC.Commands.Config.cs index 2054b3a2..989005cd 100644 --- a/src/V12_002.UI.IPC.Commands.Config.cs +++ b/src/V12_002.UI.IPC.Commands.Config.cs @@ -101,20 +101,34 @@ private void HandleConfigCommand(string[] parts) { // V12 PRO: Parse the full config sync from side panel if (parts.Length <= 2) + { return; + } string configMode = parts[1]; string configContent = parts[2]; string[] settingsItems = configContent.Split(';'); foreach (string setting in settingsItems) { - if (string.IsNullOrEmpty(setting)) continue; + if (string.IsNullOrEmpty(setting)) + { + continue; + } string[] kv = setting.Split(':'); - if (kv.Length < 2) continue; + if (kv.Length < 2) + { + continue; + } string key = kv[0].ToUpperInvariant(); string val = kv[1]; - if (TryApplyConfigTargets(key, val)) continue; - if (TryApplyConfigRisk(key, val, configMode)) continue; + if (TryApplyConfigTargets(key, val)) + { + continue; + } + if (TryApplyConfigRisk(key, val, configMode)) + { + continue; + } TryApplyConfigMode(key, val); } // Build 1106: Update current mode's profile cache so mode-switch remembers these values @@ -128,74 +142,195 @@ private void HandleConfigCommand(string[] parts) /// Build 945: Config sub-handler -- target values and types (T1-T5, COUNT, CIT). private bool TryApplyConfigTargets(string key, string val) { - if (key == "T1") { if (double.TryParse(val, out double v)) Target1Value = v; return true; } - if (key == "CIT") { ChaseIfTouchPoints = val; return true; } - if (key == "T2") { - if (double.TryParse(val, out double v)) { + if (TryApplyConfigTarget_Value(key, val)) + { + return true; + } + if (TryApplyConfigTarget_Type(key, val)) + { + return true; + } + return TryApplyConfigTarget_Count(key, val); + } + + private bool TryApplyConfigTarget_Value(string key, string val) + { + if (key == "T1") + { + if (double.TryParse(val, out double v)) + { string vmReason; if (!ValidateIpcMultiplier(v, out vmReason)) + { + Print($"[IPC REJECT] T1 value {v} rejected: {vmReason}"); + } + else + { + Target1Value = v; + } + } + return true; + } + if (key == "CIT") + { + ChaseIfTouchPoints = val; + return true; + } + if (key == "T2") + { + if (double.TryParse(val, out double v)) + { + string vmReason; + if (!ValidateIpcMultiplier(v, out vmReason)) + { Print($"[IPC REJECT] T2 value {v} rejected: {vmReason}"); - else Target2Value = v; + } + else + { + Target2Value = v; + } } return true; } - if (key == "T3") { - if (double.TryParse(val, out double v)) { + if (key == "T3") + { + if (double.TryParse(val, out double v)) + { string vmReason; if (!ValidateIpcMultiplier(v, out vmReason)) + { Print($"[IPC REJECT] T3 value {v} rejected: {vmReason}"); - else Target3Value = v; + } + else + { + Target3Value = v; + } } return true; } - if (key == "T4") { - if (double.TryParse(val, out double v)) { + if (key == "T4") + { + if (double.TryParse(val, out double v)) + { string vmReason; if (!ValidateIpcMultiplier(v, out vmReason)) + { Print($"[IPC REJECT] T4 value {v} rejected: {vmReason}"); - else Target4Value = v; + } + else + { + Target4Value = v; + } } return true; } - if (key == "T5") { - if (double.TryParse(val, out double v)) { + if (key == "T5") + { + if (double.TryParse(val, out double v)) + { string vmReason; if (!ValidateIpcMultiplier(v, out vmReason)) + { Print($"[IPC REJECT] T5 value {v} rejected: {vmReason}"); - else Target5Value = v; + } + else + { + Target5Value = v; + } + } + return true; + } + return false; + } + + private bool TryApplyConfigTarget_Type(string key, string val) + { + if (key == "T1TYPE") + { + if (TryParseTargetMode(val, out var parsed)) + { + T1Type = parsed; + } + return true; + } + if (key == "T2TYPE") + { + if (TryParseTargetMode(val, out var parsed)) + { + T2Type = parsed; + } + return true; + } + if (key == "T3TYPE") + { + if (TryParseTargetMode(val, out var parsed)) + { + T3Type = parsed; + } + return true; + } + if (key == "T4TYPE") + { + if (TryParseTargetMode(val, out var parsed)) + { + T4Type = parsed; } return true; } - if (key == "T1TYPE") { if (TryParseTargetMode(val, out var parsed)) T1Type = parsed; return true; } - if (key == "T2TYPE") { if (TryParseTargetMode(val, out var parsed)) T2Type = parsed; return true; } - if (key == "T3TYPE") { if (TryParseTargetMode(val, out var parsed)) T3Type = parsed; return true; } - if (key == "T4TYPE") { if (TryParseTargetMode(val, out var parsed)) T4Type = parsed; return true; } - if (key == "T5TYPE") { if (TryParseTargetMode(val, out var parsed)) T5Type = parsed; return true; } - if (key == "COUNT") { - if (int.TryParse(val, out int v)) { - // FIX-B [Build 1102Z]: Clamp + lock to prevent IPC race with SIMA dispatch loop. - int clamped = Math.Max(1, Math.Min(5, v)); - activeTargetCount = clamped; + if (key == "T5TYPE") + { + if (TryParseTargetMode(val, out var parsed)) + { + T5Type = parsed; } return true; } return false; } + private bool TryApplyConfigTarget_Count(string key, string val) + { + if (key != "COUNT") + { + return false; + } + + if (int.TryParse(val, out int v)) + { + // FIX-B [Build 1102Z]: Clamp + lock to prevent IPC race with SIMA dispatch loop. + int clamped = Math.Max(1, Math.Min(5, v)); + activeTargetCount = clamped; + } + return true; + } + /// Build 945: Config sub-handler -- risk parameters (STR, MAX). private bool TryApplyConfigRisk(string key, string val, string configMode) { - if (key == "STR") { - if (double.TryParse(val, out double v)) { + if (key == "STR") + { + if (double.TryParse(val, out double v)) + { string vmReason; if (!ValidateIpcMultiplier(v, out vmReason)) + { Print($"[IPC REJECT] STR multiplier {v} rejected: {vmReason}"); - else if (configMode == "RMA") RMAStopATRMultiplier = v; else StopMultiplier = v; + } + else if (configMode == "RMA") + { + RMAStopATRMultiplier = v; + } + else + { + StopMultiplier = v; + } } return true; } - if (key == "MAX") { - if (double.TryParse(val, out double v)) { + if (key == "MAX") + { + if (double.TryParse(val, out double v)) + { MaxRiskAmount = v; RiskPerTrade = v; } @@ -207,8 +342,16 @@ private bool TryApplyConfigRisk(string key, string val, string configMode) /// Build 945: Config sub-handler -- mode flags (TRMA, RRMA). private bool TryApplyConfigMode(string key, string val) { - if (key == "TRMA") { isTrendRmaMode = (val == "1"); return true; } - if (key == "RRMA") { isRetestRmaMode = (val == "1"); return true; } + if (key == "TRMA") + { + isTrendRmaMode = (val == "1"); + return true; + } + if (key == "RRMA") + { + isRetestRmaMode = (val == "1"); + return true; + } return false; } diff --git a/src/V12_002.UI.IPC.Commands.Fleet.cs b/src/V12_002.UI.IPC.Commands.Fleet.cs index d26789e0..70bc7ccd 100644 --- a/src/V12_002.UI.IPC.Commands.Fleet.cs +++ b/src/V12_002.UI.IPC.Commands.Fleet.cs @@ -40,422 +40,528 @@ private bool TryHandleFleetCommand(string action, string[] parts, long senderTic ? action + "|" + senderTicks.ToString() : action + "|" + (DateTime.UtcNow.Ticks / TimeSpan.TicksPerMinute).ToString(); + if (TryHandleFleet_Trim(action, parts)) return true; + if (TryHandleFleet_Lock50(action)) return true; + if (TryHandleFleet_FlattenOnly(action)) return true; + if (TryHandleFleet_Flatten(action, cmdId)) return true; + if (TryHandleFleet_CancelAll(action, cmdId)) return true; + if (TryHandleFleet_ResetMemory(action)) return true; + if (TryHandleFleet_LongShort(action, cmdId)) return true; + if (TryHandleFleet_OrLong(action, cmdId)) return true; + if (TryHandleFleet_OrShort(action, cmdId)) return true; + if (TryHandleFleet_TrendManualLimit(action, parts, cmdId)) return true; + if (TryHandleFleet_RetestManualLimit(action, parts, cmdId)) return true; + if (TryHandleFleet_FfmaManualLimit(action, parts, cmdId)) return true; + if (TryHandleFleet_FfmaManualMarket(action, cmdId)) return true; + if (TryHandleFleet_CloseTarget(action)) return true; + if (TryHandleFleet_MoveTarget(action, parts)) return true; + if (TryHandleFleet_FleetState(action, parts)) return true; + if (TryHandleFleet_ToggleAccount(action, parts)) return true; + if (TryHandleFleet_SetShadow(action, parts)) return true; + return false; + } + + private bool TryHandleFleet_Trim(string action, string[] parts) + { if (action == "TRIM_25" || action == "TRIM_50") { HandleTrimCommand(action, parts); return true; } - if (action == "LOCK_50") + + return false; + } + + private bool TryHandleFleet_Lock50(string action) + { + if (action != "LOCK_50") + return false; + + // [1102Z-F]: IPC LOCK_50 -- Lock 50% of unrealized profit on all active positions. + // Delegates to ExecuteRunnerAction which already handles all account routing. + Print("[IPC LOCK_50] Received -- routing to ExecuteRunnerAction(lock50)"); + Enqueue(ctx => ctx.ExecuteRunnerAction("lock50")); + return true; + } + + private bool TryHandleFleet_FlattenOnly(string action) + { + if (action != "FLATTEN_ONLY") + return false; + + // V12.21: Flatten Only (Close Positions) - preserve pending orders + if (EnableSIMA) { - // [1102Z-F]: IPC LOCK_50 -- Lock 50% of unrealized profit on all active positions. - // Delegates to ExecuteRunnerAction which already handles all account routing. - Print("[IPC LOCK_50] Received -- routing to ExecuteRunnerAction(lock50)"); - Enqueue(ctx => ctx.ExecuteRunnerAction("lock50")); - return true; + Print("[SIMA] IPC FLATTEN_ONLY -> Closing all open positions (Pending orders preserved)"); + ClosePositionsOnlyApexAccounts(); // V12.21: Use new non-cancelling helper } - if (action == "FLATTEN_ONLY") + else { - // V12.21: Flatten Only (Close Positions) - preserve pending orders - if (EnableSIMA) - { - Print("[SIMA] IPC FLATTEN_ONLY -> Closing all open positions (Pending orders preserved)"); - ClosePositionsOnlyApexAccounts(); // V12.21: Use new non-cancelling helper - } - else - { - Print("[V12] FLATTEN_ONLY -> Closing all open positions (Pending orders preserved)"); - // NT8 Flatten() cancels orders. We must use Close() on each position instead. + Print("[V12] FLATTEN_ONLY -> Closing all open positions (Pending orders preserved)"); + // NT8 Flatten() cancels orders. We must use Close() on each position instead. - foreach (Position pos in Account.Positions) + foreach (Position pos in Account.Positions) + { + if (pos.Instrument.FullName == Instrument.FullName && pos.MarketPosition != MarketPosition.Flat) { - if (pos.Instrument.FullName == Instrument.FullName && pos.MarketPosition != MarketPosition.Flat) - { - if (pos.MarketPosition == MarketPosition.Long) - SubmitOrderUnmanaged(0, OrderAction.Sell, OrderType.Market, pos.Quantity, 0, 0, "", "FlattenOnly_ExitLong"); - else - SubmitOrderUnmanaged(0, OrderAction.BuyToCover, OrderType.Market, pos.Quantity, 0, 0, "", "FlattenOnly_ExitShort"); - } + if (pos.MarketPosition == MarketPosition.Long) + SubmitOrderUnmanaged(0, OrderAction.Sell, OrderType.Market, pos.Quantity, 0, 0, "", "FlattenOnly_ExitLong"); + else + SubmitOrderUnmanaged(0, OrderAction.BuyToCover, OrderType.Market, pos.Quantity, 0, 0, "", "FlattenOnly_ExitShort"); } } - return true; } - if (action == "FLATTEN") - { - if (!MetadataGuardDuplicate(cmdId, action)) return true; - // V12 SIMA: Use multi-account flatten when enabled - if (EnableSIMA) - { - Print("[SIMA] IPC FLATTEN -> Broadcasting to all Apex accounts"); - FlattenAllApexAccounts(); - } - else - { - FlattenAll(); - } - return true; + return true; + } + + private bool TryHandleFleet_Flatten(string action, string cmdId) + { + if (action != "FLATTEN") + return false; + + if (!MetadataGuardDuplicate(cmdId, action)) return true; + + // V12 SIMA: Use multi-account flatten when enabled + if (EnableSIMA) + { + Print("[SIMA] IPC FLATTEN -> Broadcasting to all Apex accounts"); + FlattenAllApexAccounts(); } - if (action == "CANCEL_ALL") + else { - if (!MetadataGuardDuplicate(cmdId, action)) return true; + FlattenAll(); + } - // V12.13c: Only cancels pending entry orders (stops/targets on active positions are preserved) - if (EnableSIMA) - { - int cancelled = 0; + return true; + } - // Build 1001: Use broker truth (Account.Positions) for master -- expectedPositions[master] - // is not updated on entry fill, making it stale as a liveness gate. Broker truth is authoritative. - bool masterHasPosition = Account.Positions - .Any(p => p.Instrument != null && p.Instrument.FullName == Instrument.FullName - && p.MarketPosition != MarketPosition.Flat); + private bool TryHandleFleet_CancelAll(string action, string cmdId) + { + if (action != "CANCEL_ALL") + return false; - Account masterBroker996c = Account; - foreach (Order order in masterBroker996c.Orders.ToArray()) - { - if (order == null || order.Instrument?.FullName != Instrument?.FullName) continue; - if (order.OrderState == OrderState.Cancelled || - order.OrderState == OrderState.CancelPending || - order.OrderState == OrderState.CancelSubmitted || - order.OrderState == OrderState.Filled || - order.OrderState == OrderState.Rejected) continue; - if (masterHasPosition) continue; // Master has live position: preserve all. - CancelOrderOnAccount(order, masterBroker996c); - cancelled++; - } + if (!MetadataGuardDuplicate(cmdId, action)) return true; + + // V12.13c: Only cancels pending entry orders (stops/targets on active positions are preserved) + if (EnableSIMA) + { + int cancelled = 0; - // Fleet accounts - foreach (Account acct in Account.All) + // Build 1001: Use broker truth (Account.Positions) for master -- expectedPositions[master] + // is not updated on entry fill, making it stale as a liveness gate. Broker truth is authoritative. + bool masterHasPosition = Account.Positions + .Any(p => p.Instrument != null && p.Instrument.FullName == Instrument.FullName + && p.MarketPosition != MarketPosition.Flat); + + Account masterBroker996c = Account; + foreach (Order order in masterBroker996c.Orders.ToArray()) + { + if (order == null || order.Instrument?.FullName != Instrument?.FullName) continue; + if (order.OrderState == OrderState.Cancelled || + order.OrderState == OrderState.CancelPending || + order.OrderState == OrderState.CancelSubmitted || + order.OrderState == OrderState.Filled || + order.OrderState == OrderState.Rejected) continue; + if (masterHasPosition) continue; // Master has live position: preserve all. + CancelOrderOnAccount(order, masterBroker996c); + cancelled++; + } + + // Fleet accounts + foreach (Account acct in Account.All) + { + if (IsFleetAccount(acct)) { - if (IsFleetAccount(acct)) + if (acct == this.Account) continue; // already processed above + var acctFsms = _followerBrackets.Values.Where(f => f.AccountName == acct.Name).ToList(); + bool acctHasActiveFsm = acctFsms.Any(f => f.State == FollowerBracketState.Active); + foreach (Order order in acct.Orders) { - if (acct == this.Account) continue; // already processed above - var acctFsms = _followerBrackets.Values.Where(f => f.AccountName == acct.Name).ToList(); - bool acctHasActiveFsm = acctFsms.Any(f => f.State == FollowerBracketState.Active); - foreach (Order order in acct.Orders) + if (order != null && order.Instrument.FullName == Instrument.FullName && + (order.OrderState == OrderState.Working || + order.OrderState == OrderState.Accepted || + order.OrderState == OrderState.Submitted || + order.OrderState == OrderState.ChangePending || + order.OrderState == OrderState.ChangeSubmitted)) { - if (order != null && order.Instrument.FullName == Instrument.FullName && - (order.OrderState == OrderState.Working || - order.OrderState == OrderState.Accepted || - order.OrderState == OrderState.Submitted || - order.OrderState == OrderState.ChangePending || - order.OrderState == OrderState.ChangeSubmitted)) + string oName = order.Name; + if (oName.StartsWith("Stop_") || oName.StartsWith("S_") || + oName.StartsWith("T1_") || oName.StartsWith("T2_") || + oName.StartsWith("T3_") || oName.StartsWith("T4_") || oName.StartsWith("T5_")) { - string oName = order.Name; - if (oName.StartsWith("Stop_") || oName.StartsWith("S_") || - oName.StartsWith("T1_") || oName.StartsWith("T2_") || - oName.StartsWith("T3_") || oName.StartsWith("T4_") || oName.StartsWith("T5_")) - { - // Build 1104.1: Preserve brackets ONLY if FSM is active AND Master has position. - // If Master is FLAT, orphaned follower brackets MUST be swept regardless of FSM state. - if (acctHasActiveFsm && masterHasPosition) continue; - } - - CancelOrderOnAccount(order, acct); - cancelled++; + // Build 1104.1: Preserve brackets ONLY if FSM is active AND Master has position. + // If Master is FLAT, orphaned follower brackets MUST be swept regardless of FSM state. + if (acctHasActiveFsm && masterHasPosition) continue; } + + CancelOrderOnAccount(order, acct); + cancelled++; } } } - Print($"[SIMA] CANCEL_ALL -> Cancelled {cancelled} orders (Entries + Orphaned Brackets) (local + fleet) [1001]"); } - else + Print($"[SIMA] CANCEL_ALL -> Cancelled {cancelled} orders (Entries + Orphaned Brackets) (local + fleet) [1001]"); + } + else + { + int cancelled = 0; + foreach (Order order in Account.Orders) { - int cancelled = 0; - foreach (Order order in Account.Orders) + if (order != null && order.Instrument.FullName == Instrument.FullName && + (order.OrderState == OrderState.Working || + order.OrderState == OrderState.Accepted || + order.OrderState == OrderState.Submitted || + order.OrderState == OrderState.ChangePending || + order.OrderState == OrderState.ChangeSubmitted)) { - if (order != null && order.Instrument.FullName == Instrument.FullName && - (order.OrderState == OrderState.Working || - order.OrderState == OrderState.Accepted || - order.OrderState == OrderState.Submitted || - order.OrderState == OrderState.ChangePending || - order.OrderState == OrderState.ChangeSubmitted)) - { - string oName = order.Name; - if (oName.StartsWith("Stop_") || oName.StartsWith("S_") || - oName.StartsWith("T1_") || oName.StartsWith("T2_") || - oName.StartsWith("T3_") || oName.StartsWith("T4_") || oName.StartsWith("T5_")) - continue; - - CancelOrderOnAccount(order, order.Account); - cancelled++; - } + string oName = order.Name; + if (oName.StartsWith("Stop_") || oName.StartsWith("S_") || + oName.StartsWith("T1_") || oName.StartsWith("T2_") || + oName.StartsWith("T3_") || oName.StartsWith("T4_") || oName.StartsWith("T5_")) + continue; + + CancelOrderOnAccount(order, order.Account); + cancelled++; } - Print($"[V12] CANCEL_ALL -> Cancelled {cancelled} pending entry orders"); } + Print($"[V12] CANCEL_ALL -> Cancelled {cancelled} pending entry orders"); + } - // V1102Z-HARDEN: Ghost Memory Teardown removed (V2 Forensic Fix) - // We no longer zero expectedPositions immediately upon command launch. - // State mutation is now reactive to broker confirmation via OnAccountOrderUpdate. + // V1102Z-HARDEN: Ghost Memory Teardown removed (V2 Forensic Fix) + // We no longer zero expectedPositions immediately upon command launch. + // State mutation is now reactive to broker confirmation via OnAccountOrderUpdate. - // Clean up local position objects for anything not filled - foreach (var kvp in activePositions.ToArray()) + // Clean up local position objects for anything not filled + foreach (var kvp in activePositions.ToArray()) + { + if (!kvp.Value.EntryFilled) { - if (!kvp.Value.EntryFilled) - { - CleanupPosition(kvp.Key); - Print(string.Format("V12.13b: CANCEL_ALL cleaned unfilled memory entry: {0}", kvp.Key)); - } + CleanupPosition(kvp.Key); + Print(string.Format("V12.13b: CANCEL_ALL cleaned unfilled memory entry: {0}", kvp.Key)); } - return true; } - if (action == "RESET_MEMORY") + + return true; + } + + private bool TryHandleFleet_ResetMemory(string action) + { + if (action != "RESET_MEMORY") + return false; + + int resetAcctCount = 0; + foreach (Account acct in Account.All) { - int resetAcctCount = 0; - foreach (Account acct in Account.All) + if (IsFleetAccount(acct) || acct == this.Account) { - if (IsFleetAccount(acct) || acct == this.Account) - { - SetExpectedPositionLocked(ExpKey(acct.Name), 0); - resetAcctCount++; - } + SetExpectedPositionLocked(ExpKey(acct.Name), 0); + resetAcctCount++; } - Print($"[V1102Z] RESET_MEMORY: Zeroed all fleet/master expectedPositions for {Instrument.FullName} across {resetAcctCount} accounts."); - SendResponseToRemote("MSG|Memory Reset Complete"); - return true; } - if (action == "LONG" || action == "SHORT") - { - if (!MetadataGuardDuplicate(cmdId, action)) return true; + Print($"[V1102Z] RESET_MEMORY: Zeroed all fleet/master expectedPositions for {Instrument.FullName} across {resetAcctCount} accounts."); + SendResponseToRemote("MSG|Memory Reset Complete"); + return true; + } - if (isTosSyncMode) + private bool TryHandleFleet_LongShort(string action, string cmdId) + { + if (action != "LONG" && action != "SHORT") + return false; + + if (!MetadataGuardDuplicate(cmdId, action)) return true; + + if (isTosSyncMode) + { + bool armed = (action == "LONG") ? isLongArmed : isShortArmed; + if (!armed) { - bool armed = (action == "LONG") ? isLongArmed : isShortArmed; - if (!armed) - { - Print($"[SYNC] ToS Signal IGNORED: {action} received but {action} is not ARMED locally."); - return true; - } - else - { - Print($"[SYNC] ToS Handshake Received -> Executing {action} Fleet Entry"); - if (action == "LONG") isLongArmed = false; else isShortArmed = false; - } + Print($"[SYNC] ToS Signal IGNORED: {action} received but {action} is not ARMED locally."); + return true; } + else + { + Print($"[SYNC] ToS Handshake Received -> Executing {action} Fleet Entry"); + if (action == "LONG") isLongArmed = false; else isShortArmed = false; + } + } - if (EnableSIMA) + if (EnableSIMA) + { + OrderAction orderAction = action == "LONG" ? OrderAction.Buy : OrderAction.SellShort; + int qty; + try { - OrderAction orderAction = action == "LONG" ? OrderAction.Buy : OrderAction.SellShort; - int qty; - try + double stopDist = CalculateATRStopDistance(RMAStopATRMultiplier); + if (stopDist <= 0) { - double stopDist = CalculateATRStopDistance(RMAStopATRMultiplier); - if (stopDist <= 0) - { - stopDist = MinimumStop; - Print($"[IPC SIZING] ATR latency detected. Falling back to MinimumStop={MinimumStop:F4}"); - } - qty = stopDist > 0 ? CalculatePositionSize(stopDist) : Math.Max(1, minContracts); - Print($"[IPC SIZING] Calculation: StopDist={stopDist:F4}, Risk={MaxRiskAmount}, TargetQty={qty}"); + stopDist = MinimumStop; + Print($"[IPC SIZING] ATR latency detected. Falling back to MinimumStop={MinimumStop:F4}"); } - catch - { - qty = Math.Max(1, minContracts); - } - qty = Math.Max(1, qty); + qty = stopDist > 0 ? CalculatePositionSize(stopDist) : Math.Max(1, minContracts); + Print($"[IPC SIZING] Calculation: StopDist={stopDist:F4}, Risk={MaxRiskAmount}, TargetQty={qty}"); + } + catch + { + qty = Math.Max(1, minContracts); + } + qty = Math.Max(1, qty); - if (EnablePathB) - { - Print($"[SIMA] PATH B {action} -> Broadcasting {qty} contracts with FIXED BRACKETS to all Apex accounts"); - ExecuteMultiAccountBracket(orderAction, qty, "PATHB_" + action, PathBStopPoints, PathBTargetPoints); - } - else - { - Print($"[SIMA] IPC {action} -> Broadcasting {qty} contracts to all Apex accounts"); - ExecuteMultiAccountMarket(orderAction, qty, "SIMA_" + action); - } + if (EnablePathB) + { + Print($"[SIMA] PATH B {action} -> Broadcasting {qty} contracts with FIXED BRACKETS to all Apex accounts"); + ExecuteMultiAccountBracket(orderAction, qty, "PATHB_" + action, PathBStopPoints, PathBTargetPoints); } else { - MarketPosition direction = action == "LONG" ? MarketPosition.Long : MarketPosition.Short; - double currentPrice = lastKnownPrice > 0 ? lastKnownPrice : Close[0]; - if (currentPrice <= 0) - { - Print("[IPC] ABORT RMA dispatch: currentPrice=0. Skipping command."); - return true; - } - double stopDist = CalculateATRStopDistance(RMAStopATRMultiplier); - int contracts = CalculatePositionSize(stopDist); - Enqueue(ctx => ctx.ExecuteRMAEntryV2(currentPrice, direction, contracts)); + Print($"[SIMA] IPC {action} -> Broadcasting {qty} contracts to all Apex accounts"); + ExecuteMultiAccountMarket(orderAction, qty, "SIMA_" + action); } - return true; } - if (action == "OR_LONG") + else { - if (!MetadataGuardDuplicate(cmdId, action)) return true; - - if (isTosSyncMode) + MarketPosition direction = action == "LONG" ? MarketPosition.Long : MarketPosition.Short; + double currentPrice = lastKnownPrice > 0 ? lastKnownPrice : Close[0]; + if (currentPrice <= 0) { - if (isLongArmed) - { - double orStopDist = CalculateORStopDistance(); - int orContracts = CalculatePositionSize(orStopDist); - Enqueue(ctx => ctx.ExecuteLong(orContracts)); - isLongArmed = false; - } + Print("[IPC] ABORT RMA dispatch: currentPrice=0. Skipping command."); + return true; } - else + double stopDist = CalculateATRStopDistance(RMAStopATRMultiplier); + int contracts = CalculatePositionSize(stopDist); + Enqueue(ctx => ctx.ExecuteRMAEntryV2(currentPrice, direction, contracts)); + } + + return true; + } + + private bool TryHandleFleet_OrLong(string action, string cmdId) + { + if (action != "OR_LONG") + return false; + + if (!MetadataGuardDuplicate(cmdId, action)) return true; + + if (isTosSyncMode) + { + if (isLongArmed) { double orStopDist = CalculateORStopDistance(); int orContracts = CalculatePositionSize(orStopDist); Enqueue(ctx => ctx.ExecuteLong(orContracts)); + isLongArmed = false; } - return true; } - if (action == "OR_SHORT") + else { - if (!MetadataGuardDuplicate(cmdId, action)) return true; + double orStopDist = CalculateORStopDistance(); + int orContracts = CalculatePositionSize(orStopDist); + Enqueue(ctx => ctx.ExecuteLong(orContracts)); + } - if (isTosSyncMode) - { - if (isShortArmed) - { - double orStopDist = CalculateORStopDistance(); - int orContracts = CalculatePositionSize(orStopDist); - Enqueue(ctx => ctx.ExecuteShort(orContracts)); - isShortArmed = false; - } - } - else + return true; + } + + private bool TryHandleFleet_OrShort(string action, string cmdId) + { + if (action != "OR_SHORT") + return false; + + if (!MetadataGuardDuplicate(cmdId, action)) return true; + + if (isTosSyncMode) + { + if (isShortArmed) { double orStopDist = CalculateORStopDistance(); int orContracts = CalculatePositionSize(orStopDist); Enqueue(ctx => ctx.ExecuteShort(orContracts)); + isShortArmed = false; } - return true; } - if (action == "TREND_MANUAL_LIMIT") + else { - if (!MetadataGuardDuplicate(cmdId, action)) return true; + double orStopDist = CalculateORStopDistance(); + int orContracts = CalculatePositionSize(orStopDist); + Enqueue(ctx => ctx.ExecuteShort(orContracts)); + } + + return true; + } - if (parts.Length > 3) + private bool TryHandleFleet_TrendManualLimit(string action, string[] parts, string cmdId) + { + if (action != "TREND_MANUAL_LIMIT") + return false; + + if (!MetadataGuardDuplicate(cmdId, action)) return true; + + if (parts.Length > 3) + { + string dir = parts[2].Trim().ToUpperInvariant(); + MarketPosition mp = dir == "LONG" ? MarketPosition.Long : MarketPosition.Short; + if (double.TryParse(parts[3], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double price) && price > 0) { - string dir = parts[2].Trim().ToUpperInvariant(); - MarketPosition mp = dir == "LONG" ? MarketPosition.Long : MarketPosition.Short; - if (double.TryParse(parts[3], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double price) && price > 0) - { - double trendDist = CalculateTRENDStopDistance(); - int trendContracts = CalculatePositionSize(trendDist); - Enqueue(ctx => ctx.ExecuteTRENDManualEntry(price, mp, trendContracts)); - } + double trendDist = CalculateTRENDStopDistance(); + int trendContracts = CalculatePositionSize(trendDist); + Enqueue(ctx => ctx.ExecuteTRENDManualEntry(price, mp, trendContracts)); } - return true; } - if (action == "RETEST_MANUAL_LIMIT") - { - if (!MetadataGuardDuplicate(cmdId, action)) return true; - if (parts.Length > 3) + return true; + } + + private bool TryHandleFleet_RetestManualLimit(string action, string[] parts, string cmdId) + { + if (action != "RETEST_MANUAL_LIMIT") + return false; + + if (!MetadataGuardDuplicate(cmdId, action)) return true; + + if (parts.Length > 3) + { + string dir = parts[2].Trim().ToUpperInvariant(); + MarketPosition mp = dir == "LONG" ? MarketPosition.Long : MarketPosition.Short; + if (double.TryParse(parts[3], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double price) && price > 0) { - string dir = parts[2].Trim().ToUpperInvariant(); - MarketPosition mp = dir == "LONG" ? MarketPosition.Long : MarketPosition.Short; - if (double.TryParse(parts[3], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double price) && price > 0) - { - double retestDist = CalculateRetestStopDistance(); - int retestContracts = CalculatePositionSize(retestDist); - Enqueue(ctx => ctx.ExecuteRetestManualEntry(price, mp, retestContracts)); - } + double retestDist = CalculateRetestStopDistance(); + int retestContracts = CalculatePositionSize(retestDist); + Enqueue(ctx => ctx.ExecuteRetestManualEntry(price, mp, retestContracts)); } - return true; } - if (action == "FFMA_MANUAL_LIMIT") - { - if (!MetadataGuardDuplicate(cmdId, action)) return true; - if (parts.Length > 3) + return true; + } + + private bool TryHandleFleet_FfmaManualLimit(string action, string[] parts, string cmdId) + { + if (action != "FFMA_MANUAL_LIMIT") + return false; + + if (!MetadataGuardDuplicate(cmdId, action)) return true; + + if (parts.Length > 3) + { + string dir = parts[2].Trim().ToUpperInvariant(); + MarketPosition mp = dir == "LONG" ? MarketPosition.Long : MarketPosition.Short; + if (double.TryParse(parts[3], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double price) && price > 0) { - string dir = parts[2].Trim().ToUpperInvariant(); - MarketPosition mp = dir == "LONG" ? MarketPosition.Long : MarketPosition.Short; - if (double.TryParse(parts[3], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double price) && price > 0) - { - double ffmaStopDist = CalculateATRStopDistance(RMAStopATRMultiplier); - if (ffmaStopDist <= 0) ffmaStopDist = MinimumStop; - int contracts = CalculatePositionSize(ffmaStopDist); - Enqueue(ctx => ctx.ExecuteFFMALimitEntry(price, mp, contracts)); - } + double ffmaStopDist = CalculateATRStopDistance(RMAStopATRMultiplier); + if (ffmaStopDist <= 0) ffmaStopDist = MinimumStop; + int contracts = CalculatePositionSize(ffmaStopDist); + Enqueue(ctx => ctx.ExecuteFFMALimitEntry(price, mp, contracts)); } - return true; } - if (action == "FFMA_MANUAL_MARKET") - { - if (!MetadataGuardDuplicate(cmdId, action)) return true; - double currentPrice = lastKnownPrice > 0 ? lastKnownPrice : Close[0]; - double ema9Value = ema9[0]; - MarketPosition direction = currentPrice < ema9Value ? MarketPosition.Long : MarketPosition.Short; - double stopPrice = direction == MarketPosition.Long ? Low[0] : High[0]; - double ffmaStopDist = Math.Min(Math.Abs(currentPrice - stopPrice), MaximumStop); - if (ffmaStopDist < tickSize * 2) ffmaStopDist = tickSize * 2; - int contracts = CalculatePositionSize(ffmaStopDist); - Enqueue(ctx => ctx.ExecuteFFMAManualMarketEntry(contracts)); - return true; - } - if (action.StartsWith("CLOSE_T")) + return true; + } + + private bool TryHandleFleet_FfmaManualMarket(string action, string cmdId) + { + if (action != "FFMA_MANUAL_MARKET") + return false; + + if (!MetadataGuardDuplicate(cmdId, action)) return true; + + double currentPrice = lastKnownPrice > 0 ? lastKnownPrice : Close[0]; + double ema9Value = ema9[0]; + MarketPosition direction = currentPrice < ema9Value ? MarketPosition.Long : MarketPosition.Short; + double stopPrice = direction == MarketPosition.Long ? Low[0] : High[0]; + double ffmaStopDist = Math.Min(Math.Abs(currentPrice - stopPrice), MaximumStop); + if (ffmaStopDist < tickSize * 2) ffmaStopDist = tickSize * 2; + int contracts = CalculatePositionSize(ffmaStopDist); + Enqueue(ctx => ctx.ExecuteFFMAManualMarketEntry(contracts)); + return true; + } + + private bool TryHandleFleet_CloseTarget(string action) + { + if (!action.StartsWith("CLOSE_T")) + return false; + + int targetNum = 0; + if (action.Length > 7 && int.TryParse(action.Substring(7, 1), out targetNum)) { - int targetNum = 0; - if (action.Length > 7 && int.TryParse(action.Substring(7, 1), out targetNum)) - { - FlattenSpecificTarget(targetNum); - } - return true; + FlattenSpecificTarget(targetNum); } - if (action.StartsWith("MOVE_TARGET") || action == "SET_TARGET_PRICE") + + return true; + } + + private bool TryHandleFleet_MoveTarget(string action, string[] parts) + { + if (!action.StartsWith("MOVE_TARGET") && action != "SET_TARGET_PRICE") + return false; + + if (parts.Length >= 3) { - if (parts.Length >= 3) + string targetId = parts[1].Trim().ToUpperInvariant(); + string priceStr = parts[2].Trim(); + int targetNum = 0; + if (targetId.Length >= 2 && targetId.StartsWith("T") + && int.TryParse(targetId.Substring(1), out targetNum) + && targetNum >= 1 && targetNum <= 5) { - string targetId = parts[1].Trim().ToUpperInvariant(); - string priceStr = parts[2].Trim(); - int targetNum = 0; - if (targetId.Length >= 2 && targetId.StartsWith("T") - && int.TryParse(targetId.Substring(1), out targetNum) - && targetNum >= 1 && targetNum <= 5) + if (action == "SET_TARGET_PRICE") { - if (action == "SET_TARGET_PRICE") - { - // Build 1107: Absolute price move (from live control center) - double absPrice; - if (double.TryParse(priceStr, NumberStyles.Float, - CultureInfo.InvariantCulture, out absPrice) && absPrice > 0) - { - absPrice = Instrument.MasterInstrument.RoundToTickSize(absPrice); - MoveSpecificTargetAbsolute(targetNum, absPrice); - } - } - else + // Build 1107: Absolute price move (from live control center) + double absPrice; + if (double.TryParse(priceStr, NumberStyles.Float, + CultureInfo.InvariantCulture, out absPrice) && absPrice > 0) { - // Relative offset move (from context menu) - string distance = priceStr.ToLowerInvariant(); - double profitPoints = 0; - if (distance == "1pt") profitPoints = 1.0; - else if (distance == "2pt") profitPoints = 2.0; - else return true; - MoveSpecificTarget(targetNum, profitPoints); + absPrice = Instrument.MasterInstrument.RoundToTickSize(absPrice); + MoveSpecificTargetAbsolute(targetNum, absPrice); } } + else + { + // Relative offset move (from context menu) + string distance = priceStr.ToLowerInvariant(); + double profitPoints = 0; + if (distance == "1pt") profitPoints = 1.0; + else if (distance == "2pt") profitPoints = 2.0; + else return true; + MoveSpecificTarget(targetNum, profitPoints); + } } - return true; } + + return true; + } + + private bool TryHandleFleet_FleetState(string action, string[] parts) + { if (action.StartsWith("GET_FLEET") || action == "SET_SIMA" || action == "SET_LEADER_ACCOUNT" || action == "REQUEST_FLEET_STATE") { HandleFleetCommand(action, parts); return true; } - if (action.StartsWith("TOGGLE_ACCOUNT")) - { - HandleToggleAccountCommand(parts); - return true; - } - if (action == "SET_SHADOW") + + return false; + } + + private bool TryHandleFleet_ToggleAccount(string action, string[] parts) + { + if (!action.StartsWith("TOGGLE_ACCOUNT")) + return false; + + HandleToggleAccountCommand(parts); + return true; + } + + private bool TryHandleFleet_SetShadow(string action, string[] parts) + { + if (action != "SET_SHADOW") + return false; + + if (parts.Length >= 2) { - if (parts.Length >= 2) - { - bool enable = parts[1].Trim() == "true" || parts[1].Trim() == "1"; - ShadowModeEnabled = enable; - Print(string.Format("[IPC] Shadow Mode {0}", enable ? "ENABLED" : "DISABLED")); - } - return true; + bool enable = parts[1].Trim() == "true" || parts[1].Trim() == "1"; + ShadowModeEnabled = enable; + Print(string.Format("[IPC] Shadow Mode {0}", enable ? "ENABLED" : "DISABLED")); } - return false; + + return true; } #endregion diff --git a/src/V12_002.UI.IPC.Commands.Misc.cs b/src/V12_002.UI.IPC.Commands.Misc.cs index 126c6f22..71f503dc 100644 --- a/src/V12_002.UI.IPC.Commands.Misc.cs +++ b/src/V12_002.UI.IPC.Commands.Misc.cs @@ -72,83 +72,114 @@ private bool TryHandleComplianceCommand(string action, string[] parts) /// private void HandleFleetCommand(string action, string[] parts) { - if (action.StartsWith("GET_FLEET", StringComparison.OrdinalIgnoreCase)) - { - var fleetAccounts = GetFleetAccountsSnapshot(); - var aliasMap = BuildFleetAliasMap(fleetAccounts); - StringBuilder sb = new StringBuilder("CONFIG|FLEET"); - sb.Append("|COUNT:").Append(fleetAccounts.Count); - foreach (var acct in fleetAccounts) - sb.Append('|').Append(GetIpcFleetIdentity(acct.Name, aliasMap)); - SendResponseToRemote(sb.ToString()); - Print("[SIMA] GET_FLEET -> Responded with account list"); - } - else if (action == "SET_SIMA") + if (HandleFleet_GetFleet(action)) return; + if (HandleFleet_SetSima(action, parts)) return; + if (HandleFleet_DiagFleet(action)) return; + if (HandleFleet_SetLeader(action, parts)) return; + HandleFleet_RequestFleetState(action); + } + + private bool HandleFleet_GetFleet(string action) + { + if (!action.StartsWith("GET_FLEET", StringComparison.OrdinalIgnoreCase)) + return false; + + var fleetAccounts = GetFleetAccountsSnapshot(); + var aliasMap = BuildFleetAliasMap(fleetAccounts); + StringBuilder sb = new StringBuilder("CONFIG|FLEET"); + sb.Append("|COUNT:").Append(fleetAccounts.Count); + foreach (var acct in fleetAccounts) + sb.Append('|').Append(GetIpcFleetIdentity(acct.Name, aliasMap)); + SendResponseToRemote(sb.ToString()); + Print("[SIMA] GET_FLEET -> Responded with account list"); + return true; + } + + private bool HandleFleet_SetSima(string action, string[] parts) + { + if (action != "SET_SIMA") + return false; + + if (parts.Length > 1) { - if (parts.Length > 1) - { - bool enable = parts[1].Trim().ToUpperInvariant() == "ON"; - ApplySimaState(enable); - Print($"V12.Phase6: SET_SIMA = {enable} (lifecycle applied)"); - } + bool enable = parts[1].Trim().ToUpperInvariant() == "ON"; + ApplySimaState(enable); + Print($"V12.Phase6: SET_SIMA = {enable} (lifecycle applied)"); } - else if (action == "DIAG_FLEET") + + return true; + } + + private bool HandleFleet_DiagFleet(string action) + { + if (action != "DIAG_FLEET") + return false; + + Print("[DIAG] ##################################################"); + Print($"[DIAG] EnableSIMA = {EnableSIMA}"); + Print($"[DIAG] AccountPrefix = \"{AccountPrefix}\""); + int total = 0; + int active = 0; + foreach (Account acct in Account.All) { - Print("[DIAG] ##################################################"); - Print($"[DIAG] EnableSIMA = {EnableSIMA}"); - Print($"[DIAG] AccountPrefix = \"{AccountPrefix}\""); - int total = 0; - int active = 0; - foreach (Account acct in Account.All) + if (IsFleetAccount(acct)) { - if (IsFleetAccount(acct)) - { - total++; - bool isActive = false; - activeFleetAccounts.TryGetValue(acct.Name, out isActive); - if (isActive) active++; - Print($"[DIAG] {acct.Name} -> {(isActive ? "? ACTIVE" : "[X] INACTIVE")}"); - } + total++; + bool isActive = false; + activeFleetAccounts.TryGetValue(acct.Name, out isActive); + if (isActive) active++; + Print($"[DIAG] {acct.Name} -> {(isActive ? "? ACTIVE" : "[X] INACTIVE")}"); } - Print($"[DIAG] TOTAL: {total} accounts | {active} ACTIVE"); - Print("[DIAG] ##################################################"); } - else if (action == "SET_LEADER_ACCOUNT") + Print($"[DIAG] TOTAL: {total} accounts | {active} ACTIVE"); + Print("[DIAG] ##################################################"); + return true; + } + + private bool HandleFleet_SetLeader(string action, string[] parts) + { + if (action != "SET_LEADER_ACCOUNT") + return false; + + if (parts.Length > 1) { - if (parts.Length > 1) - { - string newLeader = parts[1].Trim(); - _stickyLeaderAccount = newLeader; // Build 1103: Store for persistence - Print($"V12.25 IPC: Leader Account synced to [{newLeader}]"); - MarkStickyDirty(); // Build 1103: Persist leader - } + string newLeader = parts[1].Trim(); + _stickyLeaderAccount = newLeader; // Build 1103: Store for persistence + Print($"V12.25 IPC: Leader Account synced to [{newLeader}]"); + MarkStickyDirty(); // Build 1103: Persist leader } - else if (action == "REQUEST_FLEET_STATE") + + return true; + } + + private void HandleFleet_RequestFleetState(string action) + { + if (action != "REQUEST_FLEET_STATE") + return; + + StringBuilder fsb = new StringBuilder("FLEET_STATE|"); + fsb.Append(Instrument.FullName).Append("|"); + fsb.Append(Position.MarketPosition).Append("|"); + + var fleetAccounts = GetFleetAccountsSnapshot(); + var aliasMap = BuildFleetAliasMap(fleetAccounts); + List acctStates = new List(); + foreach (Account acct in fleetAccounts) { - StringBuilder fsb = new StringBuilder("FLEET_STATE|"); - fsb.Append(Instrument.FullName).Append("|"); - fsb.Append(Position.MarketPosition).Append("|"); - - var fleetAccounts = GetFleetAccountsSnapshot(); - var aliasMap = BuildFleetAliasMap(fleetAccounts); - List acctStates = new List(); - foreach (Account acct in fleetAccounts) + var bPos = acct.Positions.FirstOrDefault(p => p.Instrument.FullName == Instrument.FullName); + int act = 0; + if (bPos != null && bPos.MarketPosition != MarketPosition.Flat) { - var bPos = acct.Positions.FirstOrDefault(p => p.Instrument.FullName == Instrument.FullName); - int act = 0; - if (bPos != null && bPos.MarketPosition != MarketPosition.Flat) - { - act = (bPos.MarketPosition == MarketPosition.Long) ? (int)bPos.Quantity : -(int)bPos.Quantity; - } - int exp = 0; - expectedPositions?.TryGetValue(ExpKey(acct.Name), out exp); - acctStates.Add($"{GetIpcFleetIdentity(acct.Name, aliasMap)}:{act}:{exp}"); + act = (bPos.MarketPosition == MarketPosition.Long) ? (int)bPos.Quantity : -(int)bPos.Quantity; } - fsb.Append(string.Join(";", acctStates)); - SendResponseToRemote(fsb.ToString()); - if (!string.IsNullOrEmpty(_stickyLeaderAccount)) - SendResponseToRemote("SET_LEADER_ACCOUNT|" + _stickyLeaderAccount); + int exp = 0; + expectedPositions?.TryGetValue(ExpKey(acct.Name), out exp); + acctStates.Add($"{GetIpcFleetIdentity(acct.Name, aliasMap)}:{act}:{exp}"); } + fsb.Append(string.Join(";", acctStates)); + SendResponseToRemote(fsb.ToString()); + if (!string.IsNullOrEmpty(_stickyLeaderAccount)) + SendResponseToRemote("SET_LEADER_ACCOUNT|" + _stickyLeaderAccount); } // ========================================================================= @@ -216,21 +247,8 @@ private void FlattenSpecificTarget(int targetNumber) if (!pos.EntryFilled || pos.RemainingContracts <= 0) continue; - int qtyToClose = 0; - ConcurrentDictionary targetDict = null; - string targetName = ""; - - switch (targetNumber) - { - case 1: qtyToClose = pos.T1Contracts; targetDict = target1Orders; targetName = "T1"; break; - case 2: qtyToClose = pos.T2Contracts; targetDict = target2Orders; targetName = "T2"; break; - case 3: qtyToClose = pos.T3Contracts; targetDict = target3Orders; targetName = "T3"; break; - case 4: qtyToClose = pos.T4Contracts; targetDict = target4Orders; targetName = "T4"; break; - case 5: qtyToClose = pos.T5Contracts; targetDict = target5Orders; targetName = "T5"; break; - default: - Print(string.Format("V10.3: Invalid target number {0}", targetNumber)); - return; - } + if (!FlattenSpecificTarget_ResolveTarget(targetNumber, pos, out int qtyToClose, out ConcurrentDictionary targetDict, out string targetName)) + return; if (qtyToClose <= 0) { @@ -238,33 +256,14 @@ private void FlattenSpecificTarget(int targetNumber) continue; } - // Cancel existing limit order if working - if (targetDict != null && targetDict.TryGetValue(entryName, out Order targetOrder)) - { - if (targetOrder != null && (targetOrder.OrderState == OrderState.Working || - targetOrder.OrderState == OrderState.Accepted || - targetOrder.OrderState == OrderState.Submitted)) - { - CancelOrderSafe(targetOrder, pos); - Print(string.Format("V10.3: Cancelled {0} limit order for {1}", targetName, entryName)); - } - } + FlattenSpecificTarget_CancelLimit(entryName, pos, targetName, targetDict); // Build 1108.003 [D1]: Pre-cancel stop when closing the entire remaining position. // Without this, follower accounts can have a working stop + market exit simultaneously. if (qtyToClose >= pos.RemainingContracts) - { - RequestStopCancelLifecycleSafe(entryName); - Print(string.Format("V10.3: Full close -- requested stop cancel for {0}", entryName)); - } + FlattenSpecificTarget_RequestStopCancel(entryName); - // Submit market order to close the target contracts - Order closeOrder = SubmitExitOrderForPosition( - pos, qtyToClose, OrderType.Market, 0, string.Format("Close{0}_{1}", targetName, entryName)); - if (closeOrder != null) - Print(string.Format("V10.3: Closing {0} ({1} contracts) at market for {2}", targetName, qtyToClose, entryName)); - else - Print(string.Format("V10.3: FAILED to close {0} ({1} contracts) at market for {2}", targetName, qtyToClose, entryName)); + FlattenSpecificTarget_SubmitMarketExit(entryName, pos, qtyToClose, targetName); } } catch (Exception ex) @@ -273,7 +272,78 @@ private void FlattenSpecificTarget(int targetNumber) } } + private bool FlattenSpecificTarget_ResolveTarget( + int targetNumber, + PositionInfo pos, + out int qtyToClose, + out ConcurrentDictionary targetDict, + out string targetName) + { + qtyToClose = 0; + targetDict = null; + targetName = ""; + + switch (targetNumber) + { + case 1: qtyToClose = pos.T1Contracts; targetDict = target1Orders; targetName = "T1"; return true; + case 2: qtyToClose = pos.T2Contracts; targetDict = target2Orders; targetName = "T2"; return true; + case 3: qtyToClose = pos.T3Contracts; targetDict = target3Orders; targetName = "T3"; return true; + case 4: qtyToClose = pos.T4Contracts; targetDict = target4Orders; targetName = "T4"; return true; + case 5: qtyToClose = pos.T5Contracts; targetDict = target5Orders; targetName = "T5"; return true; + default: + Print(string.Format("V10.3: Invalid target number {0}", targetNumber)); + return false; + } + } + + private void FlattenSpecificTarget_CancelLimit( + string entryName, + PositionInfo pos, + string targetName, + ConcurrentDictionary targetDict) + { + // Cancel existing limit order if working + if (targetDict != null && targetDict.TryGetValue(entryName, out Order targetOrder)) + { + if (targetOrder != null && (targetOrder.OrderState == OrderState.Working || + targetOrder.OrderState == OrderState.Accepted || + targetOrder.OrderState == OrderState.Submitted)) + { + CancelOrderSafe(targetOrder, pos); + Print(string.Format("V10.3: Cancelled {0} limit order for {1}", targetName, entryName)); + } + } + } + + private void FlattenSpecificTarget_RequestStopCancel(string entryName) + { + RequestStopCancelLifecycleSafe(entryName); + Print(string.Format("V10.3: Full close -- requested stop cancel for {0}", entryName)); + } + + private void FlattenSpecificTarget_SubmitMarketExit( + string entryName, + PositionInfo pos, + int qtyToClose, + string targetName) + { + // Submit market order to close the target contracts + Order closeOrder = SubmitExitOrderForPosition( + pos, qtyToClose, OrderType.Market, 0, string.Format("Close{0}_{1}", targetName, entryName)); + if (closeOrder != null) + Print(string.Format("V10.3: Closing {0} ({1} contracts) at market for {2}", targetName, qtyToClose, entryName)); + else + Print(string.Format("V10.3: FAILED to close {0} ({1} contracts) at market for {2}", targetName, qtyToClose, entryName)); + } + private void ToggleStrategyMode(string action) + { + ToggleStrategyMode_SetFlags(action); + ToggleStrategyMode_ExecuteModeAction(action); + ToggleStrategyMode_PublishSnapshot(action); + } + + private void ToggleStrategyMode_SetFlags(string action) { // V12.20: Atomic flag mutations if (action == "MODE_RMA") @@ -320,7 +390,10 @@ private void ToggleStrategyMode(string action) isRetestRmaMode = false; Print("IPC: RETEST Standard Mode Enabled"); } + } + private void ToggleStrategyMode_ExecuteModeAction(string action) + { // Execution calls stay outside lock (they do their own order management) if (action == "EXEC_TREND" || action == "EXEC_TREND_RMA") { @@ -335,12 +408,12 @@ private void ToggleStrategyMode(string action) Enqueue(ctx => ctx.ExecuteRetestEntry(retestContracts)); } else if (action == "EXEC_MOMO") - { - double momoStopDist = Math.Min(MOMOStopPoints, MaximumStop); - int momoContracts = CalculatePositionSize(momoStopDist); - double capturedMomoPrice = lastKnownPrice; - Enqueue(ctx => ctx.ExecuteMOMOEntry(capturedMomoPrice, momoContracts)); - } + { + double momoStopDist = Math.Min(MOMOStopPoints, MaximumStop); + int momoContracts = CalculatePositionSize(momoStopDist); + double capturedMomoPrice = lastKnownPrice; + Enqueue(ctx => ctx.ExecuteMOMOEntry(capturedMomoPrice, momoContracts)); + } else if (action == "MODE_M") { // V12.24: Immediate market entry using FFMA trade DNA @@ -354,7 +427,10 @@ private void ToggleStrategyMode(string action) int ffmaContracts = CalculatePositionSize(ffmaStopDist); Enqueue(ctx => ctx.ExecuteFFMAEntry(direction, ffmaContracts)); } + } + private void ToggleStrategyMode_PublishSnapshot(string action) + { if (action == "MODE_RMA" || action == "MODE_MOMO" || action == "MODE_FFMA" diff --git a/src/V12_002.UI.IPC.Commands.Mode.cs b/src/V12_002.UI.IPC.Commands.Mode.cs index 5a770856..6065f8b9 100644 --- a/src/V12_002.UI.IPC.Commands.Mode.cs +++ b/src/V12_002.UI.IPC.Commands.Mode.cs @@ -36,104 +36,139 @@ public partial class V12_002 : Strategy private bool TryHandleModeCommand(string action, string[] parts) { - if (action == "SET_RMA_MODE") + if (TryHandleMode_SetRmaMode(action, parts)) return true; + if (TryHandleMode_SyncMode(action, parts)) return true; + if (TryHandleMode_MktSync(action)) return true; + if (TryHandleMode_SyncAll(action)) return true; + if (TryHandleMode_SetMode(action, parts)) return true; + if (TryHandleMode_ToggleOrExecute(action)) return true; + return false; + } + + private bool TryHandleMode_SetRmaMode(string action, string[] parts) + { + if (action != "SET_RMA_MODE") + return false; + + if (parts.Length > 1) { - if (parts.Length > 1) - { - bool enable = parts[1].Trim().ToUpperInvariant() == "ON"; - isRMAModeActive = enable; - isRMAButtonClicked = enable; - if (!enable) - ClearClickTraderBorderIfInactive(); - Print(string.Format("V12.4: SET_RMA_MODE = {0} (Chart-Click RMA {1})", enable, enable ? "ENABLED" : "DISABLED")); - MarkStickyDirty(); // Build 1103: Persist RMA toggle - BumpUiConfigRevision(); - PublishUiSnapshot(); - } - return true; + bool enable = parts[1].Trim().ToUpperInvariant() == "ON"; + isRMAModeActive = enable; + isRMAButtonClicked = enable; + if (!enable) + ClearClickTraderBorderIfInactive(); + Print(string.Format("V12.4: SET_RMA_MODE = {0} (Chart-Click RMA {1})", enable, enable ? "ENABLED" : "DISABLED")); + MarkStickyDirty(); // Build 1103: Persist RMA toggle + BumpUiConfigRevision(); + PublishUiSnapshot(); } + + return true; + } + + private bool TryHandleMode_SyncMode(string action, string[] parts) + { // V12.2: SYNC_MODE|{MODE} - Relay mode sync from chart panel to external app - if (action == "SYNC_MODE") + if (action != "SYNC_MODE") + return false; + + if (parts.Length > 1) { - if (parts.Length > 1) - { - string syncMode = parts[1].Trim().ToUpperInvariant(); - // V12.13-D: Broadcast SYNC_MODE to all connected panel clients - SendResponseToRemote($"SYNC_MODE|{syncMode}"); - Print(string.Format("V12.2: SYNC_MODE Relay -> {0}", syncMode)); - } - return true; + string syncMode = parts[1].Trim().ToUpperInvariant(); + // V12.13-D: Broadcast SYNC_MODE to all connected panel clients + SendResponseToRemote($"SYNC_MODE|{syncMode}"); + Print(string.Format("V12.2: SYNC_MODE Relay -> {0}", syncMode)); } + + return true; + } + + private bool TryHandleMode_MktSync(string action) + { // Phase 9.1: MKT_SYNC -- Toggle ToS Armed Mode (Top button) - if (action == "MKT_SYNC") - { - isTosSyncMode = !isTosSyncMode; - Print(string.Format("[SYNC] ToS Sync Mode: {0}", isTosSyncMode)); - return true; - } + if (action != "MKT_SYNC") + return false; + + isTosSyncMode = !isTosSyncMode; + Print(string.Format("[SYNC] ToS Sync Mode: {0}", isTosSyncMode)); + return true; + } + + private bool TryHandleMode_SyncAll(string action) + { // Phase 9.1: SYNC_ALL -- Refresh active target orders to match current panel config (Bottom button) - if (action == "SYNC_ALL") - { - Print("[SYNC_ALL] Refresh triggered -- recalculating active target orders"); - RefreshActivePositionOrders(); - return true; - } + if (action != "SYNC_ALL") + return false; + + Print("[SYNC_ALL] Refresh triggered -- recalculating active target orders"); + RefreshActivePositionOrders(); + return true; + } + + private bool TryHandleMode_SetMode(string action, string[] parts) + { // V12.5: SET_MODE|mode - Panel is sole source of truth - if (action == "SET_MODE") + if (action != "SET_MODE") + return false; + + if (parts.Length > 1) { - if (parts.Length > 1) - { - string newMode = parts[1].Trim().ToUpperInvariant(); - - // Build 1106 Phase 1: Snapshot outgoing mode's config before switching - string outgoingMode = GetCurrentConfigMode(); - _modeProfiles[outgoingMode] = SnapshotCurrentConfig(); - - // ATOMIC mode transition: clear all flags first - isRMAModeActive = false; - isRMAButtonClicked = false; - isRetestModeActive = false; - isTRENDModeActive = false; - isMOMOModeActive = false; - isFFMAModeArmed = false; - - if (newMode == "RMA") { isRMAModeActive = true; isRMAButtonClicked = true; } - else if (newMode == "RETEST") isRetestModeActive = true; - else if (newMode == "TREND") isTRENDModeActive = true; - else if (newMode == "MOMO") { ActivateMOMOMode(); } - else if (newMode == "FFMA") isFFMAModeArmed = true; - - // Build 1106 Phase 2: Hydrate incoming mode's config (if profile exists) - ModeConfigProfile incomingProfile; - if (_modeProfiles.TryGetValue(newMode, out incomingProfile)) - { - HydrateFromProfile(incomingProfile, newMode); - Print(string.Format("[STICKY] Mode switch {0} -> {1}: hydrated profile (count={2})", - outgoingMode, newMode, incomingProfile.TargetCount)); - } - else - { - Print(string.Format("[STICKY] Mode switch {0} -> {1}: no saved profile, using current config", - outgoingMode, newMode)); - } - BumpUiConfigRevision(); - ClearClickTraderBorderIfInactive(); + string newMode = parts[1].Trim().ToUpperInvariant(); + + // Build 1106 Phase 1: Snapshot outgoing mode's config before switching + string outgoingMode = GetCurrentConfigMode(); + _modeProfiles[outgoingMode] = SnapshotCurrentConfig(); - Print(string.Format("V12.25: SET_MODE = {0} | RMA={1} RETEST={2} TREND={3} MOMO={4} FFMA={5} (no CONFIG echo)", - newMode, isRMAModeActive, isRetestModeActive, isTRENDModeActive, isMOMOModeActive, isFFMAModeArmed)); - MarkStickyDirty(); // Build 1103: Persist mode change - PublishUiSnapshot(); + // ATOMIC mode transition: clear all flags first + isRMAModeActive = false; + isRMAButtonClicked = false; + isRetestModeActive = false; + isTRENDModeActive = false; + isMOMOModeActive = false; + isFFMAModeArmed = false; - // V12.25: CONFIG broadcast REMOVED -- Panel is sole source of truth. - // Sending CONFIG back here caused the Ping-Pong overwrite bug. + if (newMode == "RMA") { isRMAModeActive = true; isRMAButtonClicked = true; } + else if (newMode == "RETEST") isRetestModeActive = true; + else if (newMode == "TREND") isTRENDModeActive = true; + else if (newMode == "MOMO") { ActivateMOMOMode(); } + else if (newMode == "FFMA") isFFMAModeArmed = true; + + // Build 1106 Phase 2: Hydrate incoming mode's config (if profile exists) + ModeConfigProfile incomingProfile; + if (_modeProfiles.TryGetValue(newMode, out incomingProfile)) + { + HydrateFromProfile(incomingProfile, newMode); + Print(string.Format("[STICKY] Mode switch {0} -> {1}: hydrated profile (count={2})", + outgoingMode, newMode, incomingProfile.TargetCount)); } - return true; + else + { + Print(string.Format("[STICKY] Mode switch {0} -> {1}: no saved profile, using current config", + outgoingMode, newMode)); + } + BumpUiConfigRevision(); + ClearClickTraderBorderIfInactive(); + + Print(string.Format("V12.25: SET_MODE = {0} | RMA={1} RETEST={2} TREND={3} MOMO={4} FFMA={5} (no CONFIG echo)", + newMode, isRMAModeActive, isRetestModeActive, isTRENDModeActive, isMOMOModeActive, isFFMAModeArmed)); + MarkStickyDirty(); // Build 1103: Persist mode change + PublishUiSnapshot(); + + // V12.25: CONFIG broadcast REMOVED -- Panel is sole source of truth. + // Sending CONFIG back here caused the Ping-Pong overwrite bug. } + + return true; + } + + private bool TryHandleMode_ToggleOrExecute(string action) + { if (action.StartsWith("MODE_") || action.StartsWith("EXEC_") || action == "FFMA_DISARM") { ToggleStrategyMode(action); return true; } + return false; } @@ -143,144 +178,185 @@ private bool TryHandleModeCommand(string action, string[] parts) /// private bool TryHandleRiskCommand(string action, string[] parts) { - if (action == "SET_TRAIL") + if (TryHandleRisk_SetTrail(action, parts)) return true; + if (TryHandleRisk_SetCit(action, parts)) return true; + if (TryHandleRisk_Breakeven(action, parts)) return true; + if (TryHandleRisk_SetMaxRisk(action, parts)) return true; + if (TryHandleRisk_SetAnchor(action, parts)) return true; + if (TryHandleRisk_SetTargets(action, parts)) return true; + if (TryHandleRisk_SetManualPrice(action, parts)) return true; + return false; + } + + private bool TryHandleRisk_SetTrail(string action, string[] parts) + { + if (action != "SET_TRAIL") + return false; + + // V12 PRO: Dynamic trail - move stop to current price +/- distance + if (parts.Length >= 2 && double.TryParse(parts[1], out double trailDistance)) { - // V12 PRO: Dynamic trail - move stop to current price +/- distance - if (parts.Length >= 2 && double.TryParse(parts[1], out double trailDistance)) + if (activePositions.Count == 0) { - if (activePositions.Count == 0) - { - Print("[V12] SET_TRAIL: No active positions"); - } - else - { - double currentPrice = lastKnownPrice > 0 ? lastKnownPrice : Close[0]; - int trailCount = 0; - - foreach (var kvp in activePositions.ToArray()) - { - if (!activePositions.ContainsKey(kvp.Key)) continue; - PositionInfo pos = kvp.Value; - string entryName = kvp.Key; - - if (!pos.EntryFilled) continue; - - // Calculate new stop: Longs = Price - Distance, Shorts = Price + Distance - double newStopPrice = pos.Direction == MarketPosition.Long - ? currentPrice - trailDistance - : currentPrice + trailDistance; - - newStopPrice = Instrument.MasterInstrument.RoundToTickSize(newStopPrice); - UpdateStopOrder(entryName, pos, newStopPrice, pos.CurrentTrailLevel); - trailCount++; - Print(string.Format("[V12] SET_TRAIL: {0} -> Stop @ {1:F2} (Price: {2:F2}, Dist: {3})", - entryName, newStopPrice, currentPrice, trailDistance)); - } - - Print(string.Format("[V12] SET_TRAIL COMPLETE: Updated {0} position(s) with {1} pt trail", trailCount, trailDistance)); - } + Print("[V12] SET_TRAIL: No active positions"); } else { - Print("[V12] SET_TRAIL: Invalid distance parameter"); + double currentPrice = lastKnownPrice > 0 ? lastKnownPrice : Close[0]; + int trailCount = 0; + + foreach (var kvp in activePositions.ToArray()) + { + if (!activePositions.ContainsKey(kvp.Key)) continue; + PositionInfo pos = kvp.Value; + string entryName = kvp.Key; + + if (!pos.EntryFilled) continue; + + // Calculate new stop: Longs = Price - Distance, Shorts = Price + Distance + double newStopPrice = pos.Direction == MarketPosition.Long + ? currentPrice - trailDistance + : currentPrice + trailDistance; + + newStopPrice = Instrument.MasterInstrument.RoundToTickSize(newStopPrice); + UpdateStopOrder(entryName, pos, newStopPrice, pos.CurrentTrailLevel); + trailCount++; + Print(string.Format("[V12] SET_TRAIL: {0} -> Stop @ {1:F2} (Price: {2:F2}, Dist: {3})", + entryName, newStopPrice, currentPrice, trailDistance)); + } + + Print(string.Format("[V12] SET_TRAIL COMPLETE: Updated {0} position(s) with {1} pt trail", trailCount, trailDistance)); } - return true; } - if (action == "SET_CIT") + else { - if (parts.Length >= 2) - { - ChaseIfTouchPoints = parts[1].Trim(); - Print($"[V12] CIT updated: {ChaseIfTouchPoints}"); - MarkStickyDirty(); // Build 1103: Persist CIT - BumpUiConfigRevision(); - PublishUiSnapshot(); - } - return true; + Print("[V12] SET_TRAIL: Invalid distance parameter"); } - if (action == "BE" || action == "BE_CUSTOM" || action == "BE_PLUS_2" || action == "BE_PLUS_1") // V12.23: +BE_CUSTOM with dynamic ticks + + return true; + } + + private bool TryHandleRisk_SetCit(string action, string[] parts) + { + if (action != "SET_CIT") + return false; + + if (parts.Length >= 2) { - double beOffset; - if (action == "BE_CUSTOM" && parts.Length >= 2) - { - // V12.23: Dynamic ticks from panel input -- syncs auto-trail BE too - int customTicks; - if (!int.TryParse(parts[1].Trim(), out customTicks) || customTicks < 0) - customTicks = BreakEvenOffsetTicks; // fallback to default - BreakEvenOffsetTicks = customTicks; // V12.23: Sync auto-trail + fleet symmetry - beOffset = customTicks * tickSize; - } - else if (action == "BE" || action == "BE_PLUS_2") - beOffset = BreakEvenOffsetTicks * tickSize; - else - beOffset = 1 * tickSize; // Legacy BE_PLUS_1 - MoveStopsToBreakevenWithOffset(beOffset); - return true; + ChaseIfTouchPoints = parts[1].Trim(); + Print($"[V12] CIT updated: {ChaseIfTouchPoints}"); + MarkStickyDirty(); // Build 1103: Persist CIT + BumpUiConfigRevision(); + PublishUiSnapshot(); } - if (action.StartsWith("SET_MAX_RISK")) + + return true; + } + + private bool TryHandleRisk_Breakeven(string action, string[] parts) + { + if (action != "BE" && action != "BE_CUSTOM" && action != "BE_PLUS_2" && action != "BE_PLUS_1") + return false; + + double beOffset; + if (action == "BE_CUSTOM" && parts.Length >= 2) { - if (parts.Length > 2 && double.TryParse(parts[2], out double val)) - { - MaxRiskAmount = val; - RiskPerTrade = val; // Sync legacy property - Print($"[V12.2] SET_MAX_RISK: {val}"); - MarkStickyDirty(); // Build 1103: Persist max risk - BumpUiConfigRevision(); - PublishUiSnapshot(); - } - return true; + // V12.23: Dynamic ticks from panel input -- syncs auto-trail BE too + int customTicks; + if (!int.TryParse(parts[1].Trim(), out customTicks) || customTicks < 0) + customTicks = BreakEvenOffsetTicks; // fallback to default + BreakEvenOffsetTicks = customTicks; // V12.23: Sync auto-trail + fleet symmetry + beOffset = customTicks * tickSize; } - if (action.StartsWith("SET_ANCHOR")) + else if (action == "BE" || action == "BE_PLUS_2") + beOffset = BreakEvenOffsetTicks * tickSize; + else + beOffset = 1 * tickSize; // Legacy BE_PLUS_1 + MoveStopsToBreakevenWithOffset(beOffset); + return true; + } + + private bool TryHandleRisk_SetMaxRisk(string action, string[] parts) + { + if (!action.StartsWith("SET_MAX_RISK")) + return false; + + if (parts.Length > 2 && double.TryParse(parts[2], out double val)) { - // V11: SET_ANCHOR|EMA30|Global - if (parts.Length > 2) - { - string anchorStr = parts[1]; - SetRmaAnchorFromIpc(anchorStr); - MarkStickyDirty(); // Build 1103: Persist anchor - } - return true; + MaxRiskAmount = val; + RiskPerTrade = val; // Sync legacy property + Print($"[V12.2] SET_MAX_RISK: {val}"); + MarkStickyDirty(); // Build 1103: Persist max risk + BumpUiConfigRevision(); + PublishUiSnapshot(); + } + + return true; + } + + private bool TryHandleRisk_SetAnchor(string action, string[] parts) + { + if (!action.StartsWith("SET_ANCHOR")) + return false; + + // V11: SET_ANCHOR|EMA30|Global + if (parts.Length > 2) + { + string anchorStr = parts[1]; + SetRmaAnchorFromIpc(anchorStr); + MarkStickyDirty(); // Build 1103: Persist anchor } + + return true; + } + + private bool TryHandleRisk_SetTargets(string action, string[] parts) + { + if (action != "SET_TARGETS") + return false; + // V12.5: SET_TARGETS|count - Panel is sole source of truth // V12.Phase8.3: Now writes to activeTargetCount -- minContracts is symbol-specific risk floor only - if (action == "SET_TARGETS") + if (parts.Length > 1 && int.TryParse(parts[1], out int targetCount)) { - if (parts.Length > 1 && int.TryParse(parts[1], out int targetCount)) - { - // FIX-B [Build 1102Z]: Clamp + lock to prevent IPC race with SIMA dispatch loop. - int clamped = Math.Max(1, Math.Min(5, targetCount)); - activeTargetCount = clamped; - Print(string.Format("V12.Phase8.3: SET_TARGETS = {0} targets (clamped from {1}; minContracts preserved at {2})", clamped, targetCount, minContracts)); - // V12.25: CONFIG broadcast REMOVED -- Panel is sole source of truth. - // Sending CONFIG back here caused the Ping-Pong overwrite bug. - // Build 1102Y [U-02]: Immediately sync panel visibility -- panel needs the count, not a CONFIG echo. - SendResponseToRemote($"SYNC_TARGET_STATE|{clamped}"); - MarkStickyDirty(); // Build 1103: Persist target count - BumpUiConfigRevision(); - PublishUiSnapshot(); - } - return true; + // FIX-B [Build 1102Z]: Clamp + lock to prevent IPC race with SIMA dispatch loop. + int clamped = Math.Max(1, Math.Min(5, targetCount)); + activeTargetCount = clamped; + Print(string.Format("V12.Phase8.3: SET_TARGETS = {0} targets (clamped from {1}; minContracts preserved at {2})", clamped, targetCount, minContracts)); + // V12.25: CONFIG broadcast REMOVED -- Panel is sole source of truth. + // Sending CONFIG back here caused the Ping-Pong overwrite bug. + // Build 1102Y [U-02]: Immediately sync panel visibility -- panel needs the count, not a CONFIG echo. + SendResponseToRemote($"SYNC_TARGET_STATE|{clamped}"); + MarkStickyDirty(); // Build 1103: Persist target count + BumpUiConfigRevision(); + PublishUiSnapshot(); } - if (action == "SET_MANUAL_PRICE") + + return true; + } + + private bool TryHandleRisk_SetManualPrice(string action, string[] parts) + { + if (action != "SET_MANUAL_PRICE") + return false; + + // Format: SET_MANUAL_PRICE|| (symbol in parts[1] for router, price in parts[2]) + // NOTE: External callers must use the new symbol-first format (updated Build 944). + if (parts.Length > 2 && double.TryParse(parts[2], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double manualPrice)) { - // Format: SET_MANUAL_PRICE|| (symbol in parts[1] for router, price in parts[2]) - // NOTE: External callers must use the new symbol-first format (updated Build 944). - if (parts.Length > 2 && double.TryParse(parts[2], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double manualPrice)) - { - cachedMnlPrice = manualPrice; - currentRmaAnchor = RmaAnchorType.Manual; - // V12.1101E [D-02]: Legacy isMnlArmed flag purged; cachedMnlPrice + anchor state is authoritative. + cachedMnlPrice = manualPrice; + currentRmaAnchor = RmaAnchorType.Manual; + // V12.1101E [D-02]: Legacy isMnlArmed flag purged; cachedMnlPrice + anchor state is authoritative. - Print(string.Format("IPC SET_MANUAL_PRICE: {0:F2} | Anchor set to MANUAL", manualPrice)); - MarkStickyDirty(); // Build 1103: Persist manual price - } - else - { - Print(string.Format("IPC SET_MANUAL_PRICE: Invalid price format in command: {0}", string.Join("|", parts))); - } - return true; + Print(string.Format("IPC SET_MANUAL_PRICE: {0:F2} | Anchor set to MANUAL", manualPrice)); + MarkStickyDirty(); // Build 1103: Persist manual price } - return false; + else + { + Print(string.Format("IPC SET_MANUAL_PRICE: Invalid price format in command: {0}", string.Join("|", parts))); + } + + return true; } /// diff --git a/src/V12_002.UI.IPC.Server.cs b/src/V12_002.UI.IPC.Server.cs index 62d1d1b7..5f126df8 100644 --- a/src/V12_002.UI.IPC.Server.cs +++ b/src/V12_002.UI.IPC.Server.cs @@ -76,8 +76,7 @@ private void ListenForRemote() { try { - ipcListener = new TcpListener(IPAddress.Loopback, IpcPort); - ipcListener.Start(); + ListenForRemote_StartLoopback(); while (isIpcRunning) { @@ -87,36 +86,11 @@ private void ListenForRemote() continue; } - // Accept new client - TcpClient client = ipcListener.AcceptTcpClient(); - int clientId = Interlocked.Increment(ref _ipcClientIdSeed); - connectedClients[clientId] = new IpcClientSession(clientId, client); - Print($"V12 IPC: New Client Connected [id={clientId}]"); - - // V12.13-D: Send REQUEST_FLEET_STATE directly to the newly connected client - // (Previously called SendToExternalApp which connected back to port 5001 = self, causing infinite flood loop) - try - { - byte[] reqBytes = Encoding.UTF8.GetBytes("REQUEST_FLEET_STATE|ALL\n"); - NetworkStream ns = client.GetStream(); - ns.Write(reqBytes, 0, reqBytes.Length); - ns.Flush(); - Print("V12 IPC: Sent REQUEST_FLEET_STATE to new client"); - if (!string.IsNullOrEmpty(_stickyLeaderAccount)) - { - byte[] leaderBytes = Encoding.UTF8.GetBytes("SET_LEADER_ACCOUNT|" + _stickyLeaderAccount + "\n"); - ns.Write(leaderBytes, 0, leaderBytes.Length); - ns.Flush(); - Print("V12 IPC: Sent leader sync to new client"); - } - } - catch (Exception ex) - { - Print("V12 IPC: Failed to send fleet state request: " + ex.Message); - } + IpcClientSession session = ListenForRemote_AcceptClient(); + ListenForRemote_SendInitialState(session); // Handle client in a separate task - Task.Run(() => HandleClient(connectedClients[clientId])); + Task.Run(() => HandleClient(session)); } } catch (Exception) @@ -126,11 +100,58 @@ private void ListenForRemote() } finally { - if (ipcListener != null) + ListenForRemote_StopListener(); + } + } + + private void ListenForRemote_StartLoopback() + { + ipcListener = new TcpListener(IPAddress.Loopback, IpcPort); + ipcListener.Start(); + } + + private IpcClientSession ListenForRemote_AcceptClient() + { + // Accept new client + TcpClient client = ipcListener.AcceptTcpClient(); + int clientId = Interlocked.Increment(ref _ipcClientIdSeed); + IpcClientSession session = new IpcClientSession(clientId, client); + connectedClients[clientId] = session; + Print($"V12 IPC: New Client Connected [id={clientId}]"); + return session; + } + + private void ListenForRemote_SendInitialState(IpcClientSession session) + { + // V12.13-D: Send REQUEST_FLEET_STATE directly to the newly connected client + // (Previously called SendToExternalApp which connected back to port 5001 = self, causing infinite flood loop) + try + { + byte[] reqBytes = Encoding.UTF8.GetBytes("REQUEST_FLEET_STATE|ALL\n"); + NetworkStream ns = session.Client.GetStream(); + ns.Write(reqBytes, 0, reqBytes.Length); + ns.Flush(); + Print("V12 IPC: Sent REQUEST_FLEET_STATE to new client"); + if (!string.IsNullOrEmpty(_stickyLeaderAccount)) { - try { ipcListener.Stop(); } catch { } + byte[] leaderBytes = Encoding.UTF8.GetBytes("SET_LEADER_ACCOUNT|" + _stickyLeaderAccount + "\n"); + ns.Write(leaderBytes, 0, leaderBytes.Length); + ns.Flush(); + Print("V12 IPC: Sent leader sync to new client"); } } + catch (Exception ex) + { + Print("V12 IPC: Failed to send fleet state request: " + ex.Message); + } + } + + private void ListenForRemote_StopListener() + { + if (ipcListener != null) + { + try { ipcListener.Stop(); } catch { } + } } private void HandleClient(IpcClientSession session) @@ -167,57 +188,96 @@ private void ProcessClientStream(IpcClientSession session) while (isIpcRunning && client.Connected) { - if (!stream.DataAvailable) - { - Thread.Sleep(50); - continue; - } - - int bytesRead = stream.Read(buffer, 0, buffer.Length); + int bytesRead = ProcessClientStream_ReadChunk(stream, buffer); + if (bytesRead < 0) continue; if (bytesRead == 0) break; - string chunk; - try - { - int charCount = utf8Decoder.GetChars(buffer, 0, bytesRead, charBuf, 0, false); - chunk = new string(charBuf, 0, charCount); - } - catch (DecoderFallbackException) - { - Interlocked.Increment(ref _ipcInvalidUtf8Count); - Print($"V12 IPC: Invalid UTF-8 payload from client {clientId}; disconnecting."); + if (!ProcessClientStream_DecodeUtf8(clientId, utf8Decoder, buffer, bytesRead, charBuf, out string chunk)) break; - } lineBuffer.Append(chunk); - if (lineBuffer.Length > IpcMaxBufferedChars) - { - Print($"V12 IPC: Client {clientId} exceeded max buffered payload ({IpcMaxBufferedChars}); disconnecting."); + string[] lines = ProcessClientStream_ExtractLines(clientId, lineBuffer, out bool disconnectClient); + if (disconnectClient) break; + if (lines == null) continue; + foreach (string line in lines) + { + ProcessClientStream_DispatchLine(session, line); } + } + } + + private int ProcessClientStream_ReadChunk(NetworkStream stream, byte[] buffer) + { + if (!stream.DataAvailable) + { + Thread.Sleep(50); + return -1; + } - string accumulated = lineBuffer.ToString(); - int lastNewline = accumulated.LastIndexOf('\n'); - if (lastNewline < 0) continue; + return stream.Read(buffer, 0, buffer.Length); + } - string completeLines = accumulated.Substring(0, lastNewline); - lineBuffer.Clear(); - if (lastNewline + 1 < accumulated.Length) - { - lineBuffer.Append(accumulated.Substring(lastNewline + 1)); - if (lineBuffer.Length > IpcMaxBufferedChars) - { - Print($"V12 IPC: Client {clientId} residue exceeded max buffered payload ({IpcMaxBufferedChars}); disconnecting."); - break; - } - } + private bool ProcessClientStream_DecodeUtf8( + int clientId, + Decoder utf8Decoder, + byte[] buffer, + int bytesRead, + char[] charBuf, + out string chunk) + { + chunk = null; + try + { + int charCount = utf8Decoder.GetChars(buffer, 0, bytesRead, charBuf, 0, false); + chunk = new string(charBuf, 0, charCount); + return true; + } + catch (DecoderFallbackException) + { + Interlocked.Increment(ref _ipcInvalidUtf8Count); + Print($"V12 IPC: Invalid UTF-8 payload from client {clientId}; disconnecting."); + return false; + } + } - string[] lines = completeLines.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); - foreach (string line in lines) + private string[] ProcessClientStream_ExtractLines( + int clientId, + StringBuilder lineBuffer, + out bool disconnectClient) + { + disconnectClient = false; + + if (lineBuffer.Length > IpcMaxBufferedChars) + { + Print($"V12 IPC: Client {clientId} exceeded max buffered payload ({IpcMaxBufferedChars}); disconnecting."); + disconnectClient = true; + return null; + } + + string accumulated = lineBuffer.ToString(); + int lastNewline = accumulated.LastIndexOf('\n'); + if (lastNewline < 0) return null; + + string completeLines = accumulated.Substring(0, lastNewline); + lineBuffer.Clear(); + if (lastNewline + 1 < accumulated.Length) + { + lineBuffer.Append(accumulated.Substring(lastNewline + 1)); + if (lineBuffer.Length > IpcMaxBufferedChars) { - HandleIncomingIpcLine(session, line); + Print($"V12 IPC: Client {clientId} residue exceeded max buffered payload ({IpcMaxBufferedChars}); disconnecting."); + disconnectClient = true; + return null; } } + + return completeLines.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); + } + + private void ProcessClientStream_DispatchLine(IpcClientSession session, string line) + { + HandleIncomingIpcLine(session, line); } private void HandleIncomingIpcLine(IpcClientSession session, string line) @@ -227,50 +287,68 @@ private void HandleIncomingIpcLine(IpcClientSession session, string line) string message = line.Trim(); if (string.IsNullOrEmpty(message)) return; - // Handle GET_LAYOUT (Synchronous Response to THIS client only) - if (message.StartsWith("GET_LAYOUT")) - { - // Build 935 [R-04]: Snapshot scalar state under lock; format string outside - // to minimize critical section duration (removes string allocation from lock). - string snapMode; double snapStop; int snapCount; - double snapT1, snapT2, snapT3, snapT4, snapT5; - TargetMode snapT1Type, snapT2Type, snapT3Type, snapT4Type, snapT5Type; - string snapCit; string snapLeader; bool snapTrma, snapRrma; - snapMode = GetCurrentConfigMode(); - snapStop = snapMode == "RMA" ? RMAStopATRMultiplier : StopMultiplier; - snapCount = activeTargetCount; - snapT1 = Target1Value; snapT1Type = T1Type; - snapT2 = Target2Value; snapT2Type = T2Type; - snapT3 = Target3Value; snapT3Type = T3Type; - snapT4 = Target4Value; snapT4Type = T4Type; - snapT5 = Target5Value; snapT5Type = T5Type; - snapCit = ChaseIfTouchPoints ?? "0"; - snapLeader = _stickyLeaderAccount ?? string.Empty; - snapTrma = isTrendRmaMode; - snapRrma = isRetestRmaMode; - string configResponse = string.Format( - "CONFIG|{0}|MODE:{0};COUNT:{1};T1:{2};T1TYPE:{3};T2:{4};T2TYPE:{5};T3:{6};T3TYPE:{7};T4:{8};T4TYPE:{9};T5:{10};T5TYPE:{11};STR:{12};STRTYPE:ATR;MAX:{13};CIT:{14};OT:Limit;TRMA:{15};RRMA:{16};LEADER:{17};\n", - snapMode, snapCount, snapT1, ToIpcTargetMode(snapT1Type), - snapT2, ToIpcTargetMode(snapT2Type), - snapT3, ToIpcTargetMode(snapT3Type), - snapT4, ToIpcTargetMode(snapT4Type), - snapT5, ToIpcTargetMode(snapT5Type), - snapStop, MaxRiskAmount, snapCit, - snapTrma ? "1" : "0", snapRrma ? "1" : "0", snapLeader); - byte[] responseBytes = Encoding.UTF8.GetBytes(configResponse); - stream.Write(responseBytes, 0, responseBytes.Length); - stream.Flush(); + if (HandleIncomingIpcLine_RespondLayout(stream, message)) return; - } + if (!HandleIncomingIpcLine_TryEnqueueCommand(clientId, message)) + return; + + HandleIncomingIpcLine_TriggerProcessing(); + } + + private bool HandleIncomingIpcLine_RespondLayout(NetworkStream stream, string message) + { + // Handle GET_LAYOUT (Synchronous Response to THIS client only) + if (!message.StartsWith("GET_LAYOUT")) + return false; + + // Build 935 [R-04]: Snapshot scalar state under lock; format string outside + // to minimize critical section duration (removes string allocation from lock). + string snapMode; double snapStop; int snapCount; + double snapT1, snapT2, snapT3, snapT4, snapT5; + TargetMode snapT1Type, snapT2Type, snapT3Type, snapT4Type, snapT5Type; + string snapCit; string snapLeader; bool snapTrma, snapRrma; + snapMode = GetCurrentConfigMode(); + snapStop = snapMode == "RMA" ? RMAStopATRMultiplier : StopMultiplier; + snapCount = activeTargetCount; + snapT1 = Target1Value; snapT1Type = T1Type; + snapT2 = Target2Value; snapT2Type = T2Type; + snapT3 = Target3Value; snapT3Type = T3Type; + snapT4 = Target4Value; snapT4Type = T4Type; + snapT5 = Target5Value; snapT5Type = T5Type; + snapCit = ChaseIfTouchPoints ?? "0"; + snapLeader = _stickyLeaderAccount ?? string.Empty; + snapTrma = isTrendRmaMode; + snapRrma = isRetestRmaMode; + string configResponse = string.Format( + "CONFIG|{0}|MODE:{0};COUNT:{1};T1:{2};T1TYPE:{3};T2:{4};T2TYPE:{5};T3:{6};T3TYPE:{7};T4:{8};T4TYPE:{9};T5:{10};T5TYPE:{11};STR:{12};STRTYPE:ATR;MAX:{13};CIT:{14};OT:Limit;TRMA:{15};RRMA:{16};LEADER:{17};\n", + snapMode, snapCount, snapT1, ToIpcTargetMode(snapT1Type), + snapT2, ToIpcTargetMode(snapT2Type), + snapT3, ToIpcTargetMode(snapT3Type), + snapT4, ToIpcTargetMode(snapT4Type), + snapT5, ToIpcTargetMode(snapT5Type), + snapStop, MaxRiskAmount, snapCit, + snapTrma ? "1" : "0", snapRrma ? "1" : "0", snapLeader); + byte[] responseBytes = Encoding.UTF8.GetBytes(configResponse); + stream.Write(responseBytes, 0, responseBytes.Length); + stream.Flush(); + return true; + } + + private bool HandleIncomingIpcLine_TryEnqueueCommand(int clientId, string message) + { // Enqueue for processing if (!TryEnqueueIpcCommand(message, out string enqueueReason)) { Print(string.Format("V12 IPC REJECT [client={0}] {1}: {2}", clientId, message, enqueueReason)); - return; + return false; } Print(string.Format("V12.1 IPC ENQUEUE [client={0}] {1}", clientId, message)); + return true; + } + private void HandleIncomingIpcLine_TriggerProcessing() + { // Trigger processing try { diff --git a/src/V12_002.UI.IPC.cs b/src/V12_002.UI.IPC.cs index 5b51230e..d263b8d4 100644 --- a/src/V12_002.UI.IPC.cs +++ b/src/V12_002.UI.IPC.cs @@ -234,91 +234,23 @@ private void ProcessIpcCommands() if (ipcCommandQueue == null || ipcCommandQueue.IsEmpty) return; int drainedCount = 0; - while (drainedCount < IpcMaxCommandsPerDrain && ipcCommandQueue.TryDequeue(out string command)) + while (ProcessIpc_DrainOneCommand(ref drainedCount, out string command)) { - if (Interlocked.Decrement(ref ipcQueuedCommandCount) < 0) - Interlocked.Exchange(ref ipcQueuedCommandCount, 0); - drainedCount++; try { - if (string.IsNullOrWhiteSpace(command) || command.Length > IpcMaxCommandLength) - { - Print($"V12 IPC REJECT: malformed/oversize command '{command}'"); + if (!ProcessIpc_ParseAction(command, out string[] parts, out string action, out long senderTicks)) continue; - } - string[] parts = command.Split('|'); - if (parts.Length == 0 || string.IsNullOrWhiteSpace(parts[0])) - { - Print($"V12 IPC REJECT: empty action in '{command}'"); - continue; - } - string action = parts[0].Trim().ToUpperInvariant(); - long senderTicks = 0; - for (int i = 1; i < parts.Length; i++) - { - if (parts[i].StartsWith("ts=", StringComparison.OrdinalIgnoreCase)) - { - long.TryParse(parts[i].Substring(3), out senderTicks); - break; - } - } if (!MetadataGuardCommandTimestamp(senderTicks, action)) continue; - if (!IsAllowedIpcAction(action)) - { - Interlocked.Increment(ref _ipcAllowlistRejectCount); - Print($"V12 IPC REJECT: action '{action}' is not allowed"); + if (!ProcessIpc_ValidateAllowlist(action)) continue; - } - string targetSymbol = parts.Length > 1 ? parts[1] : "Global"; - - // V12.9: Global commands bypass symbol filter entirely -- these are account/fleet-level, not instrument-level - // [1102Z-F] MOVE_TARGET and LOCK_50 use parts[1] for parameters (not symbol), so they must bypass - // the symbol filter. Each handler internally filters by activePositions so only charts with live - // positions act. This is the correct fix for the "For Me? False [target=T1]" rejection. - bool isGlobalCommand = action == "TOGGLE_ACCOUNT" || action == "SET_SIMA" || - action == "GET_FLEET" || action == "DIAG_FLEET" || action == "CANCEL_ALL" || - action == "FLATTEN" || action == "SYNC_ALL" || action == "MKT_SYNC" || - action == "REQUEST_FLEET_STATE" || action == "RESET_MEMORY" || - action == "DIAG_IPC" || - action.StartsWith("MOVE_TARGET") || action == "LOCK_50" || // [1102Z-F] - action == "SET_TARGETS" || action == "SET_TRAIL" || // [Build 945] numeric parts[1] bypasses symbol filter - action == "SET_CIT" || action == "BE_CUSTOM"; // [Build 945] numeric parts[1] bypasses symbol filter - - // V10.3: Robust Symbol Matching (Matches MGC to GC/MGC, MES to ES/MES, etc.) - string mySym = Instrument.MasterInstrument.Name.ToUpperInvariant(); - string myFull = Instrument.FullName.ToUpperInvariant(); - string target = targetSymbol.Trim().ToUpperInvariant(); - - bool isForMe = isGlobalCommand || // V12.9: SIMA/Fleet commands always pass through - target == "GLOBAL" || - target == "ALL" || // V12.13: Universal broadcast target (FLATTEN|ALL, REQUEST_FLEET_STATE|ALL) - target == "ON" || target == "OFF" || // V12.4: Mode toggle commands (SET_RMA_MODE|ON) - target == "RMA" || target == "ORB" || target == "OR" || target == "MOMO" || // V12.6: Mode-switch keywords are global - mySym == target || - mySym.StartsWith(target) || // "MES" matches "MES 03-26" - target.StartsWith(mySym) || // "GC" matches "GC/MGC" - myFull.Contains(target) || - (target == "MES" && mySym.Contains("ES")) || // Robustness for MES/ES - (target == "MYM" && mySym.Contains("YM")) || // Robustness for MYM/YM - (target == "MGC" && mySym.Contains("GC")); // Robustness for MGC/GC - - // V12.2: Global IPC Diagnostic Log - Print(string.Format("V12 IPC: Received '{0}' for '{1}'. For Me? {2} (My Symbol: {3}){4}", - action, target, isForMe, mySym, isGlobalCommand ? " [GLOBAL CMD]" : "")); - - if (!isForMe) - { - // Quiet ignore if it's clearly for another instrument + + if (!ProcessIpc_MatchSymbol(action, parts)) continue; - } - string queuedAction = action; - string[] queuedParts = parts; - long queuedSenderTicks = senderTicks; - Enqueue(ctx => ctx.ProcessIpcCommandCore(queuedAction, queuedParts, queuedSenderTicks)); + ProcessIpc_EnqueueCore(action, parts, senderTicks); } catch (Exception ex) { @@ -332,6 +264,120 @@ private void ProcessIpcCommands() } } + private bool ProcessIpc_DrainOneCommand(ref int drainedCount, out string command) + { + command = null; + if (drainedCount >= IpcMaxCommandsPerDrain) + return false; + + if (!ipcCommandQueue.TryDequeue(out command)) + return false; + + if (Interlocked.Decrement(ref ipcQueuedCommandCount) < 0) + Interlocked.Exchange(ref ipcQueuedCommandCount, 0); + drainedCount++; + return true; + } + + private bool ProcessIpc_ParseAction(string command, out string[] parts, out string action, out long senderTicks) + { + parts = null; + action = null; + senderTicks = 0; + + if (string.IsNullOrWhiteSpace(command) || command.Length > IpcMaxCommandLength) + { + Print($"V12 IPC REJECT: malformed/oversize command '{command}'"); + return false; + } + + parts = command.Split('|'); + if (parts.Length == 0 || string.IsNullOrWhiteSpace(parts[0])) + { + Print($"V12 IPC REJECT: empty action in '{command}'"); + return false; + } + action = parts[0].Trim().ToUpperInvariant(); + for (int i = 1; i < parts.Length; i++) + { + if (parts[i].StartsWith("ts=", StringComparison.OrdinalIgnoreCase)) + { + long.TryParse(parts[i].Substring(3), out senderTicks); + break; + } + } + + return true; + } + + private bool ProcessIpc_ValidateAllowlist(string action) + { + if (!IsAllowedIpcAction(action)) + { + Interlocked.Increment(ref _ipcAllowlistRejectCount); + Print($"V12 IPC REJECT: action '{action}' is not allowed"); + return false; + } + + return true; + } + + private bool ProcessIpc_MatchSymbol(string action, string[] parts) + { + string targetSymbol = parts.Length > 1 ? parts[1] : "Global"; + + // V12.9: Global commands bypass symbol filter entirely -- these are account/fleet-level, not instrument-level + // [1102Z-F] MOVE_TARGET and LOCK_50 use parts[1] for parameters (not symbol), so they must bypass + // the symbol filter. Each handler internally filters by activePositions so only charts with live + // positions act. This is the correct fix for the "For Me? False [target=T1]" rejection. + bool isGlobalCommand = action == "TOGGLE_ACCOUNT" || action == "SET_SIMA" || + action == "GET_FLEET" || action == "DIAG_FLEET" || action == "CANCEL_ALL" || + action == "FLATTEN" || action == "SYNC_ALL" || action == "MKT_SYNC" || + action == "REQUEST_FLEET_STATE" || action == "RESET_MEMORY" || + action == "DIAG_IPC" || + action.StartsWith("MOVE_TARGET") || action == "LOCK_50" || // [1102Z-F] + action == "SET_TARGETS" || action == "SET_TRAIL" || // [Build 945] numeric parts[1] bypasses symbol filter + action == "SET_CIT" || action == "BE_CUSTOM"; // [Build 945] numeric parts[1] bypasses symbol filter + + // V10.3: Robust Symbol Matching (Matches MGC to GC/MGC, MES to ES/MES, etc.) + string mySym = Instrument.MasterInstrument.Name.ToUpperInvariant(); + string myFull = Instrument.FullName.ToUpperInvariant(); + string target = targetSymbol.Trim().ToUpperInvariant(); + + bool isForMe = isGlobalCommand || // V12.9: SIMA/Fleet commands always pass through + target == "GLOBAL" || + target == "ALL" || // V12.13: Universal broadcast target (FLATTEN|ALL, REQUEST_FLEET_STATE|ALL) + target == "ON" || target == "OFF" || // V12.4: Mode toggle commands (SET_RMA_MODE|ON) + target == "RMA" || target == "ORB" || target == "OR" || target == "MOMO" || // V12.6: Mode-switch keywords are global + mySym == target || + mySym.StartsWith(target) || // "MES" matches "MES 03-26" + target.StartsWith(mySym) || // "GC" matches "GC/MGC" + myFull.Contains(target) || + (target == "MES" && mySym.Contains("ES")) || // Robustness for MES/ES + (target == "MYM" && mySym.Contains("YM")) || // Robustness for MYM/YM + (target == "MGC" && mySym.Contains("GC")); // Robustness for MGC/GC + + // V12.2: Global IPC Diagnostic Log + Print(string.Format("V12 IPC: Received '{0}' for '{1}'. For Me? {2} (My Symbol: {3}){4}", + action, target, isForMe, mySym, isGlobalCommand ? " [GLOBAL CMD]" : "")); + + if (!isForMe) + { + // Quiet ignore if it's clearly for another instrument + return false; + } + + return true; + } + + private void ProcessIpc_EnqueueCore(string action, string[] parts, long senderTicks) + { + string queuedAction = action; + string[] queuedParts = parts; + long queuedSenderTicks = senderTicks; + Enqueue(ctx => ctx.ProcessIpcCommandCore(queuedAction, queuedParts, queuedSenderTicks)); + } + private void ProcessIpcCommandCore(string action, string[] parts, long senderTicks) { try diff --git a/src/V12_002.cs b/src/V12_002.cs index bd792214..1d869e62 100644 --- a/src/V12_002.cs +++ b/src/V12_002.cs @@ -44,7 +44,7 @@ namespace NinjaTrader.NinjaScript.Strategies { public partial class V12_002 : Strategy { - public const string BUILD_TAG = "1111.005-v28.0-b984"; // PR76 confirmed: D1 drain overflow log, D2 ExpKey null guard, D3 semaphore finally, D6 reconnect catch + public const string BUILD_TAG = "1111.006-v28.0-b984-complete"; // PR76 confirmed: D1 drain overflow log, D2 ExpKey null guard, D3 semaphore finally, D6 reconnect catch public class UILiveTargetSnapshot { diff --git a/tests/LogicTests.cs b/tests/LogicTests.cs index 5a6bf3ed..35de979c 100644 --- a/tests/LogicTests.cs +++ b/tests/LogicTests.cs @@ -1,5 +1,9 @@ using NUnit.Framework; using NinjaTrader.NinjaScript.Strategies; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; using System.Linq; namespace UniversalOrStrategy.Tests @@ -7,6 +11,19 @@ namespace UniversalOrStrategy.Tests [TestFixture] public class LogicTests { + private sealed class StickyStateSection + { + public StickyStateSection(string name) + { + Name = name; + Entries = new List(); + } + + public string Name { get; } + + public List Entries { get; } + } + [Test] [TestCase(10, 1, new[] { 10, 0, 0, 0, 0 })] [TestCase(10, 2, new[] { 5, 5, 0, 0, 0 })] @@ -57,5 +74,172 @@ public void CalculateATRStopDistance_ValidATR_ReturnsCeilingStop() double stop = V12_PureLogic.CalculateATRStopDistance(2.3, 2.0, 1.0, 100.0); Assert.AreEqual(5.0, stop); } + + [Test] + public void StickyState_RoundTrip_PreservesState() + { + string fixture = string.Join( + Environment.NewLine, + "# V12 StickyState v1", + "# Symbol: MES 06-26", + "[CONFIG]", + "MODE=RMA", + "COUNT=3", + "T1=10.5", + "T1TYPE=Points", + "T2=12", + "T2TYPE=ATR", + "T3=18.25", + "T3TYPE=Runner", + "STR=2.5", + "MAX=750", + "CIT=4", + "TRMA=1", + "RRMA=0", + "", + "[FLEET]", + "LEADER=Apex_Main", + "Apex_F01=1", + "Apex_F02=0", + "", + "[ANCHOR]", + "TYPE=EMA65", + "MNL_PRICE=5312.25", + "", + "[CONFIG_OR]", + "COUNT=2", + "T1=8", + "T1TYPE=Ticks", + "STR=1.5", + "MAX=500", + "", + "[CONFIG_RMA]", + "COUNT=3", + "T1=10.5", + "T1TYPE=Points", + "T2=12", + "T2TYPE=ATR", + "STR=2.5", + "MAX=750", + "", + "[POSITIONS]", + "# key|extremePrice|trailLevel|beArmed|beTriggered|initialTargetCount", + "ENTRY_1|5315.75|2|1|0|3") + Environment.NewLine; + string tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + ".v12state"); + + try + { + File.WriteAllText(tempPath, fixture, new UTF8Encoding(false)); + + List original = ParseStickyStateSections(fixture); + List loaded = LoadStickyStateFixture(tempPath); + string roundTrip = SerializeStickyStateSections(loaded); + List reparsed = ParseStickyStateSections(roundTrip); + + Assert.That(original.Count, Is.GreaterThan(0)); + Assert.That(StickyStateSectionsEqual(loaded, original), Is.True); + Assert.That(StickyStateSectionsEqual(reparsed, original), Is.True); + } + finally + { + if (File.Exists(tempPath)) + File.Delete(tempPath); + } + } + + private static List LoadStickyStateFixture(string path) + { + string content = File.ReadAllText(path, Encoding.UTF8); + return ParseStickyStateSections(content); + } + + private static List ParseStickyStateSections(string content) + { + var sections = new List(); + StickyStateSection currentSection = null; + + using (var reader = new StringReader(content ?? string.Empty)) + { + string rawLine; + while ((rawLine = reader.ReadLine()) != null) + { + string line = rawLine.Trim(); + if (string.IsNullOrEmpty(line) || line.StartsWith("#", StringComparison.Ordinal)) + continue; + + if (line.StartsWith("[", StringComparison.Ordinal) && + line.EndsWith("]", StringComparison.Ordinal) && + line.Length > 2) + { + currentSection = new StickyStateSection(line.Substring(1, line.Length - 2).ToUpperInvariant()); + sections.Add(currentSection); + continue; + } + + if (currentSection == null) + continue; + + currentSection.Entries.Add(NormalizeStickyStateEntry(currentSection.Name, line)); + } + } + + return sections; + } + + private static string SerializeStickyStateSections(IEnumerable sections) + { + var builder = new StringBuilder(); + bool isFirstSection = true; + + foreach (StickyStateSection section in sections) + { + if (!isFirstSection) + builder.AppendLine(); + + builder.Append('[').Append(section.Name).AppendLine("]"); + foreach (string entry in section.Entries) + builder.AppendLine(entry); + + isFirstSection = false; + } + + return builder.ToString(); + } + + private static bool StickyStateSectionsEqual( + IReadOnlyList left, + IReadOnlyList right) + { + if (ReferenceEquals(left, right)) + return true; + if (left == null || right == null || left.Count != right.Count) + return false; + + for (int i = 0; i < left.Count; i++) + { + StickyStateSection leftSection = left[i]; + StickyStateSection rightSection = right[i]; + if (!string.Equals(leftSection.Name, rightSection.Name, StringComparison.Ordinal)) + return false; + if (!leftSection.Entries.SequenceEqual(rightSection.Entries)) + return false; + } + + return true; + } + + private static string NormalizeStickyStateEntry(string sectionName, string line) + { + if (string.Equals(sectionName, "POSITIONS", StringComparison.Ordinal)) + return line; + + int eq = line.IndexOf('='); + if (eq < 1) + return line; + + string key = line.Substring(0, eq).Trim().ToUpperInvariant(); + string value = line.Substring(eq + 1).Trim(); + return key + "=" + value; + } } }