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,
|
recentExecutionResults,
|
||||||
limit: 20,
|
limit: 20,
|
||||||
});
|
});
|
||||||
|
const tradeFunnel = buildTradeFunnelSummary(lifecycleRows);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
strategy_state: {
|
strategy_state: {
|
||||||
|
|
@ -1028,6 +1029,7 @@ function buildStrategySummary({
|
||||||
? recentDecisions
|
? recentDecisions
|
||||||
: [...durableDecisionsById.values()].slice(0, 20),
|
: [...durableDecisionsById.values()].slice(0, 20),
|
||||||
recent_lifecycle_rows: lifecycleRows,
|
recent_lifecycle_rows: lifecycleRows,
|
||||||
|
trade_funnel: tradeFunnel,
|
||||||
skipped_counts: strategyState.skipped_counts || {},
|
skipped_counts: strategyState.skipped_counts || {},
|
||||||
durable_control_state: strategyState.durable_control_state || null,
|
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 }) {
|
function buildSystemSummary({ servicesByName, activeAlerts, recentAlerts }) {
|
||||||
const historyWriterState = servicesByName['history-writer']?.state || {};
|
const historyWriterState = servicesByName['history-writer']?.state || {};
|
||||||
void activeAlerts;
|
void activeAlerts;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
export const SUBMISSION_COPY = {
|
export const SUBMISSION_COPY = {
|
||||||
statusTileLabel: 'Submissions',
|
statusTileLabel: 'Submissions',
|
||||||
statusTileSubtitle: 'Successful quote-response submissions from durable history',
|
statusTileSubtitle: 'Quote responses accepted by the relay; not completed trades',
|
||||||
statusTileValueSuffix: 'submissions',
|
statusTileValueSuffix: 'submissions',
|
||||||
lastStatusTileLabel: 'Last Submission',
|
lastStatusTileLabel: 'Last Submission',
|
||||||
recentMetricLabel: 'Recent submissions',
|
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 }) {
|
export default function StrategyPage({ strategy }) {
|
||||||
|
const funnel = strategy.strategy_state.trade_funnel || {};
|
||||||
|
const counts = funnel.counts || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className="panel">
|
<section className="panel">
|
||||||
|
|
@ -78,28 +121,44 @@ export default function StrategyPage({ strategy }) {
|
||||||
<div className="eyebrow">Trading state</div>
|
<div className="eyebrow">Trading state</div>
|
||||||
<h2>Strategy and executor</h2>
|
<h2>Strategy and executor</h2>
|
||||||
<div className="panel-subtitle">
|
<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>
|
||||||
</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="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="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 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)} />
|
<MetricCard label="Signer registered" meta={strategy.executor_state.account_id || ''} value={formatBoolean(strategy.executor_state.signer_registered)} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<section className="strategy-layout">
|
||||||
<div className="panel">
|
<div className="panel">
|
||||||
<div className="panel-head">
|
<div className="panel-head">
|
||||||
<div>
|
<div>
|
||||||
<div className="eyebrow">Lifecycle truth</div>
|
<div className="eyebrow">Why quotes are not trades</div>
|
||||||
<h3>Recent quote decisions and execution evidence</h3>
|
<h3>Recent quote outcomes and blockers</h3>
|
||||||
<div className="panel-subtitle">
|
<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>
|
</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.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_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.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/);
|
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', () => {
|
test('system service state ignores sentinel alert severity and keeps alert surfaces empty', () => {
|
||||||
const config = buildConfig();
|
const config = buildConfig();
|
||||||
const bootstrap = buildDashboardBootstrap({
|
const bootstrap = buildDashboardBootstrap({
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue