# Capital Flow Validation This document records the capital ledger behavior across the new deterministic flows introduced in Phase 3. Each scenario references the exact hooks added to `TradeExecutor` and `CapitalLedger` so you can trace the state transitions. ## Baseline assumptions - Every profile has an entry in `capital_ledgers` with `allocated_capital` (defaults to `config.TOTAL_CAPITAL`). - `available_capital = allocated_capital - reserved_for_orders - reserved_for_positions + realized_pnl` (see `CapitalLedger.availableCapital`). - `withLock` uses the profile ID as the key, preventing intra-profile overlaps while allowing multi-profile concurrency. ## Scenario 1: Concurrent BUY signals for the same profile | Step | Action | Ledger snapshot | Notes | | --- | --- | --- | --- | | 0 | Idle | `reserved_for_orders=0`, `reserved_for_positions=0`, `realized_pnl=0` | Starting capital ready. | | 1 | Signal A calls `openPosition` | `reserveForOrder` runs because `withLock` obtains `profileId`, increments `reserved_for_orders += estimate` | Reservation happens before sending the exchange order (`TradeExecutor.openPosition`, lines 416-432). If the ledger has insufficient capital, this call throws and the entry is rejected before duplicate logic runs. | | 2 | Signal B arrives while A is holding the lock | `withLock` queues Signal B; it cannot mutate the ledger until Signal A releases locks. If Signal A is still waiting, B waits on the same `withLock` promise; once the lock releases, B runs `reserveForOrder` against the updated ledger. | | 3 | Signal A receives fill | `finalizeEntryReservation` releases the `reserved_for_orders` that came from the pending order and immediately adds the notional to `reserved_for_positions` (filled cost) plus updates `realized_pnl` when the eventual exit runs. | | 4 | Signal B runs after A completes | Ledger now reflects A’s `reserved_for_positions` (plus any realized PnL). B can only reserve capital if `allocated_capital - reserved_for_positions - reserved_for_orders` still covers its estimate. | This workflow ensures two `BUY` signals for the same profile cannot both increase `reserved_for_orders` concurrently; the second signal always sees the ledger updated with the first signal’s reservation and release before it runs. ## Scenario 2: Simultaneous BUYs on two different profiles 1. Profile X and Profile Y execute `openPosition` nearly simultaneously. 2. `withLock` uses profile-scoped keys, so each call manipulates its own ledger without waiting for the other. 3. Each signal calls `reserveForOrder` independently, and each ledger updates its own `reserved_for_orders` counter. 4. This means cross-profile capital isolation is enforced purely by the per-profile lock and the separate ledger rows (RLS also enforces row ownership in the schema). The system therefore tolerates multi-profile parallelism without leakage. ## Scenario 3: Partial fill followed by remaining exit | Step | Action | Ledger update | | --- | --- | --- | | 1 | Market entry fills partially (some qty) | `finalizeEntryReservation` releases the reserved order amount and moves `filledQty * fillPrice` to `reserved_for_positions`. The remaining partial order is kept in `pendingOrders` until fully filled. | | 2 | `applyExitFill` handles a partial exit slice | `adjustPositionReservation` is called with a negative delta equal to the released slice’s notional, immediately freeing capital while `realized_pnl` records the actual gain/loss of the slice. | | 3 | When the final close hits | `finalizeTrade` applies the last release (`reserved_for_positions -= entrySize * entryPrice`) and records the complete `realized_pnl`, bringing `reserved_for_positions` back to zero and reflecting total profits/losses. | ## Scenario 4: Restart recovery 1. `rebuildStartupState` replays database/exchange state to recover pending orders and lifecycle data. 2. It calls `rebuildCapitalLedgerFromState`, which: - Re-computes `reserved_for_orders` by summing the `reservedAmount` stored with every pending entry that still belongs to the profile. - Scans `config.SYMBOLS` and reconstructs `reserved_for_positions` from virtual open positions returned by `SupabaseService.getVirtualOpenPosition`. 3. The ledger RPC `fn_rebuild_ledger` overwrites the per-profile row with deterministic reserved amounts, so no stale reservations persist across restarts. ## Validation notes - Concurrency locking is profile-scoped, so a script that fired two signals for the same profile would find the second call waiting for the first lock to release before it even attempts `reserveForOrder`. - Partial fills release and reassign capital immediately, preventing `reserved_for_positions` from drifting up past the actual exposure. - Restart recovery recomputes reservations from persisted state instead of relying on transient memory. ## Test / Automation status - `npm run check` (build + lint + format) was executed but `check:trade-executor-lifecycle` failed because the hosted Supabase client could not reach the API (the ledger RPC layers throw `TypeError: fetch failed`, which causes the script’s assertion to fail while calling `openPosition`). The remaining checks in the suite are automated, but their upstream Supabase dependency has to be re-established before that script succeeds.