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:
parent
65d3cff595
commit
3fca125cdd
4 changed files with 150 additions and 8 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue