Persist quote lifecycle statistics
All checks were successful
deploy / deploy (push) Successful in 1m0s

Proof: Added PostgreSQL-backed quote lifecycle statistics for all-time, monthly, weekly, daily, hourly, and five-minute windows; wired authenticated dashboard API, live WebSocket counters, history-writer refresh before pruning, and dashboard controls; validated with targeted tests, full npm test, and operator dashboard build.

Assumptions: Unique quote counts are keyed by durable quote_id, quote windows use the first durable lifecycle timestamp in UTC with Monday UTC weeks, and missing identifiers or timestamps are represented as unavailable evidence instead of fabricated quote identities.

Still fake: Fee-aware PnL, cashflow, oracle-deviation, size distribution, matched-only analytics, and reconstruction of already-pruned detail remain outside this turn.
This commit is contained in:
philipp 2026-06-12 20:02:09 +02:00
parent 893617c30d
commit 2e95c95246
14 changed files with 1528 additions and 6 deletions

View file

@ -28,6 +28,7 @@ import {
loadLatestPortfolioMetric, loadLatestPortfolioMetric,
loadPortfolioMetricInputs, loadPortfolioMetricInputs,
refreshIntentRequestOutcomes, refreshIntentRequestOutcomes,
refreshQuoteLifecycleStatistics,
refreshQuoteOutcomes, refreshQuoteOutcomes,
runQuoteLifecycleRetentionMaintenance, runQuoteLifecycleRetentionMaintenance,
seedTradingConfig, seedTradingConfig,
@ -202,6 +203,10 @@ const state = {
last_quote_lifecycle_rollup_at: null, last_quote_lifecycle_rollup_at: null,
quote_lifecycle_rollup_count: 0, quote_lifecycle_rollup_count: 0,
quote_lifecycle_rollup_error: null, quote_lifecycle_rollup_error: null,
last_quote_lifecycle_statistics_at: null,
quote_lifecycle_statistics_count: 0,
quote_lifecycle_statistics_error: null,
latest_quote_lifecycle_statistics: null,
last_retention_maintenance_at: null, last_retention_maintenance_at: null,
retention_maintenance: null, retention_maintenance: null,
retention_summary: null, retention_summary: null,
@ -233,6 +238,9 @@ await refreshIntentRequestOutcomeAttributions().then((records) => publishIntentR
await refreshQuoteLifecycleRetentionState().catch((error) => { await refreshQuoteLifecycleRetentionState().catch((error) => {
state.retention_error = serializeError(error); state.retention_error = serializeError(error);
}); });
await refreshQuoteLifecycleStatisticsState({ persist: true }).catch((error) => {
state.quote_lifecycle_statistics_error = serializeError(error);
});
let quoteLifecycleRetentionMaintenanceInFlight = null; let quoteLifecycleRetentionMaintenanceInFlight = null;
const quoteLifecycleRetentionMaintenanceTimer = setInterval(() => { const quoteLifecycleRetentionMaintenanceTimer = setInterval(() => {
@ -340,9 +348,15 @@ async function maybeRunQuoteLifecycleRetentionMaintenance({ force = false } = {}
quoteLifecycleRetentionMaintenanceInFlight = (async () => { quoteLifecycleRetentionMaintenanceInFlight = (async () => {
try { try {
const result = await runQuoteLifecycleRetentionMaintenance(pool, { const nowIso = new Date(nowMs).toISOString();
now: new Date(nowMs).toISOString(), const statisticsResult = await refreshQuoteLifecycleStatisticsState({
now: nowIso,
persist: true,
}); });
const result = await runQuoteLifecycleRetentionMaintenance(pool, {
now: nowIso,
});
result.quote_lifecycle_statistics = statisticsResult;
applyRetentionMaintenanceResult(result); applyRetentionMaintenanceResult(result);
await refreshQuoteLifecycleRetentionState(); await refreshQuoteLifecycleRetentionState();
if (result.status === 'success' && Number(result.pruned_counts?.deletedCount || 0) > 0) { if (result.status === 'success' && Number(result.pruned_counts?.deletedCount || 0) > 0) {
@ -419,6 +433,33 @@ async function refreshQuoteLifecycleRetentionState() {
return summary; return summary;
} }
async function refreshQuoteLifecycleStatisticsState({
now = new Date().toISOString(),
persist = false,
} = {}) {
let result = null;
try {
result = persist
? await refreshQuoteLifecycleStatistics(pool, { now })
: null;
} catch (error) {
state.quote_lifecycle_statistics_error = serializeError(error);
throw error;
}
if (!result) return state.latest_quote_lifecycle_statistics;
state.last_quote_lifecycle_statistics_at = result.computed_at;
state.quote_lifecycle_statistics_count = result.statistic_row_count;
state.quote_lifecycle_statistics_error = null;
state.latest_quote_lifecycle_statistics = {
computed_at: result.computed_at,
quote_count: result.quote_count,
statistic_row_count: result.statistic_row_count,
evidence_counts: result.evidence_counts,
};
return state.latest_quote_lifecycle_statistics;
}
async function handleWrittenHistoryEvent({ async function handleWrittenHistoryEvent({
topic, topic,
partition, partition,
@ -560,6 +601,10 @@ const controlApi = startControlApi({
last_retention_maintenance_at: state.last_retention_maintenance_at, last_retention_maintenance_at: state.last_retention_maintenance_at,
retention_error: state.retention_error, retention_error: state.retention_error,
storage_pressure: state.storage_pressure, storage_pressure: state.storage_pressure,
last_quote_lifecycle_statistics_at: state.last_quote_lifecycle_statistics_at,
quote_lifecycle_statistics_count: state.quote_lifecycle_statistics_count,
quote_lifecycle_statistics_error: state.quote_lifecycle_statistics_error,
latest_quote_lifecycle_statistics: state.latest_quote_lifecycle_statistics,
last_quote_lifecycle_rollup_at: state.last_quote_lifecycle_rollup_at, last_quote_lifecycle_rollup_at: state.last_quote_lifecycle_rollup_at,
quote_lifecycle_rollup_count: state.quote_lifecycle_rollup_count, quote_lifecycle_rollup_count: state.quote_lifecycle_rollup_count,
quote_lifecycle_rollup_error: state.quote_lifecycle_rollup_error, quote_lifecycle_rollup_error: state.quote_lifecycle_rollup_error,

View file

@ -13,6 +13,7 @@ import {
applyDashboardLiveEvent, applyDashboardLiveEvent,
buildDashboardBootstrap, buildDashboardBootstrap,
buildDashboardControlErrorResponse, buildDashboardControlErrorResponse,
buildLiveQuoteLifecycleStatistics,
buildLiveQuoteLifecycleRows, buildLiveQuoteLifecycleRows,
buildQuoteLifecycleLookupResponse, buildQuoteLifecycleLookupResponse,
buildLiveStatusBar, buildLiveStatusBar,
@ -47,6 +48,7 @@ import {
loadPairConfigSummary, loadPairConfigSummary,
loadQuoteLifecycleLookupEvidence, loadQuoteLifecycleLookupEvidence,
loadQuoteLifecycleRetentionSummary, loadQuoteLifecycleRetentionSummary,
loadQuoteLifecycleStatistics,
loadRecentAlertTransitions, loadRecentAlertTransitions,
loadRecentDepositStatuses, loadRecentDepositStatuses,
loadRecentEnvironmentStatuses, loadRecentEnvironmentStatuses,
@ -60,6 +62,7 @@ import {
loadSubmissionPage, loadSubmissionPage,
loadSubmissionSummary, loadSubmissionSummary,
pauseTradingPair, pauseTradingPair,
refreshQuoteLifecycleStatistics,
seedTradingConfig, seedTradingConfig,
setTradingPairMode, setTradingPairMode,
} from '../lib/postgres.mjs'; } from '../lib/postgres.mjs';
@ -231,6 +234,7 @@ webSocketServer.on('connection', (socket, _req, authContext) => {
webSockets.add(socket); webSockets.add(socket);
dashboardRuntimeState.websocket_clients = webSockets.size; dashboardRuntimeState.websocket_clients = webSockets.size;
const recentLifecycleRows = buildLiveQuoteLifecycleRows(liveState); const recentLifecycleRows = buildLiveQuoteLifecycleRows(liveState);
const quoteLifecycleStatistics = buildLiveQuoteLifecycleStatistics(liveState);
dashboardRuntimeState.latest_maker_competitiveness = buildMakerCompetitivenessSummary({ dashboardRuntimeState.latest_maker_competitiveness = buildMakerCompetitivenessSummary({
lifecycleRows: recentLifecycleRows, lifecycleRows: recentLifecycleRows,
}); });
@ -240,6 +244,7 @@ webSocketServer.on('connection', (socket, _req, authContext) => {
live: { live: {
recent_quotes: liveState.recent_quotes, recent_quotes: liveState.recent_quotes,
recent_lifecycle_rows: recentLifecycleRows, recent_lifecycle_rows: recentLifecycleRows,
quote_lifecycle_statistics: quoteLifecycleStatistics,
maker_competitiveness: dashboardRuntimeState.latest_maker_competitiveness, maker_competitiveness: dashboardRuntimeState.latest_maker_competitiveness,
status_bar: buildLiveStatusBar(liveState), status_bar: buildLiveStatusBar(liveState),
}, },
@ -380,6 +385,14 @@ async function handleApiRequest({ req, res, url, auth }) {
return sendJson(res, 200, submissionPage); return sendJson(res, 200, submissionPage);
} }
if (req.method === 'GET' && url.pathname === '/api/strategy/quote-lifecycle/statistics') {
const payload = await loadQuoteLifecycleStatisticsPayload({
grains: url.searchParams.get('grains') || undefined,
limit: Number(url.searchParams.get('limit') || 120),
});
return sendJson(res, 200, payload);
}
const lifecycleLookupMatch = req.method === 'GET' const lifecycleLookupMatch = req.method === 'GET'
? url.pathname.match(/^\/api\/strategy\/quote-lifecycle\/(.+)$/) ? url.pathname.match(/^\/api\/strategy\/quote-lifecycle\/(.+)$/)
: null; : null;
@ -484,6 +497,7 @@ async function loadBootstrapPayload({ auth, page, pageSize }) {
recentQuoteOutcomes, recentQuoteOutcomes,
quoteLifecycleRetention, quoteLifecycleRetention,
quoteLifecycleRollups, quoteLifecycleRollups,
quoteLifecycleStatistics,
recentIntentRequests, recentIntentRequests,
recentAlertTransitions, recentAlertTransitions,
recentEnvironmentStatuses, recentEnvironmentStatuses,
@ -567,6 +581,12 @@ async function loadBootstrapPayload({ auth, page, pageSize }) {
[], [],
sourceErrors, sourceErrors,
), ),
safeSourceLoad(
'quote_lifecycle_statistics',
() => loadQuoteLifecycleStatistics(pool, { limit: 120 }),
[],
sourceErrors,
),
safeSourceLoad( safeSourceLoad(
'recent_intent_requests', 'recent_intent_requests',
() => loadRecentIntentRequests(pool, { () => loadRecentIntentRequests(pool, {
@ -613,6 +633,14 @@ async function loadBootstrapPayload({ auth, page, pageSize }) {
recentQuoteOutcomes, recentQuoteOutcomes,
quoteLifecycleRetention, quoteLifecycleRetention,
quoteLifecycleRollups, quoteLifecycleRollups,
quoteLifecycleStatistics: {
persisted: {
source: 'persisted',
generated_at: quoteLifecycleStatistics[0]?.computed_at || null,
rows: quoteLifecycleStatistics,
},
live: buildLiveQuoteLifecycleStatistics(liveState),
},
recentIntentRequests, recentIntentRequests,
recentAlertTransitions, recentAlertTransitions,
recentEnvironmentStatuses, recentEnvironmentStatuses,
@ -644,6 +672,32 @@ async function loadQuoteLifecycleLookupPayload({ identifier }) {
}); });
} }
async function loadQuoteLifecycleStatisticsPayload({ grains, limit } = {}) {
const refreshed = await refreshQuoteLifecycleStatistics(pool, {
now: new Date().toISOString(),
grains,
});
const rows = await loadQuoteLifecycleStatistics(pool, {
grains,
limit,
});
return {
ok: true,
refreshed_at: refreshed.computed_at,
persisted: {
source: 'persisted',
generated_at: rows[0]?.computed_at || refreshed.computed_at,
rows,
},
live: buildLiveQuoteLifecycleStatistics(liveState),
refresh: {
statistic_row_count: refreshed.statistic_row_count,
quote_count: refreshed.quote_count,
evidence_counts: refreshed.evidence_counts,
},
};
}
async function loadServiceSnapshots() { async function loadServiceSnapshots() {
const services = listDashboardServices(config); const services = listDashboardServices(config);
return Promise.all(services.map((service) => loadServiceSnapshot(service))); return Promise.all(services.map((service) => loadServiceSnapshot(service)));

View file

@ -3,6 +3,7 @@ import { bridgeDepositObservedAt } from './bridge-assets.mjs';
import { summarizeFundingObservations } from './funding-observations.mjs'; import { summarizeFundingObservations } from './funding-observations.mjs';
import { buildMakerCompetitivenessSummary } from './maker-competitiveness.mjs'; import { buildMakerCompetitivenessSummary } from './maker-competitiveness.mjs';
import { resolveDashboardRequestAuth } from './operator-dashboard-auth.mjs'; import { resolveDashboardRequestAuth } from './operator-dashboard-auth.mjs';
import { buildQuoteLifecycleStatistics } from './quote-lifecycle-statistics.mjs';
import { TERMINAL_SETTLEMENT_ATTRIBUTION_STATUSES } from './quote-outcomes.mjs'; import { TERMINAL_SETTLEMENT_ATTRIBUTION_STATUSES } from './quote-outcomes.mjs';
import { inferServiceFreshnessTimestamp as inferRuntimeFreshnessTimestamp } from './runtime-health.mjs'; import { inferServiceFreshnessTimestamp as inferRuntimeFreshnessTimestamp } from './runtime-health.mjs';
@ -417,6 +418,27 @@ export function buildLiveQuoteLifecycleRows(state, { flashQuoteId = null, flashA
)); ));
} }
export function buildLiveQuoteLifecycleStatistics(state, {
computedAt = new Date().toISOString(),
} = {}) {
const lifecycleRows = deriveQuoteLifecycleRows({
recentQuotes: state.recent_quotes,
recentTradeDecisions: state.recent_trade_decisions,
recentExecuteTradeCommands: state.recent_execute_trade_commands,
recentExecutionResults: state.recent_execution_results,
recentQuoteOutcomes: state.recent_quote_outcomes,
limit: null,
}).map((row) => enrichLifecycleRowForUi({ config: state.config, row }));
return {
source: 'live_recent',
generated_at: computedAt,
rows: buildQuoteLifecycleStatistics({
lifecycleRows,
computedAt,
}),
};
}
export function applyDashboardLiveEvent(state, { topic, event }) { export function applyDashboardLiveEvent(state, { topic, event }) {
if (!event?.payload) return []; if (!event?.payload) return [];
@ -538,6 +560,7 @@ export function buildDashboardBootstrap({
recentQuoteOutcomes = [], recentQuoteOutcomes = [],
quoteLifecycleRetention = null, quoteLifecycleRetention = null,
quoteLifecycleRollups = [], quoteLifecycleRollups = [],
quoteLifecycleStatistics = null,
recentIntentRequests = [], recentIntentRequests = [],
recentAlertTransitions, recentAlertTransitions,
recentEnvironmentStatuses = [], recentEnvironmentStatuses = [],
@ -633,6 +656,7 @@ export function buildDashboardBootstrap({
recentExecutionResults, recentExecutionResults,
recentQuoteOutcomes, recentQuoteOutcomes,
quoteLifecycleRollups, quoteLifecycleRollups,
quoteLifecycleStatistics,
}), }),
system: buildSystemSummary({ system: buildSystemSummary({
servicesByName, servicesByName,
@ -1728,6 +1752,7 @@ function buildStrategySummary({
recentExecutionResults = [], recentExecutionResults = [],
recentQuoteOutcomes = [], recentQuoteOutcomes = [],
quoteLifecycleRollups = [], quoteLifecycleRollups = [],
quoteLifecycleStatistics = null,
}) { }) {
const strategyState = servicesByName['strategy-engine']?.state || {}; const strategyState = servicesByName['strategy-engine']?.state || {};
const executorState = servicesByName['trade-executor']?.state || {}; const executorState = servicesByName['trade-executor']?.state || {};
@ -1796,6 +1821,18 @@ function buildStrategySummary({
? recentDecisions ? recentDecisions
: [...durableDecisionsById.values()].slice(0, 20), : [...durableDecisionsById.values()].slice(0, 20),
recent_lifecycle_rows: lifecycleRows, recent_lifecycle_rows: lifecycleRows,
quote_lifecycle_statistics: quoteLifecycleStatistics || {
persisted: {
source: 'persisted',
generated_at: null,
rows: [],
},
live: {
source: 'live_recent',
generated_at: null,
rows: [],
},
},
trade_funnel: tradeFunnel, trade_funnel: tradeFunnel,
maker_competitiveness: makerCompetitiveness, maker_competitiveness: makerCompetitiveness,
skipped_counts: strategyState.skipped_counts || {}, skipped_counts: strategyState.skipped_counts || {},
@ -2051,6 +2088,18 @@ function buildSystemSummary({
|| historyWriterState.retention_summary || historyWriterState.retention_summary
|| null, || null,
quote_lifecycle_rollups: quoteLifecycleRollups || [], quote_lifecycle_rollups: quoteLifecycleRollups || [],
quote_lifecycle_statistics:
historyWriterState.latest_quote_lifecycle_statistics
|| null,
last_quote_lifecycle_statistics_at:
historyWriterState.last_quote_lifecycle_statistics_at
|| null,
quote_lifecycle_statistics_count:
historyWriterState.quote_lifecycle_statistics_count
?? null,
quote_lifecycle_statistics_error:
historyWriterState.quote_lifecycle_statistics_error
|| null,
last_retention_maintenance_at: last_retention_maintenance_at:
quoteLifecycleRetention?.latest_run?.completed_at quoteLifecycleRetention?.latest_run?.completed_at
|| historyWriterState.last_retention_maintenance_at || historyWriterState.last_retention_maintenance_at
@ -2687,6 +2736,14 @@ function buildQuoteLifecycleUpdate(state, { flashQuoteId = null } = {}) {
type: 'quote_lifecycle.updated', type: 'quote_lifecycle.updated',
recent_lifecycle_rows: lifecycleRows, recent_lifecycle_rows: lifecycleRows,
maker_competitiveness: buildMakerCompetitivenessSummary({ lifecycleRows, generatedAt: receivedAt }), maker_competitiveness: buildMakerCompetitivenessSummary({ lifecycleRows, generatedAt: receivedAt }),
quote_lifecycle_statistics: {
source: 'live_recent',
generated_at: receivedAt,
rows: buildQuoteLifecycleStatistics({
lifecycleRows,
computedAt: receivedAt,
}),
},
flash_quote_id: flashQuoteId || null, flash_quote_id: flashQuoteId || null,
received_at: receivedAt, received_at: receivedAt,
}; };

View file

@ -0,0 +1,474 @@
export const QUOTE_LIFECYCLE_STATISTIC_GRAINS = Object.freeze([
'all_time',
'month',
'week',
'day',
'hour',
'five_minute',
]);
export const QUOTE_LIFECYCLE_STATISTIC_BUCKETS = Object.freeze([
'success',
'not_filled',
'submission_failed',
'submitted_no_reply',
'awaiting_executor',
'approved_no_command',
'blocked_before_submit',
'rejected_by_strategy',
'observed_no_decision',
'unavailable',
]);
const BUCKET_RANK = Object.freeze({
unavailable: 0,
observed_no_decision: 1,
approved_no_command: 2,
awaiting_executor: 3,
rejected_by_strategy: 4,
blocked_before_submit: 4,
submitted_no_reply: 5,
submission_failed: 6,
not_filled: 7,
success: 8,
});
export function normalizeQuoteLifecycleStatisticsGrains(grains = QUOTE_LIFECYCLE_STATISTIC_GRAINS) {
const requested = Array.isArray(grains) ? grains : String(grains || '').split(',');
const normalized = requested
.map((grain) => String(grain || '').trim())
.filter((grain) => QUOTE_LIFECYCLE_STATISTIC_GRAINS.includes(grain));
return normalized.length ? [...new Set(normalized)] : [...QUOTE_LIFECYCLE_STATISTIC_GRAINS];
}
export function emptyQuoteLifecycleBucketCounts() {
return Object.fromEntries(QUOTE_LIFECYCLE_STATISTIC_BUCKETS.map((bucket) => [bucket, 0]));
}
export function classifyQuoteLifecycleStatisticsBucket(row = null) {
if (!row) return 'unavailable';
const lifecycleState = normalizeToken(row.lifecycle_state);
const executionStatus = normalizeToken(row.execution?.status || row.stage_details?.execution_status);
const outcomeStatus = normalizeToken(
row.outcome_status
|| row.outcome?.outcome_status
|| row.execution?.outcome_status
|| row.stage_details?.durable_outcome_status
|| row.stage_details?.execution_outcome_status,
);
const reasonCode = normalizeToken(
row.reason_code
|| row.execution?.failure_category
|| row.execution?.result_code
|| row.outcome?.outcome_reason,
);
if (
lifecycleState === 'completed'
|| ['completed', 'filled', 'settled', 'finalized'].includes(outcomeStatus)
) {
return 'success';
}
if (
lifecycleState === 'not_filled'
|| ['not_filled', 'expired', 'cancelled'].includes(outcomeStatus)
) {
return 'not_filled';
}
if (
lifecycleState === 'failed'
|| executionStatus === 'failed'
|| reasonCode === 'submission_failed'
|| reasonCode === 'quote_not_found_or_finished'
) {
return 'submission_failed';
}
if (
lifecycleState === 'submitted'
|| lifecycleState === 'awaiting_outcome'
|| executionStatus === 'submitted'
|| outcomeStatus === 'awaiting_outcome'
) {
return 'submitted_no_reply';
}
if (lifecycleState === 'command_emitted') return 'awaiting_executor';
if (lifecycleState === 'evaluated') return 'approved_no_command';
if (lifecycleState === 'blocked') return 'blocked_before_submit';
if (lifecycleState === 'rejected') return 'rejected_by_strategy';
if (lifecycleState === 'observed') return 'observed_no_decision';
return 'unavailable';
}
export function quoteLifecycleStatisticWindow(timestamp, grain) {
if (grain === 'all_time') {
return {
grain,
window_start: null,
window_end: null,
};
}
const date = new Date(timestamp || '');
if (Number.isNaN(date.getTime())) return null;
const start = new Date(date.getTime());
switch (grain) {
case 'five_minute':
start.setUTCSeconds(0, 0);
start.setUTCMinutes(Math.floor(start.getUTCMinutes() / 5) * 5);
break;
case 'hour':
start.setUTCMinutes(0, 0, 0);
break;
case 'day':
start.setUTCHours(0, 0, 0, 0);
break;
case 'week': {
start.setUTCHours(0, 0, 0, 0);
const daysSinceMonday = (start.getUTCDay() + 6) % 7;
start.setUTCDate(start.getUTCDate() - daysSinceMonday);
break;
}
case 'month':
start.setUTCDate(1);
start.setUTCHours(0, 0, 0, 0);
break;
default:
return null;
}
const end = new Date(start.getTime());
switch (grain) {
case 'five_minute':
end.setUTCMinutes(end.getUTCMinutes() + 5);
break;
case 'hour':
end.setUTCHours(end.getUTCHours() + 1);
break;
case 'day':
end.setUTCDate(end.getUTCDate() + 1);
break;
case 'week':
end.setUTCDate(end.getUTCDate() + 7);
break;
case 'month':
end.setUTCMonth(end.getUTCMonth() + 1);
break;
default:
return null;
}
return {
grain,
window_start: start.toISOString(),
window_end: end.toISOString(),
};
}
export function buildQuoteLifecycleStatistics({
lifecycleRows = [],
grains = QUOTE_LIFECYCLE_STATISTIC_GRAINS,
computedAt = new Date().toISOString(),
} = {}) {
const normalizedGrains = normalizeQuoteLifecycleStatisticsGrains(grains);
const groups = new Map();
const deduped = dedupeLifecycleRows(lifecycleRows);
if (normalizedGrains.includes('all_time')) {
ensureStatisticGroup(groups, {
grain: 'all_time',
window_start: null,
window_end: null,
computedAt,
});
}
for (const entry of deduped.identified) {
const row = entry.row;
const bucket = classifyQuoteLifecycleStatisticsBucket(row);
const activityAt = entry.first_activity_at;
addRowToStatisticGroup(groups, {
grain: 'all_time',
window_start: null,
window_end: null,
bucket,
computedAt,
hasQuoteIdentifier: true,
hasTimestamp: Boolean(activityAt),
enabled: normalizedGrains.includes('all_time'),
});
if (!activityAt) continue;
for (const grain of normalizedGrains.filter((candidate) => candidate !== 'all_time')) {
const window = quoteLifecycleStatisticWindow(activityAt, grain);
if (!window) continue;
addRowToStatisticGroup(groups, {
...window,
bucket,
computedAt,
hasQuoteIdentifier: true,
hasTimestamp: true,
enabled: true,
});
}
}
for (const row of deduped.missingIdentifier) {
const bucket = 'unavailable';
const activityAt = firstLifecycleTimestamp(row);
addRowToStatisticGroup(groups, {
grain: 'all_time',
window_start: null,
window_end: null,
bucket,
computedAt,
hasQuoteIdentifier: false,
hasTimestamp: Boolean(activityAt),
enabled: normalizedGrains.includes('all_time'),
});
if (!activityAt) continue;
for (const grain of normalizedGrains.filter((candidate) => candidate !== 'all_time')) {
const window = quoteLifecycleStatisticWindow(activityAt, grain);
if (!window) continue;
addRowToStatisticGroup(groups, {
...window,
bucket,
computedAt,
hasQuoteIdentifier: false,
hasTimestamp: true,
enabled: true,
});
}
}
return [...groups.values()]
.map((group) => ({
...group,
stat_id: quoteLifecycleStatisticId(group),
bucket_counts: normalizeBucketCounts(group.bucket_counts),
}))
.sort(compareStatisticRows);
}
export function normalizeQuoteLifecycleStatisticsRow(row = {}) {
return {
stat_id: row.stat_id || quoteLifecycleStatisticId(row),
grain: row.grain || 'all_time',
window_start: toIsoTimestamp(row.window_start),
window_end: toIsoTimestamp(row.window_end),
quote_count: Number(row.quote_count || 0),
missing_identifier_count: Number(row.missing_identifier_count || 0),
missing_timestamp_count: Number(row.missing_timestamp_count || 0),
bucket_counts: normalizeBucketCounts(row.bucket_counts),
computed_at: toIsoTimestamp(row.computed_at),
};
}
export function quoteLifecycleStatisticId(row = {}) {
const grain = row.grain || 'all_time';
const windowStart = row.window_start || 'all';
return `quote-lifecycle-stat:${grain}:${windowStart}`;
}
function dedupeLifecycleRows(rows = []) {
const identifiedByQuote = new Map();
const missingIdentifier = [];
for (const row of rows || []) {
const quoteId = String(row?.quote_id || '').trim();
if (!quoteId) {
missingIdentifier.push(row);
continue;
}
const activityAt = firstLifecycleTimestamp(row);
const bucket = classifyQuoteLifecycleStatisticsBucket(row);
const existing = identifiedByQuote.get(quoteId);
if (!existing) {
identifiedByQuote.set(quoteId, {
row,
first_activity_at: activityAt,
latest_stage_at: latestLifecycleTimestamp(row),
bucket,
});
continue;
}
const firstActivityAt = earlierTimestamp(existing.first_activity_at, activityAt);
const latestStageAt = laterTimestamp(existing.latest_stage_at, latestLifecycleTimestamp(row));
const existingRank = BUCKET_RANK[existing.bucket] ?? 0;
const nextRank = BUCKET_RANK[bucket] ?? 0;
const useNextRow = nextRank > existingRank
|| (nextRank === existingRank && timestampValue(latestLifecycleTimestamp(row)) >= timestampValue(existing.latest_stage_at));
identifiedByQuote.set(quoteId, {
row: useNextRow ? row : existing.row,
first_activity_at: firstActivityAt,
latest_stage_at: latestStageAt,
bucket: useNextRow ? bucket : existing.bucket,
});
}
return {
identified: [...identifiedByQuote.values()],
missingIdentifier,
};
}
function addRowToStatisticGroup(groups, {
grain,
window_start,
window_end,
bucket,
computedAt,
hasQuoteIdentifier,
hasTimestamp,
enabled,
}) {
if (!enabled) return;
const group = ensureStatisticGroup(groups, {
grain,
window_start,
window_end,
computedAt,
});
if (hasQuoteIdentifier) {
group.quote_count += 1;
} else {
group.missing_identifier_count += 1;
}
if (!hasTimestamp) group.missing_timestamp_count += 1;
group.bucket_counts[bucket] = Number(group.bucket_counts[bucket] || 0) + 1;
}
function ensureStatisticGroup(groups, {
grain,
window_start,
window_end,
computedAt,
}) {
const key = `${grain}:${window_start || 'all'}`;
if (!groups.has(key)) {
groups.set(key, {
stat_id: null,
grain,
window_start,
window_end,
quote_count: 0,
missing_identifier_count: 0,
missing_timestamp_count: 0,
bucket_counts: emptyQuoteLifecycleBucketCounts(),
computed_at: toIsoTimestamp(computedAt) || new Date().toISOString(),
});
}
return groups.get(key);
}
function compareStatisticRows(left, right) {
const grainOrder = new Map(QUOTE_LIFECYCLE_STATISTIC_GRAINS.map((grain, index) => [grain, index]));
const grainComparison = (grainOrder.get(left.grain) ?? 99) - (grainOrder.get(right.grain) ?? 99);
if (grainComparison !== 0) return grainComparison;
const windowComparison = timestampValue(right.window_start) - timestampValue(left.window_start);
return Number.isFinite(windowComparison) ? windowComparison : 0;
}
function firstLifecycleTimestamp(row = {}) {
return earliestTimestamp([
row.quote_activity_at,
row.quote_observed_at,
row.decision_at,
row.command_at,
row.execution_result_at,
row.outcome_observed_at,
row.latest_stage_at,
]);
}
function latestLifecycleTimestamp(row = {}) {
return latestTimestamp([
row.latest_stage_at,
row.outcome_observed_at,
row.execution_result_at,
row.command_at,
row.decision_at,
row.quote_observed_at,
row.quote_activity_at,
]);
}
function normalizeBucketCounts(value = {}) {
const source = typeof value === 'string' ? parseJsonObject(value) : value || {};
return Object.fromEntries(
QUOTE_LIFECYCLE_STATISTIC_BUCKETS.map((bucket) => [bucket, Number(source[bucket] || 0)]),
);
}
function parseJsonObject(value) {
try {
const parsed = JSON.parse(value);
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
} catch {
return {};
}
}
function normalizeToken(value) {
return String(value || '')
.trim()
.toLowerCase()
.replace(/[\s-]+/g, '_');
}
function timestampValue(value) {
const parsed = Date.parse(value || '');
return Number.isFinite(parsed) ? parsed : -Infinity;
}
function toIsoTimestamp(value) {
if (!value) return null;
const date = new Date(value);
return Number.isNaN(date.getTime()) ? null : date.toISOString();
}
function earliestTimestamp(values = []) {
let earliest = null;
for (const value of values) {
const iso = toIsoTimestamp(value);
if (!iso) continue;
earliest = earliestTimestampPair(earliest, iso);
}
return earliest;
}
function latestTimestamp(values = []) {
let latest = null;
for (const value of values) {
const iso = toIsoTimestamp(value);
if (!iso) continue;
latest = laterTimestamp(latest, iso);
}
return latest;
}
function earliestTimestampPair(left, right) {
if (!left) return toIsoTimestamp(right);
if (!right) return toIsoTimestamp(left);
return new Date(Math.min(timestampValue(left), timestampValue(right))).toISOString();
}
function earlierTimestamp(left, right) {
if (!left) return toIsoTimestamp(right);
if (!right) return toIsoTimestamp(left);
return new Date(Math.min(timestampValue(left), timestampValue(right))).toISOString();
}
function laterTimestamp(left, right) {
if (!left) return toIsoTimestamp(right);
if (!right) return toIsoTimestamp(left);
return new Date(Math.max(timestampValue(left), timestampValue(right))).toISOString();
}

View file

@ -1,8 +1,15 @@
import { Pool } from 'pg'; import { Pool } from 'pg';
import { deriveIntentRequestOutcomeRecords } from '../core/intent-request-outcomes.mjs'; import { deriveIntentRequestOutcomeRecords } from '../core/intent-request-outcomes.mjs';
import { deriveQuoteLifecycleRows } from '../core/operator-dashboard.mjs';
import { buildCashEquivalentValuationAssets } from '../core/portfolio-metrics.mjs'; import { buildCashEquivalentValuationAssets } from '../core/portfolio-metrics.mjs';
import { deriveQuoteOutcomeRecords } from '../core/quote-outcomes.mjs'; import { deriveQuoteOutcomeRecords } from '../core/quote-outcomes.mjs';
import {
QUOTE_LIFECYCLE_STATISTIC_GRAINS,
buildQuoteLifecycleStatistics,
normalizeQuoteLifecycleStatisticsGrains,
normalizeQuoteLifecycleStatisticsRow,
} from '../core/quote-lifecycle-statistics.mjs';
import { import {
DEFAULT_QUOTE_LIFECYCLE_RETENTION_POLICY, DEFAULT_QUOTE_LIFECYCLE_RETENTION_POLICY,
QUOTE_LIFECYCLE_DATA_CLASSES, QUOTE_LIFECYCLE_DATA_CLASSES,
@ -53,6 +60,7 @@ const RETENTION_POLICIES_TABLE = 'retention_policies';
const QUOTE_LIFECYCLE_ROLLUPS_TABLE = 'quote_lifecycle_rollups'; const QUOTE_LIFECYCLE_ROLLUPS_TABLE = 'quote_lifecycle_rollups';
const QUOTE_LIFECYCLE_ROLLUP_WATERMARKS_TABLE = 'quote_lifecycle_rollup_watermarks'; const QUOTE_LIFECYCLE_ROLLUP_WATERMARKS_TABLE = 'quote_lifecycle_rollup_watermarks';
const QUOTE_LIFECYCLE_RETENTION_RUNS_TABLE = 'quote_lifecycle_retention_runs'; const QUOTE_LIFECYCLE_RETENTION_RUNS_TABLE = 'quote_lifecycle_retention_runs';
const QUOTE_LIFECYCLE_STATISTICS_TABLE = 'quote_lifecycle_statistics';
const QUOTE_LIFECYCLE_ROLLUP_WINDOWS_PER_PASS = 48; const QUOTE_LIFECYCLE_ROLLUP_WINDOWS_PER_PASS = 48;
const SUPPORTED_ASSET_IMPORT_RUNS_TABLE = 'supported_asset_import_runs'; const SUPPORTED_ASSET_IMPORT_RUNS_TABLE = 'supported_asset_import_runs';
const TRADING_ASSETS_TABLE = 'trading_assets'; const TRADING_ASSETS_TABLE = 'trading_assets';
@ -389,6 +397,28 @@ export async function ensureQuoteLifecycleRetentionSchema(pool) {
ON ${QUOTE_LIFECYCLE_ROLLUPS_TABLE} (source_data_class, window_end DESC) ON ${QUOTE_LIFECYCLE_ROLLUPS_TABLE} (source_data_class, window_end DESC)
`); `);
await pool.query(`
CREATE TABLE IF NOT EXISTS ${QUOTE_LIFECYCLE_STATISTICS_TABLE} (
stat_id TEXT PRIMARY KEY,
grain TEXT NOT NULL,
window_start TIMESTAMPTZ,
window_end TIMESTAMPTZ,
quote_count INTEGER NOT NULL DEFAULT 0,
missing_identifier_count INTEGER NOT NULL DEFAULT 0,
missing_timestamp_count INTEGER NOT NULL DEFAULT 0,
bucket_counts JSONB NOT NULL DEFAULT '{}'::jsonb,
computed_at TIMESTAMPTZ NOT NULL
)
`);
await pool.query(`
CREATE INDEX IF NOT EXISTS ${QUOTE_LIFECYCLE_STATISTICS_TABLE}_grain_window_idx
ON ${QUOTE_LIFECYCLE_STATISTICS_TABLE} (grain, window_start DESC NULLS LAST)
`);
await pool.query(`
CREATE INDEX IF NOT EXISTS ${QUOTE_LIFECYCLE_STATISTICS_TABLE}_computed_at_idx
ON ${QUOTE_LIFECYCLE_STATISTICS_TABLE} (computed_at DESC)
`);
await pool.query(` await pool.query(`
CREATE TABLE IF NOT EXISTS ${QUOTE_LIFECYCLE_ROLLUP_WATERMARKS_TABLE} ( CREATE TABLE IF NOT EXISTS ${QUOTE_LIFECYCLE_ROLLUP_WATERMARKS_TABLE} (
rollup_key TEXT PRIMARY KEY, rollup_key TEXT PRIMARY KEY,
@ -3149,6 +3179,193 @@ export async function loadRecentQuoteLifecycleRollups(pool, { limit = 50 } = {})
return result.rows.map(normalizeQuoteLifecycleRollupRow); return result.rows.map(normalizeQuoteLifecycleRollupRow);
} }
export async function refreshQuoteLifecycleStatistics(pool, {
now = new Date().toISOString(),
grains = QUOTE_LIFECYCLE_STATISTIC_GRAINS,
} = {}) {
const evidence = await loadQuoteLifecycleStatisticsEvidence(pool);
const lifecycleRows = deriveQuoteLifecycleRows({
recentQuotes: evidence.recentQuotes,
recentTradeDecisions: evidence.recentTradeDecisions,
recentExecuteTradeCommands: evidence.recentExecuteTradeCommands,
recentExecutionResults: evidence.recentExecutionResults,
recentQuoteOutcomes: evidence.recentQuoteOutcomes,
limit: null,
});
const statistics = buildQuoteLifecycleStatistics({
lifecycleRows,
grains,
computedAt: now,
});
await upsertQuoteLifecycleStatistics(pool, statistics);
return {
computed_at: now,
quote_count: statistics.find((row) => row.grain === 'all_time')?.quote_count || 0,
statistic_row_count: statistics.length,
evidence_counts: {
quotes: evidence.recentQuotes.length,
decisions: evidence.recentTradeDecisions.length,
commands: evidence.recentExecuteTradeCommands.length,
execution_results: evidence.recentExecutionResults.length,
outcomes: evidence.recentQuoteOutcomes.length,
lifecycle_rows: lifecycleRows.length,
},
statistics,
};
}
export async function loadQuoteLifecycleStatistics(pool, {
grains = QUOTE_LIFECYCLE_STATISTIC_GRAINS,
limit = 96,
} = {}) {
const normalizedGrains = normalizeQuoteLifecycleStatisticsGrains(grains);
const normalizedLimit = Math.max(1, Math.min(1000, Number(limit) || 96));
const result = await pool.query(
`
WITH ranked AS (
SELECT
*,
ROW_NUMBER() OVER (
PARTITION BY grain
ORDER BY window_start DESC NULLS LAST
) AS grain_rank
FROM ${QUOTE_LIFECYCLE_STATISTICS_TABLE}
WHERE grain = ANY($1::text[])
)
SELECT *
FROM ranked
WHERE grain = 'all_time'
OR grain_rank <= $2
ORDER BY
CASE grain
WHEN 'all_time' THEN 0
WHEN 'month' THEN 1
WHEN 'week' THEN 2
WHEN 'day' THEN 3
WHEN 'hour' THEN 4
WHEN 'five_minute' THEN 5
ELSE 99
END,
window_start DESC NULLS LAST
`,
[normalizedGrains, normalizedLimit],
);
return result.rows.map(normalizeQuoteLifecycleStatisticsRow);
}
async function loadQuoteLifecycleStatisticsEvidence(pool) {
const [
quotesResult,
decisionsResult,
commandsResult,
executionResultsResult,
outcomesResult,
] = await Promise.all([
pool.query(`
SELECT observed_at, ingested_at, payload
FROM swap_demand_events
ORDER BY COALESCE(observed_at, ingested_at) ASC
`),
pool.query(`
SELECT observed_at, ingested_at, payload
FROM trade_decisions
ORDER BY COALESCE(observed_at, ingested_at) ASC
`),
pool.query(`
SELECT observed_at, ingested_at, payload
FROM execute_trade_commands
ORDER BY COALESCE(observed_at, ingested_at) ASC
`),
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,
o.payload AS outcome_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')
LEFT JOIN ${QUOTE_OUTCOMES_TABLE} o
ON o.quote_id = r.quote_id
ORDER BY COALESCE(r.observed_at, r.ingested_at) ASC
`),
pool.query(`
SELECT
quote_id,
decision_id,
command_id,
execution_result_status,
execution_result_code,
submitted_at,
command_at,
outcome_status,
outcome_observed_at,
outcome_source,
attribution_status,
attribution_method,
attributed_inventory_delta,
computed_at,
payload
FROM ${QUOTE_OUTCOMES_TABLE}
ORDER BY COALESCE(outcome_observed_at, computed_at) ASC
`),
]);
return {
recentQuotes: quotesResult.rows.map(normalizeRecentQuoteRow),
recentTradeDecisions: decisionsResult.rows.map((row) => ({
observed_at: toIsoTimestamp(row.observed_at),
ingested_at: toIsoTimestamp(row.ingested_at),
payload: row.payload,
})),
recentExecuteTradeCommands: commandsResult.rows.map((row) => normalizeExecuteTradeCommandRow(row)),
recentExecutionResults: executionResultsResult.rows.map((row) => normalizeExecutionResultRow(row)),
recentQuoteOutcomes: outcomesResult.rows.map((row) => normalizeQuoteOutcomeRow(row)),
};
}
async function upsertQuoteLifecycleStatistics(pool, statistics = []) {
for (const row of statistics || []) {
await pool.query(
`
INSERT INTO ${QUOTE_LIFECYCLE_STATISTICS_TABLE} (
stat_id,
grain,
window_start,
window_end,
quote_count,
missing_identifier_count,
missing_timestamp_count,
bucket_counts,
computed_at
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8::jsonb,$9)
ON CONFLICT (stat_id) DO UPDATE SET
quote_count = EXCLUDED.quote_count,
missing_identifier_count = EXCLUDED.missing_identifier_count,
missing_timestamp_count = EXCLUDED.missing_timestamp_count,
bucket_counts = EXCLUDED.bucket_counts,
computed_at = EXCLUDED.computed_at
`,
[
row.stat_id,
row.grain,
row.window_start,
row.window_end,
row.quote_count,
row.missing_identifier_count,
row.missing_timestamp_count,
JSON.stringify(row.bucket_counts || {}),
row.computed_at,
],
);
}
}
async function countSuccessfulQuoteEvidence(pool) { async function countSuccessfulQuoteEvidence(pool) {
const result = await pool.query(` const result = await pool.query(`
SELECT COUNT(*)::INT AS count SELECT COUNT(*)::INT AS count

View file

@ -31,6 +31,26 @@ const QUOTE_LIFECYCLE_PROBLEM_FILTERS = [
['strategy_rejected', 'Strategy rejected'], ['strategy_rejected', 'Strategy rejected'],
['submitted_awaiting_outcome', 'Submitted or awaiting outcome'], ['submitted_awaiting_outcome', 'Submitted or awaiting outcome'],
]; ];
const QUOTE_LIFECYCLE_STAT_GRAINS = [
['five_minute', '5m'],
['hour', 'Hour'],
['day', 'Day'],
['week', 'Week'],
['month', 'Month'],
['all_time', 'All-time'],
];
const QUOTE_LIFECYCLE_STAT_BUCKETS = [
['success', 'Success'],
['submitted_no_reply', 'Submitted no reply'],
['submission_failed', 'Submission failed'],
['awaiting_executor', 'Awaiting executor'],
['approved_no_command', 'Approved no command'],
['rejected_by_strategy', 'Rejected by strategy'],
['blocked_before_submit', 'Blocked before submit'],
['not_filled', 'Not filled'],
['observed_no_decision', 'Observed no decision'],
['unavailable', 'Unavailable'],
];
async function copyIdentifier(value) { async function copyIdentifier(value) {
if (!value || !navigator?.clipboard?.writeText) return; if (!value || !navigator?.clipboard?.writeText) return;
@ -57,6 +77,21 @@ async function fetchQuoteLifecycleLookup(identifier) {
return payload; return payload;
} }
async function fetchQuoteLifecycleStatistics() {
const response = await fetch('/api/strategy/quote-lifecycle/statistics?limit=160', {
headers: {
accept: 'application/json',
},
});
const text = await response.text();
const payload = text.trim() ? JSON.parse(text) : null;
if (!response.ok) {
throw new Error(payload?.error || text.trim() || `HTTP ${response.status}`);
}
return payload;
}
function useNow(intervalMs = 1000) { function useNow(intervalMs = 1000) {
const [now, setNow] = useState(() => Date.now()); const [now, setNow] = useState(() => Date.now());
@ -201,6 +236,41 @@ function plainCodeLabel(value, fallback = 'Unavailable') {
return text.replaceAll('_', ' '); return text.replaceAll('_', ' ');
} }
function formatInteger(value) {
const number = Number(value);
if (!Number.isFinite(number)) return '0';
return number.toLocaleString();
}
function statisticBucketCount(row, bucket) {
return Number(row?.bucket_counts?.[bucket] || 0);
}
function quoteLifecycleStatisticRows(statistics, source = 'persisted') {
return statistics?.[source]?.rows || [];
}
function latestStatisticRow(rows, grain) {
const matches = (rows || []).filter((row) => row?.grain === grain);
if (grain === 'all_time') return matches[0] || null;
return matches.sort((left, right) => (
Date.parse(right.window_start || '') - Date.parse(left.window_start || '')
))[0] || null;
}
function formatStatisticWindow(row) {
if (!row) return 'Unavailable';
if (row.grain === 'all_time') return 'All-time';
const start = formatTimestamp(row.window_start);
const end = formatTimestamp(row.window_end);
return `${start} to ${end}`;
}
function statisticFreshnessLabel(value) {
if (!value) return 'Unavailable';
return formatTimestamp(value);
}
function strategyDecisionStatus(decision) { function strategyDecisionStatus(decision) {
if (decision?.decision === 'approved') return 'Strategy approved'; if (decision?.decision === 'approved') return 'Strategy approved';
if (decision?.decision === 'rejected') return 'Strategy rejected'; if (decision?.decision === 'rejected') return 'Strategy rejected';
@ -852,10 +922,125 @@ function MakerCompetitivenessSection({ summary, pairConfig }) {
); );
} }
function QuoteLifecycleTable({ items }) { function QuoteLifecycleStatisticsPanel({
error,
grain,
liveRows,
onGrainChange,
onRefresh,
persistedRows,
state,
}) {
const allTime = latestStatisticRow(persistedRows, 'all_time');
const selectedRows = grain === 'all_time'
? [allTime].filter(Boolean)
: (persistedRows || []).filter((row) => row?.grain === grain).slice(0, 8);
const selectedCurrent = latestStatisticRow(persistedRows, grain);
const liveCurrent = latestStatisticRow(liveRows, grain);
const persistedGeneratedAt = persistedRows?.[0]?.computed_at || null;
const liveGeneratedAt = liveRows?.[0]?.computed_at || null;
const rows = fixedRows(selectedRows, grain === 'all_time' ? 1 : 8);
return (
<div className="quote-statistics-panel">
<div className="quote-statistics-head">
<label className="field quote-statistics-grain-field">
<span>Stats window</span>
<select value={grain} onChange={(event) => onGrainChange(event.target.value)}>
{QUOTE_LIFECYCLE_STAT_GRAINS.map(([value, label]) => (
<option key={value} value={value}>{label}</option>
))}
</select>
</label>
<button className="button secondary" disabled={state === 'loading'} onClick={onRefresh} type="button">
{state === 'loading' ? 'Refreshing stats' : 'Refresh stats'}
</button>
<Pill label={`Persisted ${statisticFreshnessLabel(persistedGeneratedAt)}`} stateLabel={persistedGeneratedAt ? 'healthy' : 'unknown'} />
<Pill label={`Live ${statisticFreshnessLabel(liveGeneratedAt)}`} stateLabel={liveGeneratedAt ? 'healthy' : 'unknown'} />
</div>
{error ? <div className="banner error">{error}</div> : null}
<div className="quote-statistics-summary">
<div>
<span>All-time unique quotes</span>
<strong>{formatInteger(allTime?.quote_count)}</strong>
</div>
<div>
<span>{grain === 'all_time' ? 'All-time selected' : 'Current persisted window'}</span>
<strong>{formatInteger(selectedCurrent?.quote_count)}</strong>
</div>
<div>
<span>Live recent window</span>
<strong>{formatInteger(liveCurrent?.quote_count)}</strong>
</div>
<div>
<span>Unavailable evidence</span>
<strong>{formatInteger((selectedCurrent?.missing_identifier_count || 0) + (selectedCurrent?.missing_timestamp_count || 0))}</strong>
</div>
</div>
<TableFrame className="quote-statistics-frame">
<table className="quote-statistics-table">
<thead>
<tr>
<th>Window</th>
<th>Unique quotes</th>
<th>Success</th>
<th>Submitted no reply</th>
<th>Submission failed</th>
<th>Awaiting executor</th>
<th>Rejected</th>
<th>Other / unavailable</th>
</tr>
</thead>
<tbody>
{rows.map((row, index) => {
if (!row) {
return (
<tr className="quote-statistics-placeholder-row" key={`quote-stat-placeholder:${index}`}>
<td colSpan={8}>{index === 0 ? 'No persisted quote statistics are available yet.' : ''}</td>
</tr>
);
}
const other = statisticBucketCount(row, 'approved_no_command')
+ statisticBucketCount(row, 'blocked_before_submit')
+ statisticBucketCount(row, 'not_filled')
+ statisticBucketCount(row, 'observed_no_decision');
const unavailable = statisticBucketCount(row, 'unavailable')
+ Number(row.missing_identifier_count || 0)
+ Number(row.missing_timestamp_count || 0);
return (
<tr key={row.stat_id || `${row.grain}:${row.window_start || index}`}>
<td>
<div>{formatStatisticWindow(row)}</div>
<div className="status-subtle">{plainCodeLabel(row.grain)}</div>
</td>
<td>{formatInteger(row.quote_count)}</td>
<td>{formatInteger(statisticBucketCount(row, 'success'))}</td>
<td>{formatInteger(statisticBucketCount(row, 'submitted_no_reply'))}</td>
<td>{formatInteger(statisticBucketCount(row, 'submission_failed'))}</td>
<td>{formatInteger(statisticBucketCount(row, 'awaiting_executor'))}</td>
<td>{formatInteger(statisticBucketCount(row, 'rejected_by_strategy'))}</td>
<td>
<div>{formatInteger(other)} other</div>
<div className="status-subtle">{formatInteger(unavailable)} unavailable</div>
</td>
</tr>
);
})}
</tbody>
</table>
</TableFrame>
</div>
);
}
function QuoteLifecycleTable({ items, statistics }) {
const [expanded, setExpanded] = useState(() => new Set()); const [expanded, setExpanded] = useState(() => new Set());
const [showStrategyRejected, setShowStrategyRejected] = useState(true); const [showStrategyRejected, setShowStrategyRejected] = useState(true);
const [problemFilter, setProblemFilter] = useState('all'); const [problemFilter, setProblemFilter] = useState('all');
const [statGrain, setStatGrain] = useState('five_minute');
const [statsState, setStatsState] = useState('idle');
const [statsError, setStatsError] = useState(null);
const [statisticsSnapshot, setStatisticsSnapshot] = useState(() => statistics || null);
const latestItemsRef = useRef(items || []); const latestItemsRef = useRef(items || []);
const [quoteDisplayPaused, setQuoteDisplayPaused] = useState(false); const [quoteDisplayPaused, setQuoteDisplayPaused] = useState(false);
const [displayItems, setDisplayItems] = useState(() => items || []); const [displayItems, setDisplayItems] = useState(() => items || []);
@ -872,6 +1057,14 @@ function QuoteLifecycleTable({ items }) {
selectedInvestigationRef.current = selectedInvestigation; selectedInvestigationRef.current = selectedInvestigation;
}, [selectedInvestigation]); }, [selectedInvestigation]);
useEffect(() => {
if (!statistics) return;
setStatisticsSnapshot((current) => ({
persisted: statistics.persisted || current?.persisted || { source: 'persisted', generated_at: null, rows: [] },
live: statistics.live || current?.live || { source: 'live_recent', generated_at: null, rows: [] },
}));
}, [statistics]);
useEffect(() => () => { useEffect(() => () => {
if (lookupTimerRef.current) window.clearTimeout(lookupTimerRef.current); if (lookupTimerRef.current) window.clearTimeout(lookupTimerRef.current);
}, []); }, []);
@ -1008,13 +1201,41 @@ function QuoteLifecycleTable({ items }) {
setDisplayNow(Date.now()); setDisplayNow(Date.now());
} }
async function refreshStatistics() {
setStatsState('loading');
setStatsError(null);
try {
const payload = await fetchQuoteLifecycleStatistics();
setStatisticsSnapshot((current) => ({
persisted: payload?.persisted || current?.persisted || { source: 'persisted', generated_at: null, rows: [] },
live: payload?.live || current?.live || { source: 'live_recent', generated_at: null, rows: [] },
}));
setStatsState('loaded');
} catch (error) {
setStatsState('error');
setStatsError(error?.message || 'Statistics refresh failed');
}
}
function toggleQuoteDisplayPaused() { function toggleQuoteDisplayPaused() {
if (quoteDisplayPaused) applyLatestLifecycleDisplay(); if (quoteDisplayPaused) applyLatestLifecycleDisplay();
setQuoteDisplayPaused((paused) => !paused); setQuoteDisplayPaused((paused) => !paused);
} }
const persistedStatisticRows = quoteLifecycleStatisticRows(statisticsSnapshot, 'persisted');
const liveStatisticRows = quoteLifecycleStatisticRows(statisticsSnapshot, 'live');
return ( return (
<> <>
<QuoteLifecycleStatisticsPanel
error={statsError}
grain={statGrain}
liveRows={liveStatisticRows}
onGrainChange={setStatGrain}
onRefresh={() => refreshStatistics().catch(() => {})}
persistedRows={persistedStatisticRows}
state={statsState}
/>
<form className="quote-lifecycle-lookup" onSubmit={submitLookup}> <form className="quote-lifecycle-lookup" onSubmit={submitLookup}>
<label className="field quote-lifecycle-lookup-field"> <label className="field quote-lifecycle-lookup-field">
<span>Lookup identifier</span> <span>Lookup identifier</span>
@ -1819,7 +2040,7 @@ function SuccessfulTradesSection({ funnel, counts }) {
); );
} }
function QuoteLifecycleSection({ items }) { function QuoteLifecycleSection({ items, statistics }) {
return ( return (
<section className="panel full-width-evidence-panel"> <section className="panel full-width-evidence-panel">
<div className="panel-head"> <div className="panel-head">
@ -1831,7 +2052,7 @@ function QuoteLifecycleSection({ items }) {
</div> </div>
</div> </div>
</div> </div>
<QuoteLifecycleTable items={items} /> <QuoteLifecycleTable items={items} statistics={statistics} />
</section> </section>
); );
} }
@ -1855,7 +2076,12 @@ export default function StrategyPage({ strategy, onControl, page = 'strategy' })
case 'strategy-trades': case 'strategy-trades':
return <SuccessfulTradesSection counts={counts} funnel={funnel} />; return <SuccessfulTradesSection counts={counts} funnel={funnel} />;
case 'strategy-lifecycle': case 'strategy-lifecycle':
return <QuoteLifecycleSection items={strategy.strategy_state.recent_lifecycle_rows} />; return (
<QuoteLifecycleSection
items={strategy.strategy_state.recent_lifecycle_rows}
statistics={strategy.strategy_state.quote_lifecycle_statistics}
/>
);
case 'strategy': case 'strategy':
default: default:
return <StrategyOverviewSection strategy={strategy} />; return <StrategyOverviewSection strategy={strategy} />;

View file

@ -19,6 +19,13 @@ function applySocketMessage(dashboard, payload, session) {
maker_competitiveness: maker_competitiveness:
payload.live.maker_competitiveness payload.live.maker_competitiveness
|| dashboard.strategy.strategy_state.maker_competitiveness, || dashboard.strategy.strategy_state.maker_competitiveness,
quote_lifecycle_statistics: {
...(dashboard.strategy.strategy_state.quote_lifecycle_statistics || {}),
live:
payload.live.quote_lifecycle_statistics
|| dashboard.strategy.strategy_state.quote_lifecycle_statistics?.live
|| null,
},
}, },
} : dashboard.strategy, } : dashboard.strategy,
status_bar: { status_bar: {
@ -53,6 +60,13 @@ function applySocketMessage(dashboard, payload, session) {
maker_competitiveness: maker_competitiveness:
payload.maker_competitiveness payload.maker_competitiveness
|| dashboard.strategy.strategy_state.maker_competitiveness, || dashboard.strategy.strategy_state.maker_competitiveness,
quote_lifecycle_statistics: {
...(dashboard.strategy.strategy_state.quote_lifecycle_statistics || {}),
live:
payload.quote_lifecycle_statistics
|| dashboard.strategy.strategy_state.quote_lifecycle_statistics?.live
|| null,
},
}, },
}, },
}, },

View file

@ -610,6 +610,77 @@ table.lifecycle-table th:nth-child(5) {
table-layout: fixed; table-layout: fixed;
} }
.quote-statistics-panel {
display: grid;
gap: 12px;
margin-bottom: 14px;
padding: 14px;
border: 1px solid var(--line);
border-radius: 8px;
background: rgba(255, 255, 255, 0.46);
}
.quote-statistics-head {
display: flex;
flex-wrap: wrap;
align-items: end;
gap: 10px;
}
.quote-statistics-grain-field {
min-width: 180px;
}
.quote-statistics-grain-field span {
font-size: 0.84rem;
color: var(--muted);
}
.quote-statistics-summary {
display: grid;
gap: 10px;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}
.quote-statistics-summary > div {
min-width: 0;
padding: 10px 12px;
border: 1px solid var(--line);
border-radius: 8px;
background: rgba(255, 255, 255, 0.42);
}
.quote-statistics-summary span {
display: block;
color: var(--muted);
font-size: 0.82rem;
}
.quote-statistics-summary strong {
display: block;
margin-top: 4px;
font-size: 1.25rem;
font-variant-numeric: tabular-nums;
}
.quote-statistics-table {
min-width: 980px;
table-layout: fixed;
}
.quote-statistics-table td {
font-variant-numeric: tabular-nums;
}
.quote-statistics-table th:nth-child(1),
.quote-statistics-table td:nth-child(1) {
width: 260px;
}
.quote-statistics-placeholder-row td {
color: var(--muted);
}
.quote-lifecycle-controls { .quote-lifecycle-controls {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;

View file

@ -22,6 +22,13 @@ test('history writer replays durable topics but joins the raw quote firehose liv
assert.match(source, /insertHistoryEvents/); assert.match(source, /insertHistoryEvents/);
assert.match(source, /quoteLifecycleRetentionMaintenanceIntervalMs\s*=\s*60 \* 1000/); assert.match(source, /quoteLifecycleRetentionMaintenanceIntervalMs\s*=\s*60 \* 1000/);
assert.match(source, /runQuoteLifecycleRetentionMaintenance/); assert.match(source, /runQuoteLifecycleRetentionMaintenance/);
assert.match(source, /refreshQuoteLifecycleStatistics/);
assert.match(source, /refreshQuoteLifecycleStatisticsState\(\{[\s\S]*persist:\s*true/);
assert.match(source, /const statisticsResult = await refreshQuoteLifecycleStatisticsState/);
assert.match(source, /result\.quote_lifecycle_statistics = statisticsResult/);
assert.match(source, /last_quote_lifecycle_statistics_at/);
assert.match(source, /quote_lifecycle_statistics_count/);
assert.match(source, /quote_lifecycle_statistics_error/);
assert.match(source, /loadQuoteLifecycleRetentionSummary/); assert.match(source, /loadQuoteLifecycleRetentionSummary/);
assert.match(source, /maybeRunQuoteLifecycleRetentionMaintenance/); assert.match(source, /maybeRunQuoteLifecycleRetentionMaintenance/);
assert.match(source, /quoteLifecycleRetentionMaintenanceInFlight/); assert.match(source, /quoteLifecycleRetentionMaintenanceInFlight/);

View file

@ -47,6 +47,14 @@ test('operator dashboard exposes authenticated durable quote lifecycle lookup AP
assert.match(source, /payload\.ok \? 200 : 404/); assert.match(source, /payload\.ok \? 200 : 404/);
}); });
test('operator dashboard exposes authenticated persisted quote lifecycle statistics API', () => {
assert.match(source, /\/api\/strategy\/quote-lifecycle\/statistics/);
assert.match(source, /loadQuoteLifecycleStatisticsPayload/);
assert.match(source, /refreshQuoteLifecycleStatistics/);
assert.match(source, /loadQuoteLifecycleStatistics/);
assert.match(source, /quote_lifecycle_statistics/);
});
test('operator dashboard bootstrap does not synchronously refresh intent request outcomes', () => { test('operator dashboard bootstrap does not synchronously refresh intent request outcomes', () => {
assert.match(source, /loadRecentIntentRequests\(pool, \{[\s\S]*refreshOutcomes: false/); assert.match(source, /loadRecentIntentRequests\(pool, \{[\s\S]*refreshOutcomes: false/);
}); });

View file

@ -40,6 +40,17 @@ test('strategy page owns consolidated quote lifecycle and successful trade table
assert.match(strategySource, /fetchQuoteLifecycleLookup/); assert.match(strategySource, /fetchQuoteLifecycleLookup/);
assert.match(strategySource, /\/api\/strategy\/quote-lifecycle\//); assert.match(strategySource, /\/api\/strategy\/quote-lifecycle\//);
assert.match(strategySource, /QUOTE_LIFECYCLE_PROBLEM_FILTERS/); assert.match(strategySource, /QUOTE_LIFECYCLE_PROBLEM_FILTERS/);
assert.match(strategySource, /QUOTE_LIFECYCLE_STAT_GRAINS/);
assert.match(strategySource, /QUOTE_LIFECYCLE_STAT_BUCKETS/);
assert.match(strategySource, /fetchQuoteLifecycleStatistics/);
assert.match(strategySource, /\/api\/strategy\/quote-lifecycle\/statistics/);
assert.match(strategySource, /five_minute/);
assert.match(strategySource, /all_time/);
assert.match(strategySource, /submitted_no_reply/);
assert.match(strategySource, /submission_failed/);
assert.match(strategySource, /success/);
assert.match(strategySource, /quote_lifecycle_statistics/);
assert.match(strategySource, /Refresh stats/);
assert.match(strategySource, /awaiting_executor/); assert.match(strategySource, /awaiting_executor/);
assert.match(strategySource, /quote_not_found_or_finished/); assert.match(strategySource, /quote_not_found_or_finished/);
assert.match(strategySource, /approved_no_command/); assert.match(strategySource, /approved_no_command/);
@ -53,6 +64,8 @@ test('strategy page owns consolidated quote lifecycle and successful trade table
assert.match(strategySource, /visibleRows/); assert.match(strategySource, /visibleRows/);
assert.match(strategySource, /quote-lifecycle-placeholder-row/); assert.match(strategySource, /quote-lifecycle-placeholder-row/);
assert.match(stylesSource, /\.quote-investigator-panel/); assert.match(stylesSource, /\.quote-investigator-panel/);
assert.match(stylesSource, /\.quote-statistics-panel/);
assert.match(stylesSource, /\.quote-statistics-table/);
assert.match(stylesSource, /\.quote-lifecycle-lookup/); assert.match(stylesSource, /\.quote-lifecycle-lookup/);
assert.match(stylesSource, /\.quote-lifecycle-table tbody tr\.quote-lifecycle-row\.is-selected/); assert.match(stylesSource, /\.quote-lifecycle-table tbody tr\.quote-lifecycle-row\.is-selected/);
assert.match(stylesSource, /\.shell \{[\s\S]*?width: 100%;[\s\S]*?margin: 0;[\s\S]*?\}/); assert.match(stylesSource, /\.shell \{[\s\S]*?width: 100%;[\s\S]*?margin: 0;[\s\S]*?\}/);

View file

@ -310,6 +310,26 @@ test('dashboard bootstrap exposes quote lifecycle retention and rollup storage t
quote_count: 4, quote_count: 4,
completed_count: 0, completed_count: 0,
}], }],
quoteLifecycleStatistics: {
persisted: {
source: 'persisted',
generated_at: '2026-06-06T11:00:00.000Z',
rows: [{
grain: 'all_time',
quote_count: 4,
bucket_counts: { submission_failed: 4 },
}],
},
live: {
source: 'live_recent',
generated_at: '2026-06-06T11:00:01.000Z',
rows: [{
grain: 'five_minute',
quote_count: 1,
bucket_counts: { awaiting_executor: 1 },
}],
},
},
recentIntentRequests: [], recentIntentRequests: [],
recentAlertTransitions: [], recentAlertTransitions: [],
recentEnvironmentStatuses: [], recentEnvironmentStatuses: [],
@ -329,6 +349,8 @@ test('dashboard bootstrap exposes quote lifecycle retention and rollup storage t
assert.equal(payload.system.persistence.preserved_successful_quote_count, 2); assert.equal(payload.system.persistence.preserved_successful_quote_count, 2);
assert.equal(payload.system.persistence.quote_lifecycle_rollups[0].rollup_id, 'rollup-1'); assert.equal(payload.system.persistence.quote_lifecycle_rollups[0].rollup_id, 'rollup-1');
assert.equal(payload.strategy.strategy_state.maker_competitiveness.persisted_rollup_count, 1); assert.equal(payload.strategy.strategy_state.maker_competitiveness.persisted_rollup_count, 1);
assert.equal(payload.strategy.strategy_state.quote_lifecycle_statistics.persisted.rows[0].quote_count, 4);
assert.equal(payload.strategy.strategy_state.quote_lifecycle_statistics.live.rows[0].bucket_counts.awaiting_executor, 1);
}); });
test('basic auth resolves operator identity and reuses a session cookie', () => { test('basic auth resolves operator identity and reuses a session cookie', () => {
@ -572,6 +594,60 @@ test('socket lifecycle messages replace strategy rows without page refresh', ()
assert.equal(next.dashboard.strategy.strategy_state.recent_lifecycle_rows[0].live_flash_at, '2026-04-04T09:00:00.000Z'); assert.equal(next.dashboard.strategy.strategy_state.recent_lifecycle_rows[0].live_flash_at, '2026-04-04T09:00:00.000Z');
}); });
test('socket lifecycle statistics update live counts while preserving persisted totals', () => {
const dashboard = {
funds: { recent_quotes: [] },
status_bar: {},
strategy: {
strategy_state: {
recent_lifecycle_rows: [],
quote_lifecycle_statistics: {
persisted: {
source: 'persisted',
rows: [{
grain: 'all_time',
quote_count: 12,
bucket_counts: { success: 2 },
}],
},
live: {
source: 'live_recent',
rows: [],
},
},
},
},
};
const state = {
dashboard,
session: { authenticated: true },
page: 'strategy-lifecycle',
};
const next = dashboardReducer(state, {
type: 'socket.message.received',
payload: {
type: 'quote_lifecycle.updated',
recent_lifecycle_rows: [],
quote_lifecycle_statistics: {
source: 'live_recent',
rows: [{
grain: 'five_minute',
quote_count: 1,
bucket_counts: { awaiting_executor: 1 },
}],
},
},
});
assert.equal(next.dashboard.strategy.strategy_state.quote_lifecycle_statistics.persisted.rows[0].quote_count, 12);
assert.equal(next.dashboard.strategy.strategy_state.quote_lifecycle_statistics.live.rows[0].quote_count, 1);
assert.equal(
next.dashboard.strategy.strategy_state.quote_lifecycle_statistics.live.rows[0].bucket_counts.awaiting_executor,
1,
);
});
test('selected awaiting executor quote remains inspectable after live rows move and refreshes from durable result', () => { test('selected awaiting executor quote remains inspectable after live rows move and refreshes from durable result', () => {
const config = buildConfig(); const config = buildConfig();
const awaiting = buildQuoteLifecycleLookupResponse({ const awaiting = buildQuoteLifecycleLookupResponse({

View file

@ -0,0 +1,154 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
loadQuoteLifecycleStatistics,
refreshQuoteLifecycleStatistics,
} from '../src/lib/postgres.mjs';
test('postgres quote lifecycle statistics refresh idempotently upserts state buckets by original quote window', async () => {
const upserts = [];
let includeFailure = false;
const pool = {
async query(sql, params = []) {
if (sql.includes('FROM swap_demand_events') && sql.includes('ORDER BY COALESCE')) {
return {
rows: [{
observed_at: '2026-06-12T17:31:01.000Z',
ingested_at: '2026-06-12T17:31:01.000Z',
payload: {
quote_id: 'quote-stat',
pair: 'btc->eure',
asset_in: 'btc',
asset_out: 'eure',
amount_in: '1',
amount_out: '2',
},
}],
};
}
if (sql.includes('FROM trade_decisions') && sql.includes('ORDER BY COALESCE')) {
return {
rows: [{
observed_at: '2026-06-12T17:31:02.000Z',
ingested_at: '2026-06-12T17:31:02.000Z',
payload: {
quote_id: 'quote-stat',
decision_id: 'decision-stat',
pair: 'btc->eure',
decision: 'approved',
decision_reason: 'strategy_approved',
},
}],
};
}
if (sql.includes('FROM execute_trade_commands') && sql.includes('ORDER BY COALESCE')) {
return {
rows: [{
observed_at: '2026-06-12T17:31:03.000Z',
ingested_at: '2026-06-12T17:31:03.000Z',
payload: {
quote_id: 'quote-stat',
decision_id: 'decision-stat',
command_id: 'cmd-stat',
pair: 'btc->eure',
},
}],
};
}
if (sql.includes('FROM trade_execution_results r')) {
return {
rows: includeFailure ? [{
result_observed_at: '2026-06-12T17:36:10.000Z',
result_ingested_at: '2026-06-12T17:36:10.000Z',
result_payload: {
quote_id: 'quote-stat',
decision_id: 'decision-stat',
command_id: 'cmd-stat',
status: 'failed',
result_code: 'submission_failed',
failure_category: 'quote_not_found_or_finished',
},
command_ingested_at: '2026-06-12T17:31:03.000Z',
command_payload: {
quote_id: 'quote-stat',
decision_id: 'decision-stat',
command_id: 'cmd-stat',
},
decision_payload: {
quote_id: 'quote-stat',
decision_id: 'decision-stat',
decision: 'approved',
},
outcome_payload: null,
}] : [],
};
}
if (sql.includes('FROM quote_outcome_attributions') && sql.includes('ORDER BY COALESCE')) {
return { rows: [] };
}
if (sql.includes('INSERT INTO quote_lifecycle_statistics')) {
upserts.push({ sql, params });
return { rows: [], rowCount: 1 };
}
throw new Error(`unexpected query: ${sql}`);
},
};
await refreshQuoteLifecycleStatistics(pool, {
now: '2026-06-12T17:32:00.000Z',
});
includeFailure = true;
await refreshQuoteLifecycleStatistics(pool, {
now: '2026-06-12T17:37:00.000Z',
});
const fiveMinuteUpserts = upserts.filter((query) => (
query.params[1] === 'five_minute'
&& query.params[2] === '2026-06-12T17:30:00.000Z'
));
assert.equal(fiveMinuteUpserts.length, 2);
assert.equal(fiveMinuteUpserts.at(-1).params[4], 1);
const latestBuckets = JSON.parse(fiveMinuteUpserts.at(-1).params[7]);
assert.equal(latestBuckets.submission_failed, 1);
assert.equal(latestBuckets.awaiting_executor, 0);
assert.equal(
upserts.some((query) => (
query.params[1] === 'five_minute'
&& query.params[2] === '2026-06-12T17:35:00.000Z'
)),
false,
);
});
test('postgres quote lifecycle statistics loader returns all-time and clamped grain rows', async () => {
const pool = {
async query(sql, params = []) {
assert.match(sql, /FROM quote_lifecycle_statistics/);
assert.deepEqual(params, [['all_time', 'five_minute'], 2]);
return {
rows: [{
stat_id: 'quote-lifecycle-stat:all_time:all',
grain: 'all_time',
window_start: null,
window_end: null,
quote_count: 3,
missing_identifier_count: 1,
missing_timestamp_count: 0,
bucket_counts: { success: 1, submission_failed: 2 },
computed_at: '2026-06-12T17:40:00.000Z',
}],
};
},
};
const rows = await loadQuoteLifecycleStatistics(pool, {
grains: ['all_time', 'five_minute'],
limit: 2,
});
assert.equal(rows.length, 1);
assert.equal(rows[0].quote_count, 3);
assert.equal(rows[0].bucket_counts.success, 1);
assert.equal(rows[0].bucket_counts.submitted_no_reply, 0);
});

View file

@ -0,0 +1,106 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
buildQuoteLifecycleStatistics,
classifyQuoteLifecycleStatisticsBucket,
quoteLifecycleStatisticWindow,
} from '../src/core/quote-lifecycle-statistics.mjs';
test('quote lifecycle statistics classify states without upgrading submitted-only rows to success', () => {
assert.equal(classifyQuoteLifecycleStatisticsBucket({
lifecycle_state: 'submitted',
execution: { status: 'submitted', result_code: 'quote_response_ok' },
}), 'submitted_no_reply');
assert.equal(classifyQuoteLifecycleStatisticsBucket({
lifecycle_state: 'failed',
execution: { status: 'failed', failure_category: 'quote_not_found_or_finished' },
}), 'submission_failed');
assert.equal(classifyQuoteLifecycleStatisticsBucket({
lifecycle_state: 'command_emitted',
}), 'awaiting_executor');
assert.equal(classifyQuoteLifecycleStatisticsBucket({
lifecycle_state: 'completed',
outcome_status: 'completed',
has_settlement_evidence: true,
}), 'success');
});
test('quote lifecycle statistics use UTC grain boundaries with Monday UTC weeks', () => {
assert.deepEqual(quoteLifecycleStatisticWindow('2026-06-12T17:34:44.123Z', 'five_minute'), {
grain: 'five_minute',
window_start: '2026-06-12T17:30:00.000Z',
window_end: '2026-06-12T17:35:00.000Z',
});
assert.deepEqual(quoteLifecycleStatisticWindow('2026-06-14T23:59:00.000Z', 'week'), {
grain: 'week',
window_start: '2026-06-08T00:00:00.000Z',
window_end: '2026-06-15T00:00:00.000Z',
});
assert.deepEqual(quoteLifecycleStatisticWindow('2026-06-01T00:00:00.000Z', 'month'), {
grain: 'month',
window_start: '2026-06-01T00:00:00.000Z',
window_end: '2026-07-01T00:00:00.000Z',
});
});
test('quote lifecycle statistics dedupe quotes and refresh later results in the original quote window', () => {
const rows = [
{
quote_id: 'quote-follow',
lifecycle_state: 'command_emitted',
quote_activity_at: '2026-06-12T17:31:01.000Z',
latest_stage_at: '2026-06-12T17:31:03.000Z',
},
{
quote_id: 'quote-follow',
lifecycle_state: 'failed',
quote_activity_at: '2026-06-12T17:31:01.000Z',
latest_stage_at: '2026-06-12T17:36:10.000Z',
execution: {
status: 'failed',
failure_category: 'quote_not_found_or_finished',
},
},
];
const statistics = buildQuoteLifecycleStatistics({
lifecycleRows: rows,
computedAt: '2026-06-12T17:40:00.000Z',
});
const allTime = statistics.find((row) => row.grain === 'all_time');
const fiveMinute = statistics.find((row) => (
row.grain === 'five_minute'
&& row.window_start === '2026-06-12T17:30:00.000Z'
));
const laterFiveMinute = statistics.find((row) => (
row.grain === 'five_minute'
&& row.window_start === '2026-06-12T17:35:00.000Z'
));
assert.equal(allTime.quote_count, 1);
assert.equal(allTime.bucket_counts.submission_failed, 1);
assert.equal(allTime.bucket_counts.awaiting_executor, 0);
assert.equal(fiveMinute.quote_count, 1);
assert.equal(fiveMinute.bucket_counts.submission_failed, 1);
assert.equal(laterFiveMinute, undefined);
});
test('quote lifecycle statistics preserve missing identifiers as unavailable evidence', () => {
const statistics = buildQuoteLifecycleStatistics({
lifecycleRows: [{
quote_id: null,
lifecycle_state: 'observed',
quote_activity_at: '2026-06-12T17:31:01.000Z',
}],
computedAt: '2026-06-12T17:40:00.000Z',
});
const allTime = statistics.find((row) => row.grain === 'all_time');
const fiveMinute = statistics.find((row) => row.grain === 'five_minute');
assert.equal(allTime.quote_count, 0);
assert.equal(allTime.missing_identifier_count, 1);
assert.equal(allTime.bucket_counts.unavailable, 1);
assert.equal(fiveMinute.quote_count, 0);
assert.equal(fiveMinute.missing_identifier_count, 1);
});