# Reconciliation EXIT Backfill Rollout Checklist ## Scope Guardrails - No entry/exit/risk/signal logic changes. - Data-only repair: insert compensating `EXIT` rows only. - No deletes. Rollback uses status updates only. - Backfill must be paused-mode gated and auditable by `batch_id`. ## Feature Flags (default safe state) - `ENABLE_RECON_EXIT_BACKFILL=false` - `RECON_EXIT_BACKFILL_DRY_RUN=true` - `RECON_EXIT_BACKFILL_REQUIRE_PAUSE=true` - `RECON_EXIT_BACKFILL_DUST_ABS_QTY=0.001` - `RECON_EXIT_BACKFILL_DUST_REL_PCT=0.002` - `RECON_EXIT_BACKFILL_LOOKBACK_HOURS=72` - `EXCHANGE_STATE_MISMATCH_THROTTLE_MS=300000` ## 1. Detect Affected `trade_id` Values ```sql WITH lifecycle AS ( SELECT profile_id, symbol, trade_id, SUM(CASE WHEN action = 'ENTRY' THEN COALESCE(quantity, qty, 0) ELSE 0 END) AS entry_qty, SUM(CASE WHEN action = 'EXIT' THEN COALESCE(quantity, qty, 0) ELSE 0 END) AS exit_qty FROM orders WHERE profile_id = '<>' AND status IN ('filled', 'partially_filled', 'partially-filled') AND trade_id IS NOT NULL GROUP BY profile_id, symbol, trade_id ) SELECT profile_id, symbol, trade_id, entry_qty, exit_qty, (entry_qty - exit_qty) AS open_qty FROM lifecycle WHERE (entry_qty - exit_qty) > 0 ORDER BY symbol, trade_id; ``` ## 2. Allow Criteria for Backfill `EXIT` Row Backfill is allowed only when all are true: - Profile is paused (`/internal/trading/status` returns `PAUSED`). - Exchange is flat for symbol. - No pending lifecycle blocker row exists for that `trade_id`. - Close qty is supported by exchange fill evidence OR unresolved remainder is dust. - Idempotency key resolves to unique deterministic `order_id`. ## 3. Idempotency Rules - Deterministic `order_id`: - `BFILL-` - Insert path: - `ON CONFLICT (order_id) DO NOTHING` (implemented via upsert + `ignoreDuplicates`). - Re-runs are safe: existing backfill `order_id` rows are skipped. - Exchange `client_order_id` (if present) is stored in `reconciliation_backfill_audit.exchange_client_order_id`. ## 4. Dust Threshold Handling - Dust threshold per trade: - `MAX(dust_abs_qty, open_qty * dust_rel_pct)` - Default: - `dust_abs_qty = 0.001` - `dust_rel_pct = 0.2%` - Auto-close allowed: - evidence-covered qty always - remainder only if `remainder <= threshold` - No-Go: - `remainder > threshold` with missing exchange fill evidence. ## 5. Dry-Run Execution and Verification 1. Set: - `ENABLE_RECON_EXIT_BACKFILL=true` - `RECON_EXIT_BACKFILL_DRY_RUN=true` - `RECON_EXIT_BACKFILL_REQUIRE_PAUSE=true` 2. Pause trading from admin. 3. Let one reconciliation cycle run. 4. Verify audit-only effect: ```sql SELECT batch_id, profile_id, symbol, trade_id, decision, reason, filled_qty, backfill_order_id, created_at FROM reconciliation_backfill_audit WHERE profile_id = '<>' ORDER BY created_at DESC LIMIT 200; ``` 5. Confirm `orders` table unchanged in dry-run: ```sql SELECT COUNT(*) AS dry_run_backfill_rows FROM orders WHERE profile_id = '<>' AND order_id LIKE 'BFILL-%'; ``` ## 6. Go / No-Go Before Apply Go only if: - Pause state confirmed. - Dry-run shows expected `DRY_RUN` or `PENDING_APPLY` candidates. - No unexpected `NO_GO` due to non-flat exchange or pending blockers. - Candidate qty aligns with known exchange fills. - Audit table writes succeed. No-Go if any: - Exchange not flat for target symbol. - Large unmatched remainder (`missing_fill_evidence_for_large_remainder`). - Audit table unavailable/write failure. - Pending blocker rows for target `trade_id`. ## 7. Apply Step (still paused) 1. Set `RECON_EXIT_BACKFILL_DRY_RUN=false`. 2. Run one reconciliation cycle. 3. Validate inserted rows: ```sql SELECT order_id, profile_id, symbol, trade_id, action, status, qty, price, filled_at, created_at FROM orders WHERE profile_id = '<>' AND order_id LIKE 'BFILL-%' ORDER BY created_at DESC; ``` ## 8. Post-Fix Validation (ghost positions removed) Open lifecycle should be zero/near-zero: ```sql WITH lifecycle AS ( SELECT profile_id, symbol, trade_id, SUM(CASE WHEN action = 'ENTRY' THEN COALESCE(quantity, qty, 0) ELSE 0 END) AS entry_qty, SUM(CASE WHEN action = 'EXIT' THEN COALESCE(quantity, qty, 0) ELSE 0 END) AS exit_qty FROM orders WHERE profile_id = '<>' AND status IN ('filled', 'partially_filled', 'partially-filled') AND trade_id IS NOT NULL GROUP BY profile_id, symbol, trade_id ) SELECT * FROM lifecycle WHERE (entry_qty - exit_qty) > 0.000001 ORDER BY symbol, trade_id; ``` Mismatch noise check (audit + events): ```sql SELECT decision, COUNT(*) AS rows FROM reconciliation_backfill_audit WHERE profile_id = '<>' GROUP BY decision ORDER BY decision; ``` ## 9. Rollback (Reversible, Non-Destructive) Rollback is status-only, never delete: ```sql UPDATE orders SET status = 'canceled', updated_at = now() WHERE order_id IN ( SELECT backfill_order_id FROM reconciliation_backfill_audit WHERE batch_id = '<>' AND decision IN ('APPLIED', 'SKIP_EXISTING') AND backfill_order_id IS NOT NULL ); ``` Record rollback marker: ```sql UPDATE reconciliation_backfill_audit SET reverted_at = now() WHERE batch_id = '<>'; ```