Fix quote lifecycle dashboard semantics
All checks were successful
deploy / deploy (push) Successful in 32s
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:
parent
b695f60bc6
commit
e35bb9ab8f
10 changed files with 721 additions and 163 deletions
|
|
@ -32,10 +32,12 @@ import {
|
||||||
loadLatestPortfolioMetric,
|
loadLatestPortfolioMetric,
|
||||||
loadRecentAlertTransitions,
|
loadRecentAlertTransitions,
|
||||||
loadRecentDepositStatuses,
|
loadRecentDepositStatuses,
|
||||||
|
loadRecentExecuteTradeCommands,
|
||||||
|
loadRecentExecutionResults,
|
||||||
loadRecentTradeDecisions,
|
loadRecentTradeDecisions,
|
||||||
loadRecentQuotes,
|
loadRecentQuotes,
|
||||||
loadSuccessfulTradeSummary,
|
loadSubmissionPage,
|
||||||
loadSuccessfulTradesPage,
|
loadSubmissionSummary,
|
||||||
} from '../lib/postgres.mjs';
|
} from '../lib/postgres.mjs';
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
@ -79,10 +81,10 @@ const initialRecentQuotes = await safeSourceLoad(
|
||||||
}),
|
}),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
const initialSuccessfulTradeSummary = await safeSourceLoad(
|
const initialSubmissionSummary = await safeSourceLoad(
|
||||||
'successful_trade_summary',
|
'submission_summary',
|
||||||
() => loadSuccessfulTradeSummary(pool),
|
() => loadSubmissionSummary(pool),
|
||||||
{ total: 0, last_successful_trade_at: null },
|
{ total: 0, last_submission_at: null },
|
||||||
);
|
);
|
||||||
const initialMarketPrice = await safeSourceLoad(
|
const initialMarketPrice = await safeSourceLoad(
|
||||||
'latest_market_price',
|
'latest_market_price',
|
||||||
|
|
@ -100,8 +102,8 @@ const liveState = createDashboardLiveState({
|
||||||
recentQuotes: initialRecentQuotes,
|
recentQuotes: initialRecentQuotes,
|
||||||
latestMarketPrice: initialMarketPrice,
|
latestMarketPrice: initialMarketPrice,
|
||||||
latestInventory: initialInventory,
|
latestInventory: initialInventory,
|
||||||
successfulTradeCount: initialSuccessfulTradeSummary.total,
|
recentSubmissionCount: initialSubmissionSummary.total,
|
||||||
lastSuccessfulTradeAt: initialSuccessfulTradeSummary.last_successful_trade_at,
|
lastSubmissionAt: initialSubmissionSummary.last_submission_at,
|
||||||
activeAlerts:
|
activeAlerts:
|
||||||
initialServiceSnapshots.find((snapshot) => snapshot.service === 'ops-sentinel')?.state?.active_alerts
|
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);
|
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 page = Number(url.searchParams.get('page') || 1);
|
||||||
const pageSize = Number(
|
const pageSize = Number(
|
||||||
url.searchParams.get('page_size') || config.operatorDashboardTradePageSize,
|
url.searchParams.get('page_size') || config.operatorDashboardTradePageSize,
|
||||||
);
|
);
|
||||||
const successfulTrades = await loadSuccessfulTradesPage(pool, {
|
const submissionPage = await loadSubmissionPage(pool, {
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
});
|
});
|
||||||
return sendJson(res, 200, successfulTrades);
|
return sendJson(res, 200, submissionPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
const controlMatch = req.method === 'POST'
|
const controlMatch = req.method === 'POST'
|
||||||
|
|
@ -334,11 +336,13 @@ async function loadBootstrapPayload({ auth, page, pageSize }) {
|
||||||
inventorySnapshot,
|
inventorySnapshot,
|
||||||
marketPrice,
|
marketPrice,
|
||||||
recentQuotes,
|
recentQuotes,
|
||||||
successfulTradeSummary,
|
submissionSummary,
|
||||||
successfulTrades,
|
submissionPage,
|
||||||
fundingObservations,
|
fundingObservations,
|
||||||
recentDepositStatuses,
|
recentDepositStatuses,
|
||||||
recentTradeDecisions,
|
recentTradeDecisions,
|
||||||
|
recentExecuteTradeCommands,
|
||||||
|
recentExecutionResults,
|
||||||
recentAlertTransitions,
|
recentAlertTransitions,
|
||||||
serviceSnapshots,
|
serviceSnapshots,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
|
|
@ -354,14 +358,14 @@ async function loadBootstrapPayload({ auth, page, pageSize }) {
|
||||||
sourceErrors,
|
sourceErrors,
|
||||||
),
|
),
|
||||||
safeSourceLoad(
|
safeSourceLoad(
|
||||||
'successful_trade_summary',
|
'submission_summary',
|
||||||
() => loadSuccessfulTradeSummary(pool),
|
() => loadSubmissionSummary(pool),
|
||||||
{ total: 0, last_successful_trade_at: null },
|
{ total: 0, last_submission_at: null },
|
||||||
sourceErrors,
|
sourceErrors,
|
||||||
),
|
),
|
||||||
safeSourceLoad(
|
safeSourceLoad(
|
||||||
'successful_trades',
|
'submission_page',
|
||||||
() => loadSuccessfulTradesPage(pool, {
|
() => loadSubmissionPage(pool, {
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
}),
|
}),
|
||||||
|
|
@ -387,6 +391,18 @@ async function loadBootstrapPayload({ auth, page, pageSize }) {
|
||||||
[],
|
[],
|
||||||
sourceErrors,
|
sourceErrors,
|
||||||
),
|
),
|
||||||
|
safeSourceLoad(
|
||||||
|
'recent_execute_trade_commands',
|
||||||
|
() => loadRecentExecuteTradeCommands(pool, { limit: 40 }),
|
||||||
|
[],
|
||||||
|
sourceErrors,
|
||||||
|
),
|
||||||
|
safeSourceLoad(
|
||||||
|
'recent_execution_results',
|
||||||
|
() => loadRecentExecutionResults(pool, { limit: 40 }),
|
||||||
|
[],
|
||||||
|
sourceErrors,
|
||||||
|
),
|
||||||
safeSourceLoad(
|
safeSourceLoad(
|
||||||
'recent_alert_transitions',
|
'recent_alert_transitions',
|
||||||
() => loadRecentAlertTransitions(pool, { limit: 20 }),
|
() => loadRecentAlertTransitions(pool, { limit: 20 }),
|
||||||
|
|
@ -403,11 +419,13 @@ async function loadBootstrapPayload({ auth, page, pageSize }) {
|
||||||
inventorySnapshot,
|
inventorySnapshot,
|
||||||
marketPrice,
|
marketPrice,
|
||||||
recentQuotes,
|
recentQuotes,
|
||||||
successfulTrades,
|
submissionPage,
|
||||||
successfulTradeSummary,
|
submissionSummary,
|
||||||
fundingObservations,
|
fundingObservations,
|
||||||
recentDepositStatuses,
|
recentDepositStatuses,
|
||||||
recentTradeDecisions,
|
recentTradeDecisions,
|
||||||
|
recentExecuteTradeCommands,
|
||||||
|
recentExecutionResults,
|
||||||
recentAlertTransitions,
|
recentAlertTransitions,
|
||||||
serviceSnapshots,
|
serviceSnapshots,
|
||||||
sourceErrors,
|
sourceErrors,
|
||||||
|
|
|
||||||
|
|
@ -227,8 +227,8 @@ export function createDashboardLiveState({
|
||||||
recentQuotes = [],
|
recentQuotes = [],
|
||||||
latestMarketPrice = null,
|
latestMarketPrice = null,
|
||||||
latestInventory = null,
|
latestInventory = null,
|
||||||
successfulTradeCount = 0,
|
recentSubmissionCount = 0,
|
||||||
lastSuccessfulTradeAt = null,
|
lastSubmissionAt = null,
|
||||||
activeAlerts = [],
|
activeAlerts = [],
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const state = {
|
const state = {
|
||||||
|
|
@ -239,8 +239,8 @@ export function createDashboardLiveState({
|
||||||
recent_quotes: recentQuotes.slice(0, config.operatorDashboardQuoteLimit || DASHBOARD_LIVE_QUOTE_LIMIT),
|
recent_quotes: recentQuotes.slice(0, config.operatorDashboardQuoteLimit || DASHBOARD_LIVE_QUOTE_LIMIT),
|
||||||
latest_market_price: latestMarketPrice?.payload || latestMarketPrice || null,
|
latest_market_price: latestMarketPrice?.payload || latestMarketPrice || null,
|
||||||
latest_inventory: latestInventory?.payload || latestInventory || null,
|
latest_inventory: latestInventory?.payload || latestInventory || null,
|
||||||
successful_trade_count: Number(successfulTradeCount || 0),
|
recent_submission_count: Number(recentSubmissionCount || 0),
|
||||||
last_successful_trade_at: lastSuccessfulTradeAt || null,
|
last_submission_at: lastSubmissionAt || null,
|
||||||
active_alerts: new Map(),
|
active_alerts: new Map(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -306,8 +306,8 @@ export function applyDashboardLiveEvent(state, { topic, event }) {
|
||||||
}
|
}
|
||||||
case 'exec.trade_result':
|
case 'exec.trade_result':
|
||||||
if (event.payload.status !== 'submitted') return [];
|
if (event.payload.status !== 'submitted') return [];
|
||||||
state.successful_trade_count += 1;
|
state.recent_submission_count += 1;
|
||||||
state.last_successful_trade_at = event.observed_at || event.ingested_at || new Date().toISOString();
|
state.last_submission_at = event.observed_at || event.ingested_at || new Date().toISOString();
|
||||||
return [{
|
return [{
|
||||||
type: 'status_bar.updated',
|
type: 'status_bar.updated',
|
||||||
status_bar: buildLiveStatusBar(state),
|
status_bar: buildLiveStatusBar(state),
|
||||||
|
|
@ -324,11 +324,13 @@ export function buildDashboardBootstrap({
|
||||||
inventorySnapshot,
|
inventorySnapshot,
|
||||||
marketPrice,
|
marketPrice,
|
||||||
recentQuotes,
|
recentQuotes,
|
||||||
successfulTrades,
|
submissionPage,
|
||||||
successfulTradeSummary,
|
submissionSummary,
|
||||||
fundingObservations,
|
fundingObservations,
|
||||||
recentDepositStatuses,
|
recentDepositStatuses,
|
||||||
recentTradeDecisions,
|
recentTradeDecisions,
|
||||||
|
recentExecuteTradeCommands,
|
||||||
|
recentExecutionResults,
|
||||||
recentAlertTransitions,
|
recentAlertTransitions,
|
||||||
serviceSnapshots,
|
serviceSnapshots,
|
||||||
sourceErrors = [],
|
sourceErrors = [],
|
||||||
|
|
@ -346,7 +348,7 @@ export function buildDashboardBootstrap({
|
||||||
));
|
));
|
||||||
const profitability = buildProfitabilitySummary({
|
const profitability = buildProfitabilitySummary({
|
||||||
metric: portfolioMetric,
|
metric: portfolioMetric,
|
||||||
successfulTradeSummary,
|
submissionSummary,
|
||||||
});
|
});
|
||||||
const balances = buildBalanceSummary({
|
const balances = buildBalanceSummary({
|
||||||
inventorySnapshot,
|
inventorySnapshot,
|
||||||
|
|
@ -359,9 +361,9 @@ export function buildDashboardBootstrap({
|
||||||
recentDepositStatuses,
|
recentDepositStatuses,
|
||||||
liquidityState: servicesByName['liquidity-manager']?.state || {},
|
liquidityState: servicesByName['liquidity-manager']?.state || {},
|
||||||
});
|
});
|
||||||
const tradesPage = normalizeSuccessfulTradesPage({
|
const normalizedSubmissionPage = normalizeSubmissionPage({
|
||||||
config,
|
config,
|
||||||
successfulTrades,
|
submissionPage,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -385,19 +387,22 @@ export function buildDashboardBootstrap({
|
||||||
config,
|
config,
|
||||||
liquidityState: servicesByName['liquidity-manager']?.state || {},
|
liquidityState: servicesByName['liquidity-manager']?.state || {},
|
||||||
}),
|
}),
|
||||||
trade_asset_changes: buildTradeAssetChanges({
|
recent_submission_terms: buildRecentSubmissionTerms({
|
||||||
config,
|
config,
|
||||||
trades: tradesPage.items,
|
submissions: normalizedSubmissionPage.items,
|
||||||
}),
|
}),
|
||||||
recent_quotes: (recentQuotes || []).slice(0, config.operatorDashboardQuoteLimit || DASHBOARD_LIVE_QUOTE_LIMIT),
|
recent_quotes: (recentQuotes || []).slice(0, config.operatorDashboardQuoteLimit || DASHBOARD_LIVE_QUOTE_LIMIT),
|
||||||
successful_trades: tradesPage,
|
submission_ledger: normalizedSubmissionPage,
|
||||||
controls: listDashboardControls({ page: 'funds' }),
|
controls: listDashboardControls({ page: 'funds' }),
|
||||||
caveats: profitability.caveats,
|
caveats: profitability.caveats,
|
||||||
},
|
},
|
||||||
strategy: buildStrategySummary({
|
strategy: buildStrategySummary({
|
||||||
servicesByName,
|
servicesByName,
|
||||||
activeAlerts,
|
activeAlerts,
|
||||||
|
recentQuotes,
|
||||||
recentTradeDecisions,
|
recentTradeDecisions,
|
||||||
|
recentExecuteTradeCommands,
|
||||||
|
recentExecutionResults,
|
||||||
}),
|
}),
|
||||||
system: buildSystemSummary({
|
system: buildSystemSummary({
|
||||||
servicesByName,
|
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 externalCashFlows = metric?.payload?.external_cash_flows || {};
|
||||||
const externalFlowCount = Number(externalCashFlows.flow_count || 0);
|
const externalFlowCount = Number(externalCashFlows.flow_count || 0);
|
||||||
const externalFlowAdjusted = externalFlowCount > 0;
|
const externalFlowAdjusted = externalFlowCount > 0;
|
||||||
|
|
@ -428,8 +433,8 @@ export function buildProfitabilitySummary({ metric, successfulTradeSummary } = {
|
||||||
external_withdrawal_count: Number(externalCashFlows.withdrawal_count || 0),
|
external_withdrawal_count: Number(externalCashFlows.withdrawal_count || 0),
|
||||||
latest_external_flow_at: externalCashFlows.latest_effective_at || null,
|
latest_external_flow_at: externalCashFlows.latest_effective_at || null,
|
||||||
net_external_flow_value_eure: externalCashFlows.net_value_eure_at_flow_time || '0',
|
net_external_flow_value_eure: externalCashFlows.net_value_eure_at_flow_time || '0',
|
||||||
recent_trade_count: successfulTradeSummary?.total ?? metric?.payload?.result_count ?? 0,
|
recent_submission_count: submissionSummary?.total ?? 0,
|
||||||
last_successful_trade_at: successfulTradeSummary?.last_successful_trade_at || null,
|
last_submission_at: submissionSummary?.last_submission_at || null,
|
||||||
caveats: [
|
caveats: [
|
||||||
'Portfolio PnL is truthful to the current durable inventory and reference price path.',
|
'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.',
|
'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,
|
active_alert_count: state.active_alerts.size,
|
||||||
highest_alert_severity: highestAlertSeverity([...state.active_alerts.values()]),
|
highest_alert_severity: highestAlertSeverity([...state.active_alerts.values()]),
|
||||||
recent_trade_count: state.successful_trade_count,
|
recent_submission_count: state.recent_submission_count,
|
||||||
last_successful_trade_at: state.last_successful_trade_at,
|
last_submission_at: state.last_submission_at,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -534,8 +539,8 @@ function buildStatusBar({
|
||||||
strategy_armed: servicesByName['strategy-engine']?.state?.armed ?? null,
|
strategy_armed: servicesByName['strategy-engine']?.state?.armed ?? null,
|
||||||
executor_armed: servicesByName['trade-executor']?.state?.armed ?? null,
|
executor_armed: servicesByName['trade-executor']?.state?.armed ?? null,
|
||||||
current_total_portfolio_value_eure: profitability.current_total_portfolio_value_eure,
|
current_total_portfolio_value_eure: profitability.current_total_portfolio_value_eure,
|
||||||
recent_trade_count: profitability.recent_trade_count,
|
recent_submission_count: profitability.recent_submission_count,
|
||||||
last_successful_trade_at: profitability.last_successful_trade_at,
|
last_submission_at: profitability.last_submission_at,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -675,37 +680,316 @@ function buildRecentWithdrawals({ config, liquidityState }) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildTradeAssetChanges({ config, trades }) {
|
function buildRecentSubmissionTerms({ config, submissions }) {
|
||||||
return (trades || []).slice(0, 10).map((trade) => {
|
return (submissions || []).slice(0, 10).map((submission) => {
|
||||||
const assetIn = config.assetRegistry.get(trade.asset_in);
|
const assetIn = config.assetRegistry.get(submission.asset_in);
|
||||||
const assetOut = config.assetRegistry.get(trade.asset_out);
|
const assetOut = config.assetRegistry.get(submission.asset_out);
|
||||||
return {
|
return {
|
||||||
observed_at: trade.observed_at,
|
observed_at: submission.observed_at,
|
||||||
quote_id: trade.quote_id,
|
quote_id: submission.quote_id,
|
||||||
request_kind: trade.request_kind,
|
request_kind: submission.request_kind,
|
||||||
asset_in: trade.asset_in,
|
asset_in: submission.asset_in,
|
||||||
asset_in_symbol: assetIn?.symbol || trade.asset_in,
|
asset_in_symbol: assetIn?.symbol || submission.asset_in,
|
||||||
amount_in_units: trade.amount_in,
|
amount_in_units: submission.amount_in,
|
||||||
amount_in: formatUnits(trade.amount_in || '0', assetIn?.decimals || 0),
|
amount_in: formatUnits(submission.amount_in || '0', assetIn?.decimals || 0),
|
||||||
asset_out: trade.asset_out,
|
asset_out: submission.asset_out,
|
||||||
asset_out_symbol: assetOut?.symbol || trade.asset_out,
|
asset_out_symbol: assetOut?.symbol || submission.asset_out,
|
||||||
amount_out_units: trade.amount_out,
|
amount_out_units: submission.amount_out,
|
||||||
amount_out: formatUnits(trade.amount_out || '0', assetOut?.decimals || 0),
|
amount_out: formatUnits(submission.amount_out || '0', assetOut?.decimals || 0),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeSuccessfulTradesPage({ config, successfulTrades }) {
|
function normalizeSubmissionPage({ config, submissionPage }) {
|
||||||
return {
|
return {
|
||||||
...successfulTrades,
|
...submissionPage,
|
||||||
items: (successfulTrades?.items || []).map((trade) => normalizeTradeForUi({
|
items: (submissionPage?.items || []).map((trade) => normalizeTradeForUi({
|
||||||
config,
|
config,
|
||||||
trade,
|
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 strategyState = servicesByName['strategy-engine']?.state || {};
|
||||||
const executorState = servicesByName['trade-executor']?.state || {};
|
const executorState = servicesByName['trade-executor']?.state || {};
|
||||||
const durableDecisionsById = new Map(
|
const durableDecisionsById = new Map(
|
||||||
|
|
@ -740,6 +1024,13 @@ function buildStrategySummary({ servicesByName, activeAlerts, recentTradeDecisio
|
||||||
|| durableDecisionsById.get(strategyState.latest_decision?.decision_id)?.decision_at
|
|| durableDecisionsById.get(strategyState.latest_decision?.decision_id)?.decision_at
|
||||||
|| null,
|
|| null,
|
||||||
});
|
});
|
||||||
|
const lifecycleRows = deriveQuoteLifecycleRows({
|
||||||
|
recentQuotes,
|
||||||
|
recentTradeDecisions,
|
||||||
|
recentExecuteTradeCommands,
|
||||||
|
recentExecutionResults,
|
||||||
|
limit: 20,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
strategy_state: {
|
strategy_state: {
|
||||||
|
|
@ -751,6 +1042,7 @@ function buildStrategySummary({ servicesByName, activeAlerts, recentTradeDecisio
|
||||||
recent_decisions: recentDecisions.length
|
recent_decisions: recentDecisions.length
|
||||||
? recentDecisions
|
? recentDecisions
|
||||||
: [...durableDecisionsById.values()].slice(0, 20),
|
: [...durableDecisionsById.values()].slice(0, 20),
|
||||||
|
recent_lifecycle_rows: lifecycleRows,
|
||||||
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,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -292,22 +292,22 @@ export async function loadRecentQuotes(pool, { limit = 10 } = {}) {
|
||||||
return result.rows.map(normalizeRecentQuoteRow);
|
return result.rows.map(normalizeRecentQuoteRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadSuccessfulTradeSummary(pool) {
|
export async function loadSubmissionSummary(pool) {
|
||||||
const result = await pool.query(`
|
const result = await pool.query(`
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*)::INT AS total,
|
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
|
FROM trade_execution_results
|
||||||
WHERE payload->>'status' = 'submitted'
|
WHERE payload->>'status' = 'submitted'
|
||||||
`);
|
`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
total: Number(result.rows[0]?.total || 0),
|
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 normalizedPage = Math.max(1, Number(page) || 1);
|
||||||
const normalizedPageSize = Math.max(1, Number(pageSize) || 20);
|
const normalizedPageSize = Math.max(1, Number(pageSize) || 20);
|
||||||
const offset = (normalizedPage - 1) * normalizedPageSize;
|
const offset = (normalizedPage - 1) * normalizedPageSize;
|
||||||
|
|
@ -347,10 +347,48 @@ export async function loadSuccessfulTradesPage(pool, { page = 1, pageSize = 20 }
|
||||||
page_size: normalizedPageSize,
|
page_size: normalizedPageSize,
|
||||||
total,
|
total,
|
||||||
total_pages: total > 0 ? Math.ceil(total / normalizedPageSize) : 1,
|
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) {
|
export async function loadCurrentFundingObservations(pool) {
|
||||||
const result = await pool.query(`
|
const result = await pool.query(`
|
||||||
SELECT DISTINCT ON (decision_key)
|
SELECT DISTINCT ON (decision_key)
|
||||||
|
|
@ -640,7 +678,7 @@ function normalizeRecentQuoteRow(row) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeSuccessfulTradeRow(row) {
|
function normalizeSubmissionRow(row) {
|
||||||
const resultPayload = row.result_payload || {};
|
const resultPayload = row.result_payload || {};
|
||||||
const commandPayload = row.command_payload || {};
|
const commandPayload = row.command_payload || {};
|
||||||
const decisionPayload = row.decision_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) {
|
function resolveTradeAmount(commandPayload, field) {
|
||||||
const quoteOutputField = commandPayload?.quote_output?.[field];
|
const quoteOutputField = commandPayload?.quote_output?.[field];
|
||||||
const proposedField = commandPayload?.[`proposed_${field}`];
|
const proposedField = commandPayload?.[`proposed_${field}`];
|
||||||
|
|
|
||||||
|
|
@ -37,12 +37,12 @@ export default function App() {
|
||||||
async function loadTradesPage(page) {
|
async function loadTradesPage(page) {
|
||||||
if (!Number.isFinite(page) || page < 1) return;
|
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 });
|
dispatch({ type: 'error.changed', error: null });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const successfulTrades = await fetchJson(`/api/trades?page=${page}&page_size=${TRADE_PAGE_SIZE}`);
|
const submissionLedger = await fetchJson(`/api/submissions?page=${page}&page_size=${TRADE_PAGE_SIZE}`);
|
||||||
dispatch({ type: 'trades.loaded', successfulTrades });
|
dispatch({ type: 'submissionLedger.loaded', submissionLedger });
|
||||||
dispatch({ type: 'notice.changed', notice: null });
|
dispatch({ type: 'notice.changed', notice: null });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch({ type: 'error.changed', error: error.message });
|
dispatch({ type: 'error.changed', error: error.message });
|
||||||
|
|
@ -67,7 +67,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?.successful_trades?.page || 1;
|
const page = state.dashboard?.funds?.submission_ledger?.page || 1;
|
||||||
await loadBootstrap(page);
|
await loadBootstrap(page);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -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()],
|
['Alerts', `${status.active_alert_count || 0} ${status.highest_alert_severity ? `(${status.highest_alert_severity})` : ''}`.trim()],
|
||||||
['Strategy Armed', formatBoolean(status.strategy_armed)],
|
['Strategy Armed', formatBoolean(status.strategy_armed)],
|
||||||
['Executor Armed', formatBoolean(status.executor_armed)],
|
['Executor Armed', formatBoolean(status.executor_armed)],
|
||||||
[SUBMISSION_COPY.statusTileLabel, `${status.recent_trade_count || 0} ${SUBMISSION_COPY.statusTileValueSuffix}`],
|
[SUBMISSION_COPY.statusTileLabel, `${status.recent_submission_count || 0} ${SUBMISSION_COPY.statusTileValueSuffix}`],
|
||||||
[SUBMISSION_COPY.lastStatusTileLabel, formatTimestamp(status.last_successful_trade_at)],
|
[SUBMISSION_COPY.lastStatusTileLabel, formatTimestamp(status.last_submission_at)],
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -370,7 +370,7 @@ export default function FundsPage({
|
||||||
lastControlResult,
|
lastControlResult,
|
||||||
}) {
|
}) {
|
||||||
const profitability = funds.profitability;
|
const profitability = funds.profitability;
|
||||||
const trades = funds.successful_trades;
|
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;
|
||||||
|
|
@ -448,8 +448,8 @@ export default function FundsPage({
|
||||||
/>
|
/>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label={SUBMISSION_COPY.recentMetricLabel}
|
label={SUBMISSION_COPY.recentMetricLabel}
|
||||||
meta={formatTimestamp(profitability.last_successful_trade_at)}
|
meta={formatTimestamp(profitability.last_submission_at)}
|
||||||
value={`${profitability.recent_trade_count || 0} ${SUBMISSION_COPY.recentMetricValueSuffix}`}
|
value={`${profitability.recent_submission_count || 0} ${SUBMISSION_COPY.recentMetricValueSuffix}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="panel-subtitle">
|
<div className="panel-subtitle">
|
||||||
|
|
@ -573,7 +573,7 @@ export default function FundsPage({
|
||||||
<h3>{SUBMISSION_COPY.termsTitle}</h3>
|
<h3>{SUBMISSION_COPY.termsTitle}</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AssetChangeTable items={funds.trade_asset_changes} />
|
<AssetChangeTable items={funds.recent_submission_terms} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -585,22 +585,22 @@ export default function FundsPage({
|
||||||
</div>
|
</div>
|
||||||
<div className="status-subtle">{SUBMISSION_COPY.ledgerSubtitle}</div>
|
<div className="status-subtle">{SUBMISSION_COPY.ledgerSubtitle}</div>
|
||||||
</div>
|
</div>
|
||||||
<TradesTable items={trades.items} />
|
<TradesTable items={submissionLedger.items} />
|
||||||
<div className="pagination">
|
<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">
|
<div className="button-row">
|
||||||
<button
|
<button
|
||||||
className="button secondary"
|
className="button secondary"
|
||||||
disabled={trades.page <= 1}
|
disabled={submissionLedger.page <= 1}
|
||||||
onClick={() => onTradesPageChange(trades.page - 1)}
|
onClick={() => onTradesPageChange(submissionLedger.page - 1)}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
Previous
|
Previous
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="button secondary"
|
className="button secondary"
|
||||||
disabled={trades.page >= trades.total_pages}
|
disabled={submissionLedger.page >= submissionLedger.total_pages}
|
||||||
onClick={() => onTradesPageChange(trades.page + 1)}
|
onClick={() => onTradesPageChange(submissionLedger.page + 1)}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
Next
|
Next
|
||||||
|
|
|
||||||
|
|
@ -5,57 +5,65 @@ 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, formatTimestamp, truncateMiddle } from '../lib/format.js';
|
||||||
|
|
||||||
function describeStrategyDecision(decision) {
|
async function copyIdentifier(value) {
|
||||||
if (decision === 'actionable') {
|
if (!value || !navigator?.clipboard?.writeText) return;
|
||||||
return {
|
try {
|
||||||
label: 'Actionable',
|
await navigator.clipboard.writeText(value);
|
||||||
stateLabel: 'healthy',
|
} catch {
|
||||||
};
|
// Best-effort copy affordance; keep the full identifier visible regardless.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (decision === 'rejected') {
|
function IdentifierRow({ label, value }) {
|
||||||
return {
|
if (!value) return <div className="status-subtle">{`${label}: unavailable`}</div>;
|
||||||
label: 'Rejected by strategy',
|
|
||||||
stateLabel: 'warning',
|
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 {
|
function LifecycleTable({ items }) {
|
||||||
label: decision || 'Unknown',
|
if (!items?.length) return <EmptyState>No quote lifecycle evidence has been observed yet.</EmptyState>;
|
||||||
stateLabel: decision || 'unknown',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function DecisionsTable({ items }) {
|
|
||||||
if (!items?.length) return <EmptyState>No strategy decisions have been observed yet.</EmptyState>;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableFrame>
|
<TableFrame>
|
||||||
<table className="decision-table">
|
<table className="decision-table lifecycle-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>At</th>
|
<th>At</th>
|
||||||
<th>Strategy verdict</th>
|
<th>Lifecycle</th>
|
||||||
<th>Pair</th>
|
|
||||||
<th>Reason</th>
|
<th>Reason</th>
|
||||||
|
<th>Pair</th>
|
||||||
|
<th>Trace</th>
|
||||||
<th>Edge %</th>
|
<th>Edge %</th>
|
||||||
<th>Notional</th>
|
<th>Notional</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{items.map((item, index) => {
|
{items.map((item, index) => (
|
||||||
const verdict = describeStrategyDecision(item.decision);
|
<tr key={`${item.quote_id || item.decision_id || item.command_id || item.latest_stage_at || index}`}>
|
||||||
return (
|
<td>{formatTimestamp(item.latest_stage_at)}</td>
|
||||||
<tr key={`${item.decision_id || item.decision_at || index}`}>
|
<td><Pill label={item.lifecycle_label} stateLabel={item.lifecycle_tone} /></td>
|
||||||
<td>{formatTimestamp(item.decision_at)}</td>
|
<td>
|
||||||
<td><Pill label={verdict.label} stateLabel={verdict.stateLabel} /></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 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 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>
|
<td>{item.eure_notional || ''}</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
))}
|
||||||
})}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</TableFrame>
|
</TableFrame>
|
||||||
|
|
@ -71,7 +79,7 @@ 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 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>
|
</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="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="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={`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)} />
|
<MetricCard label="Signer registered" meta={strategy.executor_state.account_id || ''} value={formatBoolean(strategy.executor_state.signer_registered)} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="stack-grid">
|
<section className="strategy-layout">
|
||||||
<div className="panel">
|
<div className="panel">
|
||||||
<div className="panel-head">
|
<div className="panel-head">
|
||||||
<div>
|
<div>
|
||||||
<div className="eyebrow">Decision flow</div>
|
<div className="eyebrow">Lifecycle truth</div>
|
||||||
<h3>Recent decisions and skip reasons</h3>
|
<h3>Recent quote decisions and execution evidence</h3>
|
||||||
<div className="panel-subtitle">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DecisionsTable items={strategy.strategy_state.recent_decisions} />
|
<LifecycleTable items={strategy.strategy_state.recent_lifecycle_rows} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="panel">
|
<div className="panel strategy-side-panel">
|
||||||
<div className="panel-head">
|
<div className="panel-head">
|
||||||
<div>
|
<div>
|
||||||
<div className="eyebrow">Guard rails</div>
|
<div className="eyebrow">Guard rails</div>
|
||||||
|
|
|
||||||
|
|
@ -41,12 +41,12 @@ function applySocketMessage(dashboard, payload, session) {
|
||||||
...dashboard.funds,
|
...dashboard.funds,
|
||||||
profitability: {
|
profitability: {
|
||||||
...dashboard.funds.profitability,
|
...dashboard.funds.profitability,
|
||||||
recent_trade_count:
|
recent_submission_count:
|
||||||
payload.status_bar.recent_trade_count
|
payload.status_bar.recent_submission_count
|
||||||
?? dashboard.funds.profitability.recent_trade_count,
|
?? dashboard.funds.profitability.recent_submission_count,
|
||||||
last_successful_trade_at:
|
last_submission_at:
|
||||||
payload.status_bar.last_successful_trade_at
|
payload.status_bar.last_submission_at
|
||||||
|| dashboard.funds.profitability.last_successful_trade_at,
|
|| dashboard.funds.profitability.last_submission_at,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -91,14 +91,14 @@ export function dashboardReducer(state, action) {
|
||||||
dashboard: action.dashboard,
|
dashboard: action.dashboard,
|
||||||
page: state.page || action.dashboard.default_page || 'funds',
|
page: state.page || action.dashboard.default_page || 'funds',
|
||||||
};
|
};
|
||||||
case 'trades.loaded':
|
case 'submissionLedger.loaded':
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
dashboard: {
|
dashboard: {
|
||||||
...state.dashboard,
|
...state.dashboard,
|
||||||
funds: {
|
funds: {
|
||||||
...state.dashboard.funds,
|
...state.dashboard.funds,
|
||||||
successful_trades: action.successfulTrades,
|
submission_ledger: action.submissionLedger,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -254,6 +254,17 @@ select {
|
||||||
grid-template-columns: 1fr;
|
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 {
|
.table-wrap {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
|
|
@ -322,12 +333,18 @@ pre {
|
||||||
|
|
||||||
table.decision-table td:nth-child(2),
|
table.decision-table td:nth-child(2),
|
||||||
table.decision-table th: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 td:nth-child(3),
|
||||||
table.decision-table th: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,
|
.pills,
|
||||||
|
|
@ -388,6 +405,29 @@ table.decision-table th:nth-child(3) {
|
||||||
margin-top: 12px;
|
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 {
|
.form-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
|
@ -465,7 +505,8 @@ table.decision-table th:nth-child(3) {
|
||||||
|
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
.app-grid,
|
.app-grid,
|
||||||
.split {
|
.split,
|
||||||
|
.strategy-layout {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
buildDashboardBootstrap,
|
buildDashboardBootstrap,
|
||||||
buildProfitabilitySummary,
|
buildProfitabilitySummary,
|
||||||
createDashboardLiveState,
|
createDashboardLiveState,
|
||||||
|
deriveQuoteLifecycleRows,
|
||||||
resolveDashboardControl,
|
resolveDashboardControl,
|
||||||
} from '../src/core/operator-dashboard.mjs';
|
} from '../src/core/operator-dashboard.mjs';
|
||||||
import {
|
import {
|
||||||
|
|
@ -52,9 +53,9 @@ test('profitability summary separates baseline, hold, market move, and trading c
|
||||||
baseline_portfolio_value_eure_at_current_price: '105',
|
baseline_portfolio_value_eure_at_current_price: '105',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
successfulTradeSummary: {
|
submissionSummary: {
|
||||||
total: 4,
|
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.market_move_contribution_eure, '5');
|
||||||
assert.equal(summary.trading_contribution_eure, '5');
|
assert.equal(summary.trading_contribution_eure, '5');
|
||||||
assert.equal(summary.computed_at, '2026-04-04T09:05:00.000Z');
|
assert.equal(summary.computed_at, '2026-04-04T09:05:00.000Z');
|
||||||
assert.equal(summary.recent_trade_count, 4);
|
assert.equal(summary.recent_submission_count, 4);
|
||||||
assert.equal(summary.last_successful_trade_at, '2026-04-04T09:00:00.000Z');
|
assert.equal(summary.last_submission_at, '2026-04-04T09:00:00.000Z');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('profitability summary flags cash-flow-adjusted benchmarks after later funding changes', () => {
|
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,
|
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');
|
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 config = buildConfig();
|
||||||
const state = createDashboardLiveState({
|
const state = createDashboardLiveState({
|
||||||
config,
|
config,
|
||||||
successfulTradeCount: 2,
|
recentSubmissionCount: 2,
|
||||||
lastSuccessfulTradeAt: '2026-04-04T08:00:00.000Z',
|
lastSubmissionAt: '2026-04-04T08:00:00.000Z',
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let index = 0; index < 11; index += 1) {
|
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.length, 10);
|
||||||
assert.equal(state.recent_quotes[0].quote_id, 'quote-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.recent_quotes.at(-1).quote_id, 'quote-1');
|
||||||
assert.equal(state.successful_trade_count, 3);
|
assert.equal(state.recent_submission_count, 3);
|
||||||
assert.equal(state.last_successful_trade_at, '2026-04-04T08:30:00.000Z');
|
assert.equal(state.last_submission_at, '2026-04-04T08:30:00.000Z');
|
||||||
assert.equal(updates[0].type, 'status_bar.updated');
|
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', () => {
|
test('bootstrap aggregation keeps Funds as default and carries live control state', () => {
|
||||||
const config = buildConfig();
|
const config = buildConfig();
|
||||||
const bootstrap = buildDashboardBootstrap({
|
const bootstrap = buildDashboardBootstrap({
|
||||||
|
|
@ -247,16 +348,16 @@ test('bootstrap aggregation keeps Funds as default and carries live control stat
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
recentQuotes: [],
|
recentQuotes: [],
|
||||||
successfulTrades: {
|
submissionPage: {
|
||||||
page: 1,
|
page: 1,
|
||||||
page_size: 20,
|
page_size: 20,
|
||||||
total: 0,
|
total: 0,
|
||||||
total_pages: 1,
|
total_pages: 1,
|
||||||
items: [],
|
items: [],
|
||||||
},
|
},
|
||||||
successfulTradeSummary: {
|
submissionSummary: {
|
||||||
total: 1,
|
total: 1,
|
||||||
last_successful_trade_at: '2026-04-04T09:30:00.000Z',
|
last_submission_at: '2026-04-04T09:30:00.000Z',
|
||||||
},
|
},
|
||||||
fundingObservations: [
|
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.default_page, 'funds');
|
||||||
assert.equal(bootstrap.funds.profitability.computed_at, '2026-04-04T09:05:00.000Z');
|
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.control_state.withdrawals_frozen, true);
|
||||||
assert.equal(bootstrap.funds.funding.handles[0].address, 'btc-address');
|
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.strategy_armed, true);
|
||||||
assert.equal(bootstrap.status_bar.executor_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_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_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', () => {
|
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,
|
inventorySnapshot: null,
|
||||||
marketPrice: null,
|
marketPrice: null,
|
||||||
recentQuotes: [],
|
recentQuotes: [],
|
||||||
successfulTrades: {
|
submissionPage: {
|
||||||
page: 1,
|
page: 1,
|
||||||
page_size: 20,
|
page_size: 20,
|
||||||
total: 0,
|
total: 0,
|
||||||
total_pages: 1,
|
total_pages: 1,
|
||||||
items: [],
|
items: [],
|
||||||
},
|
},
|
||||||
successfulTradeSummary: {
|
submissionSummary: {
|
||||||
total: 0,
|
total: 0,
|
||||||
last_successful_trade_at: null,
|
last_submission_at: null,
|
||||||
},
|
},
|
||||||
fundingObservations: [],
|
fundingObservations: [],
|
||||||
recentTradeDecisions: [],
|
recentTradeDecisions: [],
|
||||||
|
|
@ -496,16 +603,16 @@ test('ingest disconnected still renders as a critical transport failure', () =>
|
||||||
inventorySnapshot: null,
|
inventorySnapshot: null,
|
||||||
marketPrice: null,
|
marketPrice: null,
|
||||||
recentQuotes: [],
|
recentQuotes: [],
|
||||||
successfulTrades: {
|
submissionPage: {
|
||||||
page: 1,
|
page: 1,
|
||||||
page_size: 20,
|
page_size: 20,
|
||||||
total: 0,
|
total: 0,
|
||||||
total_pages: 1,
|
total_pages: 1,
|
||||||
items: [],
|
items: [],
|
||||||
},
|
},
|
||||||
successfulTradeSummary: {
|
submissionSummary: {
|
||||||
total: 0,
|
total: 0,
|
||||||
last_successful_trade_at: null,
|
last_submission_at: null,
|
||||||
},
|
},
|
||||||
fundingObservations: [],
|
fundingObservations: [],
|
||||||
recentTradeDecisions: [],
|
recentTradeDecisions: [],
|
||||||
|
|
@ -583,16 +690,16 @@ test('recent alert history collapses repeated flapping transitions into one read
|
||||||
inventorySnapshot: null,
|
inventorySnapshot: null,
|
||||||
marketPrice: null,
|
marketPrice: null,
|
||||||
recentQuotes: [],
|
recentQuotes: [],
|
||||||
successfulTrades: {
|
submissionPage: {
|
||||||
page: 1,
|
page: 1,
|
||||||
page_size: 20,
|
page_size: 20,
|
||||||
total: 0,
|
total: 0,
|
||||||
total_pages: 1,
|
total_pages: 1,
|
||||||
items: [],
|
items: [],
|
||||||
},
|
},
|
||||||
successfulTradeSummary: {
|
submissionSummary: {
|
||||||
total: 0,
|
total: 0,
|
||||||
last_successful_trade_at: null,
|
last_submission_at: null,
|
||||||
},
|
},
|
||||||
fundingObservations: [],
|
fundingObservations: [],
|
||||||
recentTradeDecisions: [],
|
recentTradeDecisions: [],
|
||||||
|
|
@ -674,16 +781,16 @@ test('funding summary includes credited bridge deposits without observer-backed
|
||||||
inventorySnapshot: null,
|
inventorySnapshot: null,
|
||||||
marketPrice: null,
|
marketPrice: null,
|
||||||
recentQuotes: [],
|
recentQuotes: [],
|
||||||
successfulTrades: {
|
submissionPage: {
|
||||||
page: 1,
|
page: 1,
|
||||||
page_size: 20,
|
page_size: 20,
|
||||||
total: 0,
|
total: 0,
|
||||||
total_pages: 1,
|
total_pages: 1,
|
||||||
items: [],
|
items: [],
|
||||||
},
|
},
|
||||||
successfulTradeSummary: {
|
submissionSummary: {
|
||||||
total: 0,
|
total: 0,
|
||||||
last_successful_trade_at: null,
|
last_submission_at: null,
|
||||||
},
|
},
|
||||||
fundingObservations: [],
|
fundingObservations: [],
|
||||||
recentDepositStatuses: [
|
recentDepositStatuses: [
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue