Fix quote lifecycle dashboard semantics
All checks were successful
deploy / deploy (push) Successful in 32s

Proof: Quote lifecycle truth and execution explanation is rendered from durable quote, decision, command, and execution evidence without treating submission as completion or realized asset movement.
Assumptions: Pushes to main trigger the repo-owned deployment workflow, and current durable records do not yet contain authoritative downstream venue outcomes for recent submissions.
Still fake: Completed and not-filled terminal trade outcomes remain unavailable unless explicit durable outcome evidence is stored; submitted rows therefore remain submission-only evidence.
This commit is contained in:
philipp 2026-04-09 18:02:34 +02:00
parent b695f60bc6
commit e35bb9ab8f
10 changed files with 721 additions and 163 deletions

View file

@ -32,10 +32,12 @@ import {
loadLatestPortfolioMetric,
loadRecentAlertTransitions,
loadRecentDepositStatuses,
loadRecentExecuteTradeCommands,
loadRecentExecutionResults,
loadRecentTradeDecisions,
loadRecentQuotes,
loadSuccessfulTradeSummary,
loadSuccessfulTradesPage,
loadSubmissionPage,
loadSubmissionSummary,
} from '../lib/postgres.mjs';
const config = loadConfig();
@ -79,10 +81,10 @@ const initialRecentQuotes = await safeSourceLoad(
}),
[],
);
const initialSuccessfulTradeSummary = await safeSourceLoad(
'successful_trade_summary',
() => loadSuccessfulTradeSummary(pool),
{ total: 0, last_successful_trade_at: null },
const initialSubmissionSummary = await safeSourceLoad(
'submission_summary',
() => loadSubmissionSummary(pool),
{ total: 0, last_submission_at: null },
);
const initialMarketPrice = await safeSourceLoad(
'latest_market_price',
@ -100,8 +102,8 @@ const liveState = createDashboardLiveState({
recentQuotes: initialRecentQuotes,
latestMarketPrice: initialMarketPrice,
latestInventory: initialInventory,
successfulTradeCount: initialSuccessfulTradeSummary.total,
lastSuccessfulTradeAt: initialSuccessfulTradeSummary.last_successful_trade_at,
recentSubmissionCount: initialSubmissionSummary.total,
lastSubmissionAt: initialSubmissionSummary.last_submission_at,
activeAlerts:
initialServiceSnapshots.find((snapshot) => snapshot.service === 'ops-sentinel')?.state?.active_alerts
|| [],
@ -287,16 +289,16 @@ async function handleApiRequest({ req, res, url, auth }) {
return sendJson(res, 200, payload);
}
if (req.method === 'GET' && url.pathname === '/api/trades') {
if (req.method === 'GET' && (url.pathname === '/api/submissions' || url.pathname === '/api/trades')) {
const page = Number(url.searchParams.get('page') || 1);
const pageSize = Number(
url.searchParams.get('page_size') || config.operatorDashboardTradePageSize,
);
const successfulTrades = await loadSuccessfulTradesPage(pool, {
const submissionPage = await loadSubmissionPage(pool, {
page,
pageSize,
});
return sendJson(res, 200, successfulTrades);
return sendJson(res, 200, submissionPage);
}
const controlMatch = req.method === 'POST'
@ -334,11 +336,13 @@ async function loadBootstrapPayload({ auth, page, pageSize }) {
inventorySnapshot,
marketPrice,
recentQuotes,
successfulTradeSummary,
successfulTrades,
submissionSummary,
submissionPage,
fundingObservations,
recentDepositStatuses,
recentTradeDecisions,
recentExecuteTradeCommands,
recentExecutionResults,
recentAlertTransitions,
serviceSnapshots,
] = await Promise.all([
@ -354,14 +358,14 @@ async function loadBootstrapPayload({ auth, page, pageSize }) {
sourceErrors,
),
safeSourceLoad(
'successful_trade_summary',
() => loadSuccessfulTradeSummary(pool),
{ total: 0, last_successful_trade_at: null },
'submission_summary',
() => loadSubmissionSummary(pool),
{ total: 0, last_submission_at: null },
sourceErrors,
),
safeSourceLoad(
'successful_trades',
() => loadSuccessfulTradesPage(pool, {
'submission_page',
() => loadSubmissionPage(pool, {
page,
pageSize,
}),
@ -387,6 +391,18 @@ async function loadBootstrapPayload({ auth, page, pageSize }) {
[],
sourceErrors,
),
safeSourceLoad(
'recent_execute_trade_commands',
() => loadRecentExecuteTradeCommands(pool, { limit: 40 }),
[],
sourceErrors,
),
safeSourceLoad(
'recent_execution_results',
() => loadRecentExecutionResults(pool, { limit: 40 }),
[],
sourceErrors,
),
safeSourceLoad(
'recent_alert_transitions',
() => loadRecentAlertTransitions(pool, { limit: 20 }),
@ -403,11 +419,13 @@ async function loadBootstrapPayload({ auth, page, pageSize }) {
inventorySnapshot,
marketPrice,
recentQuotes,
successfulTrades,
successfulTradeSummary,
submissionPage,
submissionSummary,
fundingObservations,
recentDepositStatuses,
recentTradeDecisions,
recentExecuteTradeCommands,
recentExecutionResults,
recentAlertTransitions,
serviceSnapshots,
sourceErrors,

View file

@ -227,8 +227,8 @@ export function createDashboardLiveState({
recentQuotes = [],
latestMarketPrice = null,
latestInventory = null,
successfulTradeCount = 0,
lastSuccessfulTradeAt = null,
recentSubmissionCount = 0,
lastSubmissionAt = null,
activeAlerts = [],
} = {}) {
const state = {
@ -239,8 +239,8 @@ export function createDashboardLiveState({
recent_quotes: recentQuotes.slice(0, config.operatorDashboardQuoteLimit || DASHBOARD_LIVE_QUOTE_LIMIT),
latest_market_price: latestMarketPrice?.payload || latestMarketPrice || null,
latest_inventory: latestInventory?.payload || latestInventory || null,
successful_trade_count: Number(successfulTradeCount || 0),
last_successful_trade_at: lastSuccessfulTradeAt || null,
recent_submission_count: Number(recentSubmissionCount || 0),
last_submission_at: lastSubmissionAt || null,
active_alerts: new Map(),
};
@ -306,8 +306,8 @@ export function applyDashboardLiveEvent(state, { topic, event }) {
}
case 'exec.trade_result':
if (event.payload.status !== 'submitted') return [];
state.successful_trade_count += 1;
state.last_successful_trade_at = event.observed_at || event.ingested_at || new Date().toISOString();
state.recent_submission_count += 1;
state.last_submission_at = event.observed_at || event.ingested_at || new Date().toISOString();
return [{
type: 'status_bar.updated',
status_bar: buildLiveStatusBar(state),
@ -324,11 +324,13 @@ export function buildDashboardBootstrap({
inventorySnapshot,
marketPrice,
recentQuotes,
successfulTrades,
successfulTradeSummary,
submissionPage,
submissionSummary,
fundingObservations,
recentDepositStatuses,
recentTradeDecisions,
recentExecuteTradeCommands,
recentExecutionResults,
recentAlertTransitions,
serviceSnapshots,
sourceErrors = [],
@ -346,7 +348,7 @@ export function buildDashboardBootstrap({
));
const profitability = buildProfitabilitySummary({
metric: portfolioMetric,
successfulTradeSummary,
submissionSummary,
});
const balances = buildBalanceSummary({
inventorySnapshot,
@ -359,9 +361,9 @@ export function buildDashboardBootstrap({
recentDepositStatuses,
liquidityState: servicesByName['liquidity-manager']?.state || {},
});
const tradesPage = normalizeSuccessfulTradesPage({
const normalizedSubmissionPage = normalizeSubmissionPage({
config,
successfulTrades,
submissionPage,
});
return {
@ -385,19 +387,22 @@ export function buildDashboardBootstrap({
config,
liquidityState: servicesByName['liquidity-manager']?.state || {},
}),
trade_asset_changes: buildTradeAssetChanges({
recent_submission_terms: buildRecentSubmissionTerms({
config,
trades: tradesPage.items,
submissions: normalizedSubmissionPage.items,
}),
recent_quotes: (recentQuotes || []).slice(0, config.operatorDashboardQuoteLimit || DASHBOARD_LIVE_QUOTE_LIMIT),
successful_trades: tradesPage,
submission_ledger: normalizedSubmissionPage,
controls: listDashboardControls({ page: 'funds' }),
caveats: profitability.caveats,
},
strategy: buildStrategySummary({
servicesByName,
activeAlerts,
recentQuotes,
recentTradeDecisions,
recentExecuteTradeCommands,
recentExecutionResults,
}),
system: buildSystemSummary({
servicesByName,
@ -407,7 +412,7 @@ export function buildDashboardBootstrap({
};
}
export function buildProfitabilitySummary({ metric, successfulTradeSummary } = {}) {
export function buildProfitabilitySummary({ metric, submissionSummary } = {}) {
const externalCashFlows = metric?.payload?.external_cash_flows || {};
const externalFlowCount = Number(externalCashFlows.flow_count || 0);
const externalFlowAdjusted = externalFlowCount > 0;
@ -428,8 +433,8 @@ export function buildProfitabilitySummary({ metric, successfulTradeSummary } = {
external_withdrawal_count: Number(externalCashFlows.withdrawal_count || 0),
latest_external_flow_at: externalCashFlows.latest_effective_at || null,
net_external_flow_value_eure: externalCashFlows.net_value_eure_at_flow_time || '0',
recent_trade_count: successfulTradeSummary?.total ?? metric?.payload?.result_count ?? 0,
last_successful_trade_at: successfulTradeSummary?.last_successful_trade_at || null,
recent_submission_count: submissionSummary?.total ?? 0,
last_submission_at: submissionSummary?.last_submission_at || null,
caveats: [
'Portfolio PnL is truthful to the current durable inventory and reference price path.',
'Fees and per-trade realized net settlement deltas are not fully tracked yet.',
@ -506,8 +511,8 @@ export function buildLiveStatusBar(state) {
}),
active_alert_count: state.active_alerts.size,
highest_alert_severity: highestAlertSeverity([...state.active_alerts.values()]),
recent_trade_count: state.successful_trade_count,
last_successful_trade_at: state.last_successful_trade_at,
recent_submission_count: state.recent_submission_count,
last_submission_at: state.last_submission_at,
};
}
@ -534,8 +539,8 @@ function buildStatusBar({
strategy_armed: servicesByName['strategy-engine']?.state?.armed ?? null,
executor_armed: servicesByName['trade-executor']?.state?.armed ?? null,
current_total_portfolio_value_eure: profitability.current_total_portfolio_value_eure,
recent_trade_count: profitability.recent_trade_count,
last_successful_trade_at: profitability.last_successful_trade_at,
recent_submission_count: profitability.recent_submission_count,
last_submission_at: profitability.last_submission_at,
};
}
@ -675,37 +680,316 @@ function buildRecentWithdrawals({ config, liquidityState }) {
});
}
function buildTradeAssetChanges({ config, trades }) {
return (trades || []).slice(0, 10).map((trade) => {
const assetIn = config.assetRegistry.get(trade.asset_in);
const assetOut = config.assetRegistry.get(trade.asset_out);
function buildRecentSubmissionTerms({ config, submissions }) {
return (submissions || []).slice(0, 10).map((submission) => {
const assetIn = config.assetRegistry.get(submission.asset_in);
const assetOut = config.assetRegistry.get(submission.asset_out);
return {
observed_at: trade.observed_at,
quote_id: trade.quote_id,
request_kind: trade.request_kind,
asset_in: trade.asset_in,
asset_in_symbol: assetIn?.symbol || trade.asset_in,
amount_in_units: trade.amount_in,
amount_in: formatUnits(trade.amount_in || '0', assetIn?.decimals || 0),
asset_out: trade.asset_out,
asset_out_symbol: assetOut?.symbol || trade.asset_out,
amount_out_units: trade.amount_out,
amount_out: formatUnits(trade.amount_out || '0', assetOut?.decimals || 0),
observed_at: submission.observed_at,
quote_id: submission.quote_id,
request_kind: submission.request_kind,
asset_in: submission.asset_in,
asset_in_symbol: assetIn?.symbol || submission.asset_in,
amount_in_units: submission.amount_in,
amount_in: formatUnits(submission.amount_in || '0', assetIn?.decimals || 0),
asset_out: submission.asset_out,
asset_out_symbol: assetOut?.symbol || submission.asset_out,
amount_out_units: submission.amount_out,
amount_out: formatUnits(submission.amount_out || '0', assetOut?.decimals || 0),
};
});
}
function normalizeSuccessfulTradesPage({ config, successfulTrades }) {
function normalizeSubmissionPage({ config, submissionPage }) {
return {
...successfulTrades,
items: (successfulTrades?.items || []).map((trade) => normalizeTradeForUi({
...submissionPage,
items: (submissionPage?.items || []).map((trade) => normalizeTradeForUi({
config,
trade,
})),
};
}
function buildStrategySummary({ servicesByName, activeAlerts, recentTradeDecisions = [] }) {
const LIFECYCLE_TONE_BY_STATE = {
observed: 'info',
evaluated: 'healthy',
command_emitted: 'info',
rejected: 'warning',
blocked: 'warning',
failed: 'critical',
submitted: 'healthy',
awaiting_outcome: 'info',
not_filled: 'warning',
completed: 'healthy',
};
const HUMAN_REASON_TEXT = {
actionable: 'Strategy approved the quote.',
below_edge_threshold: 'Below edge threshold.',
duplicate_quote_id: 'Duplicate quote id.',
executor_disarmed: 'Executor is disarmed.',
executor_paused: 'Executor intake is paused.',
inventory_unavailable: 'Inventory unavailable.',
pending_deposit_not_credited: 'Funding is not credited yet.',
quote_expired: 'Quote expired.',
quote_response_ack: 'Quote response acknowledged by the relay.',
quote_response_ok: 'Quote response accepted by the relay.',
reason_unknown: 'Reason not recorded.',
stale_reference_price: 'Reference price is stale.',
strategy_disarmed: 'Strategy is disarmed.',
submission_failed: 'Submission failed.',
unsupported_pair: 'Unsupported pair.',
};
const COMPLETED_OUTCOME_STATUSES = new Set(['completed', 'filled', 'settled', 'finalized']);
const NOT_FILLED_OUTCOME_STATUSES = new Set(['expired', 'not_filled', 'cancelled']);
export function deriveQuoteLifecycleRows({
recentQuotes = [],
recentTradeDecisions = [],
recentExecuteTradeCommands = [],
recentExecutionResults = [],
limit = 20,
} = {}) {
const rowsByKey = new Map();
for (const quote of recentQuotes || []) {
const row = ensureLifecycleRow(rowsByKey, quote?.quote_id || `quote:${quote?.observed_at || quote?.ingested_at || rowsByKey.size}`);
mergeLifecycleEvidence(row, {
quote_id: quote?.quote_id || null,
pair: quote?.pair || null,
request_kind: quote?.request_kind || null,
quote_observed_at: quote?.observed_at || quote?.ingested_at || null,
});
}
for (const decisionEntry of recentTradeDecisions || []) {
const decision = normalizeDecision({
...(decisionEntry?.payload || decisionEntry || {}),
decision_at:
decisionEntry?.payload?.decision_at
|| decisionEntry?.decision_at
|| decisionEntry?.observed_at
|| decisionEntry?.ingested_at
|| null,
});
if (!decision) continue;
const row = ensureLifecycleRow(rowsByKey, decision.quote_id || decision.decision_id || `decision:${decision.decision_at || rowsByKey.size}`);
mergeLifecycleEvidence(row, {
quote_id: decision.quote_id,
decision_id: decision.decision_id,
pair: decision.pair,
direction: decision.direction,
request_kind: decision.request_kind,
gross_edge_pct: decision.gross_edge_pct,
eure_notional: decision.eure_notional,
decision,
decision_at: decision.decision_at || null,
});
}
for (const commandEntry of recentExecuteTradeCommands || []) {
const command = normalizeCommand({
...(commandEntry?.payload || commandEntry || {}),
command_at:
commandEntry?.command_at
|| commandEntry?.observed_at
|| commandEntry?.ingested_at
|| null,
});
if (!command) continue;
const row = ensureLifecycleRow(rowsByKey, command.quote_id || command.decision_id || command.command_id || `command:${command.command_at || rowsByKey.size}`);
mergeLifecycleEvidence(row, {
quote_id: command.quote_id,
decision_id: command.decision_id,
command_id: command.command_id,
pair: command.pair,
direction: command.direction,
request_kind: command.request_kind,
command,
command_at: command.command_at || null,
});
}
for (const execution of recentExecutionResults || []) {
const row = ensureLifecycleRow(rowsByKey, execution?.quote_id || execution?.decision_id || execution?.command_id || `execution:${execution?.result_at || rowsByKey.size}`);
mergeLifecycleEvidence(row, {
quote_id: execution?.quote_id || null,
decision_id: execution?.decision_id || null,
command_id: execution?.command_id || null,
pair: execution?.pair || null,
execution,
execution_result_at: execution?.result_at || null,
});
}
return [...rowsByKey.values()]
.map((row) => finalizeLifecycleRow(row))
.sort((left, right) => sortTimestamps(right.latest_stage_at, left.latest_stage_at))
.slice(0, limit);
}
function ensureLifecycleRow(rowsByKey, key) {
if (!rowsByKey.has(key)) {
rowsByKey.set(key, {
quote_id: null,
decision_id: null,
command_id: null,
pair: null,
direction: null,
request_kind: null,
gross_edge_pct: null,
eure_notional: null,
quote_observed_at: null,
decision_at: null,
command_at: null,
execution_result_at: null,
decision: null,
command: null,
execution: null,
});
}
return rowsByKey.get(key);
}
function mergeLifecycleEvidence(row, next) {
for (const [key, value] of Object.entries(next || {})) {
if (value != null && row[key] == null) {
row[key] = value;
}
}
if (next?.decision) row.decision = next.decision;
if (next?.command) row.command = next.command;
if (next?.execution) row.execution = next.execution;
}
function finalizeLifecycleRow(row) {
const decision = row.decision || null;
const execution = row.execution || null;
const outcomeStatus = normalizeLifecycleToken(execution?.outcome_status || execution?.outcome_reason || null);
let lifecycle_state = 'observed';
let lifecycle_label = 'Observed';
let reason_code = 'reason_unknown';
let reason_text = 'Strategy evaluation not recorded yet.';
if (outcomeStatus && COMPLETED_OUTCOME_STATUSES.has(outcomeStatus)) {
lifecycle_state = 'completed';
lifecycle_label = 'Completed';
reason_code = normalizeLifecycleToken(execution?.outcome_reason || execution?.result_code || 'completed');
reason_text = humanizeReasonCode(reason_code, 'Completed');
} else if (outcomeStatus && NOT_FILLED_OUTCOME_STATUSES.has(outcomeStatus)) {
lifecycle_state = 'not_filled';
lifecycle_label = 'Not filled';
reason_code = normalizeLifecycleToken(execution?.outcome_reason || execution?.result_code || outcomeStatus);
reason_text = humanizeReasonCode(reason_code, 'Not filled');
} else if (execution?.status === 'submitted') {
lifecycle_state = 'submitted';
lifecycle_label = 'Submitted';
reason_code = normalizeLifecycleToken(execution?.result_code || 'awaiting_outcome');
reason_text = `${humanizeReasonCode(reason_code, 'Submitted to the relay.')} No durable venue outcome is recorded yet.`;
} else if (execution?.status === 'failed') {
lifecycle_state = 'failed';
lifecycle_label = 'Submission failed';
reason_code = normalizeLifecycleToken(execution?.result_code || 'submission_failed');
reason_text = buildExecutionFailureText(execution, reason_code);
} else if (execution?.status === 'rejected') {
lifecycle_state = 'blocked';
lifecycle_label = 'Blocked before submit';
reason_code = normalizeLifecycleToken(execution?.result_code || 'reason_unknown');
reason_text = buildExecutorBlockText(execution, reason_code);
} else if (row.command_id || row.command_at) {
lifecycle_state = 'command_emitted';
lifecycle_label = 'Awaiting executor';
reason_code = 'awaiting_executor';
reason_text = 'Execute command recorded, but no executor result is stored yet.';
} else if (decision?.decision === 'rejected') {
lifecycle_state = 'rejected';
lifecycle_label = 'Rejected by strategy';
reason_code = normalizeLifecycleToken(decision?.decision_reason || 'reason_unknown');
reason_text = humanizeReasonCode(reason_code, 'Strategy rejected the quote.');
} else if (decision?.decision) {
lifecycle_state = 'evaluated';
lifecycle_label = 'Approved by strategy';
reason_code = normalizeLifecycleToken(decision?.decision_reason || 'actionable');
reason_text = 'Strategy approved the quote, but no durable execute command is recorded yet.';
}
return {
...row,
lifecycle_state,
lifecycle_label,
lifecycle_tone: LIFECYCLE_TONE_BY_STATE[lifecycle_state] || 'unknown',
reason_code,
reason_text,
latest_stage_at:
row.execution_result_at
|| row.command_at
|| row.decision_at
|| row.quote_observed_at
|| null,
stage_details: {
quote_observed_at: row.quote_observed_at,
decision_at: row.decision_at,
command_at: row.command_at,
execution_result_at: row.execution_result_at,
strategy_decision: decision?.decision || null,
strategy_reason_code: decision?.decision_reason || null,
execution_status: execution?.status || null,
execution_result_code: execution?.result_code || null,
execution_outcome_status: execution?.outcome_status || null,
},
};
}
function buildExecutionFailureText(execution, reasonCode) {
const base = humanizeReasonCode(reasonCode, 'Submission failed.');
if (execution?.error_message) return `${base} ${execution.error_message}`;
if (execution?.note) return `${base} ${execution.note}`;
return base;
}
function buildExecutorBlockText(execution, reasonCode) {
if (execution?.note) return execution.note;
return humanizeReasonCode(reasonCode, 'Executor blocked the submission.');
}
function humanizeReasonCode(code, fallback = 'Reason not recorded.') {
const normalized = normalizeLifecycleToken(code || '');
if (!normalized) return fallback;
return HUMAN_REASON_TEXT[normalized] || normalized.replaceAll('_', ' ');
}
function normalizeLifecycleToken(value) {
return String(value || '')
.trim()
.toLowerCase()
.replace(/[\s-]+/g, '_');
}
function normalizeCommand(command) {
if (!command) return null;
return {
command_id: command.command_id || null,
decision_id: command.decision_id || null,
execution_key: command.execution_key || null,
quote_id: command.quote_id || null,
pair: command.pair || null,
direction: command.direction || null,
request_kind: command.request_kind || null,
asset_in: command.asset_in || null,
asset_out: command.asset_out || null,
command_at: command.command_at || command.observed_at || command.ingested_at || null,
};
}
function buildStrategySummary({
servicesByName,
activeAlerts,
recentQuotes = [],
recentTradeDecisions = [],
recentExecuteTradeCommands = [],
recentExecutionResults = [],
}) {
const strategyState = servicesByName['strategy-engine']?.state || {};
const executorState = servicesByName['trade-executor']?.state || {};
const durableDecisionsById = new Map(
@ -740,6 +1024,13 @@ function buildStrategySummary({ servicesByName, activeAlerts, recentTradeDecisio
|| durableDecisionsById.get(strategyState.latest_decision?.decision_id)?.decision_at
|| null,
});
const lifecycleRows = deriveQuoteLifecycleRows({
recentQuotes,
recentTradeDecisions,
recentExecuteTradeCommands,
recentExecutionResults,
limit: 20,
});
return {
strategy_state: {
@ -751,6 +1042,7 @@ function buildStrategySummary({ servicesByName, activeAlerts, recentTradeDecisio
recent_decisions: recentDecisions.length
? recentDecisions
: [...durableDecisionsById.values()].slice(0, 20),
recent_lifecycle_rows: lifecycleRows,
skipped_counts: strategyState.skipped_counts || {},
durable_control_state: strategyState.durable_control_state || null,
},

View file

@ -292,22 +292,22 @@ export async function loadRecentQuotes(pool, { limit = 10 } = {}) {
return result.rows.map(normalizeRecentQuoteRow);
}
export async function loadSuccessfulTradeSummary(pool) {
export async function loadSubmissionSummary(pool) {
const result = await pool.query(`
SELECT
COUNT(*)::INT AS total,
MAX(COALESCE(observed_at, ingested_at)) AS last_successful_trade_at
MAX(COALESCE(observed_at, ingested_at)) AS last_submission_at
FROM trade_execution_results
WHERE payload->>'status' = 'submitted'
`);
return {
total: Number(result.rows[0]?.total || 0),
last_successful_trade_at: toIsoTimestamp(result.rows[0]?.last_successful_trade_at),
last_submission_at: toIsoTimestamp(result.rows[0]?.last_submission_at),
};
}
export async function loadSuccessfulTradesPage(pool, { page = 1, pageSize = 20 } = {}) {
export async function loadSubmissionPage(pool, { page = 1, pageSize = 20 } = {}) {
const normalizedPage = Math.max(1, Number(page) || 1);
const normalizedPageSize = Math.max(1, Number(pageSize) || 20);
const offset = (normalizedPage - 1) * normalizedPageSize;
@ -347,10 +347,48 @@ export async function loadSuccessfulTradesPage(pool, { page = 1, pageSize = 20 }
page_size: normalizedPageSize,
total,
total_pages: total > 0 ? Math.ceil(total / normalizedPageSize) : 1,
items: rowsResult.rows.map(normalizeSuccessfulTradeRow),
items: rowsResult.rows.map(normalizeSubmissionRow),
};
}
export async function loadRecentExecutionResults(pool, { limit = 20 } = {}) {
const result = await pool.query(
`
SELECT
r.observed_at AS result_observed_at,
r.ingested_at AS result_ingested_at,
r.payload AS result_payload,
c.ingested_at AS command_ingested_at,
c.payload AS command_payload,
d.payload AS decision_payload
FROM trade_execution_results r
LEFT JOIN execute_trade_commands c
ON c.decision_key = r.decision_key
LEFT JOIN trade_decisions d
ON d.decision_key = COALESCE(c.payload->>'decision_id', r.payload->>'decision_id')
ORDER BY COALESCE(r.observed_at, r.ingested_at) DESC
LIMIT $1
`,
[limit],
);
return result.rows.map((row) => normalizeExecutionResultRow(row));
}
export async function loadRecentExecuteTradeCommands(pool, { limit = 20 } = {}) {
const result = await pool.query(
`
SELECT observed_at, ingested_at, payload
FROM execute_trade_commands
ORDER BY COALESCE(observed_at, ingested_at) DESC
LIMIT $1
`,
[limit],
);
return result.rows.map((row) => normalizeExecuteTradeCommandRow(row));
}
export async function loadCurrentFundingObservations(pool) {
const result = await pool.query(`
SELECT DISTINCT ON (decision_key)
@ -640,7 +678,7 @@ function normalizeRecentQuoteRow(row) {
};
}
function normalizeSuccessfulTradeRow(row) {
function normalizeSubmissionRow(row) {
const resultPayload = row.result_payload || {};
const commandPayload = row.command_payload || {};
const decisionPayload = row.decision_payload || {};
@ -672,6 +710,60 @@ function normalizeSuccessfulTradeRow(row) {
};
}
function normalizeExecuteTradeCommandRow(row) {
const payload = row.payload || {};
return {
command_id: payload.command_id || null,
decision_id: payload.decision_id || null,
execution_key: payload.execution_key || null,
quote_id: payload.quote_id || null,
pair: payload.pair || null,
direction: payload.direction || null,
request_kind: payload.request_kind || null,
asset_in: payload.asset_in || null,
asset_out: payload.asset_out || null,
amount_in: resolveTradeAmount(payload, 'amount_in'),
amount_out: resolveTradeAmount(payload, 'amount_out'),
observed_at: toIsoTimestamp(row.observed_at || row.ingested_at),
ingested_at: toIsoTimestamp(row.ingested_at),
};
}
function normalizeExecutionResultRow(row) {
const resultPayload = row.result_payload || {};
const commandPayload = row.command_payload || {};
const decisionPayload = row.decision_payload || {};
return {
command_id: resultPayload.command_id || commandPayload.command_id || null,
decision_id:
commandPayload.decision_id
|| resultPayload.decision_id
|| decisionPayload.decision_id
|| null,
execution_key: resultPayload.execution_key || commandPayload.execution_key || null,
quote_id: resultPayload.quote_id || commandPayload.quote_id || decisionPayload.quote_id || null,
pair: resultPayload.pair || commandPayload.pair || decisionPayload.pair || null,
command_at: toIsoTimestamp(row.command_ingested_at),
result_at: toIsoTimestamp(row.result_observed_at || row.result_ingested_at),
status: resultPayload.status || null,
result_code: resultPayload.result_code || null,
outcome_status:
resultPayload.outcome_status
|| resultPayload.venue_outcome_status
|| resultPayload.trade_outcome_status
|| null,
outcome_reason:
resultPayload.outcome_reason
|| resultPayload.venue_outcome_reason
|| resultPayload.trade_outcome_reason
|| null,
venue_response: resultPayload.venue_response || null,
error_message: resultPayload.error?.message || null,
note: resultPayload.note || null,
};
}
function resolveTradeAmount(commandPayload, field) {
const quoteOutputField = commandPayload?.quote_output?.[field];
const proposedField = commandPayload?.[`proposed_${field}`];

View file

@ -37,12 +37,12 @@ export default function App() {
async function loadTradesPage(page) {
if (!Number.isFinite(page) || page < 1) return;
dispatch({ type: 'notice.changed', notice: 'Loading trade history page...' });
dispatch({ type: 'notice.changed', notice: 'Loading submission history page...' });
dispatch({ type: 'error.changed', error: null });
try {
const successfulTrades = await fetchJson(`/api/trades?page=${page}&page_size=${TRADE_PAGE_SIZE}`);
dispatch({ type: 'trades.loaded', successfulTrades });
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 });
@ -67,7 +67,7 @@ export default function App() {
dispatch({ type: 'notice.changed', notice: `${action} completed` });
if (reload) {
const page = state.dashboard?.funds?.successful_trades?.page || 1;
const page = state.dashboard?.funds?.submission_ledger?.page || 1;
await loadBootstrap(page);
}
} catch (error) {

View file

@ -26,8 +26,8 @@ export default function StatusBar({ status, websocketState }) {
['Alerts', `${status.active_alert_count || 0} ${status.highest_alert_severity ? `(${status.highest_alert_severity})` : ''}`.trim()],
['Strategy Armed', formatBoolean(status.strategy_armed)],
['Executor Armed', formatBoolean(status.executor_armed)],
[SUBMISSION_COPY.statusTileLabel, `${status.recent_trade_count || 0} ${SUBMISSION_COPY.statusTileValueSuffix}`],
[SUBMISSION_COPY.lastStatusTileLabel, formatTimestamp(status.last_successful_trade_at)],
[SUBMISSION_COPY.statusTileLabel, `${status.recent_submission_count || 0} ${SUBMISSION_COPY.statusTileValueSuffix}`],
[SUBMISSION_COPY.lastStatusTileLabel, formatTimestamp(status.last_submission_at)],
];
return (

View file

@ -370,7 +370,7 @@ export default function FundsPage({
lastControlResult,
}) {
const profitability = funds.profitability;
const trades = funds.successful_trades;
const submissionLedger = funds.submission_ledger;
const controlState = funds.funding.control_state || {};
const externalFlowAdjusted = profitability.external_flow_adjusted;
const externalFlowCount = profitability.external_flow_count || 0;
@ -448,8 +448,8 @@ export default function FundsPage({
/>
<MetricCard
label={SUBMISSION_COPY.recentMetricLabel}
meta={formatTimestamp(profitability.last_successful_trade_at)}
value={`${profitability.recent_trade_count || 0} ${SUBMISSION_COPY.recentMetricValueSuffix}`}
meta={formatTimestamp(profitability.last_submission_at)}
value={`${profitability.recent_submission_count || 0} ${SUBMISSION_COPY.recentMetricValueSuffix}`}
/>
</div>
<div className="panel-subtitle">
@ -573,7 +573,7 @@ export default function FundsPage({
<h3>{SUBMISSION_COPY.termsTitle}</h3>
</div>
</div>
<AssetChangeTable items={funds.trade_asset_changes} />
<AssetChangeTable items={funds.recent_submission_terms} />
</div>
</section>
@ -585,22 +585,22 @@ export default function FundsPage({
</div>
<div className="status-subtle">{SUBMISSION_COPY.ledgerSubtitle}</div>
</div>
<TradesTable items={trades.items} />
<TradesTable items={submissionLedger.items} />
<div className="pagination">
<div className="status-subtle">{SUBMISSION_COPY.ledgerCountLabel(trades.page, trades.total_pages, trades.total)}</div>
<div className="status-subtle">{SUBMISSION_COPY.ledgerCountLabel(submissionLedger.page, submissionLedger.total_pages, submissionLedger.total)}</div>
<div className="button-row">
<button
className="button secondary"
disabled={trades.page <= 1}
onClick={() => onTradesPageChange(trades.page - 1)}
disabled={submissionLedger.page <= 1}
onClick={() => onTradesPageChange(submissionLedger.page - 1)}
type="button"
>
Previous
</button>
<button
className="button secondary"
disabled={trades.page >= trades.total_pages}
onClick={() => onTradesPageChange(trades.page + 1)}
disabled={submissionLedger.page >= submissionLedger.total_pages}
onClick={() => onTradesPageChange(submissionLedger.page + 1)}
type="button"
>
Next

View file

@ -5,57 +5,65 @@ import Pill from '../components/Pill.jsx';
import TableFrame from '../components/TableFrame.jsx';
import { formatBoolean, formatTimestamp, truncateMiddle } from '../lib/format.js';
function describeStrategyDecision(decision) {
if (decision === 'actionable') {
return {
label: 'Actionable',
stateLabel: 'healthy',
};
async function copyIdentifier(value) {
if (!value || !navigator?.clipboard?.writeText) return;
try {
await navigator.clipboard.writeText(value);
} catch {
// Best-effort copy affordance; keep the full identifier visible regardless.
}
}
if (decision === 'rejected') {
return {
label: 'Rejected by strategy',
stateLabel: 'warning',
};
function IdentifierRow({ label, value }) {
if (!value) return <div className="status-subtle">{`${label}: unavailable`}</div>;
return (
<div className="trace-row">
<span className="status-subtle">{`${label}:`}</span>
<span className="mono trace-id">{value}</span>
<button className="button secondary trace-copy-button" onClick={() => copyIdentifier(value)} type="button">
Copy
</button>
</div>
);
}
return {
label: decision || 'Unknown',
stateLabel: decision || 'unknown',
};
}
function DecisionsTable({ items }) {
if (!items?.length) return <EmptyState>No strategy decisions have been observed yet.</EmptyState>;
function LifecycleTable({ items }) {
if (!items?.length) return <EmptyState>No quote lifecycle evidence has been observed yet.</EmptyState>;
return (
<TableFrame>
<table className="decision-table">
<table className="decision-table lifecycle-table">
<thead>
<tr>
<th>At</th>
<th>Strategy verdict</th>
<th>Pair</th>
<th>Lifecycle</th>
<th>Reason</th>
<th>Pair</th>
<th>Trace</th>
<th>Edge %</th>
<th>Notional</th>
</tr>
</thead>
<tbody>
{items.map((item, index) => {
const verdict = describeStrategyDecision(item.decision);
return (
<tr key={`${item.decision_id || item.decision_at || index}`}>
<td>{formatTimestamp(item.decision_at)}</td>
<td><Pill label={verdict.label} stateLabel={verdict.stateLabel} /></td>
{items.map((item, index) => (
<tr key={`${item.quote_id || item.decision_id || item.command_id || item.latest_stage_at || index}`}>
<td>{formatTimestamp(item.latest_stage_at)}</td>
<td><Pill label={item.lifecycle_label} stateLabel={item.lifecycle_tone} /></td>
<td>
<div>{item.reason_text}</div>
<div className="status-subtle mono">{item.reason_code || 'reason_unknown'}</div>
</td>
<td className="mono truncate-cell" title={item.pair || ''}>{truncateMiddle(item.pair || '', 32)}</td>
<td>{item.decision_reason || ''}</td>
<td>
<IdentifierRow label="Quote" value={item.quote_id} />
<IdentifierRow label="Decision" value={item.decision_id} />
<IdentifierRow label="Command" value={item.command_id} />
</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>{item.eure_notional || ''}</td>
</tr>
);
})}
))}
</tbody>
</table>
</TableFrame>
@ -71,7 +79,7 @@ export default function StrategyPage({ strategy }) {
<div className="eyebrow">Trading state</div>
<h2>Strategy and executor</h2>
<div className="panel-subtitle">
This page shows whether the system is actively deciding and submitting without offering risky arming controls.
This page shows the strongest durable claim for each recent quote, from strategy evaluation through executor submission evidence.
</div>
</div>
</div>
@ -80,26 +88,26 @@ export default function StrategyPage({ strategy }) {
<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={`Completed ${strategy.executor_state.completed_count || 0}`} value={String(strategy.executor_state.in_flight_count || 0)} />
<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="stack-grid">
<section className="strategy-layout">
<div className="panel">
<div className="panel-head">
<div>
<div className="eyebrow">Decision flow</div>
<h3>Recent decisions and skip reasons</h3>
<div className="eyebrow">Lifecycle truth</div>
<h3>Recent quote decisions and execution evidence</h3>
<div className="panel-subtitle">
Rejected by strategy means the strategy engine did not emit a trade command. Venue or executor rejections appear elsewhere.
Strategy rejection, executor blocking, submission failure, and submission success are separated. Submission never implies trade completion or realized asset movement.
</div>
</div>
</div>
<DecisionsTable items={strategy.strategy_state.recent_decisions} />
<LifecycleTable items={strategy.strategy_state.recent_lifecycle_rows} />
</div>
<div className="panel">
<div className="panel strategy-side-panel">
<div className="panel-head">
<div>
<div className="eyebrow">Guard rails</div>

View file

@ -41,12 +41,12 @@ function applySocketMessage(dashboard, payload, session) {
...dashboard.funds,
profitability: {
...dashboard.funds.profitability,
recent_trade_count:
payload.status_bar.recent_trade_count
?? dashboard.funds.profitability.recent_trade_count,
last_successful_trade_at:
payload.status_bar.last_successful_trade_at
|| dashboard.funds.profitability.last_successful_trade_at,
recent_submission_count:
payload.status_bar.recent_submission_count
?? dashboard.funds.profitability.recent_submission_count,
last_submission_at:
payload.status_bar.last_submission_at
|| dashboard.funds.profitability.last_submission_at,
},
},
},
@ -91,14 +91,14 @@ export function dashboardReducer(state, action) {
dashboard: action.dashboard,
page: state.page || action.dashboard.default_page || 'funds',
};
case 'trades.loaded':
case 'submissionLedger.loaded':
return {
...state,
dashboard: {
...state.dashboard,
funds: {
...state.dashboard.funds,
successful_trades: action.successfulTrades,
submission_ledger: action.submissionLedger,
},
},
};

View file

@ -254,6 +254,17 @@ select {
grid-template-columns: 1fr;
}
.strategy-layout {
display: grid;
gap: 18px;
grid-template-columns: minmax(0, 1.75fr) minmax(320px, 0.85fr);
align-items: start;
}
.strategy-side-panel {
height: 100%;
}
.table-wrap {
overflow-x: auto;
border: 1px solid var(--line);
@ -322,12 +333,18 @@ pre {
table.decision-table td:nth-child(2),
table.decision-table th:nth-child(2) {
width: 34%;
width: 180px;
white-space: nowrap;
}
table.decision-table td:nth-child(3),
table.decision-table th:nth-child(3) {
width: 36%;
width: 28%;
}
table.lifecycle-table td:nth-child(5),
table.lifecycle-table th:nth-child(5) {
width: 34%;
}
.pills,
@ -388,6 +405,29 @@ table.decision-table th:nth-child(3) {
margin-top: 12px;
}
.trace-row {
display: flex;
align-items: flex-start;
gap: 8px;
margin-bottom: 6px;
}
.trace-row:last-child {
margin-bottom: 0;
}
.trace-id {
flex: 1;
min-width: 0;
overflow-wrap: anywhere;
}
.trace-copy-button {
padding: 4px 8px;
font-size: 0.75rem;
line-height: 1.2;
}
.form-grid {
display: grid;
gap: 12px;
@ -465,7 +505,8 @@ table.decision-table th:nth-child(3) {
@media (max-width: 1100px) {
.app-grid,
.split {
.split,
.strategy-layout {
grid-template-columns: 1fr;
}

View file

@ -6,6 +6,7 @@ import {
buildDashboardBootstrap,
buildProfitabilitySummary,
createDashboardLiveState,
deriveQuoteLifecycleRows,
resolveDashboardControl,
} from '../src/core/operator-dashboard.mjs';
import {
@ -52,9 +53,9 @@ test('profitability summary separates baseline, hold, market move, and trading c
baseline_portfolio_value_eure_at_current_price: '105',
},
},
successfulTradeSummary: {
submissionSummary: {
total: 4,
last_successful_trade_at: '2026-04-04T09:00:00.000Z',
last_submission_at: '2026-04-04T09:00:00.000Z',
},
});
@ -63,8 +64,8 @@ test('profitability summary separates baseline, hold, market move, and trading c
assert.equal(summary.market_move_contribution_eure, '5');
assert.equal(summary.trading_contribution_eure, '5');
assert.equal(summary.computed_at, '2026-04-04T09:05:00.000Z');
assert.equal(summary.recent_trade_count, 4);
assert.equal(summary.last_successful_trade_at, '2026-04-04T09:00:00.000Z');
assert.equal(summary.recent_submission_count, 4);
assert.equal(summary.last_submission_at, '2026-04-04T09:00:00.000Z');
});
test('profitability summary flags cash-flow-adjusted benchmarks after later funding changes', () => {
@ -86,9 +87,9 @@ test('profitability summary flags cash-flow-adjusted benchmarks after later fund
},
},
},
successfulTradeSummary: {
submissionSummary: {
total: 7,
last_successful_trade_at: '2026-04-02T20:17:44.768Z',
last_submission_at: '2026-04-02T20:17:44.768Z',
},
});
@ -154,12 +155,12 @@ test('basic auth resolves operator identity and reuses a session cookie', () =>
assert.equal(second.via, 'session_cookie');
});
test('live quote updates stay capped at ten items and successful trades update live counters', () => {
test('live quote updates stay capped at ten items and submitted results update live counters', () => {
const config = buildConfig();
const state = createDashboardLiveState({
config,
successfulTradeCount: 2,
lastSuccessfulTradeAt: '2026-04-04T08:00:00.000Z',
recentSubmissionCount: 2,
lastSubmissionAt: '2026-04-04T08:00:00.000Z',
});
for (let index = 0; index < 11; index += 1) {
@ -195,11 +196,111 @@ test('live quote updates stay capped at ten items and successful trades update l
assert.equal(state.recent_quotes.length, 10);
assert.equal(state.recent_quotes[0].quote_id, 'quote-10');
assert.equal(state.recent_quotes.at(-1).quote_id, 'quote-1');
assert.equal(state.successful_trade_count, 3);
assert.equal(state.last_successful_trade_at, '2026-04-04T08:30:00.000Z');
assert.equal(state.recent_submission_count, 3);
assert.equal(state.last_submission_at, '2026-04-04T08:30:00.000Z');
assert.equal(updates[0].type, 'status_bar.updated');
});
test('lifecycle derivation keeps executor blocking distinct from strategy rejection', () => {
const rows = deriveQuoteLifecycleRows({
recentQuotes: [{
quote_id: 'quote-1',
pair: 'btc->eure',
request_kind: 'exact_in',
observed_at: '2026-04-09T09:00:00.000Z',
}],
recentTradeDecisions: [{
observed_at: '2026-04-09T09:00:01.000Z',
payload: {
decision_id: 'decision-1',
quote_id: 'quote-1',
pair: 'btc->eure',
decision: 'actionable',
decision_reason: 'actionable',
gross_edge_pct: '1.2',
},
}],
recentExecuteTradeCommands: [{
observed_at: '2026-04-09T09:00:02.000Z',
command_id: 'command-1',
decision_id: 'decision-1',
quote_id: 'quote-1',
pair: 'btc->eure',
}],
recentExecutionResults: [{
command_id: 'command-1',
decision_id: 'decision-1',
quote_id: 'quote-1',
pair: 'btc->eure',
result_at: '2026-04-09T09:00:03.000Z',
status: 'rejected',
result_code: 'executor_disarmed',
note: 'executor is disarmed',
}],
});
assert.equal(rows[0].lifecycle_state, 'blocked');
assert.equal(rows[0].lifecycle_label, 'Blocked before submit');
assert.equal(rows[0].reason_code, 'executor_disarmed');
assert.notEqual(rows[0].lifecycle_state, 'rejected');
});
test('lifecycle derivation never upgrades submitted evidence into completion or asset movement', () => {
const rows = deriveQuoteLifecycleRows({
recentTradeDecisions: [{
observed_at: '2026-04-09T09:10:01.000Z',
payload: {
decision_id: 'decision-2',
quote_id: 'quote-2',
pair: 'btc->eure',
decision: 'actionable',
decision_reason: 'actionable',
},
}],
recentExecuteTradeCommands: [{
observed_at: '2026-04-09T09:10:02.000Z',
command_id: 'command-2',
decision_id: 'decision-2',
quote_id: 'quote-2',
pair: 'btc->eure',
}],
recentExecutionResults: [{
command_id: 'command-2',
decision_id: 'decision-2',
quote_id: 'quote-2',
pair: 'btc->eure',
result_at: '2026-04-09T09:10:03.000Z',
status: 'submitted',
result_code: 'quote_response_ok',
}],
});
assert.equal(rows[0].lifecycle_state, 'submitted');
assert.equal(rows[0].lifecycle_label, 'Submitted');
assert.match(rows[0].reason_text, /no durable venue outcome/i);
assert.notEqual(rows[0].lifecycle_state, 'completed');
assert.doesNotMatch(`${rows[0].lifecycle_label} ${rows[0].reason_text}`.toLowerCase(), /asset delta|realized/);
});
test('strategy approval no longer renders the forbidden actionable label', () => {
const rows = deriveQuoteLifecycleRows({
recentTradeDecisions: [{
observed_at: '2026-04-09T09:20:01.000Z',
payload: {
decision_id: 'decision-3',
quote_id: 'quote-3',
pair: 'btc->eure',
decision: 'actionable',
decision_reason: 'actionable',
},
}],
});
assert.equal(rows[0].lifecycle_state, 'evaluated');
assert.equal(rows[0].lifecycle_label, 'Approved by strategy');
assert.notEqual(rows[0].lifecycle_label, 'Actionable');
});
test('bootstrap aggregation keeps Funds as default and carries live control state', () => {
const config = buildConfig();
const bootstrap = buildDashboardBootstrap({
@ -247,16 +348,16 @@ test('bootstrap aggregation keeps Funds as default and carries live control stat
},
},
recentQuotes: [],
successfulTrades: {
submissionPage: {
page: 1,
page_size: 20,
total: 0,
total_pages: 1,
items: [],
},
successfulTradeSummary: {
submissionSummary: {
total: 1,
last_successful_trade_at: '2026-04-04T09:30:00.000Z',
last_submission_at: '2026-04-04T09:30:00.000Z',
},
fundingObservations: [
{
@ -381,12 +482,18 @@ test('bootstrap aggregation keeps Funds as default and carries live control stat
assert.equal(bootstrap.default_page, 'funds');
assert.equal(bootstrap.funds.profitability.computed_at, '2026-04-04T09:05:00.000Z');
assert.equal(bootstrap.funds.profitability.last_submission_at, '2026-04-04T09:30:00.000Z');
assert.equal(bootstrap.funds.funding.control_state.withdrawals_frozen, true);
assert.equal(bootstrap.funds.funding.handles[0].address, 'btc-address');
assert.deepEqual(bootstrap.funds.recent_submission_terms, []);
assert.deepEqual(bootstrap.funds.submission_ledger.items, []);
assert.equal(bootstrap.status_bar.strategy_armed, true);
assert.equal(bootstrap.status_bar.executor_armed, true);
assert.equal(bootstrap.status_bar.recent_submission_count, 1);
assert.equal(bootstrap.strategy.strategy_state.recent_decisions[0].decision_at, '2026-04-04T09:10:00.000Z');
assert.equal(bootstrap.strategy.strategy_state.recent_decisions[0].decision_reason, 'strategy_disarmed');
assert.equal(bootstrap.strategy.strategy_state.recent_lifecycle_rows[0].lifecycle_state, 'rejected');
assert.equal(bootstrap.strategy.strategy_state.recent_lifecycle_rows[0].reason_code, 'strategy_disarmed');
});
test('system service health uses sentinel-derived severity so stale ingest is never shown healthy', () => {
@ -403,16 +510,16 @@ test('system service health uses sentinel-derived severity so stale ingest is ne
inventorySnapshot: null,
marketPrice: null,
recentQuotes: [],
successfulTrades: {
submissionPage: {
page: 1,
page_size: 20,
total: 0,
total_pages: 1,
items: [],
},
successfulTradeSummary: {
submissionSummary: {
total: 0,
last_successful_trade_at: null,
last_submission_at: null,
},
fundingObservations: [],
recentTradeDecisions: [],
@ -496,16 +603,16 @@ test('ingest disconnected still renders as a critical transport failure', () =>
inventorySnapshot: null,
marketPrice: null,
recentQuotes: [],
successfulTrades: {
submissionPage: {
page: 1,
page_size: 20,
total: 0,
total_pages: 1,
items: [],
},
successfulTradeSummary: {
submissionSummary: {
total: 0,
last_successful_trade_at: null,
last_submission_at: null,
},
fundingObservations: [],
recentTradeDecisions: [],
@ -583,16 +690,16 @@ test('recent alert history collapses repeated flapping transitions into one read
inventorySnapshot: null,
marketPrice: null,
recentQuotes: [],
successfulTrades: {
submissionPage: {
page: 1,
page_size: 20,
total: 0,
total_pages: 1,
items: [],
},
successfulTradeSummary: {
submissionSummary: {
total: 0,
last_successful_trade_at: null,
last_submission_at: null,
},
fundingObservations: [],
recentTradeDecisions: [],
@ -674,16 +781,16 @@ test('funding summary includes credited bridge deposits without observer-backed
inventorySnapshot: null,
marketPrice: null,
recentQuotes: [],
successfulTrades: {
submissionPage: {
page: 1,
page_size: 20,
total: 0,
total_pages: 1,
items: [],
},
successfulTradeSummary: {
submissionSummary: {
total: 0,
last_successful_trade_at: null,
last_submission_at: null,
},
fundingObservations: [],
recentDepositStatuses: [