Consolidate quote lifecycle dashboard
All checks were successful
deploy / deploy (push) Successful in 31s

Proof: Operator dashboard now renders one full-width quote lifecycle table and one successful-trades-only table from durable lifecycle evidence; targeted dashboard tests, dashboard build, and full npm test pass.

Assumptions: Gross edge estimate is edge percent times EUR notional and is labeled separately from realized PnL; realized per-trade PnL remains unavailable until fee and venue-terminal fill data are stored.

Still fake: Venue-native terminal fill events, fee attribution, realized per-trade PnL, and full inventory-skew strategy controls remain incomplete.
This commit is contained in:
philipp 2026-04-10 17:25:46 +02:00
parent fa7e8c885f
commit 9eb1f7b80e
7 changed files with 495 additions and 268 deletions

View file

@ -752,12 +752,18 @@ export function deriveQuoteLifecycleRows({
const rowsByKey = new Map(); const rowsByKey = new Map();
for (const quote of recentQuotes || []) { for (const quote of recentQuotes || []) {
const row = ensureLifecycleRow(rowsByKey, quote?.quote_id || `quote:${quote?.observed_at || quote?.ingested_at || rowsByKey.size}`); const normalizedQuote = normalizeLifecycleQuote(quote);
const row = ensureLifecycleRow(rowsByKey, normalizedQuote?.quote_id || `quote:${normalizedQuote?.observed_at || normalizedQuote?.ingested_at || rowsByKey.size}`);
mergeLifecycleEvidence(row, { mergeLifecycleEvidence(row, {
quote_id: quote?.quote_id || null, quote_id: normalizedQuote?.quote_id || null,
pair: quote?.pair || null, pair: normalizedQuote?.pair || null,
request_kind: quote?.request_kind || null, request_kind: normalizedQuote?.request_kind || null,
quote_observed_at: quote?.observed_at || quote?.ingested_at || null, asset_in: normalizedQuote?.asset_in || null,
asset_out: normalizedQuote?.asset_out || null,
amount_in: normalizedQuote?.amount_in || null,
amount_out: normalizedQuote?.amount_out || null,
quote: normalizedQuote,
quote_observed_at: normalizedQuote?.observed_at || normalizedQuote?.ingested_at || null,
}); });
} }
@ -804,6 +810,10 @@ export function deriveQuoteLifecycleRows({
pair: command.pair, pair: command.pair,
direction: command.direction, direction: command.direction,
request_kind: command.request_kind, request_kind: command.request_kind,
asset_in: command.asset_in || null,
asset_out: command.asset_out || null,
amount_in: command.amount_in || null,
amount_out: command.amount_out || null,
command, command,
command_at: command.command_at || null, command_at: command.command_at || null,
}); });
@ -857,9 +867,14 @@ function ensureLifecycleRow(rowsByKey, key) {
eure_notional: null, eure_notional: null,
quote_observed_at: null, quote_observed_at: null,
decision_at: null, decision_at: null,
asset_in: null,
asset_out: null,
amount_in: null,
amount_out: null,
command_at: null, command_at: null,
execution_result_at: null, execution_result_at: null,
outcome_observed_at: null, outcome_observed_at: null,
quote: null,
decision: null, decision: null,
command: null, command: null,
execution: null, execution: null,
@ -875,6 +890,7 @@ function mergeLifecycleEvidence(row, next) {
row[key] = value; row[key] = value;
} }
} }
if (next?.quote) row.quote = next.quote;
if (next?.decision) row.decision = next.decision; if (next?.decision) row.decision = next.decision;
if (next?.command) row.command = next.command; if (next?.command) row.command = next.command;
if (next?.execution) row.execution = next.execution; if (next?.execution) row.execution = next.execution;
@ -1041,6 +1057,22 @@ function normalizeLifecycleToken(value) {
.replace(/[\s-]+/g, '_'); .replace(/[\s-]+/g, '_');
} }
function normalizeLifecycleQuote(quote) {
if (!quote) return null;
return {
quote_id: quote.quote_id || null,
pair: quote.pair || null,
asset_in: quote.asset_in || null,
asset_out: quote.asset_out || null,
request_kind: quote.request_kind || null,
amount_in: quote.amount_in ?? null,
amount_out: quote.amount_out ?? null,
min_deadline_ms: quote.min_deadline_ms ?? null,
observed_at: quote.observed_at || null,
ingested_at: quote.ingested_at || null,
};
}
function normalizeCommand(command) { function normalizeCommand(command) {
if (!command) return null; if (!command) return null;
return { return {
@ -1053,6 +1085,8 @@ function normalizeCommand(command) {
request_kind: command.request_kind || null, request_kind: command.request_kind || null,
asset_in: command.asset_in || null, asset_in: command.asset_in || null,
asset_out: command.asset_out || null, asset_out: command.asset_out || null,
amount_in: command.amount_in ?? null,
amount_out: command.amount_out ?? null,
command_at: command.command_at || command.observed_at || command.ingested_at || null, command_at: command.command_at || command.observed_at || command.ingested_at || null,
}; };
} }
@ -1345,6 +1379,15 @@ function normalizeTradeForUi({ config, trade }) {
function enrichLifecycleRowForUi({ config, row }) { function enrichLifecycleRowForUi({ config, row }) {
return { return {
...row, ...row,
request_terms: buildLifecycleTerms({
config,
terms: row.quote || row,
}),
submitted_terms: buildLifecycleTerms({
config,
terms: row.command || row.execution || null,
}),
gross_edge_value_eure: estimateGrossEdgeValueEure(row),
settlement_summary: buildSettlementSummary({ settlement_summary: buildSettlementSummary({
config, config,
delta: row.attributed_inventory_delta, delta: row.attributed_inventory_delta,
@ -1354,6 +1397,34 @@ function enrichLifecycleRowForUi({ config, row }) {
}; };
} }
function buildLifecycleTerms({ config, terms }) {
if (!terms?.asset_in && !terms?.asset_out) return null;
const assetIn = config.assetRegistry.get(terms.asset_in);
const assetOut = config.assetRegistry.get(terms.asset_out);
const amountIn = terms.amount_in ?? null;
const amountOut = terms.amount_out ?? null;
return {
asset_in: terms.asset_in || null,
asset_in_symbol: assetIn?.symbol || terms.asset_in || null,
amount_in_units: amountIn,
amount_in: amountIn == null ? null : formatUnits(amountIn, assetIn?.decimals || 0),
asset_out: terms.asset_out || null,
asset_out_symbol: assetOut?.symbol || terms.asset_out || null,
amount_out_units: amountOut,
amount_out: amountOut == null ? null : formatUnits(amountOut, assetOut?.decimals || 0),
};
}
function estimateGrossEdgeValueEure(row) {
const edge = Number(row?.gross_edge_pct);
const notional = Number(row?.eure_notional);
if (!Number.isFinite(edge) || !Number.isFinite(notional)) return null;
const value = (notional * edge) / 100;
return value.toFixed(8).replace(/\.?0+$/, '');
}
function buildSettlementSummary({ config, delta, attributionStatus, attributionMethod }) { function buildSettlementSummary({ config, delta, attributionStatus, attributionMethod }) {
if (!delta?.delta_units) { if (!delta?.delta_units) {
return { return {

View file

@ -9,7 +9,7 @@ import StrategyPage from './pages/StrategyPage.jsx';
import SystemPage from './pages/SystemPage.jsx'; import SystemPage from './pages/SystemPage.jsx';
import { dashboardReducer, initialDashboardState } from './state/dashboardReducer.js'; import { dashboardReducer, initialDashboardState } from './state/dashboardReducer.js';
const TRADE_PAGE_SIZE = 20; const BOOTSTRAP_PAGE_SIZE = 20;
function LoadingPanel() { function LoadingPanel() {
return ( return (
@ -27,27 +27,11 @@ export default function App() {
const criticalBanner = null; const criticalBanner = null;
async function loadBootstrap(page = 1) { async function loadBootstrap(page = 1) {
const dashboard = await fetchJson(`/api/bootstrap?page=${page}&page_size=${TRADE_PAGE_SIZE}`); const dashboard = await fetchJson(`/api/bootstrap?page=${page}&page_size=${BOOTSTRAP_PAGE_SIZE}`);
dispatch({ type: 'bootstrap.loaded', dashboard }); dispatch({ type: 'bootstrap.loaded', dashboard });
return dashboard; return dashboard;
} }
async function loadTradesPage(page) {
if (!Number.isFinite(page) || page < 1) return;
dispatch({ type: 'notice.changed', notice: 'Loading submission history page...' });
dispatch({ type: 'error.changed', error: null });
try {
const submissionLedger = await fetchJson(`/api/submissions?page=${page}&page_size=${TRADE_PAGE_SIZE}`);
dispatch({ type: 'submissionLedger.loaded', submissionLedger });
dispatch({ type: 'notice.changed', notice: null });
} catch (error) {
dispatch({ type: 'error.changed', error: error.message });
dispatch({ type: 'notice.changed', notice: null });
}
}
async function submitControl(service, action, body = {}, { reload = true } = {}) { async function submitControl(service, action, body = {}, { reload = true } = {}) {
dispatch({ type: 'notice.changed', notice: `${action} in progress` }); dispatch({ type: 'notice.changed', notice: `${action} in progress` });
dispatch({ type: 'error.changed', error: null }); dispatch({ type: 'error.changed', error: null });
@ -65,8 +49,7 @@ export default function App() {
dispatch({ type: 'notice.changed', notice: `${action} completed` }); dispatch({ type: 'notice.changed', notice: `${action} completed` });
if (reload) { if (reload) {
const page = state.dashboard?.funds?.submission_ledger?.page || 1; await loadBootstrap(1);
await loadBootstrap(page);
} }
} catch (error) { } catch (error) {
dispatch({ type: 'error.changed', error: error.message }); dispatch({ type: 'error.changed', error: error.message });
@ -168,7 +151,6 @@ export default function App() {
funds={state.dashboard.funds} funds={state.dashboard.funds}
lastControlResult={state.lastControlResult} lastControlResult={state.lastControlResult}
onControl={submitControl} onControl={submitControl}
onTradesPageChange={loadTradesPage}
/> />
) : null} ) : null}
{currentPage === 'strategy' ? ( {currentPage === 'strategy' ? (

View file

@ -5,7 +5,6 @@ import MetricCard from '../components/MetricCard.jsx';
import Pill from '../components/Pill.jsx'; import Pill from '../components/Pill.jsx';
import TableFrame from '../components/TableFrame.jsx'; import TableFrame from '../components/TableFrame.jsx';
import { formatEur, formatTimestamp, stringifyJson, truncateMiddle } from '../lib/format.js'; import { formatEur, formatTimestamp, stringifyJson, truncateMiddle } from '../lib/format.js';
import { SUBMISSION_COPY } from '../lib/submissionCopy.js';
function buildInitialEstimateForm(balances, withdrawalDefaults) { function buildInitialEstimateForm(balances, withdrawalDefaults) {
const firstAssetId = balances?.[0]?.asset_id || ''; const firstAssetId = balances?.[0]?.asset_id || '';
@ -173,99 +172,6 @@ function WithdrawalsTable({ items }) {
); );
} }
function QuotesTable({ items }) {
if (!items?.length) return <EmptyState>No quotes have been captured yet.</EmptyState>;
return (
<TableFrame>
<table className="pair-compact-table">
<thead>
<tr>
<th>Observed</th>
<th>Pair</th>
<th>Kind</th>
<th>Amount in</th>
<th>Amount out</th>
</tr>
</thead>
<tbody>
{items.map((item) => (
<tr key={`${item.quote_id}:${item.ingested_at || item.observed_at}`}>
<td>{formatTimestamp(item.observed_at || item.ingested_at)}</td>
<td className="mono truncate-cell" title={item.pair || ''}>{truncateMiddle(item.pair || '', 38)}</td>
<td>{item.request_kind || ''}</td>
<td className="mono">{item.amount_in || ''}</td>
<td className="mono">{item.amount_out || ''}</td>
</tr>
))}
</tbody>
</table>
</TableFrame>
);
}
function AssetChangeTable({ items }) {
if (!items?.length) return <EmptyState>{SUBMISSION_COPY.termsEmpty}</EmptyState>;
return (
<TableFrame>
<table>
<thead>
<tr>
<th>Observed</th>
<th>Quote</th>
<th>Input</th>
<th>Output</th>
</tr>
</thead>
<tbody>
{items.map((item) => (
<tr key={`${item.quote_id}:${item.observed_at}`}>
<td>{formatTimestamp(item.observed_at)}</td>
<td className="mono">{item.quote_id || ''}</td>
<td>{`${item.amount_in} ${item.asset_in_symbol}`}</td>
<td>{`${item.amount_out} ${item.asset_out_symbol}`}</td>
</tr>
))}
</tbody>
</table>
</TableFrame>
);
}
function TradesTable({ items }) {
if (!items?.length) return <EmptyState>{SUBMISSION_COPY.ledgerEmpty}</EmptyState>;
return (
<TableFrame>
<table className="pair-compact-table">
<thead>
<tr>
<th>Observed</th>
<th>Pair</th>
<th>Kind</th>
<th>Submitted</th>
<th>Edge</th>
<th>Result</th>
</tr>
</thead>
<tbody>
{items.map((trade) => (
<tr key={`${trade.command_id || trade.quote_id}:${trade.observed_at}`}>
<td>{formatTimestamp(trade.observed_at)}</td>
<td className="mono truncate-cell" title={trade.pair || ''}>{truncateMiddle(trade.pair || '', 38)}</td>
<td>{trade.request_kind || ''}</td>
<td>{`${trade.amount_in_display || ''} ${trade.asset_in_symbol || ''} -> ${trade.amount_out_display || ''} ${trade.asset_out_symbol || ''}`}</td>
<td className={Number(trade.gross_edge_pct) > 0 ? 'value-positive' : Number(trade.gross_edge_pct) < 0 ? 'value-negative' : ''}>{trade.gross_edge_pct || ''}</td>
<td><Pill label={trade.result_code || trade.status} stateLabel={trade.result_code || trade.status} /></td>
</tr>
))}
</tbody>
</table>
</TableFrame>
);
}
function WithdrawalEstimateForm({ balances, withdrawalDefaults, onControl }) { function WithdrawalEstimateForm({ balances, withdrawalDefaults, onControl }) {
const [form, setForm] = useState(() => buildInitialEstimateForm(balances, withdrawalDefaults)); const [form, setForm] = useState(() => buildInitialEstimateForm(balances, withdrawalDefaults));
@ -365,12 +271,10 @@ function LastControlResult({ result }) {
export default function FundsPage({ export default function FundsPage({
funds, funds,
onTradesPageChange,
onControl, onControl,
lastControlResult, lastControlResult,
}) { }) {
const profitability = funds.profitability; const profitability = funds.profitability;
const submissionLedger = funds.submission_ledger;
const controlState = funds.funding.control_state || {}; const controlState = funds.funding.control_state || {};
const externalFlowAdjusted = profitability.external_flow_adjusted; const externalFlowAdjusted = profitability.external_flow_adjusted;
const externalFlowCount = profitability.external_flow_count || 0; const externalFlowCount = profitability.external_flow_count || 0;
@ -446,11 +350,6 @@ export default function FundsPage({
signedValue={profitability.portfolio_vs_simple_hold_eure} signedValue={profitability.portfolio_vs_simple_hold_eure}
value={formatEur(profitability.portfolio_vs_simple_hold_eure)} value={formatEur(profitability.portfolio_vs_simple_hold_eure)}
/> />
<MetricCard
label={SUBMISSION_COPY.recentMetricLabel}
meta={formatTimestamp(profitability.last_submission_at)}
value={`${profitability.recent_submission_count || 0} ${SUBMISSION_COPY.recentMetricValueSuffix}`}
/>
</div> </div>
<div className="panel-subtitle"> <div className="panel-subtitle">
{profitability.caveats.map((item) => ( {profitability.caveats.map((item) => (
@ -554,60 +453,6 @@ export default function FundsPage({
<FundingTable items={funds.funding.recent_observations} /> <FundingTable items={funds.funding.recent_observations} />
</div> </div>
</section> </section>
<section className="stack-grid">
<div className="panel">
<div className="panel-head">
<div>
<div className="eyebrow">Live feed</div>
<h3>Recent incoming quotes</h3>
</div>
<div className="status-subtle">WebSocket live feed, capped at 10</div>
</div>
<QuotesTable items={funds.recent_quotes || []} />
</div>
<div className="panel">
<div className="panel-head">
<div>
<div className="eyebrow">{SUBMISSION_COPY.termsEyebrow}</div>
<h3>{SUBMISSION_COPY.termsTitle}</h3>
</div>
</div>
<AssetChangeTable items={funds.recent_submission_terms} />
</div>
</section>
<section className="panel">
<div className="panel-head">
<div>
<div className="eyebrow">Durable ledger</div>
<h3>{SUBMISSION_COPY.ledgerTitle}</h3>
</div>
<div className="status-subtle">{SUBMISSION_COPY.ledgerSubtitle}</div>
</div>
<TradesTable items={submissionLedger.items} />
<div className="pagination">
<div className="status-subtle">{SUBMISSION_COPY.ledgerCountLabel(submissionLedger.page, submissionLedger.total_pages, submissionLedger.total)}</div>
<div className="button-row">
<button
className="button secondary"
disabled={submissionLedger.page <= 1}
onClick={() => onTradesPageChange(submissionLedger.page - 1)}
type="button"
>
Previous
</button>
<button
className="button secondary"
disabled={submissionLedger.page >= submissionLedger.total_pages}
onClick={() => onTradesPageChange(submissionLedger.page + 1)}
type="button"
>
Next
</button>
</div>
</div>
</section>
</> </>
); );
} }

View file

@ -1,8 +1,12 @@
import { Fragment, useState } from 'react';
import EmptyState from '../components/EmptyState.jsx'; import EmptyState from '../components/EmptyState.jsx';
import MetricCard from '../components/MetricCard.jsx'; import MetricCard from '../components/MetricCard.jsx';
import Pill from '../components/Pill.jsx'; import Pill from '../components/Pill.jsx';
import TableFrame from '../components/TableFrame.jsx'; import TableFrame from '../components/TableFrame.jsx';
import { formatBoolean, formatTimestamp, truncateMiddle } from '../lib/format.js'; import { formatBoolean, formatEur, formatTimestamp, truncateMiddle } from '../lib/format.js';
const RESPONDED_STATES = new Set(['submitted', 'awaiting_outcome', 'not_filled', 'completed']);
async function copyIdentifier(value) { async function copyIdentifier(value) {
if (!value || !navigator?.clipboard?.writeText) return; if (!value || !navigator?.clipboard?.writeText) return;
@ -27,42 +31,161 @@ function IdentifierRow({ label, value }) {
); );
} }
function LifecycleTable({ items }) { function formatTerms(terms) {
if (!terms) return 'Unavailable';
const input = terms.amount_in
? `${terms.amount_in} ${terms.asset_in_symbol || ''}`.trim()
: terms.asset_in_symbol || terms.asset_in || 'input unavailable';
const output = terms.amount_out
? `${terms.amount_out} ${terms.asset_out_symbol || ''}`.trim()
: terms.asset_out_symbol || terms.asset_out || 'output unavailable';
return `${input} -> ${output}`;
}
function responseLabel(item) {
if (RESPONDED_STATES.has(item.lifecycle_state)) return 'Yes';
if (item.lifecycle_state === 'failed') return 'Attempt failed';
if (item.lifecycle_state === 'blocked') return 'No - executor blocked';
if (item.lifecycle_state === 'rejected') return 'No - strategy rejected';
if (item.lifecycle_state === 'command_emitted') return 'Pending executor';
if (item.lifecycle_state === 'evaluated') return 'Approved, not sent';
return 'No decision yet';
}
function grossEdgeEstimate(item) {
if (!item.gross_edge_value_eure) return 'Unavailable';
return formatEur(item.gross_edge_value_eure);
}
function plainCodeLabel(value, fallback = 'Unavailable') {
const text = String(value || '').trim();
if (!text) return fallback;
return text.replaceAll('_', ' ');
}
function strategyDecisionStatus(decision) {
if (decision?.decision === 'approved') return 'Strategy approved';
if (decision?.decision === 'rejected') return 'Strategy rejected';
return plainCodeLabel(decision?.decision, 'No strategy decision');
}
function StageCard({ title, at, status, children }) {
return (
<div className="lifecycle-stage-card">
<div className="stage-title">{title}</div>
<div className="stage-meta">{formatTimestamp(at)}</div>
<div className="stage-status">{status || 'Not recorded'}</div>
{children ? <div className="stage-body">{children}</div> : null}
</div>
);
}
function LifecycleDetails({ item }) {
return (
<div className="lifecycle-detail-panel">
<div className="lifecycle-stage-grid">
<StageCard at={item.quote_observed_at} status={item.quote_observed_at ? 'Quote observed' : 'Missing quote event'} title="1. Quote came in">
<div>{formatTerms(item.request_terms)}</div>
<div className="status-subtle mono">{item.pair || 'pair unavailable'}</div>
</StageCard>
<StageCard at={item.decision_at} status={strategyDecisionStatus(item.decision)} title="2. Strategy decided">
<div>{plainCodeLabel(item.decision?.decision_reason || item.reason_code, 'No decision reason recorded')}</div>
<div className="status-subtle">{item.gross_edge_pct ? `Edge ${item.gross_edge_pct}%` : 'Edge unavailable'}</div>
<div className="status-subtle">{item.eure_notional ? `Notional ${formatEur(item.eure_notional)}` : 'Notional unavailable'}</div>
</StageCard>
<StageCard at={item.command_at} status={item.command_id ? 'Command recorded' : 'No command'} title="3. Executor command">
<IdentifierRow label="Command" value={item.command_id} />
<div>{formatTerms(item.submitted_terms)}</div>
</StageCard>
<StageCard at={item.execution_result_at} status={item.execution?.status || 'No relay result'} title="4. Relay response">
<div>{item.execution?.result_code || 'No executor result code stored'}</div>
<div className="status-subtle">Submitted means the relay accepted the response; it does not prove a trade.</div>
</StageCard>
<StageCard at={item.outcome_observed_at} status={item.outcome_status || item.lifecycle_state} title="5. Outcome and settlement">
<div>{item.reason_text}</div>
<div className="status-subtle">{item.settlement_summary?.text || 'No settled inventory delta is linked to this quote.'}</div>
{item.settlement_summary?.caveat ? <div className="status-subtle">{item.settlement_summary.caveat}</div> : null}
</StageCard>
</div>
<div className="trace-block">
<IdentifierRow label="Quote" value={item.quote_id} />
<IdentifierRow label="Decision" value={item.decision_id} />
<IdentifierRow label="Command" value={item.command_id} />
</div>
</div>
);
}
function QuoteLifecycleTable({ items }) {
const [expanded, setExpanded] = useState(() => new Set());
if (!items?.length) return <EmptyState>No quote lifecycle evidence has been observed yet.</EmptyState>; if (!items?.length) return <EmptyState>No quote lifecycle evidence has been observed yet.</EmptyState>;
function toggle(rowKey) {
setExpanded((current) => {
const next = new Set(current);
if (next.has(rowKey)) next.delete(rowKey);
else next.add(rowKey);
return next;
});
}
return ( return (
<TableFrame> <TableFrame>
<table className="decision-table lifecycle-table"> <table className="quote-lifecycle-table">
<thead> <thead>
<tr> <tr>
<th>At</th> <th>Time</th>
<th>Lifecycle</th> <th>Quote id</th>
<th>Request</th>
<th>Responded?</th>
<th>Result</th>
<th>Reason</th> <th>Reason</th>
<th>Pair</th> <th>Edge / notional</th>
<th>Trace</th> <th>Lifecycle</th>
<th>Edge %</th>
<th>Notional</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{items.map((item, index) => ( {items.map((item, index) => {
<tr key={`${item.quote_id || item.decision_id || item.command_id || item.latest_stage_at || index}`}> const rowKey = item.quote_id || item.decision_id || item.command_id || item.latest_stage_at || String(index);
<td>{formatTimestamp(item.latest_stage_at)}</td> const isExpanded = expanded.has(rowKey);
<td><Pill label={item.lifecycle_label} stateLabel={item.lifecycle_tone} /></td> return (
<td> <Fragment key={rowKey}>
<div>{item.reason_text}</div> <tr key={`${rowKey}:row`}>
<div className="status-subtle mono">{item.reason_code || 'reason_unknown'}</div> <td>{formatTimestamp(item.latest_stage_at)}</td>
</td> <td><IdentifierRow label="Quote" value={item.quote_id} /></td>
<td className="mono truncate-cell" title={item.pair || ''}>{truncateMiddle(item.pair || '', 32)}</td> <td>
<td> <div>{formatTerms(item.request_terms || item.submitted_terms)}</div>
<IdentifierRow label="Quote" value={item.quote_id} /> <div className="status-subtle mono">{truncateMiddle(item.pair || '', 34)}</div>
<IdentifierRow label="Decision" value={item.decision_id} /> </td>
<IdentifierRow label="Command" value={item.command_id} /> <td>{responseLabel(item)}</td>
</td> <td><Pill label={item.lifecycle_label} stateLabel={item.lifecycle_tone} /></td>
<td className={Number(item.gross_edge_pct) > 0 ? 'value-positive' : Number(item.gross_edge_pct) < 0 ? 'value-negative' : ''}>{item.gross_edge_pct || ''}</td> <td>
<td>{item.eure_notional || ''}</td> <div>{item.reason_text}</div>
</tr> <div className="status-subtle mono">{item.reason_code || 'reason_unknown'}</div>
))} </td>
<td>
<div className={Number(item.gross_edge_pct) > 0 ? 'value-positive' : Number(item.gross_edge_pct) < 0 ? 'value-negative' : ''}>{item.gross_edge_pct ? `${item.gross_edge_pct}%` : 'Unavailable'}</div>
<div className="status-subtle">{item.eure_notional ? formatEur(item.eure_notional) : 'Notional unavailable'}</div>
</td>
<td>
<button className="button secondary" onClick={() => toggle(rowKey)} type="button">
{isExpanded ? 'Hide lifecycle' : 'Show lifecycle'}
</button>
</td>
</tr>
{isExpanded ? (
<tr className="lifecycle-expanded-row" key={`${rowKey}:details`}>
<td colSpan={8}><LifecycleDetails item={item} /></td>
</tr>
) : null}
</Fragment>
);
})}
</tbody> </tbody>
</table> </table>
</TableFrame> </TableFrame>
@ -70,6 +193,7 @@ function LifecycleTable({ items }) {
} }
function SuccessfulTradesTable({ items }) { function SuccessfulTradesTable({ items }) {
const [expanded, setExpanded] = useState(() => new Set());
if (!items?.length) { if (!items?.length) {
return ( return (
<EmptyState> <EmptyState>
@ -78,43 +202,68 @@ function SuccessfulTradesTable({ items }) {
); );
} }
function toggle(rowKey) {
setExpanded((current) => {
const next = new Set(current);
if (next.has(rowKey)) next.delete(rowKey);
else next.add(rowKey);
return next;
});
}
return ( return (
<TableFrame> <TableFrame>
<table className="decision-table lifecycle-table"> <table className="successful-trades-table">
<thead> <thead>
<tr> <tr>
<th>Completed at</th> <th>Completed</th>
<th>Pair</th> <th>Quote id</th>
<th>Trace</th> <th>Edge</th>
<th>Outcome</th> <th>Gross edge est.</th>
<th>Settlement</th> <th>Settlement</th>
<th>Realized PnL</th>
<th>Lifecycle</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{items.map((item, index) => ( {items.map((item, index) => {
<tr key={`${item.quote_id || item.command_id || item.latest_stage_at || index}`}> const rowKey = item.quote_id || item.command_id || item.latest_stage_at || String(index);
<td>{formatTimestamp(item.latest_stage_at)}</td> const isExpanded = expanded.has(rowKey);
<td className="mono truncate-cell" title={item.pair || ''}>{truncateMiddle(item.pair || '', 32)}</td> return (
<td> <Fragment key={rowKey}>
<IdentifierRow label="Quote" value={item.quote_id} /> <tr key={`${rowKey}:trade`}>
<IdentifierRow label="Decision" value={item.decision_id} /> <td>{formatTimestamp(item.latest_stage_at)}</td>
<IdentifierRow label="Command" value={item.command_id} /> <td><IdentifierRow label="Quote" value={item.quote_id} /></td>
</td> <td>
<td>{item.reason_text}</td> <div>{item.gross_edge_pct ? `${item.gross_edge_pct}%` : 'Unavailable'}</div>
<td> <div className="status-subtle">{item.eure_notional ? formatEur(item.eure_notional) : 'Notional unavailable'}</div>
<div>{item.settlement_summary?.text || 'No settled inventory delta is linked to this quote.'}</div> </td>
{item.settlement_summary?.method ? ( <td>
<div className="status-subtle mono">{item.settlement_summary.method}</div> <div>{grossEdgeEstimate(item)}</div>
<div className="status-subtle">Estimate from edge x notional, not realized PnL.</div>
</td>
<td>
<div>{item.settlement_summary?.text || 'No settled inventory delta is linked to this quote.'}</div>
{item.settlement_summary?.method ? <div className="status-subtle mono">{item.settlement_summary.method}</div> : null}
</td>
<td>
<div>Unavailable</div>
<div className="status-subtle">Fees and venue-native terminal fill are not stored.</div>
</td>
<td>
<button className="button secondary" onClick={() => toggle(rowKey)} type="button">
{isExpanded ? 'Hide lifecycle' : 'Show lifecycle'}
</button>
</td>
</tr>
{isExpanded ? (
<tr className="lifecycle-expanded-row" key={`${rowKey}:trade-details`}>
<td colSpan={7}><LifecycleDetails item={item} /></td>
</tr>
) : null} ) : null}
{item.settlement_summary?.observed_at ? ( </Fragment>
<div className="status-subtle">{`Observed ${formatTimestamp(item.settlement_summary.observed_at)}`}</div> );
) : null} })}
{item.settlement_summary?.caveat ? (
<div className="status-subtle">{item.settlement_summary.caveat}</div>
) : null}
</td>
</tr>
))}
</tbody> </tbody>
</table> </table>
</TableFrame> </TableFrame>
@ -130,66 +279,51 @@ export default function StrategyPage({ strategy }) {
<section className="panel"> <section className="panel">
<div className="panel-head"> <div className="panel-head">
<div> <div>
<div className="eyebrow">Trading state</div> <div className="eyebrow">Trading evidence</div>
<h2>Strategy and executor</h2> <h2>Quotes, responses, and proven trades</h2>
<div className="panel-subtitle"> <div className="panel-subtitle">
This page starts with real trades. Everything else explains why a quote did not become a proven asset-changing trade. One place for quote truth: every row starts at the incoming quote, then shows whether we responded, why not, and whether any asset movement was proven.
</div> </div>
</div> </div>
</div> </div>
<div className="metric-grid"> <div className="metric-grid">
<MetricCard label="Successful trades" meta="Requires linked terminal outcome and settlement" value={String(funnel.successful_trade_count || 0)} /> <MetricCard label="Successful trades" meta="Requires linked terminal outcome and settlement" value={String(funnel.successful_trade_count || 0)} />
<MetricCard label="Not filled" meta="Submitted but no settled inventory delta" value={String(counts.not_filled || 0)} />
<MetricCard label="Awaiting outcome" meta="Submitted, no durable terminal result yet" value={String(funnel.unresolved_submission_count || 0)} /> <MetricCard label="Awaiting outcome" meta="Submitted, no durable terminal result yet" value={String(funnel.unresolved_submission_count || 0)} />
<MetricCard label="No-trade rows" meta="Filtered, rejected, blocked, failed, or unresolved" value={String(funnel.no_trade_count || 0)} /> <MetricCard label="Rejected / blocked" meta="Strategy rejection or executor block" value={String((counts.rejected || 0) + (counts.blocked || 0))} />
<MetricCard label="Strategy armed" meta={`Paused ${formatBoolean(strategy.strategy_state.paused)}`} value={formatBoolean(strategy.strategy_state.armed)} /> <MetricCard label="Strategy armed" meta={`Paused ${formatBoolean(strategy.strategy_state.paused)}`} value={formatBoolean(strategy.strategy_state.armed)} />
<MetricCard label="Executor armed" meta={`Paused ${formatBoolean(strategy.executor_state.paused)}`} value={formatBoolean(strategy.executor_state.armed)} /> <MetricCard label="Executor armed" meta={`Paused ${formatBoolean(strategy.executor_state.paused)}`} value={formatBoolean(strategy.executor_state.armed)} />
<MetricCard label="Signer registered" meta={strategy.executor_state.account_id || ''} value={formatBoolean(strategy.executor_state.signer_registered)} />
</div> </div>
</section> </section>
<section className="panel"> <section className="panel">
<div className="panel-head"> <div className="panel-head">
<div> <div>
<div className="eyebrow">Successful trades</div> <div className="eyebrow">Successful trades only</div>
<h3>Trades with proven asset movement</h3> <h3>Trades with proven asset movement</h3>
<div className="panel-subtitle">{funnel.caveat}</div> <div className="panel-subtitle">
This table excludes submitted-only quote responses. Realized PnL remains unavailable until fees and venue-native terminal fills are stored.
</div>
</div> </div>
<div className="pills"> <div className="pills">
<Pill label={`${counts.completed || 0} completed`} stateLabel={(counts.completed || 0) > 0 ? 'healthy' : 'unknown'} /> <Pill label={`${counts.completed || 0} completed`} stateLabel={(counts.completed || 0) > 0 ? 'healthy' : 'unknown'} />
<Pill label={`${counts.not_filled || 0} not filled`} stateLabel={(counts.not_filled || 0) > 0 ? 'warning' : 'unknown'} /> <Pill label={`${counts.not_filled || 0} not filled`} stateLabel={(counts.not_filled || 0) > 0 ? 'warning' : 'unknown'} />
<Pill label={`${counts.submitted || 0} submitted only`} stateLabel={(counts.submitted || 0) > 0 ? 'info' : 'unknown'} />
</div> </div>
</div> </div>
<SuccessfulTradesTable items={funnel.successful_trades} /> <SuccessfulTradesTable items={funnel.successful_trades} />
</section> </section>
<section className="strategy-layout"> <section className="panel full-width-evidence-panel">
<div className="panel"> <div className="panel-head">
<div className="panel-head"> <div>
<div> <div className="eyebrow">Quote lifecycle</div>
<div className="eyebrow">Why quotes are not trades</div> <h3>Incoming quotes and what happened next</h3>
<h3>Recent quote outcomes and blockers</h3> <div className="panel-subtitle">
<div className="panel-subtitle"> Full-width quote table: incoming quote, response decision, result, decisive reason, and expandable lifecycle stages.
Each row answers why the quote was filtered, rejected, blocked, submitted without outcome, failed, not filled, or completed. Submission still never means asset movement.
</div>
</div> </div>
</div> </div>
<LifecycleTable items={strategy.strategy_state.recent_lifecycle_rows} />
</div>
<div className="panel strategy-side-panel">
<div className="panel-head">
<div>
<div className="eyebrow">Controls</div>
<h3>Omitted risky controls</h3>
</div>
</div>
<EmptyState>
{strategy.omitted_controls.map((item) => (
<div key={item}>{item}</div>
))}
</EmptyState>
</div> </div>
<QuoteLifecycleTable items={strategy.strategy_state.recent_lifecycle_rows} />
</section> </section>
</> </>
); );

View file

@ -535,3 +535,112 @@ table.lifecycle-table th:nth-child(5) {
min-width: 540px; min-width: 540px;
} }
} }
.quote-lifecycle-table,
.successful-trades-table {
min-width: 1180px;
table-layout: fixed;
}
.quote-lifecycle-table th:nth-child(1),
.quote-lifecycle-table td:nth-child(1) {
width: 150px;
}
.quote-lifecycle-table th:nth-child(2),
.quote-lifecycle-table td:nth-child(2) {
width: 260px;
}
.quote-lifecycle-table th:nth-child(3),
.quote-lifecycle-table td:nth-child(3) {
width: 210px;
}
.quote-lifecycle-table th:nth-child(4),
.quote-lifecycle-table td:nth-child(4) {
width: 140px;
}
.quote-lifecycle-table th:nth-child(5),
.quote-lifecycle-table td:nth-child(5) {
width: 150px;
}
.quote-lifecycle-table th:nth-child(7),
.quote-lifecycle-table td:nth-child(7) {
width: 140px;
}
.quote-lifecycle-table th:nth-child(8),
.quote-lifecycle-table td:nth-child(8),
.successful-trades-table th:nth-child(7),
.successful-trades-table td:nth-child(7) {
width: 150px;
}
.successful-trades-table {
min-width: 1040px;
}
.successful-trades-table th:nth-child(1),
.successful-trades-table td:nth-child(1) {
width: 150px;
}
.successful-trades-table th:nth-child(2),
.successful-trades-table td:nth-child(2) {
width: 260px;
}
.successful-trades-table th:nth-child(3),
.successful-trades-table td:nth-child(3),
.successful-trades-table th:nth-child(4),
.successful-trades-table td:nth-child(4) {
width: 140px;
}
.lifecycle-expanded-row td {
background: rgba(24, 33, 30, 0.05);
}
.lifecycle-detail-panel {
display: grid;
gap: 14px;
}
.lifecycle-stage-grid {
display: grid;
gap: 12px;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.lifecycle-stage-card {
min-width: 0;
padding: 14px;
border-radius: 16px;
background: var(--panel-2);
border: 1px solid var(--line);
}
.stage-title {
font-weight: 700;
}
.stage-meta,
.stage-status,
.stage-body {
margin-top: 6px;
color: var(--muted);
}
.stage-status {
color: var(--ink);
}
.trace-block {
padding: 14px;
border-radius: 16px;
background: rgba(255, 255, 255, 0.48);
border: 1px solid var(--line);
}

View file

@ -0,0 +1,23 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
const strategySource = readFileSync(new URL('../src/operator-dashboard/static/pages/StrategyPage.jsx', import.meta.url), 'utf8');
const fundsSource = readFileSync(new URL('../src/operator-dashboard/static/pages/FundsPage.jsx', import.meta.url), 'utf8');
test('strategy page owns consolidated quote lifecycle and successful trade tables', () => {
assert.match(strategySource, /Quote lifecycle/);
assert.match(strategySource, /Incoming quotes and what happened next/);
assert.match(strategySource, /Responded\?/);
assert.match(strategySource, /Successful trades only/);
assert.match(strategySource, /Show lifecycle/);
assert.match(strategySource, /Submitted means the relay accepted the response; it does not prove a trade\./);
assert.doesNotMatch(strategySource, /Actionable|actionable/);
});
test('funds page no longer renders duplicate quote and submission tables', () => {
assert.doesNotMatch(fundsSource, /Recent incoming quotes/);
assert.doesNotMatch(fundsSource, /Recent submitted quote terms/);
assert.doesNotMatch(fundsSource, /Submitted quote responses/);
assert.doesNotMatch(fundsSource, /Durable ledger/);
});

View file

@ -1127,3 +1127,66 @@ test('funding summary includes credited bridge deposits without observer-backed
assert.equal(bootstrap.funds.funding.recent_observations[0].tx_hash, 'eth-tx-1'); assert.equal(bootstrap.funds.funding.recent_observations[0].tx_hash, 'eth-tx-1');
assert.equal(bootstrap.funds.recent_deposits[0].tx_hash, 'eth-tx-1'); assert.equal(bootstrap.funds.recent_deposits[0].tx_hash, 'eth-tx-1');
}); });
test('bootstrap lifecycle rows preserve quote terms, submitted terms, and gross edge estimate', () => {
const config = buildConfig();
const bootstrap = buildDashboardBootstrap({
config,
auth: { authenticated: true, subject: 'local-operator', mode: 'stub', roles: ['operator'] },
portfolioMetric: null,
inventorySnapshot: null,
marketPrice: null,
recentQuotes: [{
quote_id: 'quote-terms-1',
pair: config.activePair,
asset_in: config.tradingBtc.assetId,
asset_out: config.tradingEure.assetId,
request_kind: 'exact_in',
amount_in: '123208',
amount_out: null,
observed_at: '2026-04-09T09:00:00.000Z',
}],
submissionPage: { page: 1, page_size: 20, total: 0, total_pages: 1, items: [] },
submissionSummary: { total: 0, last_submission_at: null },
fundingObservations: [],
recentDepositStatuses: [],
recentTradeDecisions: [{
observed_at: '2026-04-09T09:00:01.000Z',
payload: {
decision_id: 'decision-terms-1',
quote_id: 'quote-terms-1',
pair: config.activePair,
decision: 'actionable',
decision_reason: 'actionable',
gross_edge_pct: '1.5',
eure_notional: '100',
},
}],
recentExecuteTradeCommands: [{
observed_at: '2026-04-09T09:00:02.000Z',
payload: {
command_id: 'cmd-terms-1',
decision_id: 'decision-terms-1',
quote_id: 'quote-terms-1',
pair: config.activePair,
request_kind: 'exact_in',
asset_in: config.tradingBtc.assetId,
asset_out: config.tradingEure.assetId,
amount_in: '123208',
amount_out: '76000000000000000000',
},
}],
recentExecutionResults: [],
recentQuoteOutcomes: [],
recentAlertTransitions: [],
serviceSnapshots: [],
});
const row = bootstrap.strategy.strategy_state.recent_lifecycle_rows[0];
assert.equal(row.quote_id, 'quote-terms-1');
assert.equal(row.request_terms.amount_in, '0.00123208');
assert.equal(row.request_terms.asset_in_symbol, 'BTC');
assert.equal(row.submitted_terms.amount_out, '76');
assert.equal(row.submitted_terms.asset_out_symbol, 'EURe');
assert.equal(row.gross_edge_value_eure, '1.5');
});