Put real trade outcomes first
All checks were successful
deploy / deploy (push) Successful in 32s

Proof: Operator dashboard now starts from successful trades with linked outcome evidence, keeps submitted-only rows in awaiting/no-trade buckets, and explains why recent quotes are not proven asset-changing trades.

Assumptions: Until durable terminal outcome and settlement attribution are implemented, successful trade count must remain zero for submitted-only evidence.

Still fake: Per-quote terminal outcome and settled asset delta plumbing is still not implemented; the page now exposes that absence directly instead of hiding it behind submission counts.
This commit is contained in:
philipp 2026-04-10 10:01:46 +02:00
parent 65d3cff595
commit 3fca125cdd
4 changed files with 150 additions and 8 deletions

View file

@ -1016,6 +1016,7 @@ function buildStrategySummary({
recentExecutionResults,
limit: 20,
});
const tradeFunnel = buildTradeFunnelSummary(lifecycleRows);
return {
strategy_state: {
@ -1028,6 +1029,7 @@ function buildStrategySummary({
? recentDecisions
: [...durableDecisionsById.values()].slice(0, 20),
recent_lifecycle_rows: lifecycleRows,
trade_funnel: tradeFunnel,
skipped_counts: strategyState.skipped_counts || {},
durable_control_state: strategyState.durable_control_state || null,
},
@ -1053,6 +1055,52 @@ function buildStrategySummary({
};
}
function buildTradeFunnelSummary(lifecycleRows = []) {
const counts = {
observed: 0,
rejected: 0,
blocked: 0,
submitted: 0,
awaiting_outcome: 0,
failed: 0,
not_filled: 0,
completed: 0,
};
const successfulTrades = [];
const unresolvedSubmissions = [];
const noTradeRows = [];
for (const row of lifecycleRows || []) {
if (Object.hasOwn(counts, row.lifecycle_state)) {
counts[row.lifecycle_state] += 1;
}
if (row.lifecycle_state === 'completed') {
successfulTrades.push(row);
} else if (['submitted', 'awaiting_outcome'].includes(row.lifecycle_state)) {
unresolvedSubmissions.push(row);
noTradeRows.push(row);
} else if (['observed', 'evaluated', 'command_emitted', 'rejected', 'blocked', 'failed', 'not_filled'].includes(row.lifecycle_state)) {
noTradeRows.push(row);
}
}
return {
successful_trade_count: successfulTrades.length,
unresolved_submission_count: unresolvedSubmissions.length,
no_trade_count: noTradeRows.length,
successful_trades: successfulTrades,
unresolved_submissions: unresolvedSubmissions,
no_trade_rows: noTradeRows,
counts,
caveat:
successfulTrades.length > 0
? 'Successful trades require durable terminal outcome evidence.'
: 'No quote currently has linked terminal outcome and settled inventory evidence, so there are no successful trades to show yet.',
};
}
function buildSystemSummary({ servicesByName, activeAlerts, recentAlerts }) {
const historyWriterState = servicesByName['history-writer']?.state || {};
void activeAlerts;

View file

@ -1,6 +1,6 @@
export const SUBMISSION_COPY = {
statusTileLabel: 'Submissions',
statusTileSubtitle: 'Successful quote-response submissions from durable history',
statusTileSubtitle: 'Quote responses accepted by the relay; not completed trades',
statusTileValueSuffix: 'submissions',
lastStatusTileLabel: 'Last Submission',
recentMetricLabel: 'Recent submissions',

View file

@ -69,7 +69,50 @@ function LifecycleTable({ items }) {
);
}
function SuccessfulTradesTable({ items }) {
if (!items?.length) {
return (
<EmptyState>
No successful trades with linked settlement evidence yet. A submitted quote response is not counted here until a durable terminal outcome and settled asset movement are linked to the quote.
</EmptyState>
);
}
return (
<TableFrame>
<table className="decision-table lifecycle-table">
<thead>
<tr>
<th>Completed at</th>
<th>Pair</th>
<th>Trace</th>
<th>Outcome</th>
<th>Settlement</th>
</tr>
</thead>
<tbody>
{items.map((item, index) => (
<tr key={`${item.quote_id || item.command_id || item.latest_stage_at || index}`}>
<td>{formatTimestamp(item.latest_stage_at)}</td>
<td className="mono truncate-cell" title={item.pair || ''}>{truncateMiddle(item.pair || '', 32)}</td>
<td>
<IdentifierRow label="Quote" value={item.quote_id} />
<IdentifierRow label="Command" value={item.command_id} />
</td>
<td>{item.reason_text}</td>
<td>Linked asset delta not exposed yet</td>
</tr>
))}
</tbody>
</table>
</TableFrame>
);
}
export default function StrategyPage({ strategy }) {
const funnel = strategy.strategy_state.trade_funnel || {};
const counts = funnel.counts || {};
return (
<>
<section className="panel">
@ -78,28 +121,44 @@ export default function StrategyPage({ strategy }) {
<div className="eyebrow">Trading state</div>
<h2>Strategy and executor</h2>
<div className="panel-subtitle">
This page shows the strongest durable claim for each recent quote, from strategy evaluation through executor submission evidence.
This page starts with real trades. Everything else explains why a quote did not become a proven asset-changing trade.
</div>
</div>
</div>
<div className="metric-grid">
<MetricCard label="Successful trades" meta="Requires linked terminal outcome and settlement" value={String(funnel.successful_trade_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="Strategy armed" meta={`Paused ${formatBoolean(strategy.strategy_state.paused)}`} value={formatBoolean(strategy.strategy_state.armed)} />
<MetricCard label="Threshold %" meta="Current gross threshold" value={strategy.strategy_state.threshold_pct ?? 'Unavailable'} />
<MetricCard label="Max notional EURe" meta="Current cap" value={strategy.strategy_state.max_notional_eure ?? 'Unavailable'} />
<MetricCard label="Executor armed" meta={`Paused ${formatBoolean(strategy.executor_state.paused)}`} value={formatBoolean(strategy.executor_state.armed)} />
<MetricCard label="Executor in flight" meta={`Handled ${strategy.executor_state.completed_count || 0}`} value={String(strategy.executor_state.in_flight_count || 0)} />
<MetricCard label="Signer registered" meta={strategy.executor_state.account_id || ''} value={formatBoolean(strategy.executor_state.signer_registered)} />
</div>
</section>
<section className="panel">
<div className="panel-head">
<div>
<div className="eyebrow">Successful trades</div>
<h3>Trades with proven asset movement</h3>
<div className="panel-subtitle">{funnel.caveat}</div>
</div>
<div className="pills">
<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.submitted || 0} submitted only`} stateLabel={(counts.submitted || 0) > 0 ? 'info' : 'unknown'} />
</div>
</div>
<SuccessfulTradesTable items={funnel.successful_trades} />
</section>
<section className="strategy-layout">
<div className="panel">
<div className="panel-head">
<div>
<div className="eyebrow">Lifecycle truth</div>
<h3>Recent quote decisions and execution evidence</h3>
<div className="eyebrow">Why quotes are not trades</div>
<h3>Recent quote outcomes and blockers</h3>
<div className="panel-subtitle">
Strategy rejection, executor blocking, submission failure, and submission success are separated. Submission never implies trade completion or realized asset movement.
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>

View file

@ -584,9 +584,44 @@ test('bootstrap normalizes actionable decision vocabulary before exposing it to
assert.equal(bootstrap.funds.submission_ledger.items[0].decision_reason, 'strategy_approved');
assert.equal(bootstrap.strategy.strategy_state.recent_decisions[0].decision, 'approved');
assert.equal(bootstrap.strategy.strategy_state.recent_lifecycle_rows[0].reason_code, 'quote_response_ok');
assert.equal(bootstrap.strategy.strategy_state.trade_funnel.successful_trade_count, 0);
assert.equal(bootstrap.strategy.strategy_state.trade_funnel.unresolved_submission_count, 1);
assert.equal(bootstrap.strategy.strategy_state.trade_funnel.counts.submitted, 1);
assert.match(bootstrap.strategy.strategy_state.trade_funnel.caveat, /No quote currently has linked terminal outcome/);
assert.doesNotMatch(JSON.stringify(bootstrap), /Actionable/);
});
test('completed lifecycle evidence is the only source of successful trade rows', () => {
const rows = deriveQuoteLifecycleRows({
recentExecutionResults: [
{
command_id: 'cmd-submitted',
quote_id: 'quote-submitted',
result_at: '2026-04-09T09:00:00.000Z',
status: 'submitted',
result_code: 'quote_response_ok',
},
{
command_id: 'cmd-completed',
quote_id: 'quote-completed',
result_at: '2026-04-09T09:01:00.000Z',
status: 'submitted',
result_code: 'quote_response_ok',
outcome_status: 'completed',
outcome_reason: 'settled',
},
],
});
const completed = rows.filter((row) => row.lifecycle_state === 'completed');
const submitted = rows.filter((row) => row.lifecycle_state === 'submitted');
assert.equal(completed.length, 1);
assert.equal(completed[0].quote_id, 'quote-completed');
assert.equal(submitted.length, 1);
assert.equal(submitted[0].quote_id, 'quote-submitted');
});
test('system service state ignores sentinel alert severity and keeps alert surfaces empty', () => {
const config = buildConfig();
const bootstrap = buildDashboardBootstrap({