# Implementation Turn: quote lifecycle truth and execution explanation Status: open Opened: 2026-04-09 ## Goal Replace ambiguous quote and decision wording with a truthful per-quote lifecycle that tells the operator exactly why a quote was filtered, rejected, blocked, submitted, failed, not filled, or completed. ## Selected backlog items - [I021] Quote lifecycle truth and execution explanation: replace ambiguous dashboard verdicts with a per-quote state machine that shows exactly why a quote was filtered, rejected, blocked, submitted, not filled, or executed, with durable reason codes and operator-facing traceability. ## Design rules - Treat quote lifecycle as product truth, not UI decoration. - Strategy verdict is not the final operator answer. - Prefer one explicit lifecycle derivation path shared by backend and dashboard over ad hoc page-specific wording. - Do not invent downstream certainty where durable evidence is absent. - Remove `Actionable` completely from operator-facing copy. ## Problem statement for this turn The current dashboard still forces operators to infer too much: - `Actionable` does not say whether a command was emitted or submitted - the Strategy page mixes strategy and execution truth - executor-rejected rows are not clearly distinguishable from strategy-rejected rows - quote ids are truncated and awkward to use during debugging The repo already stores enough of the real lifecycle to do better: - quote id - decision id - emitted command id - execution result status and result code The turn therefore needs to improve: - lifecycle derivation - durable reason mapping - recent-row rendering - trace affordances ## Lifecycle model for this turn Implement one repo-owned lifecycle derivation for recent rows, using durable evidence in this order: 1. Quote observed 2. Strategy evaluated 3. Command emitted or not emitted 4. Executor result observed or absent 5. Venue downstream outcome when available The first mandatory states are: - `Filtered` - `Rejected` - `Blocked` - `Submitted` - `Failed` - `Awaiting outcome` - `Completed` Suggested meanings: - `Filtered` quote never entered the active trade path or was excluded before strategy decision - `Rejected` strategy evaluated the quote and decided not to trade - `Blocked` strategy approved or emitted a command, but execution did not proceed due to control state or another repo-owned gate - `Submitted` executor accepted the command and successfully submitted a quote response - `Failed` execution submission failed technically - `Awaiting outcome` submitted to venue, but no later durable terminal venue outcome exists yet - `Completed` durable evidence shows the trade completed successfully Do not show states we cannot support yet for a given row. ## Reason-code model For each lifecycle state, map durable payload fields to a small operator reason taxonomy. Examples: - strategy reason codes: - unsupported_pair - below_edge_threshold - inventory_unavailable - stale_reference_price - executor reason codes: - executor_disarmed - executor_paused - submission_failed - quote_response_ok - downstream outcome reasons if available: - expired - not_filled - completed If the exact reason is missing: - expose `reason_unknown` - keep the row truthful instead of synthesizing an explanation ## Backend changes ### 1. Add a lifecycle derivation helper Create or extend a backend module that derives quote lifecycle from: - recent trade decisions - recent execution results - successful trade records - any available quote-status or venue result surfaces It should emit a normalized row object with: - `quote_id` - `decision_id` - `command_id` - `pair` - `direction` - `lifecycle_state` - `lifecycle_label` - `reason_code` - `reason_text` - timestamps for the latest known stage - stage details for tooltips or drilldown ### 2. Join decision and execution truth explicitly The backend should no longer leave the frontend to infer execution from isolated tables. For each recent quote/decision row: - attach the matching execution result by `command_id`, `decision_id`, or `quote_id` - attach successful-trade or later terminal evidence where available - expose whether the row is strategy-only, strategy-plus-command, or strategy-plus-execution ### 3. Preserve operator drilldown identifiers Ensure the bootstrap payload exposes: - full quote id - full decision id - full command id Avoid requiring the frontend to reconstruct or guess identifiers from formatted strings. ## Dashboard changes ### 4. Remove forbidden language Remove `Actionable` from: - Strategy page tables - any lifecycle badge or verdict cell - any supporting labels or legends Replace it with explicit state labels driven by lifecycle derivation. ### 5. Make recent rows self-explanatory For each row, render: - primary lifecycle state - secondary reason text - quote id with copy action - command id if emitted - timestamps The operator should be able to scan rows and answer: - why no trade happened - whether the system tried to trade - whether failure was strategic, operational, or downstream ### 6. Add trace affordances At minimum: - copy button for quote id - avoid over-truncating ids without recovery path - show linked ids in a dedicated trace column or expanded detail panel If the row layout gets crowded, prefer an expandable detail tray over hiding identifiers. ## Page-level application ### Strategy page This page should become the primary recent quote-decision-execution lifecycle surface. It should show: - the latest recent rows for the active pair - lifecycle state rather than strategy-only verdict - explicit explanation text If a strategy-only summary remains, it must be visually separate from per-quote lifecycle truth. ### Related quote surfaces Inspect quote and system surfaces for similar ambiguity and align the wording if they expose the same concepts. Do not let one page say `Submitted` while another page still says `Actionable` for the same row. ## Data and state edge cases - Strategy decision exists, no command emitted: render as `Rejected` with strategy reason - Command emitted, no execution result yet: render as `Blocked` or `Awaiting executor` only if that distinction is durably supportable; otherwise use a truthful pending label - Execution result `executor_disarmed`: render as `Blocked` with reason `executor disarmed` - Execution result `submission_failed`: render as `Failed` - Execution result `submitted`: render as `Submitted` or `Awaiting outcome` - Successful trade summary exists but no explicit per-quote completion event: only promote to `Completed` where the durable linkage is real ## Concrete implementation order ### Phase 1. Define lifecycle derivation - inspect current durable decision and execution payloads - write the normalized lifecycle state mapping - define forbidden and allowed operator labels ### Phase 2. Implement backend aggregation - derive unified recent lifecycle rows - expose full identifiers and reason codes - keep old consumers working until the frontend is switched ### Phase 3. Update Strategy page rendering - replace verdict column with lifecycle state - add reason text - add quote-id copy affordance - surface command id and execution state where relevant ### Phase 4. Tighten wording and consistency - remove `Actionable` - align supporting labels - ensure blocked vs rejected vs submitted are clearly distinct ### Phase 5. Validate with live recent rows - verify a row rejected due to executor disarmed renders as blocked with reason - verify a submitted row renders as submitted - verify quote ids can be copied and used for tracing ## Test plan - unit tests for lifecycle derivation from: - strategy-rejected rows - executor-disarmed rows - submission-failed rows - submitted rows - dashboard bootstrap tests for: - forbidden `Actionable` removal - explicit lifecycle labels - reason text rendering - identifier exposure - frontend component tests if needed for copy affordance or row rendering logic No lifecycle ambiguity fix is complete without a regression test proving the old ambiguous wording cannot return. ## Validation checklist against the proof - `Actionable` no longer appears - strategy approval is visibly distinct from execution submission - recent blocked rows explain why they did not trade - recent submitted rows show that they were submitted - quote ids are directly usable from the dashboard ## Failure modes to plan for - the backend joins rows incorrectly and attributes the wrong execution result - the UI uses softer wording than the backend lifecycle state - older rows lack enough evidence and the UI pretends certainty - ids are still truncated without a copy or expand path