# TraderAlign — Single Source of Truth Migration Plan (Phase 4)

| | |
|---|---|
| **Status** | **PLAN ONLY — DO NOT EXECUTE WITHOUT SIGN-OFF.** No migration work has begun. This document orders and scopes the work; it does not perform it. |
| **Depends on** | [`SINGLE_SOURCE_OF_TRUTH_AUDIT.md`](SINGLE_SOURCE_OF_TRUTH_AUDIT.md) (findings) and [`SINGLE_SOURCE_OF_TRUTH_PROPOSAL.md`](SINGLE_SOURCE_OF_TRUTH_PROPOSAL.md) (architecture) — every wave below is a direct consequence of those two documents, not a new plan invented separately. |
| **Ordering principle** | By risk/impact, per Kevin's explicit instruction, starting from confirmed disagreements and working outward. Waves are sequential — each wave should be fully verified against real account data before the next begins, per the Proposal's §4 testing strategy (one consumer at a time, golden-value baseline first, never a batch). |

---

## Wave 0 — Build and verify `PropFirmRuleEngine` in isolation (no page changes)

**Files:** new `src/PropFirmRuleEngine.php`.

**What changes:** nothing user-facing yet. Build the five methods scoped in the Proposal (§1a): `resolveRules()`, `phaseAwareBalance()`, `drawdownAndDailyLoss()`, `breachStatus()`, `passProgress()`. Internally: derive phase exclusively from `trading_accounts.lifecycle_state` (never `current_stage`), reuse `JourneySegmentation::partitionAccountJourney()` for phase-bucketing, carry forward the `array_key_last()` safety check already proven correct in the 2026-08-07 fix.

**Verification before proceeding to Wave 1:** run every method against every real prop-firm account in the database (spanning every lifecycle state and challenge type — one_step/two_step/three_step/instant_funding, active/breached/funded/complete). For each account, compare the new engine's output against what `AnalyticsEngine::getChallengeMetrics()`'s current (already-fixed) `phase_balance`/`overall_drawdown`/`daily_drawdown` fields produce for the same account — these should match exactly, since `getChallengeMetrics()` is about to become a thin consumer of this new engine, not a rewrite of its logic. Any disagreement found here must be resolved before Wave 1 starts.

---

## Wave 1 — The single most severe finding in the whole audit, found last but ranked first

This item was found by the final research pass, after the rest of this plan's waves were already drafted — it is inserted here, ahead of everything else, because direct verification confirmed it is more severe than any other finding: it doesn't produce a wrong display, it silently corrupts real account data via a real, intended feature.

### 1.0 — `public/mt5-resolve.php`'s `createTradeFromMT5()` and `api/mt5/reconcile.php`'s `applyReconciliation()`
**What changes:** both functions currently write only `broker_pnl` (a straight pass-through of the parsed MT5 statement) and stop — neither computes `challenge_pnl`/`excess_pnl` nor updates `trading_accounts.current_balance`. Both must be rewritten to route through the same capping/balance-update logic `dashboard.php`'s `resolve_trade_id` already correctly implements (via `PropFirmRuleEngine` once it exists) — for prop-firm accounts, cap the trade's P&L at the phase's breach/target boundary and write `challenge_pnl`/`excess_pnl`; for either account type, apply the real balance delta to `trading_accounts.current_balance`.
**Expected before/after:** *Before* — creating or reconciling an MT5 trade on a prop-firm account leaves that trade's P&L uncapped everywhere it's read, and the account's balance never reflects it at all. *After* — MT5-sourced trades behave identically to manually-resolved trades for every downstream computation. **This must be verified with real, disposable test data before being considered fixed** — create a synthetic MT5-import trade on a real (or purpose-built test) prop-firm account, confirm `challenge_pnl` is correctly capped and the balance correctly updates, then fully revert, following the same construct-verify-revert discipline used throughout the original balance-bug fix session.
**Priority note:** given this is a genuine, silent, real-data-corrupting gap rather than a display bug, recommend confirming with Kevin whether any real account has already used the MT5 create-trade or reconcile features in production before this fix lands — if so, those accounts' balances may already be silently wrong and need a one-time reconciliation pass, separate from the code fix itself.

---

## Wave 2 — The four next-highest-severity, currently-live bugs (audit Consolidated #2-4, #6, renumbered from the original Wave 1)

These are ordered first because they are the most severe *confirmed* findings — not because they were the ones originally reported, but because direct verification during the audit proved them more severe than the originally-reported symptoms (a silently-disabled breach warning and a P&L Rule violation in a live, trader-facing endpoint outrank a display-only balance mismatch).

### 1.1 — `api/trades/validate.php`
**What changes:** delete the file's own inline balance computation (§1i — non-phase-scoped, uses `broker_pnl`/`projected_pnl` instead of `challenge_pnl`) and its own inline rules-cascade/breach-detection (§4, §6 cascade #3 — flat-only keys, defaults to 0). Replace both with calls to `PropFirmRuleEngine::phaseAwareBalance()`, `::breachStatus()`, `::drawdownAndDailyLoss()`, `::passProgress()`.
**Expected before/after:** *Before* — for any real account whose `rules` JSON carries only phase-specific keys (the documented normal case), the Calculator's live panel shows no breach warning at any risk size. *After* — the same account shows the identical breach warning, at the identical threshold, that Drafts/Dashboard would enforce on save. Verify against at least one real two-step account with phase-specific-only rules, at a risk size deliberately chosen to cross the real breach line, confirming the warning now fires client-side before the trade is ever saved.

### 1.2 — `api/account/state.php`
**What changes:** replace the file's own inline `JourneySegmentation`-based balance computation (§1c — correct today, but a separate copy) and its unclamped drawdown%/non-phase-aware rules display (§2c, §6 cascade #5) with `PropFirmRuleEngine` calls.
**Expected before/after:** *Before* — an in-profit account shows a negative drawdown% here while every other page shows a flat 0%; a Phase-2 account's displayed profit target is silently Phase 1's value. *After* — both match the canonical figures shown everywhere else. Verify against a real Phase-2 or Phase-3 account with a different declared target than Phase 1.

### 1.3 — `public/dashboard.php`'s `resolve_trade_id`, `passed_phase` branch
**What changes:** stop reading `challenge_accounts.current_stage` directly (~lines 324-342). Derive the phase to mark complete from `trading_accounts.lifecycle_state` via the new shared `lifecycleStateToPhaseLabel()` utility (Proposal §1e), matching the pattern already used for the capping logic 125 lines earlier in the same handler.
**Expected before/after:** *Before* — on a real drifted account (confirmed by CLAUDE.md's own history that this drift genuinely occurs), confirming "Passed Phase" can write the wrong `_complete` state or silently no-op. *After* — the write always matches the account's real, live lifecycle state. **Verification for this specific item needs a real drifted account to test against** — if none currently exists in the live DB, construct one synthetically (mismatch `current_stage` vs. `lifecycle_state` on a test account, confirm the bug reproduces on the *current* code, apply the fix, confirm it no longer reproduces, then fully revert the synthetic account state) before considering this closed, following the same construct-reproduce-fix-revert discipline used throughout this session's earlier balance-bug fix.

### 1.4 — Calculator's persisted trade-object fields
**What changes:** `calculator-source.html`'s two trade-object builders stop computing `account_status_label`/`drawdown_at_entry_pct`/`breathing_room_used_pct`/`breathing_room_remaining`/`trades_remaining_buffer` from URL params + editable balance inputs. `api/trades/draft.php` computes these server-side from `PropFirmRuleEngine` at save time instead.
**Expected before/after:** *Before* — these fields reflect whatever rule percentages happened to be in the Calculator's URL at load time, which can go stale across a phase promotion with no fetch to catch it. *After* — always reflect the account's real, current, live rules. Verify by saving a real draft trade immediately after a real phase promotion (URL still carrying pre-promotion params) and confirming the persisted fields reflect the *new* phase's rules, not the stale URL's.

---

## Wave 3 — Balance/rules-cascade cleanup outward from Waves 1-2 (the originally-reported symptoms)

This wave directly addresses the symptoms Kevin's original request named: the funded-account balance mismatch's remaining duplicate implementations, the growth-target % anomaly, and the Trading Center/Colex balance bugs found during the audit.

### 2.1 — `public/journal.php`'s three `rn*` balance functions
**What changes:** `rnBuildPhaseCardFromRows()`/`renderRnFundedCard()`/`renderRnCoachingSection()` (§1d/1e) call `PropFirmRuleEngine::phaseAwareBalance()` instead of their own inline `JourneySegmentation` computation.
**Caution, explicit per the Proposal and CLAUDE.md's own repeated warnings:** `journal.php`'s `rn*` functions are the single largest, most heavily-verified surface in this codebase. This item alone should be treated as its own sub-migration with dedicated live verification (every lifecycle state, every challenge type, before/after screenshots or rendered-output diffs), not folded into a larger batch.
**Expected before/after:** numerically identical output to today (per the audit's own finding that these four implementations already agree numerically as of the 2026-08-07 fix) — this migration is about eliminating the *architectural* duplication, not changing any displayed number. Verify via a broad sweep confirming zero numeric change across every real account.

### 2.2 — `src/TradingArchiveEngine.php`'s segment/balance computations
**What changes:** `gatherPropSegments()`/`successStory()`/`gatherLiveSegments()` repoint to `PropFirmRuleEngine` the same way as 2.1.
**Expected before/after:** same as 2.1 — no numeric change expected, only architectural.

### 2.3 — `public/trading-center.php`'s Account Home (§1g)
**What changes:** `tcRenderPropAccountSummary()`'s "Current Balance" row switches from raw `$account['current_balance']` to `PropFirmRuleEngine::phaseAwareBalance()`.
**Expected before/after:** *Before* — a multi-phase-promoted account shows its whole-ledger balance here, disagreeing with Journal/Trading Archive. *After* — matches exactly. Verify against a real account that has been promoted through 2+ phases with real trades in more than one phase (the exact scenario the audit constructed by hand to demonstrate the disagreement).

### 2.4 — Colex's balance reads (§1j, §16 Consolidated #5)
**What changes:** `api/colex/chat.php`'s `generateGeneralResponse()`'s total-balance sum switches to per-account `PropFirmRuleEngine::phaseAwareBalance()` for prop-firm accounts. `src/ColexHelpers::getPeriodStats()` is rewritten to delegate to `AnalyticsEngine::getUserMetrics()` per the Proposal §1d, rather than having its four independent bugs patched in place.
**Expected before/after:** *Before* — Colex's own on-page stats widget (Total Trades/Win Rate/P&L/Drawdown) disagree with Dashboard/Reports for the same user/period, and "You have N accounts with $X total balance" sums the wrong balance for prop-firm accounts. *After* — both match. Verify against a real user with at least one open/draft trade (to confirm the missing `status='closed'` filter fix), at least one prop-firm account with both `challenge_pnl` and `broker_pnl` populated on the same trade (to confirm the double-counting fix), and at least one validation-flagged trade (to confirm the missing `is_validation` filter fix).

### 3.5 — `src/AccountPicker.php`'s `apChallengeRulesSummary()` (§6 cascade #6, §20)
**What changes:** Profit Target/Max Drawdown/Daily Drawdown lookups become phase-aware (via `PropFirmRuleEngine::resolveRules()`) instead of the current single-key, no-fallback lookup, closing the gap between this function's actual behavior and its own doc-comment's claim of parity with `rnPhaseTargets()`.
**Expected before/after:** *Before* — an account whose rules JSON stores `phase_2_profit_target` (not `phase_2_target_pct`) shows a real number on Dashboard/Drafts/Reports and literally "Not specified" on Trading Center's own Account Home. *After* — matches. This is the audit's own constructed demonstration case (§20) — verify against an account with exactly this rules-JSON shape.

### 3.6 — `api/trades/validate.php` and `api/account/state.php`'s rules-cascade duplication, closed by Wave 2's fix (cross-reference only)
Waves 2.1/2.2 already repoint these two files to `PropFirmRuleEngine`, which closes their rules-cascade duplication (§20's independently-confirmed 5th/6th cascade copies) as a side effect — no separate item needed here.

### 3.7 — `api/journal/save-goal.php`'s balance reconstruction (§20, Consolidated #16)
**What changes:** the Growth Target feature's "current balance" input switches from its own hand-written reconstruction to `PropFirmRuleEngine::phaseAwareBalance()` (prop) / the existing correct live-account balance read (personal, already reads `transaction.php`-maintained fields correctly per §20's "confirmed clean" finding for that file).
**Expected before/after:** no numeric change expected (the existing formula is correct, just architecturally separate) — verify against a real goal-setting action on a prop-firm account mid-phase.

### 3.8 — `public/mt5-resolve.php`/`api/mt5/reconcile.php` — cross-reference to Wave 1
Already covered as the top-priority item in Wave 1 above; listed here only so this wave's own section index doesn't read as having missed it.

---

## Wave 4 — Scores and classification (audit §8-9)

### 3.1 — Delete Colex's independent "Risk Consistency" formula (§8c)
**What changes:** `api/colex/chat.php`'s `generateDisciplineResponse()` calls `EdgeHealthEngine::getPeriodFactors()` instead of computing raw stddev locally.
**Expected before/after:** Colex chat's discipline commentary now cites the same Consistency Score shown on Reports/Journal for the same trades, rather than a third, unrelated figure.

### 3.2 — Rename `TradingArchiveEngine::historicalConsistency()` (§8b)
**What changes:** relabel in the UI (e.g. "Historical Discipline Index") to stop colliding with "Consistency Score," and have it call `EdgeHealthEngine::scoreConsistency()`'s risk-CV component rather than reimplementing CV-of-risk-percent independently, adding only its own classification-rate term.
**Expected before/after:** the number itself may not change (it's still a legitimately different metric), but it should no longer read as if it's the same "Consistency Score" concept shown elsewhere, and the risk-CV half of its formula should be traceably the same computation as `EdgeHealthEngine`'s.

### 3.3 — Fix Colex's tilt/revenge detection (§9)
**What changes:** `generatePatternsResponse()`'s tilt/revenge inference switches from a raw `losing_streak >= 3` heuristic to reading real `trade_classification = 'revenge_trade'`/`'tilt_warning'` counts, matching what `ColexHelpers`'s own stats widget already correctly does.
**Expected before/after:** verify against a real account with a 3+ loss streak that contains zero real `revenge_trade`-classified trades, confirming Colex chat no longer warns about tilt/revenge when the real classification data says otherwise.

### 3.4 — Extract shared `GRADE_MAP` and undisciplined-classification constants (§9, Consolidated #15/#16)
**What changes:** one shared constant (location: `EdgeHealthEngine` or a new small `src/TradeGrading.php`), `reports.php`'s `$fsGradeMap` and `TraderProfileBehaviorEngine::GRADE_MAP` both read from it. The 3-value vs. 4-value "undisciplined" sets get named as two distinct, intentionally-different constants if both are genuinely still needed (grade bucket vs. star-rating severity cap), rather than one being an unlabeled, accidental variant of the other.
**Expected before/after:** no numeric change (the two copies are currently identical) — purely removing the drift risk.

---

## Wave 5 — Remaining Reports/Dashboard/win-rate duplicates (audit §16)

### 4.1 — `dashboard.php`'s hand-written Expectancy/Profit-Factor/Risk-of-Ruin SQL
**What changes:** delete, call `AnalyticsEngine::getUserMetrics($userId, ['strategy_version_id' => $svid])` (already supports this filter) for Expectancy/Profit Factor; call `EdgeHealthEngine`'s Risk-of-Ruin formula instead of the hardcoded-exponent-20 version.
**Expected before/after:** *Before* — Dashboard's Risk of Ruin disagrees with Reports/Journal/Trader Profile for any trader whose average risk% isn't exactly 5%. *After* — matches. Verify against a real strategy-filtered account with a real average risk% away from 5%.

### 4.2 — `dashboard.php`'s Strategy Adherence card
**What changes:** repoint from `mv_strategy_adherence` (zero-tolerance) to `computeFourStageRaw()`'s Stage 1 adherence (per-asset-class tolerance) — or update the materialized view's underlying query to the current tolerance formula. **This specific decision (repoint the page vs. update the view) should be made during Phase 4 execution, not pre-decided here** — it depends on how many other consumers still read the view, which requires a fresh grep at execution time.
**Expected before/after:** Dashboard's adherence % should stop being stricter than Reports/Trader Profile's for the same trades.

### 4.3 — `TradingArchiveEngine::segmentMetrics()`'s stale risk-overage tolerance (§12)
**What changes:** replace the flat 20% relative tolerance with a call into `AnalyticsEngine::calculatorRiskTolerance()`'s current per-asset-class logic, including the Contract-Constrained exclusion.
**Expected before/after:** Trading Archive's findings/patterns narrative ("Declared risk was exceeded by more than 20% on N% of trades") should match Reports' equivalent commentary for the same account/period.

### 4.4 — Extract shared `getReportingPeriodBounds()` (§19, Consolidated #23)
**What changes:** one function in `src/RenderHelpers.php`; `reports.php`, `TraderProfileService::periodBounds()`, `ColexHelpers::calculateDateRange()`, and `api/colex/chat.php`'s inline logic all call it.
**Expected before/after:** no numeric change today (all four are currently calendar-equivalent) — purely closing the latent-drift risk before it becomes a real one.

### 4.5 — Delete dead `rnAccountLifecycleBadge()` (§17, Consolidated #24)
**What changes:** remove the function outright from `journal.php` — confirmed zero call sites.
**Expected before/after:** no behavior change (already unreachable); removes a landmine for any future edit.

### 4.6 — Review `src/RenderHelpers.php`'s unused `accountCurrentBalance()` (§20, Consolidated #12)
**What changes:** this is a design decision, not a mechanical fix — a prior attempt at "one balance function" already exists, unused, with a documented reason it was reverted (broke phase-scoped math for mid-evaluation accounts). Before building `PropFirmRuleEngine` in Wave 0, review this function's design and its revert history — either fold its intent into `PropFirmRuleEngine::phaseAwareBalance()` directly, or delete it if it's fully superseded, rather than leaving a second, competing "the real one" function sitting unused in the codebase indefinitely.
**Expected before/after:** no numeric change — purely a design/cleanup decision, best made at the start of Wave 0 rather than the end of Wave 4, since it directly informs `PropFirmRuleEngine`'s design. Sequenced here in the document for narrative order only.

---

## Wave 6 — Lower-priority / cosmetic (audit §5, §15, §16, §17, §20)

- Cap Active Journeys' phase-progress bar (`rnBuildPhaseCardFromRows()`) at 100%, matching every other progress display in the app.
- Reconcile the calculator's two breach-severity threshold sets (`renderAccountImpact()` vs. `renderPropFirmFallback()`) so the online and offline modes agree with each other.
- **Decided (2026-08-08, Wave 6): NOT unified — documented, not forced.** Investigated each of the three sites directly rather than deciding blind:
  - `dashboard.php`'s no-strategy fallback (`mv_instrument_performance`/`mv_session_performance`) is genuinely **all-time, unfiltered** — the views have no date-range or strategy parameter at all.
  - `reports.php`'s own `GROUP BY t.instrument`/`t.session` queries are genuinely **period-scoped** (bound to the page's selected Weekly/Monthly/Quarterly/Annual window via `$periodParams`) and optionally strategy-filtered — a question the views structurally cannot answer without adding date-range parameters to the view DDL itself.
  - `TradingArchiveEngine::instrumentPerformance()` operates on an **already-fetched, segment-scoped** row set (one specific account/phase-bucket/lens combination) and computes a genuinely richer metric set than either view (adds win-rate and R-multiple per instrument, and its rows carry the `data-*` attributes Trading Archive's evidence-table filter chips drill through to) — repointing it at the views would be a real functional regression (losing R-multiple and evidence drill-through), not a safe mechanical refactor.
  - Conclusion: these three answer three genuinely different questions (all-time / period-scoped / segment-scoped-with-richer-metrics), not three copies of the same computation — "not confirmed wrong today" (per §16's own original finding) still holds, and forcing a shared read path would mean either adding real DDL/date-parameterization to the views (unclear blast radius on dashboard.php's own unfiltered use) or losing TradingArchiveEngine's evidence-linking capability. Left as three independent, intentionally-differently-scoped implementations. If a future session ever finds these three DISAGREEING on a claim that should be scope-comparable (e.g. two of them both claiming to answer "this account's all-time instrument breakdown" but returning different numbers), that would be the trigger to revisit this — not simply the duplication existing on its own.
- Resolve the broken `/strategy-source.html` route found during Phase 3 (remove the dead route, or restore/rename the intended file) — see [`DEAD_FILE_CLEANUP_2026-08-07.md`](DEAD_FILE_CLEANUP_2026-08-07.md).
- **Fix `public/drafts.php`'s draft-expiry urgency-threshold key mismatch** (§20, Consolidated #9) — `$urgencyThresholds`'s `swing_trader`/`position_trader` keys never match the real `strategies.trading_style` values (`swing`/`position`), so every swing/position trader's "urgent" badge silently uses `day_trader`'s much stricter threshold. Fix the key names to match the real data source. Separately, delete `config/config.php`'s fully-dead `DRAFT_EXPIRY` constant (zero consumers), and reconcile `cron/daily.php`'s own hardcoded expiry-threshold copy (keyed against `user_profiles.trading_style`, a *different* table than the UI badge's `strategies.trading_style`) so the cron job and the UI badge can't disagree about which threshold applies to the same draft. This item is unrelated to the balance/prop-firm cluster but fits the same pattern and was confirmed live during this audit — low effort, real user-facing impact, reasonable to batch into this wave.
- Consider consolidating the three independent "lifecycle_state → phase label" derivations (`AccountPicker`, `journal.php`, `PropFirmHelpers::resolvePropFirmDisplayLabel()`) into the shared `lifecycleStateToPhaseLabel()` utility from Wave 3 — currently agree in output, low urgency, but the same duplication pattern.

---

## What this plan deliberately does not touch

Per the audit's own findings and the Proposal's scoping: the Four-Stage engine, `sp_classify_trade`, `JourneySegmentation::partitionAccountJourney()`, `getChallengeMetrics()`'s already-correct boundary-dollar-amount fields (breach level, target profit amount), Journal's `rnComputeRiskRecommendation()`/`rnGoalProgress()`, and the R:R/pips/per-trade display logic are all confirmed already correctly centralized with no disagreement found — none of these are in scope for any wave above, and should not be touched as a side effect of migration work in adjacent files.

## Coverage note

All seven audit research passes are complete, including §20 (Trading Center, MT5, remaining API endpoints, cron, onboarding, Strategy Builder). Every item that pass surfaced is incorporated above (Wave 1 for the MT5 bypass, Wave 3 for the remaining rules-cascade/balance duplicates, Wave 4.6 for the unused `accountCurrentBalance()` function, Wave 6 for the draft-expiry bug). Confirmed-clean areas from that pass (`api/mt5/parse.php`, Strategy Builder/Validation, onboarding, `api/accounts/transaction.php`/`create.php`/`close.php`, the CRUD-only `api/journal/*.php` endpoints) are not listed in any wave because no finding was made against them.
