Retain quote lifecycle statistics across pruning
All checks were successful
deploy / deploy (push) Successful in 59s
All checks were successful
deploy / deploy (push) Successful in 59s
Proof: added persisted quote_lifecycle_stat_subjects snapshots, refreshed statistics from retained per-quote subjects, preserved completed/attributed execution evidence during lifecycle retention, and wired successful lifecycle evidence into the dashboard trade funnel; verified with targeted lifecycle/dashboard tests, full npm test, and operator dashboard bundle build. Assumptions: detailed successful trade rows already deleted by prior retention cannot be reconstructed from aggregate rollups without faking quote-level settlement evidence; the new subject table starts preserving surviving and future quote evidence after deployment. Still fake: fee-aware realized PnL and venue-native fill truth beyond retained settlement/inventory evidence remain out of scope; already-pruned detailed success rows remain unrecoverable.
This commit is contained in:
parent
75d7f64627
commit
503a69e71c
8 changed files with 763 additions and 16 deletions
|
|
@ -59,6 +59,7 @@ import {
|
||||||
loadRecentQuoteOutcomes,
|
loadRecentQuoteOutcomes,
|
||||||
loadRecentTradeDecisions,
|
loadRecentTradeDecisions,
|
||||||
loadRecentQuotes,
|
loadRecentQuotes,
|
||||||
|
loadSuccessfulQuoteLifecycleEvidence,
|
||||||
loadSubmissionPage,
|
loadSubmissionPage,
|
||||||
loadSubmissionSummary,
|
loadSubmissionSummary,
|
||||||
pauseTradingPair,
|
pauseTradingPair,
|
||||||
|
|
@ -495,6 +496,7 @@ async function loadBootstrapPayload({ auth, page, pageSize }) {
|
||||||
recentExecuteTradeCommands,
|
recentExecuteTradeCommands,
|
||||||
recentExecutionResults,
|
recentExecutionResults,
|
||||||
recentQuoteOutcomes,
|
recentQuoteOutcomes,
|
||||||
|
successfulQuoteLifecycle,
|
||||||
quoteLifecycleRetention,
|
quoteLifecycleRetention,
|
||||||
quoteLifecycleRollups,
|
quoteLifecycleRollups,
|
||||||
quoteLifecycleStatistics,
|
quoteLifecycleStatistics,
|
||||||
|
|
@ -569,6 +571,12 @@ async function loadBootstrapPayload({ auth, page, pageSize }) {
|
||||||
[],
|
[],
|
||||||
sourceErrors,
|
sourceErrors,
|
||||||
),
|
),
|
||||||
|
safeSourceLoad(
|
||||||
|
'successful_quote_lifecycle',
|
||||||
|
() => loadSuccessfulQuoteLifecycleEvidence(pool, { limit: 50 }),
|
||||||
|
null,
|
||||||
|
sourceErrors,
|
||||||
|
),
|
||||||
safeSourceLoad(
|
safeSourceLoad(
|
||||||
'quote_lifecycle_retention',
|
'quote_lifecycle_retention',
|
||||||
() => loadQuoteLifecycleRetentionSummary(pool),
|
() => loadQuoteLifecycleRetentionSummary(pool),
|
||||||
|
|
@ -631,6 +639,7 @@ async function loadBootstrapPayload({ auth, page, pageSize }) {
|
||||||
recentExecuteTradeCommands,
|
recentExecuteTradeCommands,
|
||||||
recentExecutionResults,
|
recentExecutionResults,
|
||||||
recentQuoteOutcomes,
|
recentQuoteOutcomes,
|
||||||
|
successfulQuoteLifecycle,
|
||||||
quoteLifecycleRetention,
|
quoteLifecycleRetention,
|
||||||
quoteLifecycleRollups,
|
quoteLifecycleRollups,
|
||||||
quoteLifecycleStatistics: {
|
quoteLifecycleStatistics: {
|
||||||
|
|
|
||||||
|
|
@ -558,6 +558,7 @@ export function buildDashboardBootstrap({
|
||||||
recentExecuteTradeCommands,
|
recentExecuteTradeCommands,
|
||||||
recentExecutionResults,
|
recentExecutionResults,
|
||||||
recentQuoteOutcomes = [],
|
recentQuoteOutcomes = [],
|
||||||
|
successfulQuoteLifecycle = null,
|
||||||
quoteLifecycleRetention = null,
|
quoteLifecycleRetention = null,
|
||||||
quoteLifecycleRollups = [],
|
quoteLifecycleRollups = [],
|
||||||
quoteLifecycleStatistics = null,
|
quoteLifecycleStatistics = null,
|
||||||
|
|
@ -655,6 +656,7 @@ export function buildDashboardBootstrap({
|
||||||
recentExecuteTradeCommands,
|
recentExecuteTradeCommands,
|
||||||
recentExecutionResults,
|
recentExecutionResults,
|
||||||
recentQuoteOutcomes,
|
recentQuoteOutcomes,
|
||||||
|
successfulQuoteLifecycle,
|
||||||
quoteLifecycleRollups,
|
quoteLifecycleRollups,
|
||||||
quoteLifecycleStatistics,
|
quoteLifecycleStatistics,
|
||||||
}),
|
}),
|
||||||
|
|
@ -1751,6 +1753,7 @@ function buildStrategySummary({
|
||||||
recentExecuteTradeCommands = [],
|
recentExecuteTradeCommands = [],
|
||||||
recentExecutionResults = [],
|
recentExecutionResults = [],
|
||||||
recentQuoteOutcomes = [],
|
recentQuoteOutcomes = [],
|
||||||
|
successfulQuoteLifecycle = null,
|
||||||
quoteLifecycleRollups = [],
|
quoteLifecycleRollups = [],
|
||||||
quoteLifecycleStatistics = null,
|
quoteLifecycleStatistics = null,
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -1788,12 +1791,20 @@ function buildStrategySummary({
|
||||||
|| durableDecisionsById.get(strategyState.latest_decision?.decision_id)?.decision_at
|
|| durableDecisionsById.get(strategyState.latest_decision?.decision_id)?.decision_at
|
||||||
|| null,
|
|| null,
|
||||||
});
|
});
|
||||||
const allLifecycleRows = deriveQuoteLifecycleRows({
|
const lifecycleEvidence = mergeSuccessfulQuoteLifecycleEvidence({
|
||||||
recentQuotes,
|
recentQuotes,
|
||||||
recentTradeDecisions,
|
recentTradeDecisions,
|
||||||
recentExecuteTradeCommands,
|
recentExecuteTradeCommands,
|
||||||
recentExecutionResults,
|
recentExecutionResults,
|
||||||
recentQuoteOutcomes,
|
recentQuoteOutcomes,
|
||||||
|
successfulQuoteLifecycle,
|
||||||
|
});
|
||||||
|
const allLifecycleRows = deriveQuoteLifecycleRows({
|
||||||
|
recentQuotes: lifecycleEvidence.recentQuotes,
|
||||||
|
recentTradeDecisions: lifecycleEvidence.recentTradeDecisions,
|
||||||
|
recentExecuteTradeCommands: lifecycleEvidence.recentExecuteTradeCommands,
|
||||||
|
recentExecutionResults: lifecycleEvidence.recentExecutionResults,
|
||||||
|
recentQuoteOutcomes: lifecycleEvidence.recentQuoteOutcomes,
|
||||||
limit: null,
|
limit: null,
|
||||||
}).map((row) => enrichLifecycleRowForUi({ config, row }));
|
}).map((row) => enrichLifecycleRowForUi({ config, row }));
|
||||||
const lifecycleRows = allLifecycleRows.slice(0, 20);
|
const lifecycleRows = allLifecycleRows.slice(0, 20);
|
||||||
|
|
@ -2000,6 +2011,66 @@ function buildFallbackPairConfig(config) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mergeSuccessfulQuoteLifecycleEvidence({
|
||||||
|
recentQuotes = [],
|
||||||
|
recentTradeDecisions = [],
|
||||||
|
recentExecuteTradeCommands = [],
|
||||||
|
recentExecutionResults = [],
|
||||||
|
recentQuoteOutcomes = [],
|
||||||
|
successfulQuoteLifecycle = null,
|
||||||
|
} = {}) {
|
||||||
|
if (!successfulQuoteLifecycle?.found) {
|
||||||
|
return {
|
||||||
|
recentQuotes,
|
||||||
|
recentTradeDecisions,
|
||||||
|
recentExecuteTradeCommands,
|
||||||
|
recentExecutionResults,
|
||||||
|
recentQuoteOutcomes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
recentQuotes: mergeLifecycleEvidenceRows(
|
||||||
|
recentQuotes,
|
||||||
|
successfulQuoteLifecycle.recentQuotes,
|
||||||
|
(row) => row?.quote_id,
|
||||||
|
),
|
||||||
|
recentTradeDecisions: mergeLifecycleEvidenceRows(
|
||||||
|
recentTradeDecisions,
|
||||||
|
successfulQuoteLifecycle.recentTradeDecisions,
|
||||||
|
(row) => row?.payload?.decision_id || row?.payload?.quote_id || row?.quote_id,
|
||||||
|
),
|
||||||
|
recentExecuteTradeCommands: mergeLifecycleEvidenceRows(
|
||||||
|
recentExecuteTradeCommands,
|
||||||
|
successfulQuoteLifecycle.recentExecuteTradeCommands,
|
||||||
|
(row) => row?.command_id || row?.decision_id || row?.quote_id,
|
||||||
|
),
|
||||||
|
recentExecutionResults: mergeLifecycleEvidenceRows(
|
||||||
|
recentExecutionResults,
|
||||||
|
successfulQuoteLifecycle.recentExecutionResults,
|
||||||
|
(row) => row?.command_id || `${row?.quote_id || 'quote'}:${row?.result_at || row?.observed_at || ''}`,
|
||||||
|
),
|
||||||
|
recentQuoteOutcomes: mergeLifecycleEvidenceRows(
|
||||||
|
recentQuoteOutcomes,
|
||||||
|
successfulQuoteLifecycle.recentQuoteOutcomes,
|
||||||
|
(row) => row?.quote_id || row?.command_id || row?.decision_id,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeLifecycleEvidenceRows(primary = [], additional = [], keyFn = null) {
|
||||||
|
const merged = [];
|
||||||
|
const seen = new Set();
|
||||||
|
for (const row of [...(primary || []), ...(additional || [])]) {
|
||||||
|
if (!row) continue;
|
||||||
|
const key = keyFn?.(row) || JSON.stringify(row);
|
||||||
|
if (key && seen.has(key)) continue;
|
||||||
|
if (key) seen.add(key);
|
||||||
|
merged.push(row);
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
function summarizeGrossEdgeEstimate(rows = []) {
|
function summarizeGrossEdgeEstimate(rows = []) {
|
||||||
let total = 0n;
|
let total = 0n;
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,9 @@ export function emptyQuoteLifecycleBucketCounts() {
|
||||||
export function classifyQuoteLifecycleStatisticsBucket(row = null) {
|
export function classifyQuoteLifecycleStatisticsBucket(row = null) {
|
||||||
if (!row) return 'unavailable';
|
if (!row) return 'unavailable';
|
||||||
|
|
||||||
|
const explicitBucket = normalizeToken(row.statistics_bucket || row.bucket);
|
||||||
|
if (QUOTE_LIFECYCLE_STATISTIC_BUCKETS.includes(explicitBucket)) return explicitBucket;
|
||||||
|
|
||||||
const lifecycleState = normalizeToken(row.lifecycle_state);
|
const lifecycleState = normalizeToken(row.lifecycle_state);
|
||||||
const executionStatus = normalizeToken(row.execution?.status || row.stage_details?.execution_status);
|
const executionStatus = normalizeToken(row.execution?.status || row.stage_details?.execution_status);
|
||||||
const outcomeStatus = normalizeToken(
|
const outcomeStatus = normalizeToken(
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { deriveQuoteOutcomeRecords } from '../core/quote-outcomes.mjs';
|
||||||
import {
|
import {
|
||||||
QUOTE_LIFECYCLE_STATISTIC_GRAINS,
|
QUOTE_LIFECYCLE_STATISTIC_GRAINS,
|
||||||
buildQuoteLifecycleStatistics,
|
buildQuoteLifecycleStatistics,
|
||||||
|
classifyQuoteLifecycleStatisticsBucket,
|
||||||
normalizeQuoteLifecycleStatisticsGrains,
|
normalizeQuoteLifecycleStatisticsGrains,
|
||||||
normalizeQuoteLifecycleStatisticsRow,
|
normalizeQuoteLifecycleStatisticsRow,
|
||||||
} from '../core/quote-lifecycle-statistics.mjs';
|
} from '../core/quote-lifecycle-statistics.mjs';
|
||||||
|
|
@ -61,6 +62,7 @@ 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_STATISTICS_TABLE = 'quote_lifecycle_statistics';
|
||||||
|
const QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE = 'quote_lifecycle_stat_subjects';
|
||||||
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';
|
||||||
|
|
@ -419,6 +421,27 @@ export async function ensureQuoteLifecycleRetentionSchema(pool) {
|
||||||
ON ${QUOTE_LIFECYCLE_STATISTICS_TABLE} (computed_at DESC)
|
ON ${QUOTE_LIFECYCLE_STATISTICS_TABLE} (computed_at DESC)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
await pool.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS ${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE} (
|
||||||
|
quote_id TEXT PRIMARY KEY,
|
||||||
|
first_activity_at TIMESTAMPTZ,
|
||||||
|
latest_stage_at TIMESTAMPTZ,
|
||||||
|
bucket TEXT NOT NULL,
|
||||||
|
lifecycle_state TEXT,
|
||||||
|
reason_code TEXT,
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL,
|
||||||
|
payload JSONB NOT NULL DEFAULT '{}'::jsonb
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
await pool.query(`
|
||||||
|
CREATE INDEX IF NOT EXISTS ${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE}_first_activity_idx
|
||||||
|
ON ${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE} (first_activity_at ASC NULLS LAST)
|
||||||
|
`);
|
||||||
|
await pool.query(`
|
||||||
|
CREATE INDEX IF NOT EXISTS ${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE}_bucket_idx
|
||||||
|
ON ${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE} (bucket, latest_stage_at DESC NULLS LAST)
|
||||||
|
`);
|
||||||
|
|
||||||
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,
|
||||||
|
|
@ -3192,8 +3215,17 @@ export async function refreshQuoteLifecycleStatistics(pool, {
|
||||||
recentQuoteOutcomes: evidence.recentQuoteOutcomes,
|
recentQuoteOutcomes: evidence.recentQuoteOutcomes,
|
||||||
limit: null,
|
limit: null,
|
||||||
});
|
});
|
||||||
const statistics = buildQuoteLifecycleStatistics({
|
const subjectUpsertResult = await upsertQuoteLifecycleStatisticSubjects(pool, {
|
||||||
lifecycleRows,
|
lifecycleRows,
|
||||||
|
now,
|
||||||
|
});
|
||||||
|
const statisticSubjectRows = await loadQuoteLifecycleStatisticSubjects(pool);
|
||||||
|
const missingIdentifierRows = lifecycleRows.filter((row) => !String(row?.quote_id || '').trim());
|
||||||
|
const statistics = buildQuoteLifecycleStatistics({
|
||||||
|
lifecycleRows: [
|
||||||
|
...statisticSubjectRows,
|
||||||
|
...missingIdentifierRows,
|
||||||
|
],
|
||||||
grains,
|
grains,
|
||||||
computedAt: now,
|
computedAt: now,
|
||||||
});
|
});
|
||||||
|
|
@ -3209,11 +3241,229 @@ export async function refreshQuoteLifecycleStatistics(pool, {
|
||||||
execution_results: evidence.recentExecutionResults.length,
|
execution_results: evidence.recentExecutionResults.length,
|
||||||
outcomes: evidence.recentQuoteOutcomes.length,
|
outcomes: evidence.recentQuoteOutcomes.length,
|
||||||
lifecycle_rows: lifecycleRows.length,
|
lifecycle_rows: lifecycleRows.length,
|
||||||
|
statistic_subjects: statisticSubjectRows.length,
|
||||||
|
statistic_subject_upserts: subjectUpsertResult.upserted_count,
|
||||||
},
|
},
|
||||||
statistics,
|
statistics,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function upsertQuoteLifecycleStatisticSubjects(pool, {
|
||||||
|
lifecycleRows = [],
|
||||||
|
now = new Date().toISOString(),
|
||||||
|
} = {}) {
|
||||||
|
let upsertedCount = 0;
|
||||||
|
for (const row of lifecycleRows || []) {
|
||||||
|
const subject = buildQuoteLifecycleStatisticSubject(row, { now });
|
||||||
|
if (!subject) continue;
|
||||||
|
await pool.query(
|
||||||
|
`
|
||||||
|
INSERT INTO ${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE} (
|
||||||
|
quote_id,
|
||||||
|
first_activity_at,
|
||||||
|
latest_stage_at,
|
||||||
|
bucket,
|
||||||
|
lifecycle_state,
|
||||||
|
reason_code,
|
||||||
|
updated_at,
|
||||||
|
payload
|
||||||
|
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8::jsonb)
|
||||||
|
ON CONFLICT (quote_id) DO UPDATE SET
|
||||||
|
first_activity_at = CASE
|
||||||
|
WHEN ${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE}.first_activity_at IS NULL THEN EXCLUDED.first_activity_at
|
||||||
|
WHEN EXCLUDED.first_activity_at IS NULL THEN ${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE}.first_activity_at
|
||||||
|
ELSE LEAST(${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE}.first_activity_at, EXCLUDED.first_activity_at)
|
||||||
|
END,
|
||||||
|
latest_stage_at = CASE
|
||||||
|
WHEN ${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE}.latest_stage_at IS NULL THEN EXCLUDED.latest_stage_at
|
||||||
|
WHEN EXCLUDED.latest_stage_at IS NULL THEN ${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE}.latest_stage_at
|
||||||
|
ELSE GREATEST(${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE}.latest_stage_at, EXCLUDED.latest_stage_at)
|
||||||
|
END,
|
||||||
|
bucket = CASE
|
||||||
|
WHEN ${quoteLifecycleBucketRankSql('EXCLUDED.bucket')} > ${quoteLifecycleBucketRankSql(`${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE}.bucket`)}
|
||||||
|
OR (
|
||||||
|
${quoteLifecycleBucketRankSql('EXCLUDED.bucket')} = ${quoteLifecycleBucketRankSql(`${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE}.bucket`)}
|
||||||
|
AND COALESCE(EXCLUDED.latest_stage_at, '-infinity'::timestamptz) >= COALESCE(${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE}.latest_stage_at, '-infinity'::timestamptz)
|
||||||
|
)
|
||||||
|
THEN EXCLUDED.bucket
|
||||||
|
ELSE ${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE}.bucket
|
||||||
|
END,
|
||||||
|
lifecycle_state = CASE
|
||||||
|
WHEN ${quoteLifecycleBucketRankSql('EXCLUDED.bucket')} > ${quoteLifecycleBucketRankSql(`${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE}.bucket`)}
|
||||||
|
OR (
|
||||||
|
${quoteLifecycleBucketRankSql('EXCLUDED.bucket')} = ${quoteLifecycleBucketRankSql(`${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE}.bucket`)}
|
||||||
|
AND COALESCE(EXCLUDED.latest_stage_at, '-infinity'::timestamptz) >= COALESCE(${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE}.latest_stage_at, '-infinity'::timestamptz)
|
||||||
|
)
|
||||||
|
THEN EXCLUDED.lifecycle_state
|
||||||
|
ELSE ${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE}.lifecycle_state
|
||||||
|
END,
|
||||||
|
reason_code = CASE
|
||||||
|
WHEN ${quoteLifecycleBucketRankSql('EXCLUDED.bucket')} > ${quoteLifecycleBucketRankSql(`${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE}.bucket`)}
|
||||||
|
OR (
|
||||||
|
${quoteLifecycleBucketRankSql('EXCLUDED.bucket')} = ${quoteLifecycleBucketRankSql(`${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE}.bucket`)}
|
||||||
|
AND COALESCE(EXCLUDED.latest_stage_at, '-infinity'::timestamptz) >= COALESCE(${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE}.latest_stage_at, '-infinity'::timestamptz)
|
||||||
|
)
|
||||||
|
THEN EXCLUDED.reason_code
|
||||||
|
ELSE ${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE}.reason_code
|
||||||
|
END,
|
||||||
|
payload = CASE
|
||||||
|
WHEN ${quoteLifecycleBucketRankSql('EXCLUDED.bucket')} > ${quoteLifecycleBucketRankSql(`${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE}.bucket`)}
|
||||||
|
OR (
|
||||||
|
${quoteLifecycleBucketRankSql('EXCLUDED.bucket')} = ${quoteLifecycleBucketRankSql(`${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE}.bucket`)}
|
||||||
|
AND COALESCE(EXCLUDED.latest_stage_at, '-infinity'::timestamptz) >= COALESCE(${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE}.latest_stage_at, '-infinity'::timestamptz)
|
||||||
|
)
|
||||||
|
THEN EXCLUDED.payload
|
||||||
|
ELSE ${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE}.payload
|
||||||
|
END,
|
||||||
|
updated_at = GREATEST(${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE}.updated_at, EXCLUDED.updated_at)
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
subject.quote_id,
|
||||||
|
subject.first_activity_at,
|
||||||
|
subject.latest_stage_at,
|
||||||
|
subject.bucket,
|
||||||
|
subject.lifecycle_state,
|
||||||
|
subject.reason_code,
|
||||||
|
subject.updated_at,
|
||||||
|
JSON.stringify(subject.payload),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
upsertedCount += 1;
|
||||||
|
}
|
||||||
|
return { upserted_count: upsertedCount };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadQuoteLifecycleStatisticSubjects(pool) {
|
||||||
|
const result = await pool.query(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
quote_id,
|
||||||
|
first_activity_at,
|
||||||
|
latest_stage_at,
|
||||||
|
bucket,
|
||||||
|
lifecycle_state,
|
||||||
|
reason_code,
|
||||||
|
updated_at,
|
||||||
|
payload
|
||||||
|
FROM ${QUOTE_LIFECYCLE_STATISTIC_SUBJECTS_TABLE}
|
||||||
|
ORDER BY first_activity_at ASC NULLS LAST, quote_id ASC
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
return result.rows.map(normalizeQuoteLifecycleStatisticSubjectRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildQuoteLifecycleStatisticSubject(row = {}, { now = new Date().toISOString() } = {}) {
|
||||||
|
const quoteId = String(row?.quote_id || '').trim();
|
||||||
|
if (!quoteId) return null;
|
||||||
|
const firstActivityAt = firstQuoteLifecycleStatisticTimestamp(row);
|
||||||
|
const latestStageAt = latestQuoteLifecycleStatisticTimestamp(row);
|
||||||
|
const bucket = classifyQuoteLifecycleStatisticsBucket(row);
|
||||||
|
return {
|
||||||
|
quote_id: quoteId,
|
||||||
|
first_activity_at: firstActivityAt,
|
||||||
|
latest_stage_at: latestStageAt,
|
||||||
|
bucket,
|
||||||
|
lifecycle_state: row.lifecycle_state || null,
|
||||||
|
reason_code: row.reason_code || null,
|
||||||
|
updated_at: toIsoTimestamp(now) || new Date().toISOString(),
|
||||||
|
payload: {
|
||||||
|
quote_id: quoteId,
|
||||||
|
decision_id: row.decision_id || null,
|
||||||
|
command_id: row.command_id || null,
|
||||||
|
lifecycle_state: row.lifecycle_state || null,
|
||||||
|
lifecycle_label: row.lifecycle_label || null,
|
||||||
|
reason_code: row.reason_code || null,
|
||||||
|
statistics_bucket: bucket,
|
||||||
|
first_activity_at: firstActivityAt,
|
||||||
|
latest_stage_at: latestStageAt,
|
||||||
|
outcome_status: row.outcome_status || row.outcome?.outcome_status || null,
|
||||||
|
attribution_status: row.attribution_status || row.outcome?.attribution_status || null,
|
||||||
|
has_settlement_evidence: row.has_settlement_evidence === true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeQuoteLifecycleStatisticSubjectRow(row = {}) {
|
||||||
|
const payload = row.payload || {};
|
||||||
|
return {
|
||||||
|
quote_id: row.quote_id || payload.quote_id || null,
|
||||||
|
decision_id: payload.decision_id || null,
|
||||||
|
command_id: payload.command_id || null,
|
||||||
|
quote_activity_at: toIsoTimestamp(row.first_activity_at || payload.first_activity_at),
|
||||||
|
latest_stage_at: toIsoTimestamp(row.latest_stage_at || payload.latest_stage_at),
|
||||||
|
lifecycle_state: row.lifecycle_state || payload.lifecycle_state || lifecycleStateForStatisticBucket(row.bucket),
|
||||||
|
reason_code: row.reason_code || payload.reason_code || row.bucket || 'unavailable',
|
||||||
|
statistics_bucket: row.bucket || payload.statistics_bucket || 'unavailable',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstQuoteLifecycleStatisticTimestamp(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 latestQuoteLifecycleStatisticTimestamp(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 lifecycleStateForStatisticBucket(bucket) {
|
||||||
|
switch (bucket) {
|
||||||
|
case 'success':
|
||||||
|
return 'completed';
|
||||||
|
case 'not_filled':
|
||||||
|
return 'not_filled';
|
||||||
|
case 'submission_failed':
|
||||||
|
return 'failed';
|
||||||
|
case 'submitted_no_reply':
|
||||||
|
return 'submitted';
|
||||||
|
case 'awaiting_executor':
|
||||||
|
return 'command_emitted';
|
||||||
|
case 'approved_no_command':
|
||||||
|
return 'evaluated';
|
||||||
|
case 'blocked_before_submit':
|
||||||
|
return 'blocked';
|
||||||
|
case 'rejected_by_strategy':
|
||||||
|
return 'rejected';
|
||||||
|
case 'observed_no_decision':
|
||||||
|
return 'observed';
|
||||||
|
case 'unavailable':
|
||||||
|
default:
|
||||||
|
return 'observed';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function quoteLifecycleBucketRankSql(expression) {
|
||||||
|
return `(
|
||||||
|
CASE ${expression}
|
||||||
|
WHEN 'success' THEN 8
|
||||||
|
WHEN 'not_filled' THEN 7
|
||||||
|
WHEN 'submission_failed' THEN 6
|
||||||
|
WHEN 'submitted_no_reply' THEN 5
|
||||||
|
WHEN 'rejected_by_strategy' THEN 4
|
||||||
|
WHEN 'blocked_before_submit' THEN 4
|
||||||
|
WHEN 'awaiting_executor' THEN 3
|
||||||
|
WHEN 'approved_no_command' THEN 2
|
||||||
|
WHEN 'observed_no_decision' THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
)`;
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadQuoteLifecycleStatistics(pool, {
|
export async function loadQuoteLifecycleStatistics(pool, {
|
||||||
grains = QUOTE_LIFECYCLE_STATISTIC_GRAINS,
|
grains = QUOTE_LIFECYCLE_STATISTIC_GRAINS,
|
||||||
limit = 96,
|
limit = 96,
|
||||||
|
|
@ -3426,14 +3676,25 @@ function quoteLifecyclePruneTableSpecs() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function successfulQuoteEvidencePredicate(spec) {
|
function successfulQuoteEvidencePredicate(spec) {
|
||||||
if (spec.selfOutcome) return successfulQuoteEvidenceTargetPredicate();
|
if (spec.selfOutcome) {
|
||||||
return `
|
return `
|
||||||
EXISTS (
|
(
|
||||||
|
${successfulQuoteEvidenceTargetPredicate()}
|
||||||
|
OR ${successfulExecutionEvidenceExistsPredicate('target')}
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return `
|
||||||
|
(
|
||||||
|
${successfulQuotePayloadPredicate('target.payload')}
|
||||||
|
OR ${successfulExecutionEvidenceExistsPredicate('target')}
|
||||||
|
OR EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM quote_outcome_attributions outcome
|
FROM quote_outcome_attributions outcome
|
||||||
WHERE outcome.quote_id = target.quote_id
|
WHERE outcome.quote_id = target.quote_id
|
||||||
AND ${successfulQuoteEvidenceSelfPredicate('outcome')}
|
AND ${successfulQuoteEvidenceSelfPredicate('outcome')}
|
||||||
)
|
)
|
||||||
|
)
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3447,6 +3708,26 @@ function successfulQuoteEvidenceSelfPredicate(alias = null) {
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function successfulQuotePayloadPredicate(payloadExpression) {
|
||||||
|
return `
|
||||||
|
(
|
||||||
|
${payloadExpression}->>'outcome_status' = 'completed'
|
||||||
|
OR ${payloadExpression}->>'attribution_status' IN ('heuristic_match', 'linked_settlement', 'exact_match', 'attributed')
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function successfulExecutionEvidenceExistsPredicate(targetExpression) {
|
||||||
|
return `
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM trade_execution_results execution
|
||||||
|
WHERE execution.quote_id = ${targetExpression}.quote_id
|
||||||
|
AND ${successfulQuotePayloadPredicate('execution.payload')}
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
function successfulQuoteEvidenceTargetPredicate() {
|
function successfulQuoteEvidenceTargetPredicate() {
|
||||||
return `
|
return `
|
||||||
(
|
(
|
||||||
|
|
@ -3555,10 +3836,31 @@ export async function pruneNonSuccessfulQuoteLifecycleHistory(pool, {
|
||||||
(
|
(
|
||||||
target.outcome_status = 'completed'
|
target.outcome_status = 'completed'
|
||||||
OR target.attribution_status IN ('heuristic_match', 'linked_settlement', 'exact_match', 'attributed')
|
OR target.attribution_status IN ('heuristic_match', 'linked_settlement', 'exact_match', 'attributed')
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM trade_execution_results execution
|
||||||
|
WHERE execution.quote_id = target.quote_id
|
||||||
|
AND (
|
||||||
|
execution.payload->>'outcome_status' = 'completed'
|
||||||
|
OR execution.payload->>'attribution_status' IN ('heuristic_match', 'linked_settlement', 'exact_match', 'attributed')
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
`
|
`
|
||||||
: `
|
: `
|
||||||
EXISTS (
|
(
|
||||||
|
target.payload->>'outcome_status' = 'completed'
|
||||||
|
OR target.payload->>'attribution_status' IN ('heuristic_match', 'linked_settlement', 'exact_match', 'attributed')
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM trade_execution_results execution
|
||||||
|
WHERE execution.quote_id = target.quote_id
|
||||||
|
AND (
|
||||||
|
execution.payload->>'outcome_status' = 'completed'
|
||||||
|
OR execution.payload->>'attribution_status' IN ('heuristic_match', 'linked_settlement', 'exact_match', 'attributed')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
OR EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM quote_outcome_attributions outcome
|
FROM quote_outcome_attributions outcome
|
||||||
WHERE outcome.quote_id = target.quote_id
|
WHERE outcome.quote_id = target.quote_id
|
||||||
|
|
@ -3567,6 +3869,7 @@ export async function pruneNonSuccessfulQuoteLifecycleHistory(pool, {
|
||||||
OR outcome.attribution_status IN ('heuristic_match', 'linked_settlement', 'exact_match', 'attributed')
|
OR outcome.attribution_status IN ('heuristic_match', 'linked_settlement', 'exact_match', 'attributed')
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
`;
|
`;
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
`
|
`
|
||||||
|
|
@ -4085,6 +4388,88 @@ export async function loadRecentQuoteOutcomes(pool, { limit = 200 } = {}) {
|
||||||
return result.rows.map(normalizeQuoteOutcomeRow);
|
return result.rows.map(normalizeQuoteOutcomeRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function loadSuccessfulQuoteLifecycleEvidence(pool, { limit = 50 } = {}) {
|
||||||
|
const lookupAt = new Date().toISOString();
|
||||||
|
const boundedLimit = Math.max(1, Math.min(500, Number(limit) || 50));
|
||||||
|
const candidatesResult = await pool.query(
|
||||||
|
`
|
||||||
|
WITH successful_quotes AS (
|
||||||
|
SELECT
|
||||||
|
quote_id,
|
||||||
|
COALESCE(outcome_observed_at, submitted_at, command_at, computed_at) AS matched_at
|
||||||
|
FROM ${QUOTE_OUTCOMES_TABLE}
|
||||||
|
WHERE quote_id IS NOT NULL
|
||||||
|
AND (
|
||||||
|
outcome_status = 'completed'
|
||||||
|
OR attribution_status IN ('heuristic_match', 'linked_settlement', 'exact_match', 'attributed')
|
||||||
|
)
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
quote_id,
|
||||||
|
COALESCE(observed_at, ingested_at) AS matched_at
|
||||||
|
FROM trade_execution_results
|
||||||
|
WHERE quote_id IS NOT NULL
|
||||||
|
AND (
|
||||||
|
payload->>'outcome_status' = 'completed'
|
||||||
|
OR payload->>'attribution_status' IN ('heuristic_match', 'linked_settlement', 'exact_match', 'attributed')
|
||||||
|
)
|
||||||
|
), ranked AS (
|
||||||
|
SELECT
|
||||||
|
quote_id,
|
||||||
|
MAX(matched_at) AS matched_at
|
||||||
|
FROM successful_quotes
|
||||||
|
GROUP BY quote_id
|
||||||
|
)
|
||||||
|
SELECT quote_id, matched_at
|
||||||
|
FROM ranked
|
||||||
|
ORDER BY matched_at DESC NULLS LAST
|
||||||
|
LIMIT $1
|
||||||
|
`,
|
||||||
|
[boundedLimit],
|
||||||
|
);
|
||||||
|
|
||||||
|
const identifiers = {
|
||||||
|
quote_ids: uniqueTextValues(candidatesResult.rows.map((row) => row.quote_id)),
|
||||||
|
decision_ids: [],
|
||||||
|
command_ids: [],
|
||||||
|
};
|
||||||
|
if (!hasLookupIdentifiers(identifiers)) {
|
||||||
|
return buildQuoteLifecycleLookupEvidence({
|
||||||
|
requestedIdentifier: 'successful_quote_lifecycle',
|
||||||
|
lookupAt,
|
||||||
|
matchedIdentifiers: identifiers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const [
|
||||||
|
recentQuotes,
|
||||||
|
recentTradeDecisions,
|
||||||
|
recentExecuteTradeCommands,
|
||||||
|
recentExecutionResults,
|
||||||
|
recentQuoteOutcomes,
|
||||||
|
] = await Promise.all([
|
||||||
|
loadLifecycleLookupQuotes(pool, identifiers),
|
||||||
|
loadLifecycleLookupTradeDecisions(pool, identifiers),
|
||||||
|
loadLifecycleLookupExecuteTradeCommands(pool, identifiers),
|
||||||
|
loadLifecycleLookupExecutionResults(pool, identifiers),
|
||||||
|
loadLifecycleLookupQuoteOutcomes(pool, identifiers),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return buildQuoteLifecycleLookupEvidence({
|
||||||
|
requestedIdentifier: 'successful_quote_lifecycle',
|
||||||
|
lookupAt,
|
||||||
|
matchedIdentifierType: 'quote_id',
|
||||||
|
matchedIdentifiers: identifiers,
|
||||||
|
recentQuotes,
|
||||||
|
recentTradeDecisions,
|
||||||
|
recentExecuteTradeCommands,
|
||||||
|
recentExecutionResults,
|
||||||
|
recentQuoteOutcomes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadQuoteLifecycleLookupEvidence(pool, { identifier } = {}) {
|
export async function loadQuoteLifecycleLookupEvidence(pool, { identifier } = {}) {
|
||||||
const requestedIdentifier = String(identifier || '').trim();
|
const requestedIdentifier = String(identifier || '').trim();
|
||||||
const lookupAt = new Date().toISOString();
|
const lookupAt = new Date().toISOString();
|
||||||
|
|
@ -5686,6 +6071,26 @@ function floorTimestamp(value, granularityMs) {
|
||||||
return new Date(Math.floor(parsed / granularity) * granularity).toISOString();
|
return new Date(Math.floor(parsed / granularity) * granularity).toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function earliestTimestamp(values = []) {
|
||||||
|
let earliest = null;
|
||||||
|
for (const value of values || []) {
|
||||||
|
const iso = toIsoTimestamp(value);
|
||||||
|
if (!iso) continue;
|
||||||
|
if (!earliest || timestampValue(iso) < timestampValue(earliest)) earliest = iso;
|
||||||
|
}
|
||||||
|
return earliest;
|
||||||
|
}
|
||||||
|
|
||||||
|
function latestTimestamp(values = []) {
|
||||||
|
let latest = null;
|
||||||
|
for (const value of values || []) {
|
||||||
|
const iso = toIsoTimestamp(value);
|
||||||
|
if (!iso) continue;
|
||||||
|
if (!latest || timestampValue(iso) > timestampValue(latest)) latest = iso;
|
||||||
|
}
|
||||||
|
return latest;
|
||||||
|
}
|
||||||
|
|
||||||
function laterTimestamp(left, right) {
|
function laterTimestamp(left, right) {
|
||||||
const leftMs = timestampValue(left);
|
const leftMs = timestampValue(left);
|
||||||
const rightMs = timestampValue(right);
|
const rightMs = timestampValue(right);
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,8 @@ test('quote lifecycle retention preserves completed trades and drains old non-su
|
||||||
for (const query of queries) {
|
for (const query of queries) {
|
||||||
assert.match(query.sql, /DELETE FROM/);
|
assert.match(query.sql, /DELETE FROM/);
|
||||||
assert.match(query.sql, /outcome_status = 'completed'/);
|
assert.match(query.sql, /outcome_status = 'completed'/);
|
||||||
|
assert.match(query.sql, /payload->>'outcome_status' = 'completed'/);
|
||||||
|
assert.match(query.sql, /FROM trade_execution_results execution/);
|
||||||
assert.match(query.sql, /attribution_status IN \('heuristic_match', 'linked_settlement', 'exact_match', 'attributed'\)/);
|
assert.match(query.sql, /attribution_status IN \('heuristic_match', 'linked_settlement', 'exact_match', 'attributed'\)/);
|
||||||
assert.deepEqual(query.params, ['2026-06-05T11:30:00.000Z', 100_000]);
|
assert.deepEqual(query.params, ['2026-06-05T11:30:00.000Z', 100_000]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1684,6 +1684,102 @@ test('successful trade rows require completed outcome with linked settled invent
|
||||||
assert.equal(funnel.counts.completed, 2);
|
assert.equal(funnel.counts.completed, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('successful trade rows include durable successful lifecycle evidence outside the recent table', () => {
|
||||||
|
const config = buildConfig();
|
||||||
|
const bootstrap = buildDashboardBootstrap({
|
||||||
|
config,
|
||||||
|
auth: {
|
||||||
|
authenticated: true,
|
||||||
|
subject: 'local-operator',
|
||||||
|
mode: 'stub',
|
||||||
|
roles: ['operator'],
|
||||||
|
},
|
||||||
|
portfolioMetric: null,
|
||||||
|
inventorySnapshot: null,
|
||||||
|
marketPrice: null,
|
||||||
|
recentQuotes: [],
|
||||||
|
submissionPage: { page: 1, page_size: 20, total: 0, total_pages: 1, items: [] },
|
||||||
|
submissionSummary: { total: 0, last_submission_at: null },
|
||||||
|
fundingObservations: [],
|
||||||
|
recentDepositStatuses: [],
|
||||||
|
recentTradeDecisions: [],
|
||||||
|
recentExecuteTradeCommands: [],
|
||||||
|
recentExecutionResults: [],
|
||||||
|
recentQuoteOutcomes: [],
|
||||||
|
successfulQuoteLifecycle: {
|
||||||
|
found: true,
|
||||||
|
requested_identifier: 'successful_quote_lifecycle',
|
||||||
|
matched_identifier_type: 'quote_id',
|
||||||
|
matched_identifiers: {
|
||||||
|
quote_ids: ['quote-durable-success'],
|
||||||
|
decision_ids: ['decision-durable-success'],
|
||||||
|
command_ids: ['cmd-durable-success'],
|
||||||
|
},
|
||||||
|
lookup_at: '2026-04-09T09:05:00.000Z',
|
||||||
|
recentQuotes: [],
|
||||||
|
recentTradeDecisions: [{
|
||||||
|
observed_at: '2026-04-09T09:00:01.000Z',
|
||||||
|
payload: {
|
||||||
|
decision_id: 'decision-durable-success',
|
||||||
|
quote_id: 'quote-durable-success',
|
||||||
|
pair: config.activePair,
|
||||||
|
decision: 'actionable',
|
||||||
|
decision_reason: 'strategy_approved',
|
||||||
|
gross_edge_pct: '0.49',
|
||||||
|
eure_notional: '50',
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
recentExecuteTradeCommands: [{
|
||||||
|
command_id: 'cmd-durable-success',
|
||||||
|
decision_id: 'decision-durable-success',
|
||||||
|
quote_id: 'quote-durable-success',
|
||||||
|
pair: config.activePair,
|
||||||
|
observed_at: '2026-04-09T09:00:02.000Z',
|
||||||
|
}],
|
||||||
|
recentExecutionResults: [{
|
||||||
|
command_id: 'cmd-durable-success',
|
||||||
|
decision_id: 'decision-durable-success',
|
||||||
|
quote_id: 'quote-durable-success',
|
||||||
|
pair: config.activePair,
|
||||||
|
result_at: '2026-04-09T09:00:03.000Z',
|
||||||
|
status: 'submitted',
|
||||||
|
result_code: 'quote_response_ok',
|
||||||
|
}],
|
||||||
|
recentQuoteOutcomes: [{
|
||||||
|
command_id: 'cmd-durable-success',
|
||||||
|
decision_id: 'decision-durable-success',
|
||||||
|
quote_id: 'quote-durable-success',
|
||||||
|
pair: config.activePair,
|
||||||
|
gross_edge_pct: '0.49',
|
||||||
|
eure_notional: '50',
|
||||||
|
submitted_at: '2026-04-09T09:00:03.000Z',
|
||||||
|
command_at: '2026-04-09T09:00:02.000Z',
|
||||||
|
outcome_status: 'completed',
|
||||||
|
outcome_reason: 'matched_inventory_delta',
|
||||||
|
outcome_observed_at: '2026-04-09T09:01:00.000Z',
|
||||||
|
outcome_source: 'intent_inventory_spendable_delta',
|
||||||
|
attribution_status: 'heuristic_match',
|
||||||
|
attribution_method: 'exact_asset_delta_within_window',
|
||||||
|
attributed_inventory_delta: {
|
||||||
|
observed_at: '2026-04-09T09:01:00.000Z',
|
||||||
|
delta_units: {
|
||||||
|
[config.tradingBtc.assetId]: '37014',
|
||||||
|
[config.tradingEure.assetId]: '-21000021200021200022',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
recentAlertTransitions: [],
|
||||||
|
serviceSnapshots: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const funnel = bootstrap.strategy.strategy_state.trade_funnel;
|
||||||
|
assert.equal(funnel.successful_trade_count, 1);
|
||||||
|
assert.equal(funnel.successful_trades[0].quote_id, 'quote-durable-success');
|
||||||
|
assert.equal(funnel.counts.completed, 1);
|
||||||
|
assert.equal(bootstrap.strategy.strategy_state.recent_lifecycle_rows[0].quote_id, 'quote-durable-success');
|
||||||
|
});
|
||||||
|
|
||||||
test('executor blocking is distinct from strategy rejection', () => {
|
test('executor blocking is distinct from strategy rejection', () => {
|
||||||
const [row] = deriveQuoteLifecycleRows({
|
const [row] = deriveQuoteLifecycleRows({
|
||||||
recentTradeDecisions: [{
|
recentTradeDecisions: [{
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
|
|
||||||
test('postgres quote lifecycle statistics refresh idempotently upserts state buckets by original quote window', async () => {
|
test('postgres quote lifecycle statistics refresh idempotently upserts state buckets by original quote window', async () => {
|
||||||
const upserts = [];
|
const upserts = [];
|
||||||
|
const subjects = new Map();
|
||||||
let includeFailure = false;
|
let includeFailure = false;
|
||||||
const pool = {
|
const pool = {
|
||||||
async query(sql, params = []) {
|
async query(sql, params = []) {
|
||||||
|
|
@ -87,6 +88,23 @@ test('postgres quote lifecycle statistics refresh idempotently upserts state buc
|
||||||
if (sql.includes('FROM quote_outcome_attributions') && sql.includes('ORDER BY COALESCE')) {
|
if (sql.includes('FROM quote_outcome_attributions') && sql.includes('ORDER BY COALESCE')) {
|
||||||
return { rows: [] };
|
return { rows: [] };
|
||||||
}
|
}
|
||||||
|
if (sql.includes('INSERT INTO quote_lifecycle_stat_subjects')) {
|
||||||
|
const existing = subjects.get(params[0]);
|
||||||
|
subjects.set(params[0], {
|
||||||
|
quote_id: params[0],
|
||||||
|
first_activity_at: existing?.first_activity_at || params[1],
|
||||||
|
latest_stage_at: params[2] || existing?.latest_stage_at || null,
|
||||||
|
bucket: params[3],
|
||||||
|
lifecycle_state: params[4],
|
||||||
|
reason_code: params[5],
|
||||||
|
updated_at: params[6],
|
||||||
|
payload: JSON.parse(params[7]),
|
||||||
|
});
|
||||||
|
return { rows: [], rowCount: 1 };
|
||||||
|
}
|
||||||
|
if (sql.includes('FROM quote_lifecycle_stat_subjects')) {
|
||||||
|
return { rows: [...subjects.values()] };
|
||||||
|
}
|
||||||
if (sql.includes('INSERT INTO quote_lifecycle_statistics')) {
|
if (sql.includes('INSERT INTO quote_lifecycle_statistics')) {
|
||||||
upserts.push({ sql, params });
|
upserts.push({ sql, params });
|
||||||
return { rows: [], rowCount: 1 };
|
return { rows: [], rowCount: 1 };
|
||||||
|
|
@ -121,6 +139,127 @@ test('postgres quote lifecycle statistics refresh idempotently upserts state buc
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('postgres quote lifecycle statistics retain per-quote buckets after detail pruning', async () => {
|
||||||
|
const subjects = new Map();
|
||||||
|
const allTimeUpserts = [];
|
||||||
|
let sourceAvailable = true;
|
||||||
|
const pool = {
|
||||||
|
async query(sql, params = []) {
|
||||||
|
if (sql.includes('FROM swap_demand_events') && sql.includes('ORDER BY COALESCE')) {
|
||||||
|
return {
|
||||||
|
rows: sourceAvailable ? [{
|
||||||
|
observed_at: '2026-06-12T14:11:01.000Z',
|
||||||
|
ingested_at: '2026-06-12T14:11:01.000Z',
|
||||||
|
payload: {
|
||||||
|
quote_id: 'quote-completed',
|
||||||
|
pair: 'btc->usdc',
|
||||||
|
asset_in: 'btc',
|
||||||
|
asset_out: 'usdc',
|
||||||
|
amount_in: '1',
|
||||||
|
amount_out: '2',
|
||||||
|
},
|
||||||
|
}] : [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (sql.includes('FROM trade_decisions') && sql.includes('ORDER BY COALESCE')) {
|
||||||
|
return {
|
||||||
|
rows: sourceAvailable ? [{
|
||||||
|
observed_at: '2026-06-12T14:11:02.000Z',
|
||||||
|
ingested_at: '2026-06-12T14:11:02.000Z',
|
||||||
|
payload: {
|
||||||
|
quote_id: 'quote-completed',
|
||||||
|
decision_id: 'decision-completed',
|
||||||
|
pair: 'btc->usdc',
|
||||||
|
decision: 'approved',
|
||||||
|
decision_reason: 'strategy_approved',
|
||||||
|
},
|
||||||
|
}] : [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (sql.includes('FROM execute_trade_commands') && sql.includes('ORDER BY COALESCE')) {
|
||||||
|
return {
|
||||||
|
rows: sourceAvailable ? [{
|
||||||
|
observed_at: '2026-06-12T14:11:03.000Z',
|
||||||
|
ingested_at: '2026-06-12T14:11:03.000Z',
|
||||||
|
payload: {
|
||||||
|
quote_id: 'quote-completed',
|
||||||
|
decision_id: 'decision-completed',
|
||||||
|
command_id: 'cmd-completed',
|
||||||
|
pair: 'btc->usdc',
|
||||||
|
},
|
||||||
|
}] : [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (sql.includes('FROM trade_execution_results r')) {
|
||||||
|
return {
|
||||||
|
rows: sourceAvailable ? [{
|
||||||
|
result_observed_at: '2026-06-12T14:11:10.000Z',
|
||||||
|
result_ingested_at: '2026-06-12T14:11:10.000Z',
|
||||||
|
result_payload: {
|
||||||
|
quote_id: 'quote-completed',
|
||||||
|
decision_id: 'decision-completed',
|
||||||
|
command_id: 'cmd-completed',
|
||||||
|
status: 'submitted',
|
||||||
|
result_code: 'quote_response_ok',
|
||||||
|
outcome_status: 'completed',
|
||||||
|
},
|
||||||
|
command_payload: {
|
||||||
|
quote_id: 'quote-completed',
|
||||||
|
decision_id: 'decision-completed',
|
||||||
|
command_id: 'cmd-completed',
|
||||||
|
},
|
||||||
|
decision_payload: {
|
||||||
|
quote_id: 'quote-completed',
|
||||||
|
decision_id: 'decision-completed',
|
||||||
|
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_stat_subjects')) {
|
||||||
|
const existing = subjects.get(params[0]);
|
||||||
|
subjects.set(params[0], {
|
||||||
|
quote_id: params[0],
|
||||||
|
first_activity_at: existing?.first_activity_at || params[1],
|
||||||
|
latest_stage_at: params[2] || existing?.latest_stage_at || null,
|
||||||
|
bucket: params[3],
|
||||||
|
lifecycle_state: params[4],
|
||||||
|
reason_code: params[5],
|
||||||
|
updated_at: params[6],
|
||||||
|
payload: JSON.parse(params[7]),
|
||||||
|
});
|
||||||
|
return { rows: [], rowCount: 1 };
|
||||||
|
}
|
||||||
|
if (sql.includes('FROM quote_lifecycle_stat_subjects')) {
|
||||||
|
return { rows: [...subjects.values()] };
|
||||||
|
}
|
||||||
|
if (sql.includes('INSERT INTO quote_lifecycle_statistics')) {
|
||||||
|
if (params[1] === 'all_time') allTimeUpserts.push({ params });
|
||||||
|
return { rows: [], rowCount: 1 };
|
||||||
|
}
|
||||||
|
throw new Error(`unexpected query: ${sql}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await refreshQuoteLifecycleStatistics(pool, {
|
||||||
|
now: '2026-06-12T14:12:00.000Z',
|
||||||
|
});
|
||||||
|
sourceAvailable = false;
|
||||||
|
await refreshQuoteLifecycleStatistics(pool, {
|
||||||
|
now: '2026-06-12T15:12:00.000Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(allTimeUpserts.length, 2);
|
||||||
|
assert.equal(allTimeUpserts[0].params[4], 1);
|
||||||
|
assert.equal(JSON.parse(allTimeUpserts[0].params[7]).success, 1);
|
||||||
|
assert.equal(allTimeUpserts[1].params[4], 1);
|
||||||
|
assert.equal(JSON.parse(allTimeUpserts[1].params[7]).success, 1);
|
||||||
|
});
|
||||||
|
|
||||||
test('postgres quote lifecycle statistics loader returns all-time and clamped grain rows', async () => {
|
test('postgres quote lifecycle statistics loader returns all-time and clamped grain rows', async () => {
|
||||||
const pool = {
|
const pool = {
|
||||||
async query(sql, params = []) {
|
async query(sql, params = []) {
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,28 @@ test('quote lifecycle statistics classify states without upgrading submitted-onl
|
||||||
}), 'success');
|
}), 'success');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('quote lifecycle statistics can aggregate retained quote subject buckets', () => {
|
||||||
|
const rows = buildQuoteLifecycleStatistics({
|
||||||
|
computedAt: '2026-06-12T14:20:00.000Z',
|
||||||
|
lifecycleRows: [{
|
||||||
|
quote_id: 'quote-retained-success',
|
||||||
|
quote_activity_at: '2026-06-12T14:11:01.000Z',
|
||||||
|
latest_stage_at: '2026-06-12T14:11:10.000Z',
|
||||||
|
statistics_bucket: 'success',
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
const allTime = rows.find((row) => row.grain === 'all_time');
|
||||||
|
const fiveMinute = rows.find((row) => (
|
||||||
|
row.grain === 'five_minute'
|
||||||
|
&& row.window_start === '2026-06-12T14:10:00.000Z'
|
||||||
|
));
|
||||||
|
assert.equal(allTime.quote_count, 1);
|
||||||
|
assert.equal(allTime.bucket_counts.success, 1);
|
||||||
|
assert.equal(fiveMinute.quote_count, 1);
|
||||||
|
assert.equal(fiveMinute.bucket_counts.success, 1);
|
||||||
|
});
|
||||||
|
|
||||||
test('quote lifecycle statistics use UTC grain boundaries with Monday UTC weeks', () => {
|
test('quote lifecycle statistics use UTC grain boundaries with Monday UTC weeks', () => {
|
||||||
assert.deepEqual(quoteLifecycleStatisticWindow('2026-06-12T17:34:44.123Z', 'five_minute'), {
|
assert.deepEqual(quoteLifecycleStatisticWindow('2026-06-12T17:34:44.123Z', 'five_minute'), {
|
||||||
grain: 'five_minute',
|
grain: 'five_minute',
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue