Add maker timing competitiveness truth
All checks were successful
deploy / deploy (push) Successful in 46s

Proof: quote-to-relay maker timing now propagates through ingest, normalized quotes, strategy decisions, commands, executor results, quote outcomes, lifecycle rows, dashboard summaries, and runtime alerts; relay failures preserve original text while classifying quote_not_found_or_finished; targeted tests, full npm test, and operator dashboard build passed before commit.

Assumptions: response-age policy stays disabled by default and is only activated through DB-backed pair strategy config after operators review timing evidence; unrelated pre-existing dirty worktree files were left unstaged.

Still fake: relay acceptance is not settlement or realized PnL; live policy thresholds still require post-deploy evidence before enabling skips for production pairs.
This commit is contained in:
philipp 2026-05-18 23:47:52 +02:00
parent c2675df141
commit 365acf7b7f
25 changed files with 1755 additions and 40 deletions

View file

@ -7,6 +7,7 @@ import { WebSocketServer } from 'ws';
import { createConsumer } from '../bus/kafka/consumer.mjs'; import { createConsumer } from '../bus/kafka/consumer.mjs';
import { parseEventMessage } from '../core/event-envelope.mjs'; import { parseEventMessage } from '../core/event-envelope.mjs';
import { buildMakerCompetitivenessSummary } from '../core/maker-competitiveness.mjs';
import { import {
applyDashboardLiveEvent, applyDashboardLiveEvent,
buildDashboardBootstrap, buildDashboardBootstrap,
@ -70,6 +71,7 @@ const dashboardRuntimeState = {
source_errors: {}, source_errors: {},
last_source_error_at: null, last_source_error_at: null,
last_live_event_error: null, last_live_event_error: null,
latest_maker_competitiveness: null,
websocket_clients: 0, websocket_clients: 0,
}; };
@ -163,6 +165,9 @@ const liveState = createDashboardLiveState({
initialServiceSnapshots.find((snapshot) => snapshot.service === 'ops-sentinel')?.state?.active_alerts initialServiceSnapshots.find((snapshot) => snapshot.service === 'ops-sentinel')?.state?.active_alerts
|| [], || [],
}); });
dashboardRuntimeState.latest_maker_competitiveness = buildMakerCompetitivenessSummary({
lifecycleRows: buildLiveQuoteLifecycleRows(liveState),
});
const liveConsumer = await createConsumer({ const liveConsumer = await createConsumer({
groupId: config.kafkaConsumerGroupOperatorDashboard, groupId: config.kafkaConsumerGroupOperatorDashboard,
@ -194,6 +199,9 @@ await liveConsumer.run({
const event = parseEventMessage(message.value.toString()); const event = parseEventMessage(message.value.toString());
const updates = applyDashboardLiveEvent(liveState, { topic, event }); const updates = applyDashboardLiveEvent(liveState, { topic, event });
for (const update of updates) { for (const update of updates) {
if (update.maker_competitiveness) {
dashboardRuntimeState.latest_maker_competitiveness = update.maker_competitiveness;
}
broadcast(update); broadcast(update);
} }
} catch (error) { } catch (error) {
@ -216,12 +224,17 @@ const webSocketServer = new WebSocketServer({
webSocketServer.on('connection', (socket, _req, authContext) => { 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);
dashboardRuntimeState.latest_maker_competitiveness = buildMakerCompetitivenessSummary({
lifecycleRows: recentLifecycleRows,
});
socket.send(JSON.stringify({ socket.send(JSON.stringify({
type: 'session.ready', type: 'session.ready',
session: authContext, session: authContext,
live: { live: {
recent_quotes: liveState.recent_quotes, recent_quotes: liveState.recent_quotes,
recent_lifecycle_rows: buildLiveQuoteLifecycleRows(liveState), recent_lifecycle_rows: recentLifecycleRows,
maker_competitiveness: dashboardRuntimeState.latest_maker_competitiveness,
status_bar: buildLiveStatusBar(liveState), status_bar: buildLiveStatusBar(liveState),
}, },
})); }));
@ -260,6 +273,7 @@ const server = http.createServer(async (req, res) => {
source_error_count: Object.keys(dashboardRuntimeState.source_errors).length, source_error_count: Object.keys(dashboardRuntimeState.source_errors).length,
last_source_error_at: dashboardRuntimeState.last_source_error_at, last_source_error_at: dashboardRuntimeState.last_source_error_at,
last_live_event_error: dashboardRuntimeState.last_live_event_error, last_live_event_error: dashboardRuntimeState.last_live_event_error,
latest_maker_competitiveness: dashboardRuntimeState.latest_maker_competitiveness,
}); });
} }
@ -556,6 +570,10 @@ async function loadBootstrapPayload({ auth, page, pageSize }) {
}); });
dashboardRuntimeState.last_bootstrap_at = new Date().toISOString(); dashboardRuntimeState.last_bootstrap_at = new Date().toISOString();
dashboardRuntimeState.last_bootstrap_error = null; dashboardRuntimeState.last_bootstrap_error = null;
dashboardRuntimeState.latest_maker_competitiveness = (
payload.strategy?.strategy_state?.maker_competitiveness
|| dashboardRuntimeState.latest_maker_competitiveness
);
return payload; return payload;
} }
@ -630,6 +648,9 @@ async function invokeControl(control, body) {
requestDefaultNotional: bodyField(body, 'request_default_notional', 'requestDefaultNotional'), requestDefaultNotional: bodyField(body, 'request_default_notional', 'requestDefaultNotional'),
requestMaxNotional: bodyField(body, 'request_max_notional', 'requestMaxNotional'), requestMaxNotional: bodyField(body, 'request_max_notional', 'requestMaxNotional'),
requestMaxSlippageBps: bodyField(body, 'request_max_slippage_bps', 'requestMaxSlippageBps'), requestMaxSlippageBps: bodyField(body, 'request_max_slippage_bps', 'requestMaxSlippageBps'),
makerMaxQuoteAgeEnabled: bodyField(body, 'maker_max_quote_age_enabled', 'makerMaxQuoteAgeEnabled'),
makerMaxQuoteAgeMs: bodyField(body, 'maker_max_quote_age_ms', 'makerMaxQuoteAgeMs'),
makerLatencyPolicyReason: bodyField(body, 'maker_latency_policy_reason', 'makerLatencyPolicyReason'),
changedBy: body.changed_by || 'operator', changedBy: body.changed_by || 'operator',
reason: body.reason || 'dashboard pair strategy config update', reason: body.reason || 'dashboard pair strategy config update',
}); });
@ -659,6 +680,9 @@ async function invokeControl(control, body) {
requestDefaultNotional: bodyField(body, 'request_default_notional', 'requestDefaultNotional'), requestDefaultNotional: bodyField(body, 'request_default_notional', 'requestDefaultNotional'),
requestMaxNotional: bodyField(body, 'request_max_notional', 'requestMaxNotional'), requestMaxNotional: bodyField(body, 'request_max_notional', 'requestMaxNotional'),
requestMaxSlippageBps: bodyField(body, 'request_max_slippage_bps', 'requestMaxSlippageBps'), requestMaxSlippageBps: bodyField(body, 'request_max_slippage_bps', 'requestMaxSlippageBps'),
makerMaxQuoteAgeEnabled: bodyField(body, 'maker_max_quote_age_enabled', 'makerMaxQuoteAgeEnabled'),
makerMaxQuoteAgeMs: bodyField(body, 'maker_max_quote_age_ms', 'makerMaxQuoteAgeMs'),
makerLatencyPolicyReason: bodyField(body, 'maker_latency_policy_reason', 'makerLatencyPolicyReason'),
changedBy: body.changed_by || 'operator', changedBy: body.changed_by || 'operator',
reason: body.reason || 'dashboard pair mode update', reason: body.reason || 'dashboard pair mode update',
}); });

View file

@ -14,6 +14,7 @@ import {
import { listDashboardServices } from '../core/operator-dashboard.mjs'; import { listDashboardServices } from '../core/operator-dashboard.mjs';
import { import {
ageMs, ageMs,
buildMakerCompetitivenessRuntimeAlerts,
buildRuntimeAlert, buildRuntimeAlert,
createRuntimeHealthThresholds, createRuntimeHealthThresholds,
evaluateRuntimeHealth, evaluateRuntimeHealth,
@ -476,6 +477,11 @@ function buildDeterministicRuntimeAlerts({ servicesByName, now, previousRuntimeE
const dashboard = servicesByName['operator-dashboard']; const dashboard = servicesByName['operator-dashboard'];
const dashboardState = dashboard?.state || {}; const dashboardState = dashboard?.state || {};
alerts.push(...buildMakerCompetitivenessRuntimeAlerts({
makerCompetitiveness: dashboardState.latest_maker_competitiveness
|| dashboardState.maker_competitiveness
|| null,
}));
const dashboardSourceErrorCount = Number( const dashboardSourceErrorCount = Number(
dashboardState.source_error_count dashboardState.source_error_count
|| dashboard?.health?.source_error_count || dashboard?.health?.source_error_count

View file

@ -6,6 +6,7 @@ import { createArmedStateStore } from '../core/armed-state-store.mjs';
import { startControlApi } from '../core/control-api.mjs'; import { startControlApi } from '../core/control-api.mjs';
import { buildEventEnvelope, parseEventMessage } from '../core/event-envelope.mjs'; import { buildEventEnvelope, parseEventMessage } from '../core/event-envelope.mjs';
import { createLogger, serializeError } from '../core/log.mjs'; import { createLogger, serializeError } from '../core/log.mjs';
import { extendMakerTiming } from '../core/maker-timing.mjs';
import { createRecentIdCache } from '../core/recent-id-cache.mjs'; import { createRecentIdCache } from '../core/recent-id-cache.mjs';
import { assertInventorySnapshotEvent, assertMarketPriceEvent, assertNormalizedSwapDemand } from '../core/schemas.mjs'; import { assertInventorySnapshotEvent, assertMarketPriceEvent, assertNormalizedSwapDemand } from '../core/schemas.mjs';
import { evaluateTradeOpportunity } from '../core/strategy.mjs'; import { evaluateTradeOpportunity } from '../core/strategy.mjs';
@ -108,6 +109,7 @@ await consumer.run({
async function handleDemand(event) { async function handleDemand(event) {
if (state.paused) return; if (state.paused) return;
const strategyReceivedAt = new Date().toISOString();
const tradingConfig = await tradingConfigStore.getConfig(); const tradingConfig = await tradingConfigStore.getConfig();
if (seenQuotes.has(event.payload.quote_id)) { if (seenQuotes.has(event.payload.quote_id)) {
@ -117,6 +119,7 @@ async function handleDemand(event) {
|| pair.priceRoute.quoteAssetId === tradingConfig.tradingEure?.assetId; || pair.priceRoute.quoteAssetId === tradingConfig.tradingEure?.assetId;
await publishDecision({ await publishDecision({
decision_id: `duplicate-${event.payload.quote_id}`, decision_id: `duplicate-${event.payload.quote_id}`,
decision_at: strategyReceivedAt,
quote_id: event.payload.quote_id, quote_id: event.payload.quote_id,
pair: event.payload.pair || `${event.payload.asset_in}->${event.payload.asset_out}`, pair: event.payload.pair || `${event.payload.asset_in}->${event.payload.asset_out}`,
pair_id: pair?.pairId || null, pair_id: pair?.pairId || null,
@ -132,6 +135,10 @@ async function handleDemand(event) {
max_notional_eure: legacyEureNotional && strategyConfig?.maxNotional != null max_notional_eure: legacyEureNotional && strategyConfig?.maxNotional != null
? String(strategyConfig.maxNotional) ? String(strategyConfig.maxNotional)
: null, : null,
maker_timing: extendMakerTiming(event.payload.maker_timing, {
strategy_received_at: strategyReceivedAt,
strategy_decided_at: strategyReceivedAt,
}),
strategy_armed: state.armed, strategy_armed: state.armed,
}); });
return; return;
@ -147,20 +154,32 @@ async function handleDemand(event) {
...config, ...config,
...tradingConfig, ...tradingConfig,
}, },
strategyReceivedAt,
now: Date.parse(strategyReceivedAt),
armed: state.armed, armed: state.armed,
}); });
await publishDecision(evaluation.decision); await publishDecision(evaluation.decision);
if (evaluation.command) { if (evaluation.command) {
const commandPublishedAt = new Date().toISOString();
const makerTiming = extendMakerTiming(evaluation.command.maker_timing, {
command_published_at: commandPublishedAt,
});
const commandPayload = {
...evaluation.command,
command_published_at: commandPublishedAt,
maker_timing: makerTiming,
quote_age_at_command_ms: makerTiming.quote_age_at_command_ms,
};
const commandEvent = buildEventEnvelope({ const commandEvent = buildEventEnvelope({
source: 'strategy-engine', source: 'strategy-engine',
venue: 'near-intents', venue: 'near-intents',
eventType: 'execute_trade', eventType: 'execute_trade',
observedAt: evaluation.command.decision_at || event.observed_at || event.ingested_at, observedAt: commandPublishedAt,
payload: evaluation.command, payload: commandPayload,
}); });
await producer.sendJson(config.kafkaTopicCmdExecuteTrade, commandEvent, { key: evaluation.command.execution_key }); await producer.sendJson(config.kafkaTopicCmdExecuteTrade, commandEvent, { key: commandPayload.execution_key });
} }
} }
@ -282,6 +301,10 @@ const controlApi = startControlApi({
const nextConfig = await createPairStrategyConfigVersion(configPool, { const nextConfig = await createPairStrategyConfigVersion(configPool, {
pairId, pairId,
edgeBps, edgeBps,
maxNotional: body.max_notional,
makerMaxQuoteAgeEnabled: body.maker_max_quote_age_enabled,
makerMaxQuoteAgeMs: body.maker_max_quote_age_ms,
makerLatencyPolicyReason: body.maker_latency_policy_reason,
changedBy: body.changed_by || 'operator', changedBy: body.changed_by || 'operator',
reason: body.reason || 'operator edge update', reason: body.reason || 'operator edge update',
}); });

View file

@ -10,6 +10,8 @@ import { buildEventEnvelope, parseEventMessage } from '../core/event-envelope.mj
import { createExecutorStateStore } from '../core/executor-state-store.mjs'; import { createExecutorStateStore } from '../core/executor-state-store.mjs';
import { createIntentRequestController } from '../core/intent-request-controller.mjs'; import { createIntentRequestController } from '../core/intent-request-controller.mjs';
import { createLogger, serializeError } from '../core/log.mjs'; import { createLogger, serializeError } from '../core/log.mjs';
import { extendMakerTiming } from '../core/maker-timing.mjs';
import { classifyRelaySubmissionFailure } from '../core/relay-failure-classification.mjs';
import { import {
assertExecuteTradeCommand, assertExecuteTradeCommand,
assertIntentRequestPreflightEvent, assertIntentRequestPreflightEvent,
@ -307,6 +309,8 @@ async function handleCommand(event) {
await publishResult(payload, withExecutorTiming({ await publishResult(payload, withExecutorTiming({
status: 'failed', status: 'failed',
result_code: 'submission_failed', result_code: 'submission_failed',
failure_category: classifyRelaySubmissionFailure(error),
relay_error_message: error?.message || String(error),
error: serializeError(error), error: serializeError(error),
}, timing)); }, timing));
} finally { } finally {
@ -325,9 +329,14 @@ function startExecutorTiming(event) {
|| event?.payload?.decision_at || event?.payload?.decision_at
|| '', || '',
); );
const makerTiming = extendMakerTiming(event?.payload?.maker_timing, {
executor_received_at: receivedAt,
});
return { return {
received_at: receivedAt.toISOString(), received_at: receivedAt.toISOString(),
started_at_ms: performance.now(), started_at_ms: performance.now(),
maker_timing: makerTiming,
quote_age_at_executor_receipt_ms: makerTiming.quote_age_at_executor_receipt_ms,
command_event_age_ms: Number.isFinite(commandEventAtMs) command_event_age_ms: Number.isFinite(commandEventAtMs)
? roundTimingMs(receivedAt.getTime() - commandEventAtMs) ? roundTimingMs(receivedAt.getTime() - commandEventAtMs)
: null, : null,
@ -340,23 +349,40 @@ function recordExecutorTiming(timing, field, startedAtMs) {
} }
function withExecutorTiming(payload, timing) { function withExecutorTiming(payload, timing) {
const finished = finishExecutorTiming(timing);
return { return {
...payload, ...payload,
executor_timing: finishExecutorTiming(timing), maker_timing: finished.maker_timing,
quote_age_at_executor_receipt_ms: finished.executor_timing?.quote_age_at_executor_receipt_ms ?? null,
quote_age_at_relay_result_ms: finished.executor_timing?.quote_age_at_relay_result_ms ?? null,
executor_timing: finished.executor_timing,
}; };
} }
function finishExecutorTiming(timing) { function finishExecutorTiming(timing) {
if (!timing) return null; if (!timing) return { maker_timing: null, executor_timing: null };
const executorResultAt = new Date().toISOString();
const relayResultAt = timing.relay_response_ms == null ? null : executorResultAt;
const makerTiming = extendMakerTiming(timing.maker_timing, {
executor_result_at: executorResultAt,
relay_result_at: relayResultAt,
});
return { return {
maker_timing: makerTiming,
executor_timing: {
received_at: timing.received_at, received_at: timing.received_at,
executor_result_at: executorResultAt,
relay_result_at: relayResultAt,
command_event_age_ms: timing.command_event_age_ms, command_event_age_ms: timing.command_event_age_ms,
quote_age_at_executor_receipt_ms: timing.quote_age_at_executor_receipt_ms ?? null,
quote_age_at_relay_result_ms: makerTiming.quote_age_at_relay_result_ms ?? null,
current_salt_ms: timing.current_salt_ms ?? null, current_salt_ms: timing.current_salt_ms ?? null,
current_salt_source: timing.current_salt_source ?? null, current_salt_source: timing.current_salt_source ?? null,
current_salt_age_ms: timing.current_salt_age_ms ?? null, current_salt_age_ms: timing.current_salt_age_ms ?? null,
sign_ms: timing.sign_ms ?? null, sign_ms: timing.sign_ms ?? null,
relay_response_ms: timing.relay_response_ms ?? null, relay_response_ms: timing.relay_response_ms ?? null,
executor_total_ms: roundTimingMs(performance.now() - timing.started_at_ms), executor_total_ms: roundTimingMs(performance.now() - timing.started_at_ms),
},
}; };
} }
@ -383,6 +409,12 @@ async function publishResult(command, extraPayload) {
edge_bps: command.edge_bps || null, edge_bps: command.edge_bps || null,
max_notional: command.max_notional || null, max_notional: command.max_notional || null,
price_route_id: command.price_route_id || null, price_route_id: command.price_route_id || null,
reference_price_id: command.reference_price_id || null,
direction: command.direction || null,
request_kind: command.request_kind || null,
notional: command.notional || null,
notional_asset_id: command.notional_asset_id || null,
notional_symbol: command.notional_symbol || null,
...extraPayload, ...extraPayload,
}, },
}); });

View file

@ -0,0 +1,305 @@
import { normalizeMakerTiming } from './maker-timing.mjs';
import { classifyRelaySubmissionFailure } from './relay-failure-classification.mjs';
const LATENCY_FIELDS = [
'quote_to_decision_ms',
'decision_to_command_ms',
'command_to_executor_ms',
'executor_to_relay_result_ms',
'quote_to_relay_result_ms',
'quote_to_outcome_ms',
];
const QUOTE_AGE_FIELDS = [
'quote_age_at_decision_ms',
'quote_age_at_executor_receipt_ms',
'quote_age_at_relay_result_ms',
];
export function buildMakerCompetitivenessSummary({ lifecycleRows = [], generatedAt = new Date().toISOString() } = {}) {
const entries = (lifecycleRows || [])
.map(normalizeCompetitivenessEntry)
.filter((entry) => entry.quote_id || entry.decision_id || entry.command_id);
const groups = [...groupEntries(entries).values()]
.map(summarizeGroup)
.sort((left, right) => right.count - left.count || String(left.pair).localeCompare(String(right.pair)));
return {
generated_at: generatedAt,
total: summarizeCounts(entries),
latency_stages: summarizeLatency(entries),
quote_age_stages: summarizeQuoteAgeStages(entries),
groups,
age_buckets: summarizeAgeBuckets(entries),
salt_sources: summarizeSaltSources(entries),
latest_errors: entries
.filter((entry) => entry.error_message || entry.failure_category)
.sort((left, right) => timestampMs(right.result_at) - timestampMs(left.result_at))
.slice(0, 10)
.map((entry) => ({
quote_id: entry.quote_id,
pair: entry.pair,
direction: entry.direction,
request_kind: entry.request_kind,
result_code: entry.result_code,
failure_category: entry.failure_category,
error_message: entry.error_message,
quote_age_bucket: entry.quote_age_bucket,
quote_age_ms: entry.quote_age_ms,
result_at: entry.result_at,
})),
policy_skips: entries
.filter((entry) => entry.outcome_status === 'policy_skip')
.slice(0, 10)
.map((entry) => ({
quote_id: entry.quote_id,
pair: entry.pair,
direction: entry.direction,
request_kind: entry.request_kind,
reason_code: entry.result_code,
quote_age_ms: entry.quote_age_ms,
quote_age_bucket: entry.quote_age_bucket,
max_quote_age_ms: entry.policy?.max_quote_age_ms ?? null,
pair_config_id: entry.pair_config_id,
pair_config_version: entry.pair_config_version,
decision_at: entry.decision_at,
})),
};
}
export function normalizeCompetitivenessEntry(row = {}) {
const timing = normalizeMakerTiming(
row.maker_timing
|| row.execution?.maker_timing
|| row.command?.maker_timing
|| row.decision?.maker_timing
|| row.quote?.maker_timing
|| {},
);
const execution = row.execution || {};
const decision = row.decision || {};
const policy = decision.response_policy || row.response_policy || null;
const resultCode = execution.result_code
|| (decision.decision === 'blocked' ? decision.decision_reason : null)
|| row.reason_code
|| null;
const failureCategory = execution.failure_category
|| (execution.status === 'failed' ? classifyRelaySubmissionFailure(execution) : null);
const quoteAgeMs = firstNumber([
timing.quote_age_at_relay_result_ms,
timing.quote_age_at_executor_receipt_ms,
timing.quote_age_at_decision_ms,
policy?.measured_quote_age_ms,
row.quote_age_at_decision_ms,
]);
const outcomeStatus = classifyOutcomeStatus({ row, execution, decision });
return {
quote_id: row.quote_id || decision.quote_id || execution.quote_id || null,
decision_id: row.decision_id || decision.decision_id || execution.decision_id || null,
command_id: row.command_id || execution.command_id || null,
pair: row.pair || decision.pair || execution.pair || null,
pair_id: row.pair_id || decision.pair_id || execution.pair_id || null,
pair_config_id: row.pair_config_id || decision.pair_config_id || execution.pair_config_id || null,
pair_config_version:
row.pair_config_version || decision.pair_config_version || execution.pair_config_version || null,
direction: row.direction || decision.direction || row.command?.direction || 'unknown',
request_kind: row.request_kind || decision.request_kind || row.command?.request_kind || 'unknown',
result_code: resultCode || 'no_result',
failure_category: failureCategory,
notional: row.notional ?? decision.notional ?? row.command?.notional ?? null,
notional_symbol: row.notional_symbol || decision.notional_symbol || row.command?.notional_symbol || null,
notional_bucket: notionalBucket(row.notional ?? decision.notional ?? row.command?.notional, row.notional_symbol || decision.notional_symbol || row.command?.notional_symbol),
quote_age_ms: quoteAgeMs,
quote_age_bucket: quoteAgeBucket(quoteAgeMs),
outcome_status: outcomeStatus,
accepted: execution.status === 'submitted',
relay_failed: execution.status === 'failed',
policy_skip: outcomeStatus === 'policy_skip',
error_message: execution.error_message || execution.error?.message || null,
result_at: row.execution_result_at || execution.result_at || null,
decision_at: row.decision_at || decision.decision_at || null,
maker_timing: timing,
executor_timing: execution.timing || null,
policy,
};
}
export function quoteAgeBucket(value) {
const number = Number(value);
if (!Number.isFinite(number)) return 'unavailable';
if (number < 50) return '<50ms';
if (number < 100) return '50-100ms';
if (number < 250) return '100-250ms';
if (number < 500) return '250-500ms';
if (number < 1000) return '500-1000ms';
return '>=1000ms';
}
export function notionalBucket(value, symbol = null) {
const number = Number(value);
const suffix = symbol ? ` ${symbol}` : '';
if (!Number.isFinite(number)) return `unavailable${suffix}`;
if (number < 1) return `<1${suffix}`;
if (number < 5) return `1-5${suffix}`;
if (number < 25) return `5-25${suffix}`;
if (number < 100) return `25-100${suffix}`;
return `>=100${suffix}`;
}
function groupEntries(entries) {
const groups = new Map();
for (const entry of entries) {
const key = [
entry.pair || 'unknown',
entry.direction || 'unknown',
entry.request_kind || 'unknown',
entry.result_code || 'no_result',
entry.failure_category || 'none',
entry.quote_age_bucket,
entry.notional_bucket,
entry.outcome_status || 'unknown',
].join('|');
const list = groups.get(key) || [];
list.push(entry);
groups.set(key, list);
}
return groups;
}
function summarizeGroup(entries) {
const first = entries[0] || {};
return {
pair: first.pair || null,
direction: first.direction || 'unknown',
request_kind: first.request_kind || 'unknown',
result_code: first.result_code || 'no_result',
failure_category: first.failure_category || null,
quote_age_bucket: first.quote_age_bucket,
notional_bucket: first.notional_bucket,
outcome_status: first.outcome_status || 'unknown',
...summarizeCounts(entries),
latency_stages: summarizeLatency(entries),
};
}
function summarizeCounts(entries) {
const count = entries.length;
const accepted = entries.filter((entry) => entry.accepted).length;
const relayFailed = entries.filter((entry) => entry.relay_failed).length;
const policySkips = entries.filter((entry) => entry.policy_skip).length;
const staleFinished = entries.filter((entry) => entry.failure_category === 'quote_not_found_or_finished').length;
return {
count,
accepted_count: accepted,
relay_failed_count: relayFailed,
policy_skip_count: policySkips,
quote_not_found_or_finished_count: staleFinished,
accepted_rate: count ? accepted / count : null,
stale_or_finished_rate: count ? staleFinished / count : null,
};
}
function summarizeLatency(entries) {
return LATENCY_FIELDS.map((field) => ({
stage: field,
...percentiles(entries.map((entry) => entry.maker_timing?.[field])),
})).filter((stage) => stage.count > 0);
}
function summarizeQuoteAgeStages(entries) {
return QUOTE_AGE_FIELDS.map((field) => ({
stage: field,
...percentiles(entries.map((entry) => entry.maker_timing?.[field])),
})).filter((stage) => stage.count > 0);
}
function summarizeAgeBuckets(entries) {
const buckets = new Map();
for (const entry of entries) {
const key = [
entry.pair || 'unknown',
entry.direction || 'unknown',
entry.request_kind || 'unknown',
entry.quote_age_bucket,
entry.outcome_status || 'unknown',
].join('|');
const list = buckets.get(key) || [];
list.push(entry);
buckets.set(key, list);
}
return [...buckets.values()].map((list) => {
const first = list[0] || {};
return {
pair: first.pair || null,
direction: first.direction || 'unknown',
request_kind: first.request_kind || 'unknown',
quote_age_bucket: first.quote_age_bucket,
outcome_status: first.outcome_status || 'unknown',
...summarizeCounts(list),
};
}).sort((left, right) => right.count - left.count);
}
function summarizeSaltSources(entries) {
const counts = new Map();
for (const entry of entries) {
const source = entry.executor_timing?.current_salt_source || 'unavailable';
counts.set(source, (counts.get(source) || 0) + 1);
}
return [...counts.entries()]
.map(([source, count]) => ({ source, count }))
.sort((left, right) => right.count - left.count);
}
function percentiles(values) {
const sorted = values
.filter(isNumericValue)
.map(Number)
.filter(Number.isFinite)
.sort((left, right) => left - right);
if (!sorted.length) return { count: 0, p50_ms: null, p90_ms: null, p99_ms: null };
return {
count: sorted.length,
p50_ms: percentile(sorted, 0.5),
p90_ms: percentile(sorted, 0.9),
p99_ms: percentile(sorted, 0.99),
};
}
function percentile(sorted, rank) {
const index = Math.min(sorted.length - 1, Math.ceil(sorted.length * rank) - 1);
return sorted[index];
}
function classifyOutcomeStatus({ row, execution, decision }) {
if (decision.decision === 'blocked' && String(decision.decision_reason || '').startsWith('maker_')) {
return 'policy_skip';
}
if (row.outcome_status) return row.outcome_status;
if (row.lifecycle_state === 'failed') return 'relay_failed';
if (execution.status === 'failed') return 'relay_failed';
if (execution.status === 'submitted') return row.outcome_status || 'submitted';
if (row.lifecycle_state) return row.lifecycle_state;
if (decision.decision === 'rejected') return 'strategy_rejected';
return 'unknown';
}
function firstNumber(values) {
for (const value of values || []) {
if (!isNumericValue(value)) continue;
const number = Number(value);
if (Number.isFinite(number)) return number;
}
return null;
}
function isNumericValue(value) {
return value != null && value !== '';
}
function timestampMs(value) {
if (!value) return Number.NEGATIVE_INFINITY;
const parsed = Date.parse(value);
return Number.isFinite(parsed) ? parsed : Number.NEGATIVE_INFINITY;
}

146
src/core/maker-timing.mjs Normal file
View file

@ -0,0 +1,146 @@
export const MAKER_TIMING_TIMESTAMP_FIELDS = [
'quote_observed_at',
'quote_received_at',
'quote_normalized_at',
'quote_published_at',
'strategy_received_at',
'strategy_decided_at',
'command_published_at',
'executor_received_at',
'executor_result_at',
'relay_result_at',
'outcome_observed_at',
];
export const MAKER_TIMING_DURATION_FIELDS = [
'quote_to_decision_ms',
'decision_to_command_ms',
'command_to_executor_ms',
'executor_to_relay_result_ms',
'quote_to_relay_result_ms',
'quote_to_outcome_ms',
'quote_age_at_decision_ms',
'quote_age_at_command_ms',
'quote_age_at_executor_receipt_ms',
'quote_age_at_relay_result_ms',
];
const DURATION_DEFINITIONS = {
quote_to_decision_ms: ['quote_start_at', 'strategy_decided_at'],
decision_to_command_ms: ['strategy_decided_at', 'command_published_at'],
command_to_executor_ms: ['command_published_at', 'executor_received_at'],
executor_to_relay_result_ms: ['executor_received_at', 'relay_result_at'],
quote_to_relay_result_ms: ['quote_start_at', 'relay_result_at'],
quote_to_outcome_ms: ['quote_start_at', 'outcome_observed_at'],
quote_age_at_decision_ms: ['quote_start_at', 'strategy_decided_at'],
quote_age_at_command_ms: ['quote_start_at', 'command_published_at'],
quote_age_at_executor_receipt_ms: ['quote_start_at', 'executor_received_at'],
quote_age_at_relay_result_ms: ['quote_start_at', 'relay_result_at'],
};
export function buildInitialMakerTiming({
quoteObservedAt = null,
quoteReceivedAt = null,
quoteNormalizedAt = null,
quotePublishedAt = null,
} = {}) {
return extendMakerTiming(null, {
quote_observed_at: quoteObservedAt,
quote_received_at: quoteReceivedAt,
quote_normalized_at: quoteNormalizedAt,
quote_published_at: quotePublishedAt,
});
}
export function extendMakerTiming(existing = null, fields = {}) {
const timing = normalizeMakerTiming(existing);
for (const field of MAKER_TIMING_TIMESTAMP_FIELDS) {
if (!Object.hasOwn(fields, field)) continue;
timing[field] = toIsoTimestamp(fields[field]);
}
for (const field of MAKER_TIMING_DURATION_FIELDS) {
if (!Object.hasOwn(fields, field)) continue;
timing[field] = roundTimingMs(fields[field]);
}
return recomputeMakerTiming(timing);
}
export function normalizeMakerTiming(value = null) {
const source = value?.maker_timing && typeof value.maker_timing === 'object'
? value.maker_timing
: value || {};
const timing = {};
for (const field of MAKER_TIMING_TIMESTAMP_FIELDS) {
timing[field] = toIsoTimestamp(source[field] ?? value?.[field]);
}
for (const field of MAKER_TIMING_DURATION_FIELDS) {
timing[field] = roundTimingMs(source[field] ?? value?.[field]);
}
const unavailable = source.unavailable_reasons || value?.unavailable_reasons || {};
timing.unavailable_reasons = isRecord(unavailable) ? { ...unavailable } : {};
return recomputeMakerTiming(timing);
}
export function quoteAgeMsAt(timingInput, timestamp) {
const timing = normalizeMakerTiming(timingInput);
return safeDurationMs(resolveQuoteStartAt(timing), timestamp).value;
}
export function resolveQuoteStartAt(timingInput) {
const timing = normalizeMakerTiming(timingInput);
return timing.quote_received_at || timing.quote_observed_at || null;
}
export function safeDurationMs(start, end) {
const startMs = timestampMs(start);
const endMs = timestampMs(end);
if (!Number.isFinite(startMs) || !Number.isFinite(endMs)) {
return { value: null, reason: 'timestamp_missing' };
}
const duration = endMs - startMs;
if (duration < 0) return { value: null, reason: 'clock_skew_or_negative_duration' };
return { value: roundTimingMs(duration), reason: null };
}
export function toIsoTimestamp(value) {
if (value == null || value === '') return null;
if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value.toISOString();
const parsed = new Date(value);
return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
}
export function roundTimingMs(value) {
const number = Number(value);
return Number.isFinite(number) ? Math.round(number * 1000) / 1000 : null;
}
function recomputeMakerTiming(timing) {
const next = { ...timing, unavailable_reasons: {} };
const quoteStartAt = timing.quote_received_at || timing.quote_observed_at || null;
for (const [field, [startField, endField]] of Object.entries(DURATION_DEFINITIONS)) {
const start = startField === 'quote_start_at' ? quoteStartAt : timing[startField];
const end = timing[endField];
const duration = safeDurationMs(start, end);
next[field] = duration.value;
if (duration.reason) next.unavailable_reasons[field] = duration.reason;
}
if (!Object.keys(next.unavailable_reasons).length) delete next.unavailable_reasons;
return next;
}
function timestampMs(value) {
if (!value) return Number.NaN;
const parsed = Date.parse(value);
return Number.isFinite(parsed) ? parsed : Number.NaN;
}
function isRecord(value) {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}

View file

@ -1,6 +1,7 @@
import { unitsToNumber } from './assets.mjs'; import { unitsToNumber } from './assets.mjs';
import { bridgeDepositObservedAt } from './bridge-assets.mjs'; 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 { resolveDashboardRequestAuth } from './operator-dashboard-auth.mjs'; import { resolveDashboardRequestAuth } from './operator-dashboard-auth.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';
@ -1105,6 +1106,9 @@ const HUMAN_REASON_TEXT = {
executor_disarmed: 'Executor is disarmed.', executor_disarmed: 'Executor is disarmed.',
executor_paused: 'Executor intake is paused.', executor_paused: 'Executor intake is paused.',
inventory_unavailable: 'Inventory unavailable.', inventory_unavailable: 'Inventory unavailable.',
maker_quote_age_unavailable: 'Maker quote age is unavailable.',
maker_quote_response_policy_invalid: 'Maker response-age policy is invalid.',
maker_quote_too_old: 'Maker quote is too old for the configured response-age policy.',
pending_deposit_not_credited: 'Funding is not credited yet.', pending_deposit_not_credited: 'Funding is not credited yet.',
quote_expired: 'Quote expired.', quote_expired: 'Quote expired.',
quote_response_ack: 'Quote response acknowledged by the relay.', quote_response_ack: 'Quote response acknowledged by the relay.',
@ -1147,6 +1151,7 @@ export function deriveQuoteLifecycleRows({
asset_out: normalizedQuote?.asset_out || null, asset_out: normalizedQuote?.asset_out || null,
amount_in: normalizedQuote?.amount_in || null, amount_in: normalizedQuote?.amount_in || null,
amount_out: normalizedQuote?.amount_out || null, amount_out: normalizedQuote?.amount_out || null,
maker_timing: normalizedQuote?.maker_timing || null,
quote: normalizedQuote, quote: normalizedQuote,
quote_observed_at: normalizedQuote?.observed_at || normalizedQuote?.ingested_at || null, quote_observed_at: normalizedQuote?.observed_at || normalizedQuote?.ingested_at || null,
}); });
@ -1179,6 +1184,7 @@ export function deriveQuoteLifecycleRows({
notional_asset_id: decision.notional_asset_id, notional_asset_id: decision.notional_asset_id,
notional_symbol: decision.notional_symbol, notional_symbol: decision.notional_symbol,
eure_notional: decision.eure_notional, eure_notional: decision.eure_notional,
maker_timing: decision.maker_timing || null,
decision, decision,
decision_at: decision.decision_at || null, decision_at: decision.decision_at || null,
}); });
@ -1214,6 +1220,7 @@ export function deriveQuoteLifecycleRows({
notional_asset_id: command.notional_asset_id || null, notional_asset_id: command.notional_asset_id || null,
notional_symbol: command.notional_symbol || null, notional_symbol: command.notional_symbol || null,
eure_notional: command.eure_notional || null, eure_notional: command.eure_notional || null,
maker_timing: command.maker_timing || null,
command, command,
command_at: command.command_at || null, command_at: command.command_at || null,
}); });
@ -1226,6 +1233,7 @@ export function deriveQuoteLifecycleRows({
decision_id: execution?.decision_id || null, decision_id: execution?.decision_id || null,
command_id: execution?.command_id || null, command_id: execution?.command_id || null,
pair: execution?.pair || null, pair: execution?.pair || null,
maker_timing: execution?.maker_timing || null,
execution, execution,
execution_result_at: execution?.result_at || null, execution_result_at: execution?.result_at || null,
}); });
@ -1245,6 +1253,7 @@ export function deriveQuoteLifecycleRows({
notional_asset_id: outcome?.notional_asset_id || null, notional_asset_id: outcome?.notional_asset_id || null,
notional_symbol: outcome?.notional_symbol || null, notional_symbol: outcome?.notional_symbol || null,
eure_notional: outcome?.eure_notional || null, eure_notional: outcome?.eure_notional || null,
maker_timing: outcome?.maker_timing || null,
outcome, outcome,
command_at: outcome?.command_at || null, command_at: outcome?.command_at || null,
execution_result_at: outcome?.submitted_at || null, execution_result_at: outcome?.submitted_at || null,
@ -1286,6 +1295,7 @@ function ensureLifecycleRow(rowsByKey, key) {
command_at: null, command_at: null,
execution_result_at: null, execution_result_at: null,
outcome_observed_at: null, outcome_observed_at: null,
maker_timing: null,
quote: null, quote: null,
decision: null, decision: null,
command: null, command: null,
@ -1369,6 +1379,11 @@ function finalizeLifecycleRow(row) {
lifecycle_label = 'Awaiting executor'; lifecycle_label = 'Awaiting executor';
reason_code = 'awaiting_executor'; reason_code = 'awaiting_executor';
reason_text = 'Execute command recorded, but no executor result is stored yet.'; reason_text = 'Execute command recorded, but no executor result is stored yet.';
} else if (decision?.decision === 'blocked') {
lifecycle_state = 'blocked';
lifecycle_label = 'Policy skip';
reason_code = normalizeLifecycleToken(decision?.decision_reason || 'reason_unknown');
reason_text = buildPolicySkipText(decision, reason_code);
} else if (decision?.decision === 'rejected') { } else if (decision?.decision === 'rejected') {
lifecycle_state = 'rejected'; lifecycle_state = 'rejected';
lifecycle_label = 'Rejected by strategy'; lifecycle_label = 'Rejected by strategy';
@ -1416,6 +1431,7 @@ function finalizeLifecycleRow(row) {
durable_outcome_source: outcome?.outcome_source || null, durable_outcome_source: outcome?.outcome_source || null,
attribution_status: outcome?.attribution_status || null, attribution_status: outcome?.attribution_status || null,
attribution_method: outcome?.attribution_method || null, attribution_method: outcome?.attribution_method || null,
maker_timing: row.maker_timing || null,
}, },
outcome_source: outcome?.outcome_source || null, outcome_source: outcome?.outcome_source || null,
outcome_status: outcome?.outcome_status || execution?.outcome_status || null, outcome_status: outcome?.outcome_status || execution?.outcome_status || null,
@ -1426,9 +1442,20 @@ function finalizeLifecycleRow(row) {
|| execution?.attributed_inventory_delta || execution?.attributed_inventory_delta
|| null, || null,
has_settlement_evidence: hasSettlementEvidence(outcome || execution), has_settlement_evidence: hasSettlementEvidence(outcome || execution),
maker_timing: row.maker_timing || null,
}; };
} }
function buildPolicySkipText(decision, reasonCode) {
const base = humanizeReasonCode(reasonCode, 'Policy skipped the response.');
const policy = decision?.response_policy || {};
const measured = policy.measured_quote_age_ms == null ? null : `${policy.measured_quote_age_ms}ms`;
const max = policy.max_quote_age_ms == null ? null : `${policy.max_quote_age_ms}ms`;
if (measured && max) return `${base} Quote age ${measured} exceeded configured max ${max}.`;
if (measured) return `${base} Quote age ${measured}.`;
return base;
}
function buildCompletedOutcomeText({ outcome, reasonCode }) { function buildCompletedOutcomeText({ outcome, reasonCode }) {
const base = humanizeReasonCode(reasonCode, 'Completed.'); const base = humanizeReasonCode(reasonCode, 'Completed.');
if (!outcome?.attribution_status) return `${base} Settlement attribution is not stored.`; if (!outcome?.attribution_status) return `${base} Settlement attribution is not stored.`;
@ -1487,6 +1514,7 @@ function normalizeLifecycleQuote(quote) {
amount_in: quote.amount_in ?? null, amount_in: quote.amount_in ?? null,
amount_out: quote.amount_out ?? null, amount_out: quote.amount_out ?? null,
min_deadline_ms: quote.min_deadline_ms ?? null, min_deadline_ms: quote.min_deadline_ms ?? null,
maker_timing: quote.maker_timing || null,
observed_at: quote.observed_at || null, observed_at: quote.observed_at || null,
ingested_at: quote.ingested_at || null, ingested_at: quote.ingested_at || null,
}; };
@ -1514,6 +1542,8 @@ function normalizeCommand(command) {
asset_out: command.asset_out || null, asset_out: command.asset_out || null,
amount_in: command.amount_in ?? null, amount_in: command.amount_in ?? null,
amount_out: command.amount_out ?? null, amount_out: command.amount_out ?? null,
maker_timing: command.maker_timing || null,
quote_age_at_command_ms: command.quote_age_at_command_ms ?? null,
command_at: command.command_at || command.observed_at || command.ingested_at || null, command_at: command.command_at || command.observed_at || command.ingested_at || null,
}; };
} }
@ -1575,6 +1605,9 @@ function buildStrategySummary({
}).map((row) => enrichLifecycleRowForUi({ config, row })); }).map((row) => enrichLifecycleRowForUi({ config, row }));
const lifecycleRows = allLifecycleRows.slice(0, 20); const lifecycleRows = allLifecycleRows.slice(0, 20);
const tradeFunnel = buildTradeFunnelSummary(allLifecycleRows); const tradeFunnel = buildTradeFunnelSummary(allLifecycleRows);
const makerCompetitiveness = buildMakerCompetitivenessSummary({
lifecycleRows: allLifecycleRows,
});
return { return {
strategy_state: { strategy_state: {
@ -1594,6 +1627,7 @@ function buildStrategySummary({
: [...durableDecisionsById.values()].slice(0, 20), : [...durableDecisionsById.values()].slice(0, 20),
recent_lifecycle_rows: lifecycleRows, recent_lifecycle_rows: lifecycleRows,
trade_funnel: tradeFunnel, trade_funnel: tradeFunnel,
maker_competitiveness: makerCompetitiveness,
skipped_counts: strategyState.skipped_counts || {}, skipped_counts: strategyState.skipped_counts || {},
durable_control_state: strategyState.durable_control_state || null, durable_control_state: strategyState.durable_control_state || null,
trading_config: strategyState.trading_config || null, trading_config: strategyState.trading_config || null,
@ -2257,6 +2291,9 @@ function normalizeDecision(decision) {
notional_asset_id: decision.notional_asset_id || null, notional_asset_id: decision.notional_asset_id || null,
notional_symbol: decision.notional_symbol || null, notional_symbol: decision.notional_symbol || null,
eure_notional: decision.eure_notional || null, eure_notional: decision.eure_notional || null,
maker_timing: decision.maker_timing || null,
quote_age_at_decision_ms: decision.quote_age_at_decision_ms ?? null,
response_policy: decision.response_policy || null,
}; };
} }
@ -2342,12 +2379,14 @@ function normalizeDashboardLiveTopic(state, topic) {
function buildQuoteLifecycleUpdate(state, { flashQuoteId = null } = {}) { function buildQuoteLifecycleUpdate(state, { flashQuoteId = null } = {}) {
const receivedAt = new Date().toISOString(); const receivedAt = new Date().toISOString();
return { const lifecycleRows = buildLiveQuoteLifecycleRows(state, {
type: 'quote_lifecycle.updated',
recent_lifecycle_rows: buildLiveQuoteLifecycleRows(state, {
flashQuoteId, flashQuoteId,
flashAt: receivedAt, flashAt: receivedAt,
}), });
return {
type: 'quote_lifecycle.updated',
recent_lifecycle_rows: lifecycleRows,
maker_competitiveness: buildMakerCompetitivenessSummary({ lifecycleRows, generatedAt: receivedAt }),
flash_quote_id: flashQuoteId || null, flash_quote_id: flashQuoteId || null,
received_at: receivedAt, received_at: receivedAt,
}; };
@ -2379,6 +2418,7 @@ function normalizeLiveDecision(payload, event) {
payload: { payload: {
...payload, ...payload,
decision_at: decisionAt, decision_at: decisionAt,
maker_timing: payload.maker_timing || null,
}, },
}; };
} }
@ -2392,6 +2432,7 @@ function normalizeLiveCommand(payload, event) {
...payload, ...payload,
amount_in: payload.quote_output?.amount_in ?? payload.proposed_amount_in ?? payload.amount_in ?? null, amount_in: payload.quote_output?.amount_in ?? payload.proposed_amount_in ?? payload.amount_in ?? null,
amount_out: payload.quote_output?.amount_out ?? payload.proposed_amount_out ?? payload.amount_out ?? null, amount_out: payload.quote_output?.amount_out ?? payload.proposed_amount_out ?? payload.amount_out ?? null,
maker_timing: payload.maker_timing || null,
}, },
}; };
} }
@ -2407,15 +2448,17 @@ function normalizeLiveExecutionResult(payload, event) {
result_at: event.observed_at || event.ingested_at || new Date().toISOString(), result_at: event.observed_at || event.ingested_at || new Date().toISOString(),
status: payload.status || null, status: payload.status || null,
result_code: payload.result_code || null, result_code: payload.result_code || null,
failure_category: payload.failure_category || null,
outcome_status: payload.outcome_status || payload.venue_outcome_status || payload.trade_outcome_status || null, outcome_status: payload.outcome_status || payload.venue_outcome_status || payload.trade_outcome_status || null,
outcome_reason: payload.outcome_reason || payload.venue_outcome_reason || payload.trade_outcome_reason || null, outcome_reason: payload.outcome_reason || payload.venue_outcome_reason || payload.trade_outcome_reason || null,
attribution_status: payload.attribution_status || null, attribution_status: payload.attribution_status || null,
attribution_method: payload.attribution_method || null, attribution_method: payload.attribution_method || null,
attributed_inventory_delta: payload.attributed_inventory_delta || null, attributed_inventory_delta: payload.attributed_inventory_delta || null,
venue_response: payload.venue_response || null, venue_response: payload.venue_response || null,
error_message: payload.error?.message || null, error_message: payload.relay_error_message || payload.error?.message || null,
note: payload.note || null, note: payload.note || null,
timing: payload.executor_timing || null, timing: payload.executor_timing || null,
maker_timing: payload.maker_timing || null,
}; };
} }
@ -2434,6 +2477,7 @@ function normalizeLiveQuote(payload, event) {
request_kind: payload.request_kind || null, request_kind: payload.request_kind || null,
amount_in: payload.amount_in ?? null, amount_in: payload.amount_in ?? null,
amount_out: payload.amount_out ?? null, amount_out: payload.amount_out ?? null,
maker_timing: payload.maker_timing || null,
observed_at: event.observed_at || null, observed_at: event.observed_at || null,
ingested_at: event.ingested_at || null, ingested_at: event.ingested_at || null,
}; };

View file

@ -1,3 +1,5 @@
import { extendMakerTiming } from './maker-timing.mjs';
const DEFAULT_ATTRIBUTION_WINDOW_MS = 10 * 60 * 1000; const DEFAULT_ATTRIBUTION_WINDOW_MS = 10 * 60 * 1000;
const DEFAULT_SETTLEMENT_GRACE_MS = 60 * 1000; const DEFAULT_SETTLEMENT_GRACE_MS = 60 * 1000;
@ -283,6 +285,10 @@ function baseOutcomeRecord({
attributed_inventory_delta, attributed_inventory_delta,
evidence, evidence,
}) { }) {
const makerTiming = extendMakerTiming(
submission.maker_timing || command?.maker_timing || decision?.maker_timing || {},
{ outcome_observed_at },
);
return { return {
quote_id: submission.quote_id, quote_id: submission.quote_id,
decision_id: command?.decision_id || submission.decision_id || decision?.decision_id || null, decision_id: command?.decision_id || submission.decision_id || decision?.decision_id || null,
@ -312,6 +318,8 @@ function baseOutcomeRecord({
eure_notional: decision?.eure_notional || command?.eure_notional || null, eure_notional: decision?.eure_notional || command?.eure_notional || null,
execution_result_status: submission.status, execution_result_status: submission.status,
execution_result_code: submission.result_code || null, execution_result_code: submission.result_code || null,
failure_category: submission.failure_category || null,
maker_timing: makerTiming,
submitted_at: submission.submitted_at, submitted_at: submission.submitted_at,
command_at: command?.command_at || null, command_at: command?.command_at || null,
outcome_status, outcome_status,
@ -413,6 +421,8 @@ function normalizeSubmission(entry) {
pair: payload.pair || null, pair: payload.pair || null,
status: payload.status || null, status: payload.status || null,
result_code: payload.result_code || null, result_code: payload.result_code || null,
failure_category: payload.failure_category || null,
maker_timing: payload.maker_timing || null,
submitted_at: toIsoTimestamp( submitted_at: toIsoTimestamp(
entry?.observed_at entry?.observed_at
|| entry?.ingested_at || entry?.ingested_at
@ -446,6 +456,7 @@ function normalizeCommand(entry) {
proposed_amount_out: payload.proposed_amount_out ?? null, proposed_amount_out: payload.proposed_amount_out ?? null,
expected_inventory_delta_units: payload.expected_inventory_delta_units || null, expected_inventory_delta_units: payload.expected_inventory_delta_units || null,
min_deadline_ms: payload.min_deadline_ms ?? null, min_deadline_ms: payload.min_deadline_ms ?? null,
maker_timing: payload.maker_timing || null,
command_at: toIsoTimestamp( command_at: toIsoTimestamp(
entry?.observed_at entry?.observed_at
|| entry?.ingested_at || entry?.ingested_at
@ -470,6 +481,7 @@ function normalizeDecision(entry) {
notional_asset_id: payload.notional_asset_id || null, notional_asset_id: payload.notional_asset_id || null,
notional_symbol: payload.notional_symbol || null, notional_symbol: payload.notional_symbol || null,
eure_notional: payload.eure_notional || null, eure_notional: payload.eure_notional || null,
maker_timing: payload.maker_timing || null,
}; };
} }

View file

@ -0,0 +1,45 @@
export const RELAY_FAILURE_CATEGORIES = new Set([
'quote_not_found_or_finished',
'relay_timeout',
'relay_disconnected',
'signing_failed',
'salt_unavailable',
'unknown_submission_failure',
]);
export function classifyRelaySubmissionFailure(errorOrPayload = null) {
const text = failureText(errorOrPayload);
if (/quote[^.]*not[^.]*found|already[^.]*finished|not[^.]*found[^.]*already[^.]*finished/i.test(text)) {
return 'quote_not_found_or_finished';
}
if (/timed?\s*out|timeout|aborted/i.test(text)) return 'relay_timeout';
if (/socket[^.]*not[^.]*connected|disconnected|connection\s*lost|connection\s*closed|websocket|econnreset|epipe/i.test(text)) {
return 'relay_disconnected';
}
if (/salt|current_salt|verifier salt/i.test(text)) return 'salt_unavailable';
if (/sign|signature|private key|ed25519|key pair/i.test(text)) return 'signing_failed';
return 'unknown_submission_failure';
}
function failureText(value) {
if (!value) return '';
if (typeof value === 'string') return value;
const error = value.error && typeof value.error === 'object' ? value.error : value;
return [
error.name,
error.code,
error.message,
error.reason,
value.error_message,
value.relay_error_message,
value.result_code,
value.result_text,
value.note,
]
.filter(Boolean)
.map(String)
.join(' ');
}

View file

@ -297,6 +297,74 @@ export function buildRuntimeAlert({
}; };
} }
export function buildMakerCompetitivenessRuntimeAlerts({
makerCompetitiveness,
minPairSamples = 5,
minQuoteNotFoundOrFinishedRate = 0.4,
} = {}) {
const groups = Array.isArray(makerCompetitiveness?.groups) ? makerCompetitiveness.groups : [];
const pairStats = new Map();
for (const group of groups) {
const pair = group.pair || null;
if (!pair) continue;
const current = pairStats.get(pair) || {
pair,
sample_count: 0,
quote_not_found_or_finished_count: 0,
accepted_count: 0,
policy_skip_count: 0,
worst_group: null,
};
const count = Number(group.count || 0);
const staleFinishedCount = group.failure_category === 'quote_not_found_or_finished'
? count
: 0;
current.sample_count += count;
current.quote_not_found_or_finished_count += staleFinishedCount;
current.accepted_count += Number(group.accepted_count || 0);
current.policy_skip_count += Number(group.policy_skip_count || 0);
if (
staleFinishedCount > 0
&& (!current.worst_group || staleFinishedCount > Number(current.worst_group.count || 0))
) {
current.worst_group = group;
}
pairStats.set(pair, current);
}
return [...pairStats.values()]
.filter((stats) => {
if (stats.sample_count < minPairSamples) return false;
if (stats.quote_not_found_or_finished_count <= 0) return false;
return stats.quote_not_found_or_finished_count / stats.sample_count >= minQuoteNotFoundOrFinishedRate;
})
.map((stats) => {
const rate = stats.quote_not_found_or_finished_count / stats.sample_count;
return buildRuntimeAlert({
alert_code: 'maker_quote_not_found_or_finished_rate_high',
severity: 'warning',
reason: `maker responses for ${stats.pair} are frequently reaching relay after the quote is finished`,
service_scope: 'strategy-engine',
pair: stats.pair,
details: {
generated_at: makerCompetitiveness?.generated_at || null,
sample_count: stats.sample_count,
quote_not_found_or_finished_count: stats.quote_not_found_or_finished_count,
quote_not_found_or_finished_rate: Number(rate.toFixed(4)),
accepted_count: stats.accepted_count,
policy_skip_count: stats.policy_skip_count,
min_pair_samples: minPairSamples,
min_quote_not_found_or_finished_rate: minQuoteNotFoundOrFinishedRate,
worst_direction: stats.worst_group?.direction || null,
worst_request_kind: stats.worst_group?.request_kind || null,
worst_quote_age_bucket: stats.worst_group?.quote_age_bucket || null,
worst_notional_bucket: stats.worst_group?.notional_bucket || null,
},
});
});
}
export function shouldRaiseIngestPublishStale({ export function shouldRaiseIngestPublishStale({
lastMatchingQuoteAt = null, lastMatchingQuoteAt = null,
lastPublishedAt = null, lastPublishedAt = null,

View file

@ -12,6 +12,10 @@ import {
classifyRouteDirection, classifyRouteDirection,
resolveRouteRates, resolveRouteRates,
} from './route-rates.mjs'; } from './route-rates.mjs';
import {
extendMakerTiming,
quoteAgeMsAt,
} from './maker-timing.mjs';
export function evaluateTradeOpportunity({ export function evaluateTradeOpportunity({
demandEvent, demandEvent,
@ -19,6 +23,7 @@ export function evaluateTradeOpportunity({
inventoryEvent, inventoryEvent,
config, config,
now = Date.now(), now = Date.now(),
strategyReceivedAt = now,
armed = false, armed = false,
thresholdPct = config.strategyGrossThresholdPct, thresholdPct = config.strategyGrossThresholdPct,
maxNotional = config.strategyMaxNotional ?? config.strategyMaxNotionalEure, maxNotional = config.strategyMaxNotional ?? config.strategyMaxNotionalEure,
@ -30,6 +35,10 @@ export function evaluateTradeOpportunity({
const legacyEureNotional = isLegacyEureNotional({ pairRuntime, config }); const legacyEureNotional = isLegacyEureNotional({ pairRuntime, config });
const decisionId = crypto.randomUUID(); const decisionId = crypto.randomUUID();
const decisionAt = new Date(now).toISOString(); const decisionAt = new Date(now).toISOString();
const makerTiming = extendMakerTiming(payload.maker_timing, {
strategy_received_at: strategyReceivedAt,
strategy_decided_at: decisionAt,
});
const baseDecision = { const baseDecision = {
decision_id: decisionId, decision_id: decisionId,
decision_at: decisionAt, decision_at: decisionAt,
@ -56,6 +65,8 @@ export function evaluateTradeOpportunity({
max_notional_eure: legacyEureNotional && effectiveMaxNotional != null max_notional_eure: legacyEureNotional && effectiveMaxNotional != null
? String(effectiveMaxNotional) ? String(effectiveMaxNotional)
: null, : null,
maker_timing: makerTiming,
quote_age_at_decision_ms: makerTiming.quote_age_at_decision_ms,
strategy_armed: armed, strategy_armed: armed,
assumptions: compact({ assumptions: compact({
eure_per_eur: pairRuntime.priceRoute?.source === 'btc_eur_reference' ? '1' : null, eure_per_eur: pairRuntime.priceRoute?.source === 'btc_eur_reference' ? '1' : null,
@ -68,6 +79,23 @@ export function evaluateTradeOpportunity({
return { decision: withReason(baseDecision, pairRuntime.reason) }; return { decision: withReason(baseDecision, pairRuntime.reason) };
} }
const responsePolicy = evaluateMakerResponseAgePolicy({
strategyConfig: pairRuntime.strategyConfig,
makerTiming,
decidedAt: decisionAt,
});
if (!responsePolicy.ok) {
return {
decision: {
...baseDecision,
decision: 'blocked',
decision_reason: responsePolicy.reason,
response_policy: responsePolicy.policy,
quote_age_at_decision_ms: responsePolicy.policy.measured_quote_age_ms,
},
};
}
if (!priceEvent) { if (!priceEvent) {
return { decision: withReason(baseDecision, 'no_reference_price') }; return { decision: withReason(baseDecision, 'no_reference_price') };
} }
@ -128,6 +156,7 @@ export function evaluateTradeOpportunity({
inventory_freshness_ms: String(inventoryAgeMs), inventory_freshness_ms: String(inventoryAgeMs),
decision: armed ? 'actionable' : 'rejected', decision: armed ? 'actionable' : 'rejected',
decision_reason: armed ? 'actionable' : 'strategy_disarmed', decision_reason: armed ? 'actionable' : 'strategy_disarmed',
response_policy: responsePolicy.policy,
}; };
if (!armed) return { decision }; if (!armed) return { decision };
@ -169,6 +198,9 @@ export function evaluateTradeOpportunity({
amount_out: payload.amount_out ?? null, amount_out: payload.amount_out ?? null,
request_kind: payload.request_kind, request_kind: payload.request_kind,
min_deadline_ms: payload.min_deadline_ms ?? '60000', min_deadline_ms: payload.min_deadline_ms ?? '60000',
maker_timing: makerTiming,
quote_age_at_decision_ms: makerTiming.quote_age_at_decision_ms,
response_policy: responsePolicy.policy,
quote_output: buildResult.quoteOutput, quote_output: buildResult.quoteOutput,
proposed_amount_in: buildResult.details.proposed_amount_in ?? null, proposed_amount_in: buildResult.details.proposed_amount_in ?? null,
proposed_amount_out: buildResult.details.proposed_amount_out ?? null, proposed_amount_out: buildResult.details.proposed_amount_out ?? null,
@ -182,6 +214,53 @@ export function evaluateTradeOpportunity({
}; };
} }
function evaluateMakerResponseAgePolicy({
strategyConfig,
makerTiming,
decidedAt,
}) {
const enabled = strategyConfig?.makerMaxQuoteAgeEnabled === true;
const rawMaxAgeMs = strategyConfig?.makerMaxQuoteAgeMs;
const maxAgeMs = rawMaxAgeMs == null ? null : Number(rawMaxAgeMs);
const measuredQuoteAgeMs = quoteAgeMsAt(makerTiming, decidedAt);
const policy = {
enabled,
max_quote_age_ms: Number.isInteger(maxAgeMs) && maxAgeMs > 0 ? maxAgeMs : null,
measured_quote_age_ms: measuredQuoteAgeMs,
reason: strategyConfig?.makerLatencyPolicyReason || null,
pair_config_id: strategyConfig?.configId || strategyConfig?.config_id || null,
pair_config_version: strategyConfig?.version == null ? null : String(strategyConfig.version),
};
if (!enabled) return { ok: true, policy: { ...policy, valid: true } };
if (!Number.isInteger(maxAgeMs) || maxAgeMs <= 0) {
return {
ok: false,
reason: 'maker_quote_response_policy_invalid',
policy: { ...policy, valid: false },
};
}
if (!Number.isFinite(measuredQuoteAgeMs)) {
return {
ok: false,
reason: 'maker_quote_age_unavailable',
policy: { ...policy, valid: true },
};
}
if (measuredQuoteAgeMs > maxAgeMs) {
return {
ok: false,
reason: 'maker_quote_too_old',
policy: { ...policy, valid: true },
};
}
return { ok: true, policy: { ...policy, valid: true } };
}
function buildQuote({ function buildQuote({
demand, demand,
price, price,

View file

@ -25,6 +25,9 @@ export const CURRENT_REQUEST_MAX_SLIPPAGE_BPS = null;
export const CURRENT_MIN_DEADLINE_MS = 60_000; export const CURRENT_MIN_DEADLINE_MS = 60_000;
export const CURRENT_PRICE_MAX_AGE_MS = 30_000; export const CURRENT_PRICE_MAX_AGE_MS = 30_000;
export const CURRENT_INVENTORY_MAX_AGE_MS = 30_000; export const CURRENT_INVENTORY_MAX_AGE_MS = 30_000;
export const CURRENT_MAKER_MAX_QUOTE_AGE_ENABLED = false;
export const CURRENT_MAKER_MAX_QUOTE_AGE_MS = null;
export const CURRENT_MAKER_LATENCY_POLICY_REASON = null;
export const PAIR_MODES = new Set(['observe_only', 'maker', 'taker', 'both']); export const PAIR_MODES = new Set(['observe_only', 'maker', 'taker', 'both']);
export const PAIR_STATUSES = new Set(['disabled', 'observe_only', 'maker', 'taker', 'both']); export const PAIR_STATUSES = new Set(['disabled', 'observe_only', 'maker', 'taker', 'both']);
@ -190,6 +193,9 @@ export function buildSeedStrategyConfig(pairId, {
requestDefaultNotional: CURRENT_REQUEST_DEFAULT_NOTIONAL_EURE, requestDefaultNotional: CURRENT_REQUEST_DEFAULT_NOTIONAL_EURE,
requestMaxNotional: CURRENT_REQUEST_MAX_NOTIONAL_EURE, requestMaxNotional: CURRENT_REQUEST_MAX_NOTIONAL_EURE,
requestMaxSlippageBps: CURRENT_REQUEST_MAX_SLIPPAGE_BPS, requestMaxSlippageBps: CURRENT_REQUEST_MAX_SLIPPAGE_BPS,
makerMaxQuoteAgeEnabled: CURRENT_MAKER_MAX_QUOTE_AGE_ENABLED,
makerMaxQuoteAgeMs: CURRENT_MAKER_MAX_QUOTE_AGE_MS,
makerLatencyPolicyReason: CURRENT_MAKER_LATENCY_POLICY_REASON,
createdBy, createdBy,
reason, reason,
}; };

View file

@ -378,12 +378,27 @@ export async function ensureTradingConfigSchema(pool) {
request_default_notional NUMERIC, request_default_notional NUMERIC,
request_max_notional NUMERIC, request_max_notional NUMERIC,
request_max_slippage_bps INTEGER, request_max_slippage_bps INTEGER,
maker_max_quote_age_enabled BOOLEAN NOT NULL DEFAULT false,
maker_max_quote_age_ms INTEGER,
maker_latency_policy_reason TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by TEXT NOT NULL, created_by TEXT NOT NULL,
reason TEXT, reason TEXT,
UNIQUE (pair_id, version) UNIQUE (pair_id, version)
) )
`); `);
await pool.query(`
ALTER TABLE ${PAIR_STRATEGY_CONFIGS_TABLE}
ADD COLUMN IF NOT EXISTS maker_max_quote_age_enabled BOOLEAN NOT NULL DEFAULT false
`);
await pool.query(`
ALTER TABLE ${PAIR_STRATEGY_CONFIGS_TABLE}
ADD COLUMN IF NOT EXISTS maker_max_quote_age_ms INTEGER
`);
await pool.query(`
ALTER TABLE ${PAIR_STRATEGY_CONFIGS_TABLE}
ADD COLUMN IF NOT EXISTS maker_latency_policy_reason TEXT
`);
await pool.query(` await pool.query(`
CREATE UNIQUE INDEX IF NOT EXISTS ${PAIR_STRATEGY_CONFIGS_TABLE}_active_one_idx CREATE UNIQUE INDEX IF NOT EXISTS ${PAIR_STRATEGY_CONFIGS_TABLE}_active_one_idx
ON ${PAIR_STRATEGY_CONFIGS_TABLE} (pair_id) ON ${PAIR_STRATEGY_CONFIGS_TABLE} (pair_id)
@ -723,6 +738,9 @@ export async function createPairStrategyConfigVersion(pool, {
requestDefaultNotional = undefined, requestDefaultNotional = undefined,
requestMaxNotional = undefined, requestMaxNotional = undefined,
requestMaxSlippageBps = undefined, requestMaxSlippageBps = undefined,
makerMaxQuoteAgeEnabled = undefined,
makerMaxQuoteAgeMs = undefined,
makerLatencyPolicyReason = undefined,
changedBy = 'operator', changedBy = 'operator',
reason = 'operator config update', reason = 'operator config update',
} = {}) { } = {}) {
@ -775,9 +793,22 @@ export async function createPairStrategyConfigVersion(pool, {
requestMaxSlippageBps === undefined requestMaxSlippageBps === undefined
? active.request_max_slippage_bps == null ? null : Number(active.request_max_slippage_bps) ? active.request_max_slippage_bps == null ? null : Number(active.request_max_slippage_bps)
: nullableNonNegativeInteger(requestMaxSlippageBps, 'request_max_slippage_bps'), : nullableNonNegativeInteger(requestMaxSlippageBps, 'request_max_slippage_bps'),
makerMaxQuoteAgeEnabled:
makerMaxQuoteAgeEnabled === undefined
? active.maker_max_quote_age_enabled === true
: Boolean(makerMaxQuoteAgeEnabled),
makerMaxQuoteAgeMs:
makerMaxQuoteAgeMs === undefined
? active.maker_max_quote_age_ms == null ? null : Number(active.maker_max_quote_age_ms)
: nullablePositiveInteger(makerMaxQuoteAgeMs, 'maker_max_quote_age_ms'),
makerLatencyPolicyReason:
makerLatencyPolicyReason === undefined
? active.maker_latency_policy_reason || null
: nullableString(makerLatencyPolicyReason),
createdBy: changedBy, createdBy: changedBy,
reason, reason,
}; };
validateMakerQuoteAgePolicy(nextConfig);
await client.query( await client.query(
`UPDATE ${PAIR_STRATEGY_CONFIGS_TABLE} SET active = false WHERE pair_id = $1 AND active = true`, `UPDATE ${PAIR_STRATEGY_CONFIGS_TABLE} SET active = false WHERE pair_id = $1 AND active = true`,
@ -810,6 +841,9 @@ export async function createPairStrategyConfigVersion(pool, {
request_default_notional: nextConfig.requestDefaultNotional, request_default_notional: nextConfig.requestDefaultNotional,
request_max_notional: nextConfig.requestMaxNotional, request_max_notional: nextConfig.requestMaxNotional,
request_max_slippage_bps: nextConfig.requestMaxSlippageBps, request_max_slippage_bps: nextConfig.requestMaxSlippageBps,
maker_max_quote_age_enabled: nextConfig.makerMaxQuoteAgeEnabled,
maker_max_quote_age_ms: nextConfig.makerMaxQuoteAgeMs,
maker_latency_policy_reason: nextConfig.makerLatencyPolicyReason,
created_by: changedBy, created_by: changedBy,
reason, reason,
}); });
@ -878,6 +912,9 @@ export async function setTradingPairMode(pool, {
requestDefaultNotional = undefined, requestDefaultNotional = undefined,
requestMaxNotional = undefined, requestMaxNotional = undefined,
requestMaxSlippageBps = undefined, requestMaxSlippageBps = undefined,
makerMaxQuoteAgeEnabled = undefined,
makerMaxQuoteAgeMs = undefined,
makerLatencyPolicyReason = undefined,
changedBy = 'operator', changedBy = 'operator',
reason = 'operator pair mode update', reason = 'operator pair mode update',
} = {}) { } = {}) {
@ -950,6 +987,9 @@ export async function setTradingPairMode(pool, {
requestDefaultNotional, requestDefaultNotional,
requestMaxNotional, requestMaxNotional,
requestMaxSlippageBps, requestMaxSlippageBps,
makerMaxQuoteAgeEnabled,
makerMaxQuoteAgeMs,
makerLatencyPolicyReason,
changedBy, changedBy,
reason, reason,
}); });
@ -969,6 +1009,9 @@ export async function setTradingPairMode(pool, {
request_default_notional: nextConfig.requestDefaultNotional, request_default_notional: nextConfig.requestDefaultNotional,
request_max_notional: nextConfig.requestMaxNotional, request_max_notional: nextConfig.requestMaxNotional,
request_max_slippage_bps: nextConfig.requestMaxSlippageBps, request_max_slippage_bps: nextConfig.requestMaxSlippageBps,
maker_max_quote_age_enabled: nextConfig.makerMaxQuoteAgeEnabled,
maker_max_quote_age_ms: nextConfig.makerMaxQuoteAgeMs,
maker_latency_policy_reason: nextConfig.makerLatencyPolicyReason,
created_by: changedBy, created_by: changedBy,
reason, reason,
}); });
@ -1246,6 +1289,9 @@ export function summarizeTradingConfigSnapshot(snapshot) {
strategy_config_version: pair.strategyConfig?.version || null, strategy_config_version: pair.strategyConfig?.version || null,
edge_bps: pair.strategyConfig?.edgeBps ?? null, edge_bps: pair.strategyConfig?.edgeBps ?? null,
max_notional: pair.strategyConfig?.maxNotional ?? null, max_notional: pair.strategyConfig?.maxNotional ?? null,
maker_max_quote_age_enabled: pair.strategyConfig?.makerMaxQuoteAgeEnabled ?? false,
maker_max_quote_age_ms: pair.strategyConfig?.makerMaxQuoteAgeMs ?? null,
maker_latency_policy_reason: pair.strategyConfig?.makerLatencyPolicyReason ?? null,
price_route_id: pair.priceRoute?.routeId || null, price_route_id: pair.priceRoute?.routeId || null,
})), })),
}; };
@ -1343,6 +1389,9 @@ function buildInitialPairStrategyConfig(pairId, {
requestDefaultNotional = undefined, requestDefaultNotional = undefined,
requestMaxNotional = undefined, requestMaxNotional = undefined,
requestMaxSlippageBps = undefined, requestMaxSlippageBps = undefined,
makerMaxQuoteAgeEnabled = undefined,
makerMaxQuoteAgeMs = undefined,
makerLatencyPolicyReason = undefined,
changedBy = 'operator', changedBy = 'operator',
reason = 'operator pair strategy config initialization', reason = 'operator pair strategy config initialization',
} = {}) { } = {}) {
@ -1351,7 +1400,7 @@ function buildInitialPairStrategyConfig(pairId, {
reason, reason,
}); });
return { const next = {
...baseConfig, ...baseConfig,
edgeBps: positiveIntegerOrDefault(edgeBps, baseConfig.edgeBps, 'edge_bps'), edgeBps: positiveIntegerOrDefault(edgeBps, baseConfig.edgeBps, 'edge_bps'),
maxNotional: positiveNumberStringOrDefault(maxNotional, baseConfig.maxNotional, 'max_notional'), maxNotional: positiveNumberStringOrDefault(maxNotional, baseConfig.maxNotional, 'max_notional'),
@ -1379,7 +1428,21 @@ function buildInitialPairStrategyConfig(pairId, {
baseConfig.requestMaxSlippageBps, baseConfig.requestMaxSlippageBps,
'request_max_slippage_bps', 'request_max_slippage_bps',
), ),
makerMaxQuoteAgeEnabled:
makerMaxQuoteAgeEnabled === undefined
? baseConfig.makerMaxQuoteAgeEnabled
: Boolean(makerMaxQuoteAgeEnabled),
makerMaxQuoteAgeMs:
makerMaxQuoteAgeMs === undefined
? baseConfig.makerMaxQuoteAgeMs
: nullablePositiveInteger(makerMaxQuoteAgeMs, 'maker_max_quote_age_ms'),
makerLatencyPolicyReason:
makerLatencyPolicyReason === undefined
? baseConfig.makerLatencyPolicyReason
: nullableString(makerLatencyPolicyReason),
}; };
validateMakerQuoteAgePolicy(next);
return next;
} }
function hasConfigOverride(value) { function hasConfigOverride(value) {
@ -1434,6 +1497,23 @@ function nullableNonNegativeInteger(value, field) {
return nonNegativeIntegerOrDefault(value, 0, field); return nonNegativeIntegerOrDefault(value, 0, field);
} }
function nullablePositiveInteger(value, field) {
if (!hasConfigOverride(value)) return null;
return positiveIntegerOrDefault(value, 1, field);
}
function nullableString(value) {
const normalized = String(value ?? '').trim();
return normalized || null;
}
function validateMakerQuoteAgePolicy(config) {
if (config.makerMaxQuoteAgeEnabled !== true) return;
if (!Number.isInteger(config.makerMaxQuoteAgeMs) || config.makerMaxQuoteAgeMs <= 0) {
throw new Error('maker_max_quote_age_ms must be a positive integer when maker quote age policy is enabled');
}
}
function normalizeStrategyConfigRow(row) { function normalizeStrategyConfigRow(row) {
if (!row) return null; if (!row) return null;
return { return {
@ -1469,6 +1549,14 @@ function normalizeStrategyConfigRow(row) {
row.request_max_slippage_bps == null ? null : Number(row.request_max_slippage_bps), row.request_max_slippage_bps == null ? null : Number(row.request_max_slippage_bps),
request_max_slippage_bps: request_max_slippage_bps:
row.request_max_slippage_bps == null ? null : Number(row.request_max_slippage_bps), row.request_max_slippage_bps == null ? null : Number(row.request_max_slippage_bps),
makerMaxQuoteAgeEnabled: row.maker_max_quote_age_enabled === true,
maker_max_quote_age_enabled: row.maker_max_quote_age_enabled === true,
makerMaxQuoteAgeMs:
row.maker_max_quote_age_ms == null ? null : Number(row.maker_max_quote_age_ms),
maker_max_quote_age_ms:
row.maker_max_quote_age_ms == null ? null : Number(row.maker_max_quote_age_ms),
makerLatencyPolicyReason: row.maker_latency_policy_reason || null,
maker_latency_policy_reason: row.maker_latency_policy_reason || null,
created_at: toIsoTimestamp(row.created_at), created_at: toIsoTimestamp(row.created_at),
created_by: row.created_by || null, created_by: row.created_by || null,
reason: row.reason || null, reason: row.reason || null,
@ -1690,9 +1778,12 @@ async function insertPairStrategyConfig(pool, { config, active = true }) {
request_default_notional, request_default_notional,
request_max_notional, request_max_notional,
request_max_slippage_bps, request_max_slippage_bps,
maker_max_quote_age_enabled,
maker_max_quote_age_ms,
maker_latency_policy_reason,
created_by, created_by,
reason reason
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16) ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19)
ON CONFLICT (config_id) DO NOTHING ON CONFLICT (config_id) DO NOTHING
`, `,
[ [
@ -1710,6 +1801,9 @@ async function insertPairStrategyConfig(pool, { config, active = true }) {
config.requestDefaultNotional, config.requestDefaultNotional,
config.requestMaxNotional, config.requestMaxNotional,
config.requestMaxSlippageBps, config.requestMaxSlippageBps,
config.makerMaxQuoteAgeEnabled === true,
config.makerMaxQuoteAgeMs,
config.makerLatencyPolicyReason,
config.createdBy, config.createdBy,
config.reason, config.reason,
], ],
@ -3303,6 +3397,8 @@ function normalizeQuoteOutcomeRow(row) {
eure_notional: payload.eure_notional || null, eure_notional: payload.eure_notional || null,
execution_result_status: row.execution_result_status || payload.execution_result_status || null, execution_result_status: row.execution_result_status || payload.execution_result_status || null,
execution_result_code: row.execution_result_code || payload.execution_result_code || null, execution_result_code: row.execution_result_code || payload.execution_result_code || null,
failure_category: payload.failure_category || null,
maker_timing: payload.maker_timing || null,
submitted_at: toIsoTimestamp(row.submitted_at || payload.submitted_at), submitted_at: toIsoTimestamp(row.submitted_at || payload.submitted_at),
command_at: toIsoTimestamp(row.command_at || payload.command_at), command_at: toIsoTimestamp(row.command_at || payload.command_at),
outcome_status: row.outcome_status || payload.outcome_status || null, outcome_status: row.outcome_status || payload.outcome_status || null,
@ -3488,6 +3584,7 @@ function normalizeRecentQuoteRow(row) {
amount_in: payload.amount_in ?? null, amount_in: payload.amount_in ?? null,
amount_out: payload.amount_out ?? null, amount_out: payload.amount_out ?? null,
min_deadline_ms: payload.min_deadline_ms ?? null, min_deadline_ms: payload.min_deadline_ms ?? null,
maker_timing: payload.maker_timing || null,
observed_at: toIsoTimestamp(row.observed_at), observed_at: toIsoTimestamp(row.observed_at),
ingested_at: toIsoTimestamp(row.ingested_at), ingested_at: toIsoTimestamp(row.ingested_at),
}; };
@ -3525,6 +3622,8 @@ function normalizeSubmissionRow(row) {
ingested_at: toIsoTimestamp(row.result_ingested_at), ingested_at: toIsoTimestamp(row.result_ingested_at),
status: resultPayload.status || null, status: resultPayload.status || null,
result_code: resultPayload.result_code || null, result_code: resultPayload.result_code || null,
failure_category: resultPayload.failure_category || null,
relay_error_message: resultPayload.relay_error_message || resultPayload.error?.message || null,
outcome_status: outcomePayload.outcome_status || null, outcome_status: outcomePayload.outcome_status || null,
outcome_reason: outcomePayload.outcome_reason || null, outcome_reason: outcomePayload.outcome_reason || null,
attribution_status: outcomePayload.attribution_status || null, attribution_status: outcomePayload.attribution_status || null,
@ -3539,6 +3638,11 @@ function normalizeSubmissionRow(row) {
gross_edge_pct: decisionPayload.gross_edge_pct || null, gross_edge_pct: decisionPayload.gross_edge_pct || null,
decision_reason: decisionPayload.decision_reason || null, decision_reason: decisionPayload.decision_reason || null,
direction: decisionPayload.direction || null, direction: decisionPayload.direction || null,
maker_timing:
resultPayload.maker_timing
|| commandPayload.maker_timing
|| decisionPayload.maker_timing
|| null,
}; };
} }
@ -3560,6 +3664,8 @@ function normalizeExecuteTradeCommandRow(row) {
asset_out: payload.asset_out || null, asset_out: payload.asset_out || null,
amount_in: resolveTradeAmount(payload, 'amount_in'), amount_in: resolveTradeAmount(payload, 'amount_in'),
amount_out: resolveTradeAmount(payload, 'amount_out'), amount_out: resolveTradeAmount(payload, 'amount_out'),
maker_timing: payload.maker_timing || null,
quote_age_at_command_ms: payload.quote_age_at_command_ms ?? null,
observed_at: toIsoTimestamp(row.observed_at || row.ingested_at), observed_at: toIsoTimestamp(row.observed_at || row.ingested_at),
ingested_at: toIsoTimestamp(row.ingested_at), ingested_at: toIsoTimestamp(row.ingested_at),
}; };
@ -3597,6 +3703,7 @@ function normalizeExecutionResultRow(row) {
result_at: toIsoTimestamp(row.result_observed_at || row.result_ingested_at), result_at: toIsoTimestamp(row.result_observed_at || row.result_ingested_at),
status: resultPayload.status || null, status: resultPayload.status || null,
result_code: resultPayload.result_code || null, result_code: resultPayload.result_code || null,
failure_category: resultPayload.failure_category || null,
outcome_status: outcome_status:
outcomePayload.outcome_status outcomePayload.outcome_status
|| resultPayload.outcome_status || resultPayload.outcome_status
@ -3615,9 +3722,14 @@ function normalizeExecutionResultRow(row) {
attributed_inventory_delta: outcomePayload.attributed_inventory_delta || null, attributed_inventory_delta: outcomePayload.attributed_inventory_delta || null,
outcome_payload: outcomePayload.quote_id ? outcomePayload : null, outcome_payload: outcomePayload.quote_id ? outcomePayload : null,
venue_response: resultPayload.venue_response || null, venue_response: resultPayload.venue_response || null,
error_message: resultPayload.error?.message || null, error_message: resultPayload.relay_error_message || resultPayload.error?.message || null,
note: resultPayload.note || null, note: resultPayload.note || null,
timing: resultPayload.executor_timing || null, timing: resultPayload.executor_timing || null,
maker_timing:
resultPayload.maker_timing
|| commandPayload.maker_timing
|| decisionPayload.maker_timing
|| null,
}; };
} }

View file

@ -40,6 +40,16 @@ function formatTimingMs(value) {
return `${number < 10 ? number.toFixed(1) : number.toFixed(0)} ms`; return `${number < 10 ? number.toFixed(1) : number.toFixed(0)} ms`;
} }
function formatRate(value) {
const number = Number(value);
if (!Number.isFinite(number)) return 'Unavailable';
return `${(number * 100).toFixed(1)}%`;
}
function stageLabel(value) {
return plainCodeLabel(value).replace(/\bms\b/i, '').trim();
}
function formatExecutionTiming(timing) { function formatExecutionTiming(timing) {
if (!timing) return null; if (!timing) return null;
const saltMs = formatTimingMs(timing.current_salt_ms); const saltMs = formatTimingMs(timing.current_salt_ms);
@ -90,7 +100,8 @@ function formatTerms(terms) {
function responseLabel(item) { function responseLabel(item) {
if (RESPONDED_STATES.has(item.lifecycle_state)) return 'Yes'; if (RESPONDED_STATES.has(item.lifecycle_state)) return 'Yes';
if (item.lifecycle_state === 'failed') return 'Attempt failed'; if (item.lifecycle_state === 'failed') return 'Attempt failed';
if (item.lifecycle_state === 'blocked') return 'No - executor blocked'; if (item.lifecycle_state === 'blocked' && item.reason_code?.startsWith('maker_')) return 'No - policy skip';
if (item.lifecycle_state === 'blocked') return 'No - blocked';
if (item.lifecycle_state === 'rejected') return 'No - strategy rejected'; if (item.lifecycle_state === 'rejected') return 'No - strategy rejected';
if (item.lifecycle_state === 'command_emitted') return 'Pending executor'; if (item.lifecycle_state === 'command_emitted') return 'Pending executor';
if (item.lifecycle_state === 'evaluated') return 'Approved, not sent'; if (item.lifecycle_state === 'evaluated') return 'Approved, not sent';
@ -139,6 +150,55 @@ function StageCard({ title, at, status, children }) {
); );
} }
function TimingWaterfall({ timing }) {
if (!timing) return null;
const rows = [
['Quote observed', timing.quote_observed_at],
['Quote received', timing.quote_received_at],
['Normalized', timing.quote_normalized_at],
['Published', timing.quote_published_at],
['Strategy received', timing.strategy_received_at],
['Strategy decided', timing.strategy_decided_at, timing.quote_to_decision_ms],
['Command published', timing.command_published_at, timing.decision_to_command_ms],
['Executor received', timing.executor_received_at, timing.command_to_executor_ms],
['Relay result', timing.relay_result_at, timing.executor_to_relay_result_ms],
['Outcome observed', timing.outcome_observed_at, timing.quote_to_outcome_ms],
];
const hasEvidence = rows.some(([, at, duration]) => at || duration != null);
if (!hasEvidence) return null;
return (
<TableFrame>
<table className="timing-waterfall-table">
<thead>
<tr>
<th>Stage</th>
<th>Timestamp</th>
<th>Step</th>
<th>From quote</th>
</tr>
</thead>
<tbody>
{rows.map(([label, at, duration]) => (
<tr key={label}>
<td>{label}</td>
<td>{formatTimestamp(at)}</td>
<td>{formatTimingMs(duration) || 'Unavailable'}</td>
<td>
{label === 'Strategy decided' ? formatTimingMs(timing.quote_age_at_decision_ms)
: label === 'Executor received' ? formatTimingMs(timing.quote_age_at_executor_receipt_ms)
: label === 'Relay result' ? formatTimingMs(timing.quote_age_at_relay_result_ms)
: label === 'Outcome observed' ? formatTimingMs(timing.quote_to_outcome_ms)
: 'Unavailable'}
</td>
</tr>
))}
</tbody>
</table>
</TableFrame>
);
}
function LifecycleDetails({ item }) { function LifecycleDetails({ item }) {
const executionTiming = formatExecutionTiming(item.execution?.timing); const executionTiming = formatExecutionTiming(item.execution?.timing);
@ -183,10 +243,222 @@ function LifecycleDetails({ item }) {
<IdentifierRow label="Decision" value={item.decision_id} /> <IdentifierRow label="Decision" value={item.decision_id} />
<IdentifierRow label="Command" value={item.command_id} /> <IdentifierRow label="Command" value={item.command_id} />
</div> </div>
<TimingWaterfall timing={item.maker_timing} />
</div> </div>
); );
} }
function pairDisplayLabel(pairId, pairConfig) {
const pair = (pairConfig?.pairs || []).find((entry) => (
(entry.pair_id || entry.pairId || entry.pair) === pairId
));
if (!pair) return truncateMiddle(pairId || 'Unknown pair', 42);
return `${pair.asset_in_symbol || pair.asset_in || pair.assetIn} -> ${pair.asset_out_symbol || pair.asset_out || pair.assetOut}`;
}
function MakerCompetitivenessSection({ summary, pairConfig }) {
const total = summary?.total || {};
const groups = summary?.groups || [];
const ageBuckets = summary?.age_buckets || [];
const latestErrors = summary?.latest_errors || [];
const policySkips = summary?.policy_skips || [];
return (
<section className="panel">
<div className="panel-head">
<div>
<div className="eyebrow">Maker competitiveness</div>
<h3>Response timing and quote-age outcomes</h3>
<div className="panel-subtitle">
Pair-native response evidence from durable quote, decision, command, executor result, and outcome rows.
</div>
</div>
<div className="pills">
<Pill label={`${total.count || 0} rows`} stateLabel="info" />
<Pill label={`${total.quote_not_found_or_finished_count || 0} stale/finished`} stateLabel={(total.quote_not_found_or_finished_count || 0) > 0 ? 'warning' : 'unknown'} />
</div>
</div>
<div className="metric-grid">
<MetricCard label="Accepted" meta={formatRate(total.accepted_rate)} value={String(total.accepted_count || 0)} />
<MetricCard label="Relay failed" meta="Executor reached result" value={String(total.relay_failed_count || 0)} />
<MetricCard label="Already finished" meta={formatRate(total.stale_or_finished_rate)} value={String(total.quote_not_found_or_finished_count || 0)} />
<MetricCard label="Policy skips" meta="No relay submission" value={String(total.policy_skip_count || 0)} />
</div>
<TableFrame>
<table>
<thead>
<tr>
<th>Pair</th>
<th>Direction</th>
<th>Request</th>
<th>Result</th>
<th>Age / notional</th>
<th>Outcome</th>
<th>Counts</th>
<th>Quote to relay</th>
</tr>
</thead>
<tbody>
{groups.length ? groups.slice(0, 12).map((group, index) => {
const quoteToRelay = group.latency_stages?.find((stage) => stage.stage === 'quote_to_relay_result_ms');
return (
<tr key={`${group.pair}:${group.direction}:${group.request_kind}:${group.result_code}:${group.quote_age_bucket}:${index}`}>
<td>
<div>{pairDisplayLabel(group.pair, pairConfig)}</div>
<div className="status-subtle mono">{truncateMiddle(group.pair || '', 42)}</div>
</td>
<td>{plainCodeLabel(group.direction)}</td>
<td>{plainCodeLabel(group.request_kind)}</td>
<td>
<div>{plainCodeLabel(group.result_code)}</div>
{group.failure_category ? <div className="status-subtle">{plainCodeLabel(group.failure_category)}</div> : null}
</td>
<td>
<div>{group.quote_age_bucket}</div>
<div className="status-subtle">{group.notional_bucket}</div>
</td>
<td>{plainCodeLabel(group.outcome_status)}</td>
<td>
<div>{group.count}</div>
<div className="status-subtle">{`${group.accepted_count || 0} accepted / ${group.policy_skip_count || 0} skipped`}</div>
</td>
<td>
<div>{formatTimingMs(quoteToRelay?.p50_ms) || 'Unavailable'}</div>
<div className="status-subtle">{`p90 ${formatTimingMs(quoteToRelay?.p90_ms) || 'Unavailable'}`}</div>
</td>
</tr>
);
}) : (
<tr><td colSpan={8}>No competitiveness rows are available yet.</td></tr>
)}
</tbody>
</table>
</TableFrame>
<div className="two-column-grid">
<TableFrame>
<table>
<thead>
<tr>
<th>Latency stage</th>
<th>p50</th>
<th>p90</th>
<th>p99</th>
</tr>
</thead>
<tbody>
{(summary?.latency_stages || []).length ? summary.latency_stages.map((stage) => (
<tr key={stage.stage}>
<td>{stageLabel(stage.stage)}</td>
<td>{formatTimingMs(stage.p50_ms)}</td>
<td>{formatTimingMs(stage.p90_ms)}</td>
<td>{formatTimingMs(stage.p99_ms)}</td>
</tr>
)) : (
<tr><td colSpan={4}>No stage timing percentiles are available yet.</td></tr>
)}
</tbody>
</table>
</TableFrame>
<TableFrame>
<table>
<thead>
<tr>
<th>Age bucket</th>
<th>Outcome</th>
<th>Count</th>
<th>Accepted</th>
</tr>
</thead>
<tbody>
{ageBuckets.length ? ageBuckets.slice(0, 12).map((bucket, index) => (
<tr key={`${bucket.pair}:${bucket.quote_age_bucket}:${bucket.outcome_status}:${index}`}>
<td>
<div>{bucket.quote_age_bucket}</div>
<div className="status-subtle">{pairDisplayLabel(bucket.pair, pairConfig)}</div>
</td>
<td>{plainCodeLabel(bucket.outcome_status)}</td>
<td>{bucket.count}</td>
<td>{bucket.accepted_count || 0}</td>
</tr>
)) : (
<tr><td colSpan={4}>No quote-age buckets are available yet.</td></tr>
)}
</tbody>
</table>
</TableFrame>
</div>
<div className="two-column-grid">
<TableFrame>
<table>
<thead>
<tr>
<th>Latest relay errors</th>
<th>Quote age</th>
<th>Error</th>
</tr>
</thead>
<tbody>
{latestErrors.length ? latestErrors.map((error) => (
<tr key={`${error.quote_id}:${error.result_at}`}>
<td>
<IdentifierRow label="Quote" value={error.quote_id} />
<div className="status-subtle">{pairDisplayLabel(error.pair, pairConfig)}</div>
<div className="status-subtle">{plainCodeLabel(error.failure_category || error.result_code)}</div>
</td>
<td>
<div>{formatTimingMs(error.quote_age_ms) || 'Unavailable'}</div>
<div className="status-subtle">{error.quote_age_bucket}</div>
</td>
<td>{error.error_message || 'Error text unavailable'}</td>
</tr>
)) : (
<tr><td colSpan={3}>No relay errors are available yet.</td></tr>
)}
</tbody>
</table>
</TableFrame>
<TableFrame>
<table>
<thead>
<tr>
<th>Policy skips</th>
<th>Age</th>
<th>Config</th>
</tr>
</thead>
<tbody>
{policySkips.length ? policySkips.map((skip) => (
<tr key={`${skip.quote_id}:${skip.decision_at}`}>
<td>
<IdentifierRow label="Quote" value={skip.quote_id} />
<div className="status-subtle">{plainCodeLabel(skip.reason_code)}</div>
</td>
<td>
<div>{formatTimingMs(skip.quote_age_ms) || 'Unavailable'}</div>
<div className="status-subtle">{`max ${formatTimingMs(skip.max_quote_age_ms) || 'Unavailable'}`}</div>
</td>
<td>
<div>{skip.pair_config_version ? `v${skip.pair_config_version}` : 'Version unavailable'}</div>
<div className="status-subtle mono">{truncateMiddle(skip.pair_config_id || '', 36)}</div>
</td>
</tr>
)) : (
<tr><td colSpan={3}>No policy skips are available yet.</td></tr>
)}
</tbody>
</table>
</TableFrame>
</div>
</section>
);
}
function QuoteLifecycleTable({ items }) { function QuoteLifecycleTable({ items }) {
const [expanded, setExpanded] = useState(() => new Set()); const [expanded, setExpanded] = useState(() => new Set());
const [showStrategyRejected, setShowStrategyRejected] = useState(true); const [showStrategyRejected, setShowStrategyRejected] = useState(true);
@ -504,6 +776,8 @@ function PairConfigSection({ assetCatalog, pairConfig, onControl }) {
}); });
const [edgeDrafts, setEdgeDrafts] = useState({}); const [edgeDrafts, setEdgeDrafts] = useState({});
const [maxNotionalDrafts, setMaxNotionalDrafts] = useState({}); const [maxNotionalDrafts, setMaxNotionalDrafts] = useState({});
const [policyEnabledDrafts, setPolicyEnabledDrafts] = useState({});
const [maxQuoteAgeDrafts, setMaxQuoteAgeDrafts] = useState({});
useEffect(() => { useEffect(() => {
if (!assets.length) return; if (!assets.length) return;
@ -527,6 +801,20 @@ function PairConfigSection({ assetCatalog, pairConfig, onControl }) {
const tradingMode = TRADING_PAIR_MODES.has(pair.mode); const tradingMode = TRADING_PAIR_MODES.has(pair.mode);
return [pairId, String(strategyConfig.max_notional ?? pair.max_notional ?? (tradingMode ? '150' : ''))]; return [pairId, String(strategyConfig.max_notional ?? pair.max_notional ?? (tradingMode ? '150' : ''))];
}))); })));
setPolicyEnabledDrafts(Object.fromEntries(pairs.map((pair) => {
const pairId = pair.pair_id || pair.pairId;
const strategyConfig = pair.strategyConfig || pair.strategy_config || {};
return [pairId, Boolean(
strategyConfig.maker_max_quote_age_enabled ?? strategyConfig.makerMaxQuoteAgeEnabled,
)];
})));
setMaxQuoteAgeDrafts(Object.fromEntries(pairs.map((pair) => {
const pairId = pair.pair_id || pair.pairId;
const strategyConfig = pair.strategyConfig || pair.strategy_config || {};
return [pairId, String(
strategyConfig.maker_max_quote_age_ms ?? strategyConfig.makerMaxQuoteAgeMs ?? '',
)];
})));
}, [pairs]); }, [pairs]);
async function updatePairConfig(pair) { async function updatePairConfig(pair) {
@ -535,7 +823,10 @@ function PairConfigSection({ assetCatalog, pairConfig, onControl }) {
const hasStrategyConfig = Boolean(strategyConfig.config_id || strategyConfig.configId); const hasStrategyConfig = Boolean(strategyConfig.config_id || strategyConfig.configId);
const edgeBps = edgeDrafts[pairId]; const edgeBps = edgeDrafts[pairId];
const maxNotional = maxNotionalDrafts[pairId]; const maxNotional = maxNotionalDrafts[pairId];
const policyEnabled = policyEnabledDrafts[pairId] === true;
const maxQuoteAgeMs = maxQuoteAgeDrafts[pairId];
if (!edgeBps || !maxNotional) return; if (!edgeBps || !maxNotional) return;
if (policyEnabled && !maxQuoteAgeMs) return;
if (!hasStrategyConfig) { if (!hasStrategyConfig) {
const mode = pair.mode || pair.status || 'observe_only'; const mode = pair.mode || pair.status || 'observe_only';
@ -550,6 +841,9 @@ function PairConfigSection({ assetCatalog, pairConfig, onControl }) {
mode, mode,
edge_bps: Number(edgeBps), edge_bps: Number(edgeBps),
max_notional: maxNotional, max_notional: maxNotional,
maker_max_quote_age_enabled: policyEnabled,
maker_max_quote_age_ms: policyEnabled ? Number(maxQuoteAgeMs) : null,
maker_latency_policy_reason: policyEnabled ? 'operator dashboard maker response-age policy' : null,
}); });
return; return;
} }
@ -558,6 +852,9 @@ function PairConfigSection({ assetCatalog, pairConfig, onControl }) {
pair_id: pairId, pair_id: pairId,
edge_bps: Number(edgeBps), edge_bps: Number(edgeBps),
max_notional: maxNotional, max_notional: maxNotional,
maker_max_quote_age_enabled: policyEnabled,
maker_max_quote_age_ms: policyEnabled ? Number(maxQuoteAgeMs) : null,
maker_latency_policy_reason: policyEnabled ? 'operator dashboard maker response-age policy' : null,
}); });
} }
@ -714,8 +1011,10 @@ function PairConfigSection({ assetCatalog, pairConfig, onControl }) {
const route = pair.priceRoute || pair.price_route || {}; const route = pair.priceRoute || pair.price_route || {};
const hasStrategyConfig = Boolean(strategyConfig.config_id || strategyConfig.configId); const hasStrategyConfig = Boolean(strategyConfig.config_id || strategyConfig.configId);
const tradingMode = TRADING_PAIR_MODES.has(pair.mode); const tradingMode = TRADING_PAIR_MODES.has(pair.mode);
const policyEnabled = policyEnabledDrafts[pairId] === true;
const configButtonDisabled = !edgeDrafts[pairId] const configButtonDisabled = !edgeDrafts[pairId]
|| !maxNotionalDrafts[pairId] || !maxNotionalDrafts[pairId]
|| (policyEnabled && !maxQuoteAgeDrafts[pairId])
|| (!hasStrategyConfig && !tradingMode); || (!hasStrategyConfig && !tradingMode);
return ( return (
<tr key={pairId}> <tr key={pairId}>
@ -738,6 +1037,11 @@ function PairConfigSection({ assetCatalog, pairConfig, onControl }) {
: `${strategyConfig.request_max_slippage_bps} bps slippage max`} : `${strategyConfig.request_max_slippage_bps} bps slippage max`}
</div> </div>
<div className="status-subtle">{strategyConfig.price_max_age_ms || 'Unavailable'} ms price max age</div> <div className="status-subtle">{strategyConfig.price_max_age_ms || 'Unavailable'} ms price max age</div>
<div className="status-subtle">
{strategyConfig.maker_max_quote_age_enabled || strategyConfig.makerMaxQuoteAgeEnabled
? `${strategyConfig.maker_max_quote_age_ms ?? strategyConfig.makerMaxQuoteAgeMs} ms response max age`
: 'Response age policy disabled'}
</div>
</td> </td>
<td>{route.source || 'Unavailable'}</td> <td>{route.source || 'Unavailable'}</td>
<td>{pair.blockReason || pair.block_reason || 'No'}</td> <td>{pair.blockReason || pair.block_reason || 'No'}</td>
@ -783,6 +1087,36 @@ function PairConfigSection({ assetCatalog, pairConfig, onControl }) {
{hasStrategyConfig ? 'Save' : 'Init'} {hasStrategyConfig ? 'Save' : 'Init'}
</button> </button>
</div> </div>
<div className="trace-row">
<label className="checkbox-row">
<input
aria-label={`Enable response age policy for ${pairId}`}
checked={policyEnabled}
onChange={(event) => setPolicyEnabledDrafts((current) => ({
...current,
[pairId]: event.target.checked,
}))}
type="checkbox"
/>
<span className="status-subtle">Age policy</span>
</label>
</div>
<div className="trace-row">
<span className="status-subtle">Max age</span>
<input
aria-label={`Max quote response age milliseconds for ${pairId}`}
disabled={!policyEnabled}
min="1"
onChange={(event) => setMaxQuoteAgeDrafts((current) => ({
...current,
[pairId]: event.target.value,
}))}
step="1"
style={{ maxWidth: 112 }}
type="number"
value={maxQuoteAgeDrafts[pairId] ?? ''}
/>
</div>
</td> </td>
<td> <td>
<div className="button-row"> <div className="button-row">
@ -843,6 +1177,11 @@ export default function StrategyPage({ strategy, onControl }) {
<PairConfigSection assetCatalog={strategy.asset_catalog} pairConfig={strategy.pair_config} onControl={onControl} /> <PairConfigSection assetCatalog={strategy.asset_catalog} pairConfig={strategy.pair_config} onControl={onControl} />
<MakerCompetitivenessSection
pairConfig={strategy.pair_config}
summary={strategy.strategy_state.maker_competitiveness}
/>
<AssetCatalogSection assetCatalog={strategy.asset_catalog} onControl={onControl} /> <AssetCatalogSection assetCatalog={strategy.asset_catalog} onControl={onControl} />
<section className="panel"> <section className="panel">

View file

@ -16,6 +16,9 @@ function applySocketMessage(dashboard, payload, session) {
strategy_state: { strategy_state: {
...dashboard.strategy.strategy_state, ...dashboard.strategy.strategy_state,
recent_lifecycle_rows: payload.live.recent_lifecycle_rows, recent_lifecycle_rows: payload.live.recent_lifecycle_rows,
maker_competitiveness:
payload.live.maker_competitiveness
|| dashboard.strategy.strategy_state.maker_competitiveness,
}, },
} : dashboard.strategy, } : dashboard.strategy,
status_bar: { status_bar: {
@ -47,6 +50,9 @@ function applySocketMessage(dashboard, payload, session) {
recent_lifecycle_rows: recent_lifecycle_rows:
payload.recent_lifecycle_rows payload.recent_lifecycle_rows
|| dashboard.strategy.strategy_state.recent_lifecycle_rows, || dashboard.strategy.strategy_state.recent_lifecycle_rows,
maker_competitiveness:
payload.maker_competitiveness
|| dashboard.strategy.strategy_state.maker_competitiveness,
}, },
}, },
}, },

View file

@ -265,6 +265,24 @@ select {
height: 100%; height: 100%;
} }
.two-column-grid {
display: grid;
gap: 14px;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
margin-top: 14px;
}
.checkbox-row {
display: inline-flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.timing-waterfall-table {
min-width: 720px;
}
.table-wrap { .table-wrap {
overflow-x: auto; overflow-x: auto;
border: 1px solid var(--line); border: 1px solid var(--line);
@ -506,7 +524,8 @@ table.lifecycle-table th:nth-child(5) {
@media (max-width: 1100px) { @media (max-width: 1100px) {
.app-grid, .app-grid,
.split, .split,
.strategy-layout { .strategy-layout,
.two-column-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }

View file

@ -1,9 +1,14 @@
import { buildEventEnvelope } from '../../core/event-envelope.mjs'; import { buildEventEnvelope } from '../../core/event-envelope.mjs';
import { buildInitialMakerTiming } from '../../core/maker-timing.mjs';
export function buildNearIntentsRawEnvelope(message, { ingestedAt = new Date() } = {}) { export function buildNearIntentsRawEnvelope(message, { ingestedAt = new Date(), receivedAt = ingestedAt } = {}) {
const raw = isRecord(message) ? message : {}; const raw = isRecord(message) ? message : {};
const quoteId = first(raw, ['quote_id', 'quoteRequestId', 'request_id', 'id', 'quote_hash']); const quoteId = first(raw, ['quote_id', 'quoteRequestId', 'request_id', 'id', 'quote_hash']);
const occurredAt = first(raw, ['created_at', 'createdAt', 'timestamp', 'ts']); const occurredAt = first(raw, ['created_at', 'createdAt', 'timestamp', 'ts']);
const makerTiming = buildInitialMakerTiming({
quoteObservedAt: occurredAt,
quoteReceivedAt: receivedAt,
});
return buildEventEnvelope({ return buildEventEnvelope({
source: 'near-intents.ws', source: 'near-intents.ws',
@ -12,17 +17,27 @@ export function buildNearIntentsRawEnvelope(message, { ingestedAt = new Date() }
eventId: quoteId || `near-intents-raw-${ingestedAt.getTime()}`, eventId: quoteId || `near-intents-raw-${ingestedAt.getTime()}`,
observedAt: occurredAt, observedAt: occurredAt,
ingestedAt, ingestedAt,
payload: { message: raw }, payload: { message: raw, maker_timing: makerTiming },
raw, raw,
}); });
} }
export function buildNearIntentsQuoteEnvelope(message, { ingestedAt = new Date() } = {}) { export function buildNearIntentsQuoteEnvelope(message, {
ingestedAt = new Date(),
receivedAt = ingestedAt,
normalizedAt = new Date(),
publishedAt = null,
} = {}) {
const raw = isRecord(message) ? message : {}; const raw = isRecord(message) ? message : {};
const payload = normalizeNearIntentsQuote(raw);
if (!payload) return null;
const occurredAt = first(raw, ['created_at', 'createdAt', 'timestamp', 'ts']); const occurredAt = first(raw, ['created_at', 'createdAt', 'timestamp', 'ts']);
const makerTiming = buildInitialMakerTiming({
quoteObservedAt: occurredAt,
quoteReceivedAt: receivedAt,
quoteNormalizedAt: normalizedAt,
quotePublishedAt: publishedAt,
});
const payload = normalizeNearIntentsQuote(raw, { makerTiming });
if (!payload) return null;
return buildEventEnvelope({ return buildEventEnvelope({
source: 'near-intents.ws', source: 'near-intents.ws',
@ -36,7 +51,7 @@ export function buildNearIntentsQuoteEnvelope(message, { ingestedAt = new Date()
}); });
} }
export function normalizeNearIntentsQuote(message) { export function normalizeNearIntentsQuote(message, { makerTiming = null } = {}) {
const quoteId = first(message, ['quote_id', 'quoteRequestId', 'request_id', 'id']); const quoteId = first(message, ['quote_id', 'quoteRequestId', 'request_id', 'id']);
const assetIn = first(message, ['defuse_asset_identifier_in', 'sellToken', 'asset_in']); const assetIn = first(message, ['defuse_asset_identifier_in', 'sellToken', 'asset_in']);
const assetOut = first(message, ['defuse_asset_identifier_out', 'buyToken', 'asset_out']); const assetOut = first(message, ['defuse_asset_identifier_out', 'buyToken', 'asset_out']);
@ -56,6 +71,7 @@ export function normalizeNearIntentsQuote(message) {
amount_in: amountIn, amount_in: amountIn,
amount_out: amountOut, amount_out: amountOut,
min_deadline_ms: stringify(first(message, ['min_deadline_ms', 'ttl_ms', 'deadline_ms'])), min_deadline_ms: stringify(first(message, ['min_deadline_ms', 'ttl_ms', 'deadline_ms'])),
maker_timing: makerTiming,
}; };
} }

View file

@ -1,5 +1,6 @@
import { matchesPairFilter } from '../../core/pair-filter.mjs'; import { matchesPairFilter } from '../../core/pair-filter.mjs';
import { serializeError } from '../../core/log.mjs'; import { serializeError } from '../../core/log.mjs';
import { extendMakerTiming } from '../../core/maker-timing.mjs';
import { assertNormalizedSwapDemand } from '../../core/schemas.mjs'; import { assertNormalizedSwapDemand } from '../../core/schemas.mjs';
import { buildNearIntentsQuoteEnvelope, buildNearIntentsRawEnvelope } from './normalize.mjs'; import { buildNearIntentsQuoteEnvelope, buildNearIntentsRawEnvelope } from './normalize.mjs';
@ -73,7 +74,8 @@ export async function startNearIntentsWs({
ws.addEventListener('message', async (event) => { ws.addEventListener('message', async (event) => {
if (activeSocket !== ws) return; if (activeSocket !== ws) return;
framesReceived += 1; framesReceived += 1;
lastMessageAt = new Date().toISOString(); const frameReceivedAt = new Date();
lastMessageAt = frameReceivedAt.toISOString();
const text = typeof event.data === 'string' ? event.data : Buffer.from(event.data).toString('utf8'); const text = typeof event.data === 'string' ? event.data : Buffer.from(event.data).toString('utf8');
let payload; let payload;
@ -114,8 +116,14 @@ export async function startNearIntentsWs({
let assetOut = null; let assetOut = null;
let publishTopic = rawTopic; let publishTopic = rawTopic;
try { try {
envelope = buildNearIntentsQuoteEnvelope(merged); envelope = buildNearIntentsQuoteEnvelope(merged, {
rawEnvelope = buildNearIntentsRawEnvelope(merged); ingestedAt: frameReceivedAt,
receivedAt: frameReceivedAt,
});
rawEnvelope = buildNearIntentsRawEnvelope(merged, {
ingestedAt: frameReceivedAt,
receivedAt: frameReceivedAt,
});
await producer.sendJson(rawTopic, rawEnvelope, { key: rawEnvelope.event_id }); await producer.sendJson(rawTopic, rawEnvelope, { key: rawEnvelope.event_id });
rawPublishedCount += 1; rawPublishedCount += 1;
@ -136,6 +144,9 @@ export async function startNearIntentsWs({
lastMatchingQuoteAt = new Date().toISOString(); lastMatchingQuoteAt = new Date().toISOString();
publishTopic = normalizedTopic; publishTopic = normalizedTopic;
envelope.payload.maker_timing = extendMakerTiming(envelope.payload.maker_timing, {
quote_published_at: new Date(),
});
await producer.sendJson(normalizedTopic, envelope, { key: envelope.payload.quote_id }); await producer.sendJson(normalizedTopic, envelope, { key: envelope.payload.quote_id });
publishedCount += 1; publishedCount += 1;
lastPublishedAt = new Date().toISOString(); lastPublishedAt = new Date().toISOString();

View file

@ -0,0 +1,161 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
buildInitialMakerTiming,
extendMakerTiming,
quoteAgeMsAt,
} from '../src/core/maker-timing.mjs';
import { buildMakerCompetitivenessSummary } from '../src/core/maker-competitiveness.mjs';
import { classifyRelaySubmissionFailure } from '../src/core/relay-failure-classification.mjs';
test('maker timing computes waterfall fields and marks clock skew unavailable', () => {
const initial = buildInitialMakerTiming({
quoteReceivedAt: '2026-05-18T10:00:00.000Z',
quoteNormalizedAt: '2026-05-18T10:00:00.010Z',
quotePublishedAt: '2026-05-18T10:00:00.020Z',
});
const timing = extendMakerTiming(initial, {
strategy_received_at: '2026-05-18T10:00:00.030Z',
strategy_decided_at: '2026-05-18T10:00:00.040Z',
command_published_at: '2026-05-18T10:00:00.050Z',
executor_received_at: '2026-05-18T10:00:00.075Z',
relay_result_at: '2026-05-18T10:00:00.140Z',
outcome_observed_at: '2026-05-18T10:00:02.000Z',
});
assert.equal(timing.quote_to_decision_ms, 40);
assert.equal(timing.decision_to_command_ms, 10);
assert.equal(timing.command_to_executor_ms, 25);
assert.equal(timing.executor_to_relay_result_ms, 65);
assert.equal(timing.quote_to_relay_result_ms, 140);
assert.equal(timing.quote_to_outcome_ms, 2000);
assert.equal(quoteAgeMsAt(timing, '2026-05-18T10:00:00.050Z'), 50);
const skewed = extendMakerTiming(initial, {
strategy_decided_at: '2026-05-18T09:59:59.999Z',
});
assert.equal(skewed.quote_to_decision_ms, null);
assert.equal(skewed.unavailable_reasons.quote_to_decision_ms, 'clock_skew_or_negative_duration');
});
test('relay submission failure classifier preserves specific already-finished category', () => {
assert.equal(
classifyRelaySubmissionFailure(new Error('quote not found or already finished')),
'quote_not_found_or_finished',
);
assert.equal(
classifyRelaySubmissionFailure({ error_message: 'quote not found or already finished' }),
'quote_not_found_or_finished',
);
assert.equal(
classifyRelaySubmissionFailure(new Error('quote_response timed out')),
'relay_timeout',
);
assert.equal(
classifyRelaySubmissionFailure(new Error('Socket not connected')),
'relay_disconnected',
);
assert.equal(
classifyRelaySubmissionFailure(new Error('current_salt unavailable')),
'salt_unavailable',
);
});
test('maker competitiveness aggregates pair, direction, request kind, result, failure, age, notional, and outcome', () => {
const nbtc = 'nep141:nbtc.bridge.near';
const eure = 'nep141:eure.omft.near';
const usdc = 'nep141:eth-0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.omft.near';
const baseTiming = buildInitialMakerTiming({
quoteReceivedAt: '2026-05-18T10:00:00.000Z',
});
const summary = buildMakerCompetitivenessSummary({
generatedAt: '2026-05-18T10:01:00.000Z',
lifecycleRows: [
{
quote_id: 'quote-eure-ok',
pair: `${nbtc}->${eure}`,
direction: 'base_to_quote',
request_kind: 'exact_in',
notional: '5',
notional_symbol: 'EURe',
outcome_status: 'submitted',
execution_result_at: '2026-05-18T10:00:00.080Z',
maker_timing: extendMakerTiming(baseTiming, {
strategy_decided_at: '2026-05-18T10:00:00.010Z',
command_published_at: '2026-05-18T10:00:00.020Z',
executor_received_at: '2026-05-18T10:00:00.030Z',
relay_result_at: '2026-05-18T10:00:00.080Z',
}),
execution: {
status: 'submitted',
result_code: 'quote_response_ok',
timing: { current_salt_source: 'cache' },
},
},
{
quote_id: 'quote-usdc-failed',
pair: `${nbtc}->${usdc}`,
direction: 'base_to_quote',
request_kind: 'exact_in',
notional: '8',
notional_symbol: 'USDC',
execution_result_at: '2026-05-18T10:00:00.400Z',
maker_timing: extendMakerTiming(baseTiming, {
strategy_decided_at: '2026-05-18T10:00:00.100Z',
command_published_at: '2026-05-18T10:00:00.150Z',
executor_received_at: '2026-05-18T10:00:00.250Z',
relay_result_at: '2026-05-18T10:00:00.400Z',
}),
execution: {
status: 'failed',
result_code: 'submission_failed',
failure_category: 'quote_not_found_or_finished',
error_message: 'quote not found or already finished',
timing: { current_salt_source: 'cache' },
},
},
{
quote_id: 'quote-usdc-skip',
pair: `${nbtc}->${usdc}`,
direction: 'base_to_quote',
request_kind: 'exact_in',
notional: '12',
notional_symbol: 'USDC',
maker_timing: extendMakerTiming(baseTiming, {
strategy_decided_at: '2026-05-18T10:00:00.600Z',
}),
decision: {
decision: 'blocked',
decision_reason: 'maker_quote_too_old',
response_policy: {
measured_quote_age_ms: 600,
max_quote_age_ms: 250,
},
},
},
],
});
assert.equal(summary.total.count, 3);
assert.equal(summary.total.accepted_count, 1);
assert.equal(summary.total.relay_failed_count, 1);
assert.equal(summary.total.policy_skip_count, 1);
assert.equal(summary.total.quote_not_found_or_finished_count, 1);
assert.ok(summary.groups.some((group) => (
group.pair === `${nbtc}->${usdc}`
&& group.request_kind === 'exact_in'
&& group.result_code === 'submission_failed'
&& group.failure_category === 'quote_not_found_or_finished'
&& group.quote_age_bucket === '250-500ms'
&& group.notional_bucket === '5-25 USDC'
&& group.outcome_status === 'relay_failed'
)));
assert.ok(summary.age_buckets.some((bucket) => (
bucket.pair === `${nbtc}->${usdc}`
&& bucket.quote_age_bucket === '500-1000ms'
&& bucket.outcome_status === 'policy_skip'
)));
assert.equal(summary.latest_errors[0].error_message, 'quote not found or already finished');
assert.equal(summary.policy_skips[0].reason_code, 'maker_quote_too_old');
});

View file

@ -111,11 +111,26 @@ test('strategy page exposes pair activation, pause, edge, and deposit address co
assert.match(strategySource, /pair-max-notional/); assert.match(strategySource, /pair-max-notional/);
assert.match(strategySource, /Edge bps for/); assert.match(strategySource, /Edge bps for/);
assert.match(strategySource, /Max notional for/); assert.match(strategySource, /Max notional for/);
assert.match(strategySource, /maker_max_quote_age_enabled/);
assert.match(strategySource, /maker_max_quote_age_ms/);
assert.match(strategySource, /Age policy/);
assert.match(strategySource, /Max age/);
assert.match(strategySource, /Init/); assert.match(strategySource, /Init/);
assert.match(strategySource, /deposit_address/); assert.match(strategySource, /deposit_address/);
assert.match(strategySource, /Copy/); assert.match(strategySource, /Copy/);
}); });
test('strategy page exposes maker timing waterfall and competitiveness summaries', () => {
assert.match(strategySource, /Maker competitiveness/);
assert.match(strategySource, /TimingWaterfall/);
assert.match(strategySource, /quote_to_relay_result_ms/);
assert.match(strategySource, /quote_not_found_or_finished/);
assert.match(strategySource, /maker_competitiveness/);
assert.match(strategySource, /pairDisplayLabel/);
assert.match(stylesSource, /\.two-column-grid/);
assert.match(stylesSource, /\.timing-waterfall-table/);
});
test('pair controls are rendered before the long asset catalog table', () => { test('pair controls are rendered before the long asset catalog table', () => {
assert.ok( assert.ok(
strategySource.indexOf('<PairConfigSection') < strategySource.indexOf('<AssetCatalogSection'), strategySource.indexOf('<PairConfigSection') < strategySource.indexOf('<AssetCatalogSection'),

View file

@ -29,3 +29,8 @@ test('ops sentinel exposes trimmed service snapshots and computed runtime alerts
assert.match(source, /activeAlerts: desiredRuntimeAlerts/); assert.match(source, /activeAlerts: desiredRuntimeAlerts/);
assert.match(source, /state\.latest_runtime_alerts = desiredRuntimeAlerts/); assert.match(source, /state\.latest_runtime_alerts = desiredRuntimeAlerts/);
}); });
test('ops sentinel turns dashboard maker competitiveness truth into runtime alerts', () => {
assert.match(source, /buildMakerCompetitivenessRuntimeAlerts/);
assert.match(source, /latest_maker_competitiveness/);
});

View file

@ -2,6 +2,7 @@ import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { import {
buildMakerCompetitivenessRuntimeAlerts,
deriveServiceHealth, deriveServiceHealth,
shouldContainExecutorForAlerts, shouldContainExecutorForAlerts,
shouldRaiseIngestPublishStale, shouldRaiseIngestPublishStale,
@ -77,3 +78,48 @@ test('armed service treats critical truth alerts for any active pair as critical
assert.equal(health.status, 'critical'); assert.equal(health.status, 'critical');
assert.equal(health.label, 'armed on stale truth'); assert.equal(health.label, 'armed on stale truth');
}); });
test('maker competitiveness alerts are pair-scoped for high quote-finished relay failure rate', () => {
const alerts = buildMakerCompetitivenessRuntimeAlerts({
makerCompetitiveness: {
generated_at: '2026-05-18T10:00:00.000Z',
groups: [
{
pair: 'nbtc->usdc',
direction: 'base_to_quote',
request_kind: 'exact_in',
result_code: 'submission_failed',
failure_category: 'quote_not_found_or_finished',
quote_age_bucket: '100-250ms',
notional_bucket: '5-10',
count: 4,
},
{
pair: 'nbtc->usdc',
direction: 'base_to_quote',
request_kind: 'exact_in',
result_code: 'quote_response_ok',
failure_category: null,
quote_age_bucket: '<100ms',
notional_bucket: '5-10',
count: 1,
accepted_count: 1,
},
{
pair: 'nbtc->eure',
result_code: 'quote_response_ok',
failure_category: null,
count: 8,
accepted_count: 8,
},
],
},
});
assert.equal(alerts.length, 1);
assert.equal(alerts[0].alert_code, 'maker_quote_not_found_or_finished_rate_high');
assert.equal(alerts[0].service_scope, 'strategy-engine');
assert.equal(alerts[0].pair, 'nbtc->usdc');
assert.equal(alerts[0].details.quote_not_found_or_finished_count, 4);
assert.equal(alerts[0].details.quote_not_found_or_finished_rate, 0.8);
});

View file

@ -11,9 +11,12 @@ test('strategy duplicate quote tracking is bounded and state-safe', () => {
assert.doesNotMatch(source, /seen_quotes:\s*\{\}/); assert.doesNotMatch(source, /seen_quotes:\s*\{\}/);
}); });
test('strategy execute commands use decision timestamp as durable observed time', () => { test('strategy execute commands stamp command publish time into durable observed time', () => {
assert.match( assert.match(
source, source,
/observedAt:\s*evaluation\.command\.decision_at\s*\|\|\s*event\.observed_at\s*\|\|\s*event\.ingested_at/, /const commandPublishedAt = new Date\(\)\.toISOString\(\)/,
); );
assert.match(source, /command_published_at: commandPublishedAt/);
assert.match(source, /quote_age_at_command_ms: makerTiming\.quote_age_at_command_ms/);
assert.match(source, /observedAt:\s*commandPublishedAt/);
}); });

View file

@ -241,7 +241,146 @@ test('strategy blocks BTC -> USDC when price event lacks USDC route fields', ()
assert.equal(result.command, undefined); assert.equal(result.command, undefined);
}); });
function makeBtcUsdcDbConfig() { test('maker response age policy skips stale BTC -> USDC quotes without emitting a relay command', () => {
const config = makeBtcUsdcDbConfig({
strategyConfigOverrides: {
makerMaxQuoteAgeEnabled: true,
makerMaxQuoteAgeMs: 100,
makerLatencyPolicyReason: 'test stale response protection',
},
});
const result = evaluateTradeOpportunity({
demandEvent: {
payload: {
quote_id: 'quote-usdc-too-old',
pair: config.activePair,
asset_in: config.tradingBtc.assetId,
asset_out: config.tradingUsdc.assetId,
request_kind: 'exact_in',
amount_in: '10000',
maker_timing: {
quote_received_at: '2026-04-02T10:00:04.800Z',
},
},
},
priceEvent: makeBtcUsdcPriceEvent(),
inventoryEvent: makeBtcUsdcInventoryEvent(),
config,
armed: true,
strategyReceivedAt: '2026-04-02T10:00:05.000Z',
now: Date.parse('2026-04-02T10:00:05.000Z'),
});
assert.equal(result.decision.decision, 'blocked');
assert.equal(result.decision.decision_reason, 'maker_quote_too_old');
assert.equal(result.decision.quote_age_at_decision_ms, 200);
assert.equal(result.decision.response_policy.enabled, true);
assert.equal(result.decision.response_policy.max_quote_age_ms, 100);
assert.equal(result.command, undefined);
});
test('maker response age policy skips when quote timing prerequisites are missing', () => {
const config = makeBtcUsdcDbConfig({
strategyConfigOverrides: {
makerMaxQuoteAgeEnabled: true,
makerMaxQuoteAgeMs: 100,
},
});
const result = evaluateTradeOpportunity({
demandEvent: {
payload: {
quote_id: 'quote-usdc-no-timing',
pair: config.activePair,
asset_in: config.tradingBtc.assetId,
asset_out: config.tradingUsdc.assetId,
request_kind: 'exact_in',
amount_in: '10000',
},
},
priceEvent: makeBtcUsdcPriceEvent(),
inventoryEvent: makeBtcUsdcInventoryEvent(),
config,
armed: true,
strategyReceivedAt: '2026-04-02T10:00:05.000Z',
now: Date.parse('2026-04-02T10:00:05.000Z'),
});
assert.equal(result.decision.decision, 'blocked');
assert.equal(result.decision.decision_reason, 'maker_quote_age_unavailable');
assert.equal(result.decision.response_policy.valid, true);
assert.equal(result.command, undefined);
});
test('maker response age policy skips invalid enabled config', () => {
const config = makeBtcUsdcDbConfig({
strategyConfigOverrides: {
makerMaxQuoteAgeEnabled: true,
makerMaxQuoteAgeMs: null,
},
});
const result = evaluateTradeOpportunity({
demandEvent: {
payload: {
quote_id: 'quote-usdc-invalid-policy',
pair: config.activePair,
asset_in: config.tradingBtc.assetId,
asset_out: config.tradingUsdc.assetId,
request_kind: 'exact_in',
amount_in: '10000',
maker_timing: {
quote_received_at: '2026-04-02T10:00:04.950Z',
},
},
},
priceEvent: makeBtcUsdcPriceEvent(),
inventoryEvent: makeBtcUsdcInventoryEvent(),
config,
armed: true,
strategyReceivedAt: '2026-04-02T10:00:05.000Z',
now: Date.parse('2026-04-02T10:00:05.000Z'),
});
assert.equal(result.decision.decision, 'blocked');
assert.equal(result.decision.decision_reason, 'maker_quote_response_policy_invalid');
assert.equal(result.decision.response_policy.valid, false);
assert.equal(result.command, undefined);
});
test('disabled maker response age policy preserves current BTC -> USDC actionability', () => {
const config = makeBtcUsdcDbConfig({
strategyConfigOverrides: {
makerMaxQuoteAgeEnabled: false,
makerMaxQuoteAgeMs: null,
},
});
const result = evaluateTradeOpportunity({
demandEvent: {
payload: {
quote_id: 'quote-usdc-policy-disabled',
pair: config.activePair,
asset_in: config.tradingBtc.assetId,
asset_out: config.tradingUsdc.assetId,
request_kind: 'exact_in',
amount_in: '10000',
maker_timing: {
quote_received_at: '2026-04-02T09:59:59.000Z',
},
},
},
priceEvent: makeBtcUsdcPriceEvent(),
inventoryEvent: makeBtcUsdcInventoryEvent(),
config,
armed: true,
strategyReceivedAt: '2026-04-02T10:00:05.000Z',
now: Date.parse('2026-04-02T10:00:05.000Z'),
});
assert.equal(result.decision.decision, 'actionable');
assert.equal(result.decision.response_policy.enabled, false);
assert.ok(result.command);
});
function makeBtcUsdcDbConfig({ strategyConfigOverrides = {} } = {}) {
const tradingBtc = { const tradingBtc = {
assetId: 'nep141:nbtc.bridge.near', assetId: 'nep141:nbtc.bridge.near',
symbol: 'BTC', symbol: 'BTC',
@ -263,6 +402,10 @@ function makeBtcUsdcDbConfig() {
priceMaxAgeMs: 30_000, priceMaxAgeMs: 30_000,
inventoryMaxAgeMs: 30_000, inventoryMaxAgeMs: 30_000,
minNotional: '0', minNotional: '0',
makerMaxQuoteAgeEnabled: false,
makerMaxQuoteAgeMs: null,
makerLatencyPolicyReason: null,
...strategyConfigOverrides,
}; };
const priceRoute = { const priceRoute = {
routeId: 'btc-usdc:v1', routeId: 'btc-usdc:v1',

View file

@ -136,6 +136,8 @@ test('seeded DB config preserves current nBTC/EURe pair, 49 bps edge, and legacy
assert.equal(snapshot.activePair, `${CURRENT_NBTC_ASSET_ID}->${CURRENT_EURE_ASSET_ID}`); assert.equal(snapshot.activePair, `${CURRENT_NBTC_ASSET_ID}->${CURRENT_EURE_ASSET_ID}`);
assert.equal(snapshot.pairs.length, 2); assert.equal(snapshot.pairs.length, 2);
assert.equal(snapshot.pairByKey.get(snapshot.activePair).strategyConfig.edgeBps, 49); assert.equal(snapshot.pairByKey.get(snapshot.activePair).strategyConfig.edgeBps, 49);
assert.equal(snapshot.pairByKey.get(snapshot.activePair).strategyConfig.makerMaxQuoteAgeEnabled, false);
assert.equal(snapshot.pairByKey.get(snapshot.activePair).strategyConfig.makerMaxQuoteAgeMs, null);
assert.equal(snapshot.trackedAssetIds.includes(LEGACY_OMFT_BTC_ASSET_ID), true); assert.equal(snapshot.trackedAssetIds.includes(LEGACY_OMFT_BTC_ASSET_ID), true);
assert.equal(snapshot.assetRegistry.get(CURRENT_USDC_ASSET_ID).chain, 'eth:1'); assert.equal(snapshot.assetRegistry.get(CURRENT_USDC_ASSET_ID).chain, 'eth:1');
assert.equal(snapshot.trackedAssetIds.includes(CURRENT_USDC_ASSET_ID), false); assert.equal(snapshot.trackedAssetIds.includes(CURRENT_USDC_ASSET_ID), false);
@ -213,6 +215,50 @@ test('edge update creates a new active strategy version', async () => {
assert.equal(versions.find((row) => row.version === 1).active, false); assert.equal(versions.find((row) => row.version === 1).active, false);
}); });
test('strategy config update can enable pair-scoped maker response age policy', async () => {
const pool = createMemoryPool();
await seedTradingConfig(pool);
const pairId = `${CURRENT_NBTC_ASSET_ID}->${CURRENT_EURE_ASSET_ID}`;
const next = await createPairStrategyConfigVersion(pool, {
pairId,
edgeBps: 49,
maxNotional: '150',
makerMaxQuoteAgeEnabled: true,
makerMaxQuoteAgeMs: 125,
makerLatencyPolicyReason: 'timing evidence shows stale responses',
changedBy: 'test',
reason: 'enable maker response age policy',
});
const snapshot = await loadTradingConfig(pool);
const strategyConfig = snapshot.pairByKey.get(pairId).strategyConfig;
assert.equal(next.makerMaxQuoteAgeEnabled, true);
assert.equal(next.makerMaxQuoteAgeMs, 125);
assert.equal(next.makerLatencyPolicyReason, 'timing evidence shows stale responses');
assert.equal(strategyConfig.edgeBps, 49);
assert.equal(strategyConfig.makerMaxQuoteAgeEnabled, true);
assert.equal(strategyConfig.makerMaxQuoteAgeMs, 125);
});
test('strategy config update rejects enabled maker response age policy without a max age', async () => {
const pool = createMemoryPool();
await seedTradingConfig(pool);
await assert.rejects(
createPairStrategyConfigVersion(pool, {
pairId: `${CURRENT_NBTC_ASSET_ID}->${CURRENT_EURE_ASSET_ID}`,
edgeBps: 49,
maxNotional: '150',
makerMaxQuoteAgeEnabled: true,
makerMaxQuoteAgeMs: null,
changedBy: 'test',
reason: 'invalid maker response age policy',
}),
/maker_max_quote_age_ms must be a positive integer/,
);
});
test('strategy config update can clear request amount and slippage caps', async () => { test('strategy config update can clear request amount and slippage caps', async () => {
const pool = createMemoryPool(); const pool = createMemoryPool();
await seedTradingConfig(pool); await seedTradingConfig(pool);
@ -558,7 +604,7 @@ function createMemoryPool() {
importRuns: new Map(), importRuns: new Map(),
audit: [], audit: [],
async query(sql, params = []) { async query(sql, params = []) {
if (/CREATE TABLE|CREATE (UNIQUE )?INDEX/i.test(sql)) return { rows: [], rowCount: 0 }; if (/CREATE TABLE|CREATE (UNIQUE )?INDEX|ALTER TABLE/i.test(sql)) return { rows: [], rowCount: 0 };
if (/SELECT \* FROM trading_assets\s*$/i.test(sql)) return rows(this.assets); if (/SELECT \* FROM trading_assets\s*$/i.test(sql)) return rows(this.assets);
if (/SELECT\s+asset_id,[\s\S]+raw_payload_available[\s\S]+FROM trading_assets/i.test(sql)) { if (/SELECT\s+asset_id,[\s\S]+raw_payload_available[\s\S]+FROM trading_assets/i.test(sql)) {
return selectAssetCatalogRows(this, params); return selectAssetCatalogRows(this, params);
@ -777,8 +823,11 @@ function insertStrategyConfig(pool, params) {
request_default_notional: params[11], request_default_notional: params[11],
request_max_notional: params[12], request_max_notional: params[12],
request_max_slippage_bps: params[13], request_max_slippage_bps: params[13],
created_by: params[14], maker_max_quote_age_enabled: params[14],
reason: params[15], maker_max_quote_age_ms: params[15],
maker_latency_policy_reason: params[16],
created_by: params[17],
reason: params[18],
created_at: '2026-05-12T16:35:00.000Z', created_at: '2026-05-12T16:35:00.000Z',
}; };
pool.strategyConfigs.set(configId, row); pool.strategyConfigs.set(configId, row);