Link quote outcomes to settled inventory
All checks were successful
deploy / deploy (push) Successful in 32s

Proof: Adds a durable quote outcome attribution model, refreshes it from submitted execution results plus inventory snapshots, and updates dashboard lifecycle rows so submitted, blocked, rejected, not-filled, and completed states are separated by durable evidence. Lowers the approved live strategy edge threshold to 1.49%.

Assumptions: Exact asset-unit inventory deltas inside the attribution window are acceptable as heuristic settlement evidence for the active BTC/EURe NEAR Intents path when the uncertainty is stored and shown. Deadline-plus-inventory non-fill is inferred until venue terminal events are persisted.

Still fake: No venue-native terminal fill event or per-trade fee/cost ledger is stored yet; heuristic completed and not-filled records remain explicitly labeled as inferred where applicable, and realized net PnL is still not claimed.
This commit is contained in:
philipp 2026-04-10 11:24:22 +02:00
parent 3fca125cdd
commit e0dfd24a8b
20 changed files with 1739 additions and 81 deletions

View file

@ -40,6 +40,16 @@ data:
TRADE_EXECUTOR_CONTROL_PORT: "8087" TRADE_EXECUTOR_CONTROL_PORT: "8087"
OPS_SENTINEL_CONTROL_HOST: 0.0.0.0 OPS_SENTINEL_CONTROL_HOST: 0.0.0.0
OPS_SENTINEL_CONTROL_PORT: "8088" OPS_SENTINEL_CONTROL_PORT: "8088"
OPERATOR_DASHBOARD_CONTROL_HOST: 0.0.0.0
OPERATOR_DASHBOARD_CONTROL_PORT: "8090"
NEAR_INTENTS_CONTROL_BASE_URL: http://near-intents-ingest.unrip.svc.cluster.local:8081
MARKET_REFERENCE_CONTROL_BASE_URL: http://market-reference-ingest.unrip.svc.cluster.local:8082
INVENTORY_SYNC_CONTROL_BASE_URL: http://inventory-sync.unrip.svc.cluster.local:8083
LIQUIDITY_MANAGER_CONTROL_BASE_URL: http://liquidity-manager.unrip.svc.cluster.local:8084
HISTORY_WRITER_CONTROL_BASE_URL: http://history-writer.unrip.svc.cluster.local:8085
STRATEGY_ENGINE_CONTROL_BASE_URL: http://strategy-engine.unrip.svc.cluster.local:8086
TRADE_EXECUTOR_CONTROL_BASE_URL: http://trade-executor.unrip.svc.cluster.local:8087
OPS_SENTINEL_CONTROL_BASE_URL: http://ops-sentinel.unrip.svc.cluster.local:8088
KAFKA_BROKERS: redpanda.unrip.svc.cluster.local:9092 KAFKA_BROKERS: redpanda.unrip.svc.cluster.local:9092
KAFKA_CLIENT_ID: unrip KAFKA_CLIENT_ID: unrip
KAFKA_TOPIC_RAW_NEAR_INTENTS_QUOTE: raw.near_intents.quote KAFKA_TOPIC_RAW_NEAR_INTENTS_QUOTE: raw.near_intents.quote
@ -57,6 +67,7 @@ data:
KAFKA_CONSUMER_GROUP_STRATEGY: strategy-engine-v1 KAFKA_CONSUMER_GROUP_STRATEGY: strategy-engine-v1
KAFKA_CONSUMER_GROUP_EXECUTOR: trade-executor-v1 KAFKA_CONSUMER_GROUP_EXECUTOR: trade-executor-v1
KAFKA_CONSUMER_GROUP_OPS_SENTINEL: ops-sentinel-v1 KAFKA_CONSUMER_GROUP_OPS_SENTINEL: ops-sentinel-v1
KAFKA_CONSUMER_GROUP_OPERATOR_DASHBOARD: operator-dashboard-v1
STRATEGY_STATE_DIR: /var/lib/unrip/strategy-state STRATEGY_STATE_DIR: /var/lib/unrip/strategy-state
EXECUTOR_STATE_DIR: /var/lib/unrip/executor-state EXECUTOR_STATE_DIR: /var/lib/unrip/executor-state
LIQUIDITY_STATE_DIR: /var/lib/unrip/liquidity-state LIQUIDITY_STATE_DIR: /var/lib/unrip/liquidity-state
@ -67,9 +78,9 @@ data:
MARKET_REFERENCE_COINGECKO_URL: https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=eur MARKET_REFERENCE_COINGECKO_URL: https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=eur
INVENTORY_SYNC_REFRESH_MS: "15000" INVENTORY_SYNC_REFRESH_MS: "15000"
LIQUIDITY_REFRESH_MS: "30000" LIQUIDITY_REFRESH_MS: "30000"
STRATEGY_GROSS_THRESHOLD_PCT: "2" STRATEGY_GROSS_THRESHOLD_PCT: "1.49"
STRATEGY_INITIAL_ARMED: "false" STRATEGY_INITIAL_ARMED: "false"
STRATEGY_MAX_NOTIONAL_EURE: "5" STRATEGY_MAX_NOTIONAL_EURE: "150"
STRATEGY_PRICE_MAX_AGE_MS: "30000" STRATEGY_PRICE_MAX_AGE_MS: "30000"
STRATEGY_INVENTORY_MAX_AGE_MS: "30000" STRATEGY_INVENTORY_MAX_AGE_MS: "30000"
EXECUTOR_INITIAL_ARMED: "false" EXECUTOR_INITIAL_ARMED: "false"
@ -83,6 +94,10 @@ data:
OPS_SENTINEL_INVENTORY_STALE_MS: "30000" OPS_SENTINEL_INVENTORY_STALE_MS: "30000"
OPS_SENTINEL_FUNDING_CREDIT_PENDING_MS: "300000" OPS_SENTINEL_FUNDING_CREDIT_PENDING_MS: "300000"
OPS_SENTINEL_FUNDING_STUCK_MS: "3600000" OPS_SENTINEL_FUNDING_STUCK_MS: "3600000"
OPERATOR_DASHBOARD_AUTH_MODE: stub
OPERATOR_DASHBOARD_QUOTE_LIMIT: "10"
OPERATOR_DASHBOARD_TRADE_PAGE_SIZE: "20"
OPERATOR_DASHBOARD_UPSTREAM_TIMEOUT_MS: "3000"
--- ---
apiVersion: v1 apiVersion: v1
kind: PersistentVolumeClaim kind: PersistentVolumeClaim
@ -117,6 +132,123 @@ spec:
requests: requests:
storage: 5Gi storage: 5Gi
--- ---
apiVersion: v1
kind: Service
metadata:
name: near-intents-ingest
namespace: unrip
spec:
selector:
app: near-intents-ingest
ports:
- name: control-api
port: 8081
targetPort: 8081
---
apiVersion: v1
kind: Service
metadata:
name: market-reference-ingest
namespace: unrip
spec:
selector:
app: market-reference-ingest
ports:
- name: control-api
port: 8082
targetPort: 8082
---
apiVersion: v1
kind: Service
metadata:
name: inventory-sync
namespace: unrip
spec:
selector:
app: inventory-sync
ports:
- name: control-api
port: 8083
targetPort: 8083
---
apiVersion: v1
kind: Service
metadata:
name: liquidity-manager
namespace: unrip
spec:
selector:
app: liquidity-manager
ports:
- name: control-api
port: 8084
targetPort: 8084
---
apiVersion: v1
kind: Service
metadata:
name: history-writer
namespace: unrip
spec:
selector:
app: history-writer
ports:
- name: control-api
port: 8085
targetPort: 8085
---
apiVersion: v1
kind: Service
metadata:
name: strategy-engine
namespace: unrip
spec:
selector:
app: strategy-engine
ports:
- name: control-api
port: 8086
targetPort: 8086
---
apiVersion: v1
kind: Service
metadata:
name: trade-executor
namespace: unrip
spec:
selector:
app: trade-executor
ports:
- name: control-api
port: 8087
targetPort: 8087
---
apiVersion: v1
kind: Service
metadata:
name: ops-sentinel
namespace: unrip
spec:
selector:
app: ops-sentinel
ports:
- name: control-api
port: 8088
targetPort: 8088
---
apiVersion: v1
kind: Service
metadata:
name: operator-dashboard
namespace: unrip
spec:
selector:
app: operator-dashboard
ports:
- name: http
port: 8090
targetPort: 8090
---
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@ -393,3 +525,35 @@ spec:
- name: executor-state - name: executor-state
persistentVolumeClaim: persistentVolumeClaim:
claimName: executor-state claimName: executor-state
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: operator-dashboard
namespace: unrip
spec:
replicas: 1
selector:
matchLabels:
app: operator-dashboard
template:
metadata:
labels:
app: operator-dashboard
app.kubernetes.io/part-of: unrip
spec:
imagePullSecrets:
- name: unrip-registry-creds
containers:
- name: app
image: ghcr.io/example/unrip:bootstrap
imagePullPolicy: IfNotPresent
command: ["node", "src/apps/operator-dashboard.mjs"]
ports:
- name: http
containerPort: 8090
envFrom:
- configMapRef:
name: unrip-config
- secretRef:
name: unrip-secrets

View file

@ -13,6 +13,7 @@ import {
insertHistoryEvent, insertHistoryEvent,
loadLatestPortfolioMetric, loadLatestPortfolioMetric,
loadPortfolioMetricInputs, loadPortfolioMetricInputs,
refreshQuoteOutcomes,
upsertPortfolioMetric, upsertPortfolioMetric,
} from '../lib/postgres.mjs'; } from '../lib/postgres.mjs';
@ -54,6 +55,11 @@ const portfolioMetricTopics = new Set([
config.kafkaTopicCmdExecuteTrade, config.kafkaTopicCmdExecuteTrade,
config.kafkaTopicExecTradeResult, config.kafkaTopicExecTradeResult,
]); ]);
const quoteOutcomeTopics = new Set([
config.kafkaTopicStateIntentInventory,
config.kafkaTopicCmdExecuteTrade,
config.kafkaTopicExecTradeResult,
]);
for (const topic of topics) { for (const topic of topics) {
await consumer.subscribe({ topic, fromBeginning: false }); await consumer.subscribe({ topic, fromBeginning: false });
@ -71,11 +77,17 @@ const state = {
offsets: {}, offsets: {},
latest_portfolio_metrics: null, latest_portfolio_metrics: null,
metrics_error: null, metrics_error: null,
last_quote_outcomes_at: null,
latest_quote_outcomes: null,
quote_outcomes_error: null,
}; };
await refreshPortfolioMetrics().catch((error) => { await refreshPortfolioMetrics().catch((error) => {
state.metrics_error = serializeError(error); state.metrics_error = serializeError(error);
}); });
await refreshQuoteOutcomeAttributions().catch((error) => {
state.quote_outcomes_error = serializeError(error);
});
await consumer.run({ await consumer.run({
eachMessage: async ({ topic, partition, message }) => { eachMessage: async ({ topic, partition, message }) => {
@ -116,6 +128,19 @@ await consumer.run({
}); });
} }
} }
if (quoteOutcomeTopics.has(topic)) {
try {
await refreshQuoteOutcomeAttributions();
} catch (error) {
state.quote_outcomes_error = serializeError(error);
logger.error('quote_outcomes_refresh_failed', {
topic,
details: {
error: serializeError(error),
},
});
}
}
} catch (error) { } catch (error) {
state.last_error = serializeError(error); state.last_error = serializeError(error);
state.error_count += 1; state.error_count += 1;
@ -257,6 +282,22 @@ async function refreshPortfolioMetrics() {
return state.latest_portfolio_metrics; return state.latest_portfolio_metrics;
} }
async function refreshQuoteOutcomeAttributions() {
const records = await refreshQuoteOutcomes(pool, {
btcAsset: config.tradingBtc,
eureAsset: config.tradingEure,
});
state.last_quote_outcomes_at = new Date().toISOString();
state.quote_outcomes_error = null;
state.latest_quote_outcomes = {
count: records.length,
completed_count: records.filter((entry) => entry.outcome_status === 'completed').length,
not_filled_count: records.filter((entry) => entry.outcome_status === 'not_filled').length,
submitted_count: records.filter((entry) => entry.outcome_status === 'submitted').length,
};
return state.latest_quote_outcomes;
}
function summarizePortfolioMetric(metric) { function summarizePortfolioMetric(metric) {
if (!metric) return null; if (!metric) return null;
return { return {
@ -265,7 +306,7 @@ function summarizePortfolioMetric(metric) {
baseline_anchor_at: metric.baseline_anchor_at, baseline_anchor_at: metric.baseline_anchor_at,
baseline_status: metric.baseline_status, baseline_status: metric.baseline_status,
current_portfolio_value_eure: metric.payload?.current_portfolio_value_eure ?? null, current_portfolio_value_eure: metric.payload?.current_portfolio_value_eure ?? null,
trade_pnl_eure: metric.payload?.trade_pnl_eure ?? null, portfolio_vs_simple_hold_eure: metric.payload?.portfolio_vs_simple_hold_eure ?? null,
mark_to_market_pnl_eure: metric.payload?.mark_to_market_pnl_eure ?? null, mark_to_market_pnl_eure: metric.payload?.mark_to_market_pnl_eure ?? null,
price_move_pnl_eure: metric.payload?.price_move_pnl_eure ?? null, price_move_pnl_eure: metric.payload?.price_move_pnl_eure ?? null,
command_count: metric.payload?.command_count ?? 0, command_count: metric.payload?.command_count ?? 0,

View file

@ -34,6 +34,7 @@ import {
loadRecentDepositStatuses, loadRecentDepositStatuses,
loadRecentExecuteTradeCommands, loadRecentExecuteTradeCommands,
loadRecentExecutionResults, loadRecentExecutionResults,
loadRecentQuoteOutcomes,
loadRecentTradeDecisions, loadRecentTradeDecisions,
loadRecentQuotes, loadRecentQuotes,
loadSubmissionPage, loadSubmissionPage,
@ -343,6 +344,7 @@ async function loadBootstrapPayload({ auth, page, pageSize }) {
recentTradeDecisions, recentTradeDecisions,
recentExecuteTradeCommands, recentExecuteTradeCommands,
recentExecutionResults, recentExecutionResults,
recentQuoteOutcomes,
recentAlertTransitions, recentAlertTransitions,
serviceSnapshots, serviceSnapshots,
] = await Promise.all([ ] = await Promise.all([
@ -403,6 +405,12 @@ async function loadBootstrapPayload({ auth, page, pageSize }) {
[], [],
sourceErrors, sourceErrors,
), ),
safeSourceLoad(
'recent_quote_outcomes',
() => loadRecentQuoteOutcomes(pool, { limit: 200 }),
[],
sourceErrors,
),
safeSourceLoad( safeSourceLoad(
'recent_alert_transitions', 'recent_alert_transitions',
() => loadRecentAlertTransitions(pool, { limit: 20 }), () => loadRecentAlertTransitions(pool, { limit: 20 }),
@ -426,6 +434,7 @@ async function loadBootstrapPayload({ auth, page, pageSize }) {
recentTradeDecisions, recentTradeDecisions,
recentExecuteTradeCommands, recentExecuteTradeCommands,
recentExecutionResults, recentExecutionResults,
recentQuoteOutcomes,
recentAlertTransitions, recentAlertTransitions,
serviceSnapshots, serviceSnapshots,
sourceErrors, sourceErrors,

View file

@ -131,18 +131,24 @@ async function handleDemand(event) {
} }
async function publishDecision(decisionPayload) { async function publishDecision(decisionPayload) {
const decisionAt = decisionPayload.decision_at || new Date().toISOString();
const normalizedDecisionPayload = {
...decisionPayload,
decision_at: decisionAt,
};
const event = buildEventEnvelope({ const event = buildEventEnvelope({
source: 'strategy-engine', source: 'strategy-engine',
venue: 'near-intents', venue: 'near-intents',
eventType: 'trade_decision', eventType: 'trade_decision',
payload: decisionPayload, observedAt: decisionAt,
payload: normalizedDecisionPayload,
}); });
await producer.sendJson(config.kafkaTopicDecisionTradeDecision, event, { key: decisionPayload.quote_id }); await producer.sendJson(config.kafkaTopicDecisionTradeDecision, event, { key: normalizedDecisionPayload.quote_id });
state.latest_decision = decisionPayload; state.latest_decision = normalizedDecisionPayload;
state.recent_decisions.unshift(decisionPayload); state.recent_decisions.unshift(normalizedDecisionPayload);
state.recent_decisions = state.recent_decisions.slice(0, 20); state.recent_decisions = state.recent_decisions.slice(0, 20);
state.skipped_counts[decisionPayload.decision_reason] = state.skipped_counts[normalizedDecisionPayload.decision_reason] =
(state.skipped_counts[decisionPayload.decision_reason] || 0) + 1; (state.skipped_counts[normalizedDecisionPayload.decision_reason] || 0) + 1;
} }
const controlApi = startControlApi({ const controlApi = startControlApi({

View file

@ -79,7 +79,7 @@ const state = {
last_quote_status: null, last_quote_status: null,
last_error: null, last_error: null,
in_flight_count: 0, in_flight_count: 0,
completed_count: 0, submitted_count: 0,
}; };
await consumer.subscribe({ topic: config.kafkaTopicCmdExecuteTrade, fromBeginning: false }); await consumer.subscribe({ topic: config.kafkaTopicCmdExecuteTrade, fromBeginning: false });
@ -107,7 +107,7 @@ async function handleCommand(event) {
state.last_command = payload; state.last_command = payload;
const existing = stateStore.get(payload.command_id); const existing = stateStore.get(payload.command_id);
if (existing?.status === 'completed') { if (existing?.status === 'submitted') {
logger.warn('duplicate_command_skipped', { logger.warn('duplicate_command_skipped', {
topic: config.kafkaTopicCmdExecuteTrade, topic: config.kafkaTopicCmdExecuteTrade,
pair: payload.pair, pair: payload.pair,
@ -158,11 +158,11 @@ async function handleCommand(event) {
result_code: response === 'OK' ? 'quote_response_ok' : 'quote_response_ack', result_code: response === 'OK' ? 'quote_response_ok' : 'quote_response_ack',
venue_response: response, venue_response: response,
}); });
stateStore.markCompleted(payload.command_id, { stateStore.markSubmitted(payload.command_id, {
quote_id: payload.quote_id, quote_id: payload.quote_id,
result: response, result: response,
}); });
state.completed_count += 1; state.submitted_count += 1;
} catch (error) { } catch (error) {
state.last_error = serializeError(error); state.last_error = serializeError(error);
stateStore.markFailed(payload.command_id, { stateStore.markFailed(payload.command_id, {

View file

@ -13,19 +13,24 @@ export function createExecutorStateStore({ stateDir, fileName = 'trade-executor-
return { return {
get(commandId) { get(commandId) {
return store.getState().commands[commandId] || null; const command = store.getState().commands[commandId] || null;
if (!command) return null;
return {
...command,
status: command.status === 'completed' ? 'submitted' : command.status,
};
}, },
markProcessing(commandId, metadata) { markProcessing(commandId, metadata) {
return updateCommand(store, commandId, metadata, 'processing'); return updateCommand(store, commandId, metadata, 'processing');
}, },
markCompleted(commandId, metadata) { markSubmitted(commandId, metadata) {
return updateCommand(store, commandId, metadata, 'completed'); return updateCommand(store, commandId, metadata, 'submitted');
}, },
markFailed(commandId, metadata) { markFailed(commandId, metadata) {
return updateCommand(store, commandId, metadata, 'failed'); return updateCommand(store, commandId, metadata, 'failed');
}, },
getState() { getState() {
return store.getState(); return normalizeState(store.getState());
}, },
}; };
} }
@ -43,3 +48,18 @@ function updateCommand(store, commandId, metadata, status) {
return nextState.commands[commandId]; return nextState.commands[commandId];
} }
function normalizeState(state) {
return {
...state,
commands: Object.fromEntries(
Object.entries(state.commands || {}).map(([commandId, command]) => [
commandId,
{
...command,
status: command.status === 'completed' ? 'submitted' : command.status,
},
]),
),
};
}

View file

@ -1,6 +1,7 @@
import { unitsToNumber } from './assets.mjs'; import { unitsToNumber } from './assets.mjs';
import { summarizeFundingObservations } from './funding-observations.mjs'; import { summarizeFundingObservations } from './funding-observations.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 { inferServiceFreshnessTimestamp as inferRuntimeFreshnessTimestamp } from './runtime-health.mjs'; import { inferServiceFreshnessTimestamp as inferRuntimeFreshnessTimestamp } from './runtime-health.mjs';
export const DASHBOARD_LIVE_QUOTE_LIMIT = 10; export const DASHBOARD_LIVE_QUOTE_LIMIT = 10;
@ -315,6 +316,7 @@ export function buildDashboardBootstrap({
recentTradeDecisions, recentTradeDecisions,
recentExecuteTradeCommands, recentExecuteTradeCommands,
recentExecutionResults, recentExecutionResults,
recentQuoteOutcomes = [],
recentAlertTransitions, recentAlertTransitions,
serviceSnapshots, serviceSnapshots,
sourceErrors = [], sourceErrors = [],
@ -381,12 +383,14 @@ export function buildDashboardBootstrap({
caveats: profitability.caveats, caveats: profitability.caveats,
}, },
strategy: buildStrategySummary({ strategy: buildStrategySummary({
config,
servicesByName, servicesByName,
activeAlerts, activeAlerts,
recentQuotes, recentQuotes,
recentTradeDecisions, recentTradeDecisions,
recentExecuteTradeCommands, recentExecuteTradeCommands,
recentExecutionResults, recentExecutionResults,
recentQuoteOutcomes,
}), }),
system: buildSystemSummary({ system: buildSystemSummary({
servicesByName, servicesByName,
@ -408,6 +412,7 @@ export function buildProfitabilitySummary({ metric, submissionSummary } = {}) {
pnl_vs_deposit_baseline_eure: null, pnl_vs_deposit_baseline_eure: null,
pnl_vs_simple_hold_eure: null, pnl_vs_simple_hold_eure: null,
market_move_contribution_eure: null, market_move_contribution_eure: null,
portfolio_vs_simple_hold_eure: null,
trading_contribution_eure: null, trading_contribution_eure: null,
baseline_anchor_at: metric?.baseline_anchor_at || null, baseline_anchor_at: metric?.baseline_anchor_at || null,
baseline_status: metric?.baseline_status || metric?.payload?.baseline_status || 'unavailable', baseline_status: metric?.baseline_status || metric?.payload?.baseline_status || 'unavailable',
@ -420,8 +425,8 @@ export function buildProfitabilitySummary({ metric, submissionSummary } = {}) {
recent_submission_count: submissionSummary?.total ?? 0, recent_submission_count: submissionSummary?.total ?? 0,
last_submission_at: submissionSummary?.last_submission_at || null, last_submission_at: submissionSummary?.last_submission_at || null,
caveats: [ caveats: [
'Portfolio PnL is truthful to the current durable inventory and reference price path.', 'Portfolio value and simple-hold comparison use durable inventory and reference prices.',
'Fees and per-trade realized net settlement deltas are not fully tracked yet.', 'This is not realized per-trade PnL; completed trades require linked outcome and settlement records.',
], ],
}; };
@ -452,10 +457,11 @@ export function buildProfitabilitySummary({ metric, submissionSummary } = {}) {
} }
if (summary.simple_hold_value_eure) { if (summary.simple_hold_value_eure) {
summary.trading_contribution_eure = formatDecimalDifference( summary.portfolio_vs_simple_hold_eure = formatDecimalDifference(
summary.current_total_portfolio_value_eure, summary.current_total_portfolio_value_eure,
summary.simple_hold_value_eure, summary.simple_hold_value_eure,
); );
summary.trading_contribution_eure = null;
} }
if (summary.external_flow_adjusted) { if (summary.external_flow_adjusted) {
@ -718,6 +724,12 @@ const HUMAN_REASON_TEXT = {
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.',
quote_response_ok: 'Quote response accepted by the relay.', quote_response_ok: 'Quote response accepted by the relay.',
awaiting_outcome: 'No durable venue outcome is recorded yet.',
deadline_elapsed_without_settlement:
'No matching inventory delta was observed after the quote-response deadline.',
matched_inventory_delta:
'Matched to an observed inventory delta. Attribution is heuristic unless the source says linked settlement.',
ambiguous_inventory_delta_match: 'Inventory movement matched more than one quote candidate.',
reason_unknown: 'Reason not recorded.', reason_unknown: 'Reason not recorded.',
stale_reference_price: 'Reference price is stale.', stale_reference_price: 'Reference price is stale.',
strategy_approved: 'Strategy approved the quote.', strategy_approved: 'Strategy approved the quote.',
@ -734,6 +746,7 @@ export function deriveQuoteLifecycleRows({
recentTradeDecisions = [], recentTradeDecisions = [],
recentExecuteTradeCommands = [], recentExecuteTradeCommands = [],
recentExecutionResults = [], recentExecutionResults = [],
recentQuoteOutcomes = [],
limit = 20, limit = 20,
} = {}) { } = {}) {
const rowsByKey = new Map(); const rowsByKey = new Map();
@ -808,10 +821,27 @@ export function deriveQuoteLifecycleRows({
}); });
} }
return [...rowsByKey.values()] for (const outcome of recentQuoteOutcomes || []) {
const row = ensureLifecycleRow(rowsByKey, outcome?.quote_id || outcome?.decision_id || outcome?.command_id || `outcome:${outcome?.outcome_observed_at || rowsByKey.size}`);
mergeLifecycleEvidence(row, {
quote_id: outcome?.quote_id || null,
decision_id: outcome?.decision_id || null,
command_id: outcome?.command_id || null,
pair: outcome?.pair || null,
direction: outcome?.direction || null,
request_kind: outcome?.request_kind || null,
gross_edge_pct: outcome?.gross_edge_pct || null,
eure_notional: outcome?.eure_notional || null,
outcome,
outcome_observed_at: outcome?.outcome_observed_at || outcome?.submitted_at || null,
});
}
const finalized = [...rowsByKey.values()]
.map((row) => finalizeLifecycleRow(row)) .map((row) => finalizeLifecycleRow(row))
.sort((left, right) => sortTimestamps(right.latest_stage_at, left.latest_stage_at)) .sort((left, right) => sortTimestamps(right.latest_stage_at, left.latest_stage_at));
.slice(0, limit);
return limit == null ? finalized : finalized.slice(0, limit);
} }
function ensureLifecycleRow(rowsByKey, key) { function ensureLifecycleRow(rowsByKey, key) {
@ -829,9 +859,11 @@ function ensureLifecycleRow(rowsByKey, key) {
decision_at: null, decision_at: null,
command_at: null, command_at: null,
execution_result_at: null, execution_result_at: null,
outcome_observed_at: null,
decision: null, decision: null,
command: null, command: null,
execution: null, execution: null,
outcome: null,
}); });
} }
return rowsByKey.get(key); return rowsByKey.get(key);
@ -846,12 +878,19 @@ function mergeLifecycleEvidence(row, next) {
if (next?.decision) row.decision = next.decision; if (next?.decision) row.decision = next.decision;
if (next?.command) row.command = next.command; if (next?.command) row.command = next.command;
if (next?.execution) row.execution = next.execution; if (next?.execution) row.execution = next.execution;
if (next?.outcome) row.outcome = next.outcome;
} }
function finalizeLifecycleRow(row) { function finalizeLifecycleRow(row) {
const decision = row.decision || null; const decision = row.decision || null;
const execution = row.execution || null; const execution = row.execution || null;
const outcomeStatus = normalizeLifecycleToken(execution?.outcome_status || execution?.outcome_reason || null); const outcome = row.outcome || null;
const outcomeStatus = normalizeLifecycleToken(
outcome?.outcome_status
|| execution?.outcome_status
|| execution?.outcome_reason
|| null,
);
let lifecycle_state = 'observed'; let lifecycle_state = 'observed';
let lifecycle_label = 'Observed'; let lifecycle_label = 'Observed';
let reason_code = 'reason_unknown'; let reason_code = 'reason_unknown';
@ -860,13 +899,28 @@ function finalizeLifecycleRow(row) {
if (outcomeStatus && COMPLETED_OUTCOME_STATUSES.has(outcomeStatus)) { if (outcomeStatus && COMPLETED_OUTCOME_STATUSES.has(outcomeStatus)) {
lifecycle_state = 'completed'; lifecycle_state = 'completed';
lifecycle_label = 'Completed'; lifecycle_label = 'Completed';
reason_code = normalizeLifecycleToken(execution?.outcome_reason || execution?.result_code || 'completed'); reason_code = normalizeLifecycleToken(
reason_text = humanizeReasonCode(reason_code, 'Completed'); outcome?.outcome_reason
|| execution?.outcome_reason
|| execution?.result_code
|| 'completed',
);
reason_text = buildCompletedOutcomeText({ outcome, reasonCode: reason_code });
} else if (outcomeStatus && NOT_FILLED_OUTCOME_STATUSES.has(outcomeStatus)) { } else if (outcomeStatus && NOT_FILLED_OUTCOME_STATUSES.has(outcomeStatus)) {
lifecycle_state = 'not_filled'; lifecycle_state = 'not_filled';
lifecycle_label = 'Not filled'; lifecycle_label = 'Not filled';
reason_code = normalizeLifecycleToken(execution?.outcome_reason || execution?.result_code || outcomeStatus); reason_code = normalizeLifecycleToken(
reason_text = humanizeReasonCode(reason_code, 'Not filled'); outcome?.outcome_reason
|| execution?.outcome_reason
|| execution?.result_code
|| outcomeStatus,
);
reason_text = buildNotFilledText({ outcome, reasonCode: reason_code });
} else if (outcomeStatus === 'awaiting_outcome') {
lifecycle_state = 'awaiting_outcome';
lifecycle_label = 'Awaiting outcome';
reason_code = normalizeLifecycleToken(outcome?.outcome_reason || 'awaiting_outcome');
reason_text = humanizeReasonCode(reason_code, 'No durable venue outcome is recorded yet.');
} else if (execution?.status === 'submitted') { } else if (execution?.status === 'submitted') {
lifecycle_state = 'submitted'; lifecycle_state = 'submitted';
lifecycle_label = 'Submitted'; lifecycle_label = 'Submitted';
@ -907,7 +961,8 @@ function finalizeLifecycleRow(row) {
reason_code, reason_code,
reason_text, reason_text,
latest_stage_at: latest_stage_at:
row.execution_result_at row.outcome_observed_at
|| row.execution_result_at
|| row.command_at || row.command_at
|| row.decision_at || row.decision_at
|| row.quote_observed_at || row.quote_observed_at
@ -922,10 +977,45 @@ function finalizeLifecycleRow(row) {
execution_status: execution?.status || null, execution_status: execution?.status || null,
execution_result_code: execution?.result_code || null, execution_result_code: execution?.result_code || null,
execution_outcome_status: execution?.outcome_status || null, execution_outcome_status: execution?.outcome_status || null,
durable_outcome_status: outcome?.outcome_status || null,
durable_outcome_source: outcome?.outcome_source || null,
attribution_status: outcome?.attribution_status || null,
attribution_method: outcome?.attribution_method || null,
}, },
outcome_source: outcome?.outcome_source || null,
outcome_status: outcome?.outcome_status || execution?.outcome_status || null,
attribution_status: outcome?.attribution_status || execution?.attribution_status || null,
attribution_method: outcome?.attribution_method || execution?.attribution_method || null,
attributed_inventory_delta:
outcome?.attributed_inventory_delta
|| execution?.attributed_inventory_delta
|| null,
has_settlement_evidence: hasSettlementEvidence(outcome || execution),
}; };
} }
function buildCompletedOutcomeText({ outcome, reasonCode }) {
const base = humanizeReasonCode(reasonCode, 'Completed.');
if (!outcome?.attribution_status) return `${base} Settlement attribution is not stored.`;
if (outcome.attribution_status === 'heuristic_match') {
return `${base} Matched to inventory movement by exact asset-unit delta; venue terminal status is not stored.`;
}
return base;
}
function buildNotFilledText({ outcome, reasonCode }) {
const base = humanizeReasonCode(reasonCode, 'Not filled.');
const uncertainty = outcome?.evidence?.uncertainty;
return uncertainty ? `${base} ${uncertainty}` : base;
}
function hasSettlementEvidence(outcome) {
return Boolean(
outcome?.attributed_inventory_delta
&& TERMINAL_SETTLEMENT_ATTRIBUTION_STATUSES.has(outcome.attribution_status),
);
}
function buildExecutionFailureText(execution, reasonCode) { function buildExecutionFailureText(execution, reasonCode) {
const base = humanizeReasonCode(reasonCode, 'Submission failed.'); const base = humanizeReasonCode(reasonCode, 'Submission failed.');
if (execution?.error_message) return `${base} ${execution.error_message}`; if (execution?.error_message) return `${base} ${execution.error_message}`;
@ -968,12 +1058,14 @@ function normalizeCommand(command) {
} }
function buildStrategySummary({ function buildStrategySummary({
config,
servicesByName, servicesByName,
activeAlerts, activeAlerts,
recentQuotes = [], recentQuotes = [],
recentTradeDecisions = [], recentTradeDecisions = [],
recentExecuteTradeCommands = [], recentExecuteTradeCommands = [],
recentExecutionResults = [], recentExecutionResults = [],
recentQuoteOutcomes = [],
}) { }) {
const strategyState = servicesByName['strategy-engine']?.state || {}; const strategyState = servicesByName['strategy-engine']?.state || {};
const executorState = servicesByName['trade-executor']?.state || {}; const executorState = servicesByName['trade-executor']?.state || {};
@ -1009,14 +1101,16 @@ function buildStrategySummary({
|| durableDecisionsById.get(strategyState.latest_decision?.decision_id)?.decision_at || durableDecisionsById.get(strategyState.latest_decision?.decision_id)?.decision_at
|| null, || null,
}); });
const lifecycleRows = deriveQuoteLifecycleRows({ const allLifecycleRows = deriveQuoteLifecycleRows({
recentQuotes, recentQuotes,
recentTradeDecisions, recentTradeDecisions,
recentExecuteTradeCommands, recentExecuteTradeCommands,
recentExecutionResults, recentExecutionResults,
limit: 20, recentQuoteOutcomes,
}); limit: null,
const tradeFunnel = buildTradeFunnelSummary(lifecycleRows); }).map((row) => enrichLifecycleRowForUi({ config, row }));
const lifecycleRows = allLifecycleRows.slice(0, 20);
const tradeFunnel = buildTradeFunnelSummary(allLifecycleRows);
return { return {
strategy_state: { strategy_state: {
@ -1038,7 +1132,7 @@ function buildStrategySummary({
paused: executorState.paused ?? null, paused: executorState.paused ?? null,
draining: executorState.draining ?? null, draining: executorState.draining ?? null,
in_flight_count: executorState.in_flight_count ?? 0, in_flight_count: executorState.in_flight_count ?? 0,
completed_count: executorState.completed_count ?? 0, submitted_count: executorState.submitted_count ?? executorState.completed_count ?? 0,
last_command: executorState.last_command || null, last_command: executorState.last_command || null,
last_venue_response: executorState.last_venue_response || null, last_venue_response: executorState.last_venue_response || null,
last_error: executorState.last_error || null, last_error: executorState.last_error || null,
@ -1076,7 +1170,7 @@ function buildTradeFunnelSummary(lifecycleRows = []) {
counts[row.lifecycle_state] += 1; counts[row.lifecycle_state] += 1;
} }
if (row.lifecycle_state === 'completed') { if (row.lifecycle_state === 'completed' && row.has_settlement_evidence) {
successfulTrades.push(row); successfulTrades.push(row);
} else if (['submitted', 'awaiting_outcome'].includes(row.lifecycle_state)) { } else if (['submitted', 'awaiting_outcome'].includes(row.lifecycle_state)) {
unresolvedSubmissions.push(row); unresolvedSubmissions.push(row);
@ -1123,9 +1217,12 @@ function buildSystemSummary({ servicesByName, activeAlerts, recentAlerts }) {
last_alert_write_at: historyWriterState.last_alert_write_at || null, last_alert_write_at: historyWriterState.last_alert_write_at || null,
last_funding_observation_write_at: historyWriterState.last_funding_observation_write_at || null, last_funding_observation_write_at: historyWriterState.last_funding_observation_write_at || null,
last_metrics_at: historyWriterState.last_metrics_at || null, last_metrics_at: historyWriterState.last_metrics_at || null,
last_quote_outcomes_at: historyWriterState.last_quote_outcomes_at || null,
latest_portfolio_metrics: historyWriterState.latest_portfolio_metrics || null, latest_portfolio_metrics: historyWriterState.latest_portfolio_metrics || null,
latest_quote_outcomes: historyWriterState.latest_quote_outcomes || null,
offsets: historyWriterState.offsets || {}, offsets: historyWriterState.offsets || {},
metrics_error: historyWriterState.metrics_error || null, metrics_error: historyWriterState.metrics_error || null,
quote_outcomes_error: historyWriterState.quote_outcomes_error || null,
}, },
controls: listDashboardControls({ page: 'system' }), controls: listDashboardControls({ page: 'system' }),
}; };
@ -1192,6 +1289,7 @@ function buildServiceSummary(service, state) {
return { return {
last_write_at: state.last_write_at || null, last_write_at: state.last_write_at || null,
last_alert_write_at: state.last_alert_write_at || null, last_alert_write_at: state.last_alert_write_at || null,
last_quote_outcomes_at: state.last_quote_outcomes_at || null,
database_connectivity: state.database_connectivity ?? null, database_connectivity: state.database_connectivity ?? null,
}; };
case 'ops-sentinel': case 'ops-sentinel':
@ -1210,7 +1308,7 @@ function buildServiceSummary(service, state) {
case 'trade-executor': case 'trade-executor':
return { return {
in_flight_count: state.in_flight_count ?? 0, in_flight_count: state.in_flight_count ?? 0,
completed_count: state.completed_count ?? 0, submitted_count: state.submitted_count ?? state.completed_count ?? 0,
signer_registered: state.signer_registered ?? null, signer_registered: state.signer_registered ?? null,
relay_connected: state.relay?.connected ?? null, relay_connected: state.relay?.connected ?? null,
relay_last_message_at: state.relay?.last_message_at || null, relay_last_message_at: state.relay?.last_message_at || null,
@ -1244,6 +1342,54 @@ function normalizeTradeForUi({ config, trade }) {
}; };
} }
function enrichLifecycleRowForUi({ config, row }) {
return {
...row,
settlement_summary: buildSettlementSummary({
config,
delta: row.attributed_inventory_delta,
attributionStatus: row.attribution_status,
attributionMethod: row.attribution_method,
}),
};
}
function buildSettlementSummary({ config, delta, attributionStatus, attributionMethod }) {
if (!delta?.delta_units) {
return {
status: attributionStatus || 'unattributed',
method: attributionMethod || null,
lines: [],
text: attributionStatus === 'ambiguous'
? 'Inventory movement is ambiguous and is not assigned to this quote.'
: 'No settled inventory delta is linked to this quote.',
};
}
const lines = Object.entries(delta.delta_units).map(([assetId, units]) => {
const asset = config.assetRegistry.get(assetId);
const symbol = asset?.symbol || assetId;
const formatted = formatUnits(units, asset?.decimals || 0);
const signed = BigInt(String(units || '0')) > 0n ? `+${formatted}` : formatted;
return {
asset_id: assetId,
symbol,
units,
amount: signed,
};
});
return {
status: attributionStatus || 'unattributed',
method: attributionMethod || null,
observed_at: delta.observed_at || null,
previous_observed_at: delta.previous_observed_at || null,
lines,
text: lines.map((line) => `${line.amount} ${line.symbol}`).join(', '),
caveat: delta.uncertainty || null,
};
}
function buildRecentDepositItems({ config, recentDepositStatuses, liquidityState }) { function buildRecentDepositItems({ config, recentDepositStatuses, liquidityState }) {
const recentItems = (recentDepositStatuses || []).map((entry) => normalizeDepositStatusForUi({ const recentItems = (recentDepositStatuses || []).map((entry) => normalizeDepositStatusForUi({
config, config,

View file

@ -5,6 +5,7 @@ export function computePortfolioMetric({
baseline = null, baseline = null,
currentInventory, currentInventory,
currentPrice, currentPrice,
externalFlows = [],
btcAsset, btcAsset,
eureAsset, eureAsset,
commandCount = 0, commandCount = 0,
@ -23,7 +24,7 @@ export function computePortfolioMetric({
const currentPortfolioValue = currentEure + currentBtcMarkValue; const currentPortfolioValue = currentEure + currentBtcMarkValue;
const payload = { const payload = {
metric_version: 1, metric_version: 2,
baseline_status: baseline ? 'active' : 'awaiting_first_execution', baseline_status: baseline ? 'active' : 'awaiting_first_execution',
command_count: commandCount, command_count: commandCount,
result_count: resultCount, result_count: resultCount,
@ -40,12 +41,27 @@ export function computePortfolioMetric({
current_portfolio_value_eure: formatScaledDecimal(currentPortfolioValue), current_portfolio_value_eure: formatScaledDecimal(currentPortfolioValue),
current_btc_mark_value_eure: formatScaledDecimal(currentBtcMarkValue), current_btc_mark_value_eure: formatScaledDecimal(currentBtcMarkValue),
current_eure_cash_value_eure: formatScaledDecimal(currentEure), current_eure_cash_value_eure: formatScaledDecimal(currentEure),
portfolio_vs_simple_hold_eure: null,
trade_pnl_eure: null, trade_pnl_eure: null,
mark_to_market_pnl_eure: null, mark_to_market_pnl_eure: null,
price_move_pnl_eure: null, price_move_pnl_eure: null,
baseline_portfolio_value_eure_at_baseline_price: null, baseline_portfolio_value_eure_at_baseline_price: null,
baseline_portfolio_value_eure_at_current_price: null, baseline_portfolio_value_eure_at_current_price: null,
current_portfolio_value_eure_at_baseline_price: null, current_portfolio_value_eure_at_baseline_price: null,
initial_baseline_portfolio_value_eure_at_baseline_price: null,
initial_baseline_portfolio_value_eure_at_current_price: null,
external_cash_flows: {
flow_count: 0,
deposit_count: 0,
withdrawal_count: 0,
latest_effective_at: null,
net_btc_units: '0',
net_btc: '0',
net_eure_units: '0',
net_eure: '0',
net_value_eure_at_flow_time: '0',
net_value_eure_at_current_price: '0',
},
inventory_delta: null, inventory_delta: null,
baseline: null, baseline: null,
}; };
@ -62,22 +78,57 @@ export function computePortfolioMetric({
const baselinePortfolioAtBaselinePrice = baselineEure + multiplyScaled(baselineBtc, baselinePriceScaled); const baselinePortfolioAtBaselinePrice = baselineEure + multiplyScaled(baselineBtc, baselinePriceScaled);
const baselinePortfolioAtCurrentPrice = baselineEure + multiplyScaled(baselineBtc, currentPriceScaled); const baselinePortfolioAtCurrentPrice = baselineEure + multiplyScaled(baselineBtc, currentPriceScaled);
const currentPortfolioAtBaselinePrice = currentEure + multiplyScaled(currentBtc, baselinePriceScaled); const currentPortfolioAtBaselinePrice = currentEure + multiplyScaled(currentBtc, baselinePriceScaled);
const tradePnl = currentPortfolioAtBaselinePrice - baselinePortfolioAtBaselinePrice; const externalFlowSummary = summarizeExternalFlows({
const markToMarketPnl = currentPortfolioValue - baselinePortfolioAtCurrentPrice; externalFlows,
const priceMovePnl = markToMarketPnl - tradePnl; currentPriceScaled,
btcAsset,
eureAsset,
});
const fundedPortfolioAtFlowTime = baselinePortfolioAtBaselinePrice
+ externalFlowSummary.netValueEureAtFlowTime;
const simpleHoldAtCurrentPrice = baselinePortfolioAtCurrentPrice
+ externalFlowSummary.netValueEureAtCurrentPrice;
const portfolioVsSimpleHold = currentPortfolioValue - simpleHoldAtCurrentPrice;
const markToMarketPnl = currentPortfolioValue - fundedPortfolioAtFlowTime;
const priceMovePnl = simpleHoldAtCurrentPrice - fundedPortfolioAtFlowTime;
payload.trade_pnl_eure = formatScaledDecimal(tradePnl); payload.portfolio_vs_simple_hold_eure = formatScaledDecimal(portfolioVsSimpleHold);
payload.trade_pnl_eure = null;
payload.mark_to_market_pnl_eure = formatScaledDecimal(markToMarketPnl); payload.mark_to_market_pnl_eure = formatScaledDecimal(markToMarketPnl);
payload.price_move_pnl_eure = formatScaledDecimal(priceMovePnl); payload.price_move_pnl_eure = formatScaledDecimal(priceMovePnl);
payload.baseline_portfolio_value_eure_at_baseline_price = formatScaledDecimal( payload.baseline_portfolio_value_eure_at_baseline_price = formatScaledDecimal(
baselinePortfolioAtBaselinePrice, fundedPortfolioAtFlowTime,
); );
payload.baseline_portfolio_value_eure_at_current_price = formatScaledDecimal( payload.baseline_portfolio_value_eure_at_current_price = formatScaledDecimal(
baselinePortfolioAtCurrentPrice, simpleHoldAtCurrentPrice,
); );
payload.current_portfolio_value_eure_at_baseline_price = formatScaledDecimal( payload.current_portfolio_value_eure_at_baseline_price = formatScaledDecimal(
currentPortfolioAtBaselinePrice, currentPortfolioAtBaselinePrice,
); );
payload.initial_baseline_portfolio_value_eure_at_baseline_price = formatScaledDecimal(
baselinePortfolioAtBaselinePrice,
);
payload.initial_baseline_portfolio_value_eure_at_current_price = formatScaledDecimal(
baselinePortfolioAtCurrentPrice,
);
payload.external_cash_flows = {
flow_count: externalFlowSummary.flowCount,
deposit_count: externalFlowSummary.depositCount,
withdrawal_count: externalFlowSummary.withdrawalCount,
latest_effective_at: externalFlowSummary.latestEffectiveAt,
net_btc_units: externalFlowSummary.netBtcUnits.toString(),
net_btc: formatScaledDecimal(unitsToScaledDecimal(
externalFlowSummary.netBtcUnits.toString(),
btcAsset.decimals,
)),
net_eure_units: externalFlowSummary.netEureUnits.toString(),
net_eure: formatScaledDecimal(unitsToScaledDecimal(
externalFlowSummary.netEureUnits.toString(),
eureAsset.decimals,
)),
net_value_eure_at_flow_time: formatScaledDecimal(externalFlowSummary.netValueEureAtFlowTime),
net_value_eure_at_current_price: formatScaledDecimal(externalFlowSummary.netValueEureAtCurrentPrice),
};
payload.inventory_delta = { payload.inventory_delta = {
btc_units: (BigInt(currentBtcUnits) - BigInt(baselineBtcUnits)).toString(), btc_units: (BigInt(currentBtcUnits) - BigInt(baselineBtcUnits)).toString(),
btc: formatScaledDecimal(currentBtc - baselineBtc), btc: formatScaledDecimal(currentBtc - baselineBtc),
@ -126,6 +177,62 @@ function buildInventoryView({ inventory, btcAsset, eureAsset }) {
}; };
} }
function summarizeExternalFlows({
externalFlows,
currentPriceScaled,
btcAsset,
eureAsset,
}) {
let flowCount = 0;
let depositCount = 0;
let withdrawalCount = 0;
let latestEffectiveAt = null;
let netBtcUnits = 0n;
let netEureUnits = 0n;
let netValueEureAtFlowTime = 0n;
let netValueEureAtCurrentPrice = 0n;
for (const flow of externalFlows || []) {
if (!flow?.asset_id || !flow?.signed_units) continue;
const signedUnits = BigInt(flow.signed_units);
if (signedUnits === 0n) continue;
flowCount += 1;
if (flow.kind === 'deposit') depositCount += 1;
if (flow.kind === 'withdrawal') withdrawalCount += 1;
if (timestampValue(flow.effective_at) > timestampValue(latestEffectiveAt)) {
latestEffectiveAt = flow.effective_at || null;
}
if (flow.asset_id === btcAsset.assetId) {
netBtcUnits += signedUnits;
const btcAmount = unitsToScaledDecimal(signedUnits.toString(), btcAsset.decimals);
const flowPriceScaled = parseScaledDecimal(
flow.reference_price_eure_per_btc_at_flow_time || '0',
);
netValueEureAtFlowTime += multiplyScaled(btcAmount, flowPriceScaled);
netValueEureAtCurrentPrice += multiplyScaled(btcAmount, currentPriceScaled);
} else if (flow.asset_id === eureAsset.assetId) {
netEureUnits += signedUnits;
const eureAmount = unitsToScaledDecimal(signedUnits.toString(), eureAsset.decimals);
netValueEureAtFlowTime += eureAmount;
netValueEureAtCurrentPrice += eureAmount;
}
}
return {
flowCount,
depositCount,
withdrawalCount,
latestEffectiveAt,
netBtcUnits,
netEureUnits,
netValueEureAtFlowTime,
netValueEureAtCurrentPrice,
};
}
function unitsToScaledDecimal(units, decimals) { function unitsToScaledDecimal(units, decimals) {
return BigInt(units || '0') * 10n ** BigInt(VALUE_SCALE - decimals); return BigInt(units || '0') * 10n ** BigInt(VALUE_SCALE - decimals);
} }
@ -160,3 +267,8 @@ function formatScaledDecimal(value) {
const fractionalText = fractional.toString().padStart(VALUE_SCALE, '0').replace(/0+$/, ''); const fractionalText = fractional.toString().padStart(VALUE_SCALE, '0').replace(/0+$/, '');
return `${negative ? '-' : ''}${whole}.${fractionalText}`; return `${negative ? '-' : ''}${whole}.${fractionalText}`;
} }
function timestampValue(value) {
const parsed = Date.parse(value || '');
return Number.isFinite(parsed) ? parsed : -Infinity;
}

495
src/core/quote-outcomes.mjs Normal file
View file

@ -0,0 +1,495 @@
const DEFAULT_ATTRIBUTION_WINDOW_MS = 10 * 60 * 1000;
const DEFAULT_SETTLEMENT_GRACE_MS = 60 * 1000;
export const TERMINAL_SETTLEMENT_ATTRIBUTION_STATUSES = new Set([
'heuristic_match',
'linked_settlement',
]);
export function deriveQuoteOutcomeRecords({
submissions = [],
commands = [],
decisions = [],
inventorySnapshots = [],
btcAsset,
eureAsset,
now = Date.now(),
attributionWindowMs = DEFAULT_ATTRIBUTION_WINDOW_MS,
settlementGraceMs = DEFAULT_SETTLEMENT_GRACE_MS,
} = {}) {
const activeAssetIds = [btcAsset?.assetId, eureAsset?.assetId].filter(Boolean);
const normalizedSubmissions = submissions
.map(normalizeSubmission)
.filter((entry) => entry?.quote_id && entry.status === 'submitted');
const commandsByQuote = new Map(
commands
.map(normalizeCommand)
.filter((entry) => entry?.quote_id)
.map((entry) => [entry.quote_id, entry]),
);
const decisionsByQuote = new Map(
decisions
.map(normalizeDecision)
.filter((entry) => entry?.quote_id)
.map((entry) => [entry.quote_id, entry]),
);
const inventoryDeltas = deriveInventoryDeltas({
inventorySnapshots,
activeAssetIds,
});
const latestInventoryAt = inventoryDeltas.length
? inventoryDeltas[inventoryDeltas.length - 1].observed_at
: latestSnapshotTimestamp(inventorySnapshots);
const candidatesByMovement = new Map();
const candidatesByQuote = new Map();
for (const submission of normalizedSubmissions) {
const command = commandsByQuote.get(submission.quote_id) || null;
const expectedDelta = buildExpectedMakerDeltas(command);
if (!expectedDelta) continue;
const matches = inventoryDeltas.filter((movement) => (
movementMatchesExpectedDelta({
movement,
expectedDelta,
submittedAt: submission.submitted_at,
attributionWindowMs,
})
));
if (matches.length) candidatesByQuote.set(submission.quote_id, matches);
for (const movement of matches) {
const existing = candidatesByMovement.get(movement.movement_id) || [];
existing.push(submission.quote_id);
candidatesByMovement.set(movement.movement_id, existing);
}
}
return normalizedSubmissions.map((submission) => {
const command = commandsByQuote.get(submission.quote_id) || null;
const decision = decisionsByQuote.get(submission.quote_id) || null;
const matches = candidatesByQuote.get(submission.quote_id) || [];
const uniqueMatch = matches.length === 1
&& (candidatesByMovement.get(matches[0].movement_id) || []).length === 1
? matches[0]
: null;
if (uniqueMatch) {
return buildCompletedOutcomeRecord({
submission,
command,
decision,
movement: uniqueMatch,
attributionWindowMs,
});
}
if (matches.length > 0) {
return buildAmbiguousOutcomeRecord({
submission,
command,
decision,
matches,
});
}
const expiredWindow = getExpiredSettlementWindow({
submission,
command,
now,
latestInventoryAt,
settlementGraceMs,
});
if (expiredWindow) {
return buildNotFilledOutcomeRecord({
submission,
command,
decision,
settlementGraceMs,
expiredAt: expiredWindow.expiresAt,
latestInventoryAt: expiredWindow.latestInventoryAt,
});
}
return buildSubmittedOutcomeRecord({
submission,
command,
decision,
});
});
}
export function deriveInventoryDeltas({ inventorySnapshots = [], activeAssetIds = [] } = {}) {
const sortedSnapshots = inventorySnapshots
.map(normalizeInventorySnapshot)
.filter((entry) => entry?.observed_at)
.sort((left, right) => timestampValue(left.observed_at) - timestampValue(right.observed_at));
const deltas = [];
for (let index = 1; index < sortedSnapshots.length; index += 1) {
const previous = sortedSnapshots[index - 1];
const current = sortedSnapshots[index];
const delta_units = {};
let changed = false;
for (const assetId of activeAssetIds) {
const currentUnits = safeBigInt(current.spendable?.[assetId]);
const previousUnits = safeBigInt(previous.spendable?.[assetId]);
const delta = currentUnits - previousUnits;
delta_units[assetId] = delta.toString();
if (delta !== 0n) changed = true;
}
if (!changed) continue;
deltas.push({
movement_id: `${previous.observed_at}->${current.observed_at}`,
observed_at: current.observed_at,
previous_observed_at: previous.observed_at,
inventory_id: current.inventory_id,
previous_inventory_id: previous.inventory_id,
delta_units,
});
}
return deltas;
}
function buildCompletedOutcomeRecord({
submission,
command,
decision,
movement,
attributionWindowMs,
}) {
return baseOutcomeRecord({
submission,
command,
decision,
outcome_status: 'completed',
outcome_observed_at: movement.observed_at,
outcome_source: 'intent_inventory_spendable_delta',
outcome_reason: 'matched_inventory_delta',
attribution_status: 'heuristic_match',
attribution_method: 'exact_asset_delta_within_window',
attributed_inventory_delta: {
inventory_id: movement.inventory_id,
previous_inventory_id: movement.previous_inventory_id,
observed_at: movement.observed_at,
previous_observed_at: movement.previous_observed_at,
delta_units: movement.delta_units,
attribution_window_ms: attributionWindowMs,
uncertainty:
'Matched by exact asset-unit delta after submission; no venue terminal event is stored.',
},
evidence: {
settlement_movement_id: movement.movement_id,
settlement_source: 'intent_inventory_snapshots',
},
});
}
function buildAmbiguousOutcomeRecord({
submission,
command,
decision,
matches,
}) {
return baseOutcomeRecord({
submission,
command,
decision,
outcome_status: 'awaiting_outcome',
outcome_observed_at: submission.submitted_at,
outcome_source: 'submission_and_inventory_snapshots',
outcome_reason: 'ambiguous_inventory_delta_match',
attribution_status: 'ambiguous',
attribution_method: null,
attributed_inventory_delta: null,
evidence: {
candidate_movement_count: matches.length,
candidate_movement_ids: matches.map((entry) => entry.movement_id),
},
});
}
function buildNotFilledOutcomeRecord({
submission,
command,
decision,
settlementGraceMs,
expiredAt,
latestInventoryAt,
}) {
return baseOutcomeRecord({
submission,
command,
decision,
outcome_status: 'not_filled',
outcome_observed_at: latestInventoryAt || expiredAt || submission.submitted_at,
outcome_source: 'submission_deadline_and_inventory_snapshots',
outcome_reason: 'deadline_elapsed_without_settlement',
attribution_status: 'unattributed',
attribution_method: null,
attributed_inventory_delta: null,
evidence: {
min_deadline_ms: command?.min_deadline_ms || null,
settlement_grace_ms: settlementGraceMs,
settlement_window_expired_at: expiredAt || null,
latest_inventory_observed_at: latestInventoryAt || null,
uncertainty:
'No matching inventory delta was observed after the quote-response deadline; no venue terminal event is stored.',
},
});
}
function buildSubmittedOutcomeRecord({
submission,
command,
decision,
}) {
return baseOutcomeRecord({
submission,
command,
decision,
outcome_status: 'submitted',
outcome_observed_at: submission.submitted_at,
outcome_source: 'executor_submission_result',
outcome_reason: submission.result_code || 'quote_response_submitted',
attribution_status: 'unattributed',
attribution_method: null,
attributed_inventory_delta: null,
evidence: {
uncertainty: 'Quote response was accepted or acknowledged; no settlement evidence is linked yet.',
},
});
}
function baseOutcomeRecord({
submission,
command,
decision,
outcome_status,
outcome_observed_at,
outcome_source,
outcome_reason,
attribution_status,
attribution_method,
attributed_inventory_delta,
evidence,
}) {
return {
quote_id: submission.quote_id,
decision_id: command?.decision_id || submission.decision_id || decision?.decision_id || null,
command_id: command?.command_id || submission.command_id || null,
execution_result_status: submission.status,
execution_result_code: submission.result_code || null,
submitted_at: submission.submitted_at,
command_at: command?.command_at || null,
outcome_status,
outcome_observed_at,
outcome_source,
outcome_reason,
attribution_status,
attribution_method,
attributed_inventory_delta,
payload: {
quote_id: submission.quote_id,
decision_id: command?.decision_id || submission.decision_id || decision?.decision_id || null,
command_id: command?.command_id || submission.command_id || null,
pair: command?.pair || decision?.pair || submission.pair || null,
direction: decision?.direction || command?.direction || null,
request_kind: command?.request_kind || decision?.request_kind || null,
gross_edge_pct: decision?.gross_edge_pct || null,
eure_notional: decision?.eure_notional || null,
execution_result_status: submission.status,
execution_result_code: submission.result_code || null,
submitted_at: submission.submitted_at,
command_at: command?.command_at || null,
outcome_status,
outcome_observed_at,
outcome_source,
outcome_reason,
attribution_status,
attribution_method,
attributed_inventory_delta,
evidence: {
submission_event_id: submission.event_id || null,
command_id: command?.command_id || submission.command_id || null,
decision_id: command?.decision_id || submission.decision_id || decision?.decision_id || null,
...evidence,
},
},
};
}
function movementMatchesExpectedDelta({
movement,
expectedDelta,
submittedAt,
attributionWindowMs,
}) {
const submittedTs = timestampValue(submittedAt);
const movementTs = timestampValue(movement.observed_at);
if (!Number.isFinite(submittedTs) || !Number.isFinite(movementTs)) return false;
if (movementTs < submittedTs) return false;
if (movementTs - submittedTs > attributionWindowMs) return false;
for (const [assetId, expected] of Object.entries(expectedDelta)) {
if (safeBigInt(movement.delta_units?.[assetId]) !== expected) return false;
}
return true;
}
function getExpiredSettlementWindow({
submission,
command,
now,
latestInventoryAt,
settlementGraceMs,
}) {
const submittedTs = timestampValue(submission.submitted_at);
if (!Number.isFinite(submittedTs)) return null;
const deadlineMs = Number(command?.min_deadline_ms || 60_000);
const expiresAt = submittedTs
+ (Number.isFinite(deadlineMs) && deadlineMs > 0 ? deadlineMs : 60_000)
+ settlementGraceMs;
const nowTs = typeof now === 'number' ? now : timestampValue(now);
const latestInventoryTs = timestampValue(latestInventoryAt);
const expired = Number.isFinite(nowTs)
&& nowTs >= expiresAt
&& Number.isFinite(latestInventoryTs)
&& latestInventoryTs >= expiresAt;
if (!expired) return null;
return {
expiresAt: new Date(expiresAt).toISOString(),
latestInventoryAt: toIsoTimestamp(latestInventoryAt),
};
}
function buildExpectedMakerDeltas(command) {
if (!command?.asset_in || !command?.asset_out || !command?.request_kind) return null;
const receiveAmount = command.request_kind === 'exact_in'
? command.amount_in
: command.quote_output?.amount_in || command.proposed_amount_in;
const sendAmount = command.request_kind === 'exact_in'
? command.quote_output?.amount_out || command.proposed_amount_out
: command.amount_out;
if (receiveAmount == null || sendAmount == null) return null;
return {
[command.asset_in]: safeBigInt(receiveAmount),
[command.asset_out]: -safeBigInt(sendAmount),
};
}
function normalizeSubmission(entry) {
const payload = payloadOf(entry);
if (!payload) return null;
return {
event_id: entry?.event_id || payload.event_id || null,
quote_id: payload.quote_id || entry?.quote_id || null,
command_id: payload.command_id || null,
decision_id: payload.decision_id || null,
pair: payload.pair || null,
status: payload.status || null,
result_code: payload.result_code || null,
submitted_at: toIsoTimestamp(
entry?.observed_at
|| entry?.ingested_at
|| payload.observed_at
|| payload.ingested_at
|| payload.submitted_at,
),
};
}
function normalizeCommand(entry) {
const payload = payloadOf(entry);
if (!payload) return null;
return {
command_id: payload.command_id || entry?.command_id || null,
decision_id: payload.decision_id || entry?.decision_id || null,
quote_id: payload.quote_id || entry?.quote_id || null,
pair: payload.pair || null,
direction: payload.direction || null,
request_kind: payload.request_kind || null,
asset_in: payload.asset_in || null,
asset_out: payload.asset_out || null,
amount_in: payload.amount_in ?? null,
amount_out: payload.amount_out ?? null,
quote_output: payload.quote_output || {},
proposed_amount_in: payload.proposed_amount_in ?? null,
proposed_amount_out: payload.proposed_amount_out ?? null,
min_deadline_ms: payload.min_deadline_ms ?? null,
command_at: toIsoTimestamp(
entry?.observed_at
|| entry?.ingested_at
|| payload.observed_at
|| payload.ingested_at
|| payload.command_at,
),
};
}
function normalizeDecision(entry) {
const payload = payloadOf(entry);
if (!payload) return null;
return {
decision_id: payload.decision_id || entry?.decision_id || null,
quote_id: payload.quote_id || entry?.quote_id || null,
pair: payload.pair || null,
direction: payload.direction || null,
request_kind: payload.request_kind || null,
gross_edge_pct: payload.gross_edge_pct || null,
eure_notional: payload.eure_notional || null,
};
}
function normalizeInventorySnapshot(entry) {
const payload = payloadOf(entry);
if (!payload?.spendable) return null;
return {
inventory_id: payload.inventory_id || null,
observed_at: toIsoTimestamp(
entry?.observed_at
|| entry?.ingested_at
|| payload.observed_at
|| payload.synced_at,
),
spendable: payload.spendable || {},
};
}
function latestSnapshotTimestamp(inventorySnapshots) {
const timestamps = inventorySnapshots
.map((entry) => normalizeInventorySnapshot(entry)?.observed_at)
.filter(Boolean)
.sort((left, right) => timestampValue(right) - timestampValue(left));
return timestamps[0] || null;
}
function payloadOf(entry) {
if (!entry) return null;
return entry.payload || entry;
}
function safeBigInt(value) {
if (value == null || value === '') return 0n;
return BigInt(String(value));
}
function toIsoTimestamp(value) {
if (!value) return null;
const date = new Date(value);
return Number.isNaN(date.getTime()) ? null : date.toISOString();
}
function timestampValue(value) {
const parsed = Date.parse(value || '');
return Number.isFinite(parsed) ? parsed : NaN;
}

View file

@ -62,7 +62,7 @@ const DEFAULTS = {
'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=eur', 'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=eur',
inventorySyncRefreshMs: 15_000, inventorySyncRefreshMs: 15_000,
liquidityRefreshMs: 30_000, liquidityRefreshMs: 30_000,
strategyGrossThresholdPct: 2, strategyGrossThresholdPct: 1.49,
strategyInitialArmed: false, strategyInitialArmed: false,
strategyMaxNotionalEure: 5, strategyMaxNotionalEure: 5,
strategyPriceMaxAgeMs: 30_000, strategyPriceMaxAgeMs: 30_000,

View file

@ -1,5 +1,7 @@
import { Pool } from 'pg'; import { Pool } from 'pg';
import { deriveQuoteOutcomeRecords } from '../core/quote-outcomes.mjs';
const TABLES = [ const TABLES = [
'raw_near_intents_quotes', 'raw_near_intents_quotes',
'swap_demand_events', 'swap_demand_events',
@ -14,6 +16,7 @@ const TABLES = [
]; ];
const PORTFOLIO_METRICS_TABLE = 'portfolio_metrics_snapshots'; const PORTFOLIO_METRICS_TABLE = 'portfolio_metrics_snapshots';
const QUOTE_OUTCOMES_TABLE = 'quote_outcome_attributions';
const CREDITED_LIQUIDITY_STATUSES = ['CREDITED', 'COMPLETED', 'FINALIZED', 'SETTLED']; const CREDITED_LIQUIDITY_STATUSES = ['CREDITED', 'COMPLETED', 'FINALIZED', 'SETTLED'];
const COMPLETED_WITHDRAWAL_STATUSES = ['COMPLETED', 'FINALIZED', 'SETTLED']; const COMPLETED_WITHDRAWAL_STATUSES = ['COMPLETED', 'FINALIZED', 'SETTLED'];
@ -109,6 +112,34 @@ export async function ensureHistorySchema(pool) {
CREATE INDEX IF NOT EXISTS ${PORTFOLIO_METRICS_TABLE}_computed_at_idx CREATE INDEX IF NOT EXISTS ${PORTFOLIO_METRICS_TABLE}_computed_at_idx
ON ${PORTFOLIO_METRICS_TABLE} (computed_at DESC) ON ${PORTFOLIO_METRICS_TABLE} (computed_at DESC)
`); `);
await pool.query(`
CREATE TABLE IF NOT EXISTS ${QUOTE_OUTCOMES_TABLE} (
quote_id TEXT PRIMARY KEY,
decision_id TEXT,
command_id TEXT,
execution_result_status TEXT NOT NULL,
execution_result_code TEXT,
submitted_at TIMESTAMPTZ,
command_at TIMESTAMPTZ,
outcome_status TEXT NOT NULL,
outcome_observed_at TIMESTAMPTZ,
outcome_source TEXT NOT NULL,
attribution_status TEXT NOT NULL,
attribution_method TEXT,
attributed_inventory_delta JSONB,
computed_at TIMESTAMPTZ NOT NULL,
payload JSONB NOT NULL
)
`);
await pool.query(`
CREATE INDEX IF NOT EXISTS ${QUOTE_OUTCOMES_TABLE}_outcome_observed_at_idx
ON ${QUOTE_OUTCOMES_TABLE} (outcome_observed_at DESC)
`);
await pool.query(`
CREATE INDEX IF NOT EXISTS ${QUOTE_OUTCOMES_TABLE}_outcome_status_idx
ON ${QUOTE_OUTCOMES_TABLE} (outcome_status)
`);
} }
export async function insertHistoryEvent(pool, { table, topic, event, record }) { export async function insertHistoryEvent(pool, { table, topic, event, record }) {
@ -260,6 +291,176 @@ export async function loadLatestPortfolioMetric(pool) {
return normalizePortfolioMetricRow(result.rows[0]); return normalizePortfolioMetricRow(result.rows[0]);
} }
export async function refreshQuoteOutcomes(pool, {
btcAsset = null,
eureAsset = null,
now = Date.now(),
} = {}) {
if (!btcAsset?.assetId || !eureAsset?.assetId) return [];
const [
submissionsResult,
commandsResult,
decisionsResult,
inventoryResult,
] = await Promise.all([
pool.query(`
SELECT event_id, observed_at, ingested_at, quote_id, payload
FROM trade_execution_results
WHERE payload->>'status' = 'submitted'
ORDER BY COALESCE(observed_at, ingested_at) ASC
`),
pool.query(`
SELECT event_id, observed_at, ingested_at, quote_id, payload
FROM execute_trade_commands
ORDER BY COALESCE(observed_at, ingested_at) ASC
`),
pool.query(`
SELECT event_id, observed_at, ingested_at, quote_id, payload
FROM trade_decisions
ORDER BY COALESCE(observed_at, ingested_at) ASC
`),
pool.query(`
SELECT event_id, observed_at, ingested_at, quote_id, payload
FROM intent_inventory_snapshots
ORDER BY COALESCE(observed_at, ingested_at) ASC
`),
]);
const records = deriveQuoteOutcomeRecords({
submissions: submissionsResult.rows,
commands: commandsResult.rows,
decisions: decisionsResult.rows,
inventorySnapshots: inventoryResult.rows,
btcAsset,
eureAsset,
now,
});
if (!records.length) return [];
const computedAt = new Date(
typeof now === 'number' ? now : Date.parse(now),
).toISOString();
for (const record of records) {
await upsertQuoteOutcome(pool, {
...record,
computedAt,
});
}
return records;
}
export async function upsertQuoteOutcome(pool, {
quote_id,
decision_id = null,
command_id = null,
execution_result_status,
execution_result_code = null,
submitted_at = null,
command_at = null,
outcome_status,
outcome_observed_at = null,
outcome_source,
attribution_status,
attribution_method = null,
attributed_inventory_delta = null,
computedAt,
payload,
}) {
await pool.query(
`
INSERT INTO ${QUOTE_OUTCOMES_TABLE} (
quote_id,
decision_id,
command_id,
execution_result_status,
execution_result_code,
submitted_at,
command_at,
outcome_status,
outcome_observed_at,
outcome_source,
attribution_status,
attribution_method,
attributed_inventory_delta,
computed_at,
payload
) VALUES (
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13::jsonb,$14,$15::jsonb
)
ON CONFLICT (quote_id) DO UPDATE SET
decision_id = EXCLUDED.decision_id,
command_id = EXCLUDED.command_id,
execution_result_status = EXCLUDED.execution_result_status,
execution_result_code = EXCLUDED.execution_result_code,
submitted_at = EXCLUDED.submitted_at,
command_at = EXCLUDED.command_at,
outcome_status = EXCLUDED.outcome_status,
outcome_observed_at = EXCLUDED.outcome_observed_at,
outcome_source = EXCLUDED.outcome_source,
attribution_status = EXCLUDED.attribution_status,
attribution_method = EXCLUDED.attribution_method,
attributed_inventory_delta = EXCLUDED.attributed_inventory_delta,
computed_at = EXCLUDED.computed_at,
payload = EXCLUDED.payload
`,
[
quote_id,
decision_id,
command_id,
execution_result_status,
execution_result_code,
submitted_at,
command_at,
outcome_status,
outcome_observed_at,
outcome_source,
attribution_status,
attribution_method,
attributed_inventory_delta ? JSON.stringify(attributed_inventory_delta) : null,
computedAt,
JSON.stringify(payload || {}),
],
);
}
export async function loadRecentQuoteOutcomes(pool, { limit = 200 } = {}) {
const result = await pool.query(
`
SELECT
quote_id,
decision_id,
command_id,
execution_result_status,
execution_result_code,
submitted_at,
command_at,
outcome_status,
outcome_observed_at,
outcome_source,
attribution_status,
attribution_method,
attributed_inventory_delta,
computed_at,
payload
FROM ${QUOTE_OUTCOMES_TABLE}
ORDER BY
CASE outcome_status
WHEN 'completed' THEN 0
WHEN 'not_filled' THEN 1
ELSE 2
END,
COALESCE(outcome_observed_at, submitted_at, computed_at) DESC
LIMIT $1
`,
[Math.max(1, Number(limit) || 200)],
);
return result.rows.map(normalizeQuoteOutcomeRow);
}
export async function loadLatestInventorySnapshot(pool) { export async function loadLatestInventorySnapshot(pool) {
const latest = await loadLatestEventPayload(pool, 'intent_inventory_snapshots'); const latest = await loadLatestEventPayload(pool, 'intent_inventory_snapshots');
if (!latest) return null; if (!latest) return null;
@ -325,12 +526,15 @@ export async function loadSubmissionPage(pool, { page = 1, pageSize = 20 } = {})
r.ingested_at AS result_ingested_at, r.ingested_at AS result_ingested_at,
r.payload AS result_payload, r.payload AS result_payload,
c.payload AS command_payload, c.payload AS command_payload,
d.payload AS decision_payload d.payload AS decision_payload,
o.payload AS outcome_payload
FROM trade_execution_results r FROM trade_execution_results r
LEFT JOIN execute_trade_commands c LEFT JOIN execute_trade_commands c
ON c.decision_key = r.decision_key ON c.decision_key = r.decision_key
LEFT JOIN trade_decisions d LEFT JOIN trade_decisions d
ON d.decision_key = COALESCE(c.payload->>'decision_id', r.payload->>'decision_id') ON d.decision_key = COALESCE(c.payload->>'decision_id', r.payload->>'decision_id')
LEFT JOIN ${QUOTE_OUTCOMES_TABLE} o
ON o.quote_id = r.quote_id
WHERE r.payload->>'status' = 'submitted' WHERE r.payload->>'status' = 'submitted'
ORDER BY COALESCE(r.observed_at, r.ingested_at) DESC ORDER BY COALESCE(r.observed_at, r.ingested_at) DESC
LIMIT $1 LIMIT $1
@ -360,12 +564,15 @@ export async function loadRecentExecutionResults(pool, { limit = 20 } = {}) {
r.payload AS result_payload, r.payload AS result_payload,
c.ingested_at AS command_ingested_at, c.ingested_at AS command_ingested_at,
c.payload AS command_payload, c.payload AS command_payload,
d.payload AS decision_payload d.payload AS decision_payload,
o.payload AS outcome_payload
FROM trade_execution_results r FROM trade_execution_results r
LEFT JOIN execute_trade_commands c LEFT JOIN execute_trade_commands c
ON c.decision_key = r.decision_key ON c.decision_key = r.decision_key
LEFT JOIN trade_decisions d LEFT JOIN trade_decisions d
ON d.decision_key = COALESCE(c.payload->>'decision_id', r.payload->>'decision_id') ON d.decision_key = COALESCE(c.payload->>'decision_id', r.payload->>'decision_id')
LEFT JOIN ${QUOTE_OUTCOMES_TABLE} o
ON o.quote_id = r.quote_id
ORDER BY COALESCE(r.observed_at, r.ingested_at) DESC ORDER BY COALESCE(r.observed_at, r.ingested_at) DESC
LIMIT $1 LIMIT $1
`, `,
@ -662,6 +869,36 @@ function normalizePortfolioMetricRow(row) {
}; };
} }
function normalizeQuoteOutcomeRow(row) {
const payload = row.payload || {};
return {
quote_id: row.quote_id || payload.quote_id || null,
decision_id: row.decision_id || payload.decision_id || null,
command_id: row.command_id || payload.command_id || null,
pair: payload.pair || null,
direction: payload.direction || null,
request_kind: payload.request_kind || null,
gross_edge_pct: payload.gross_edge_pct || null,
eure_notional: payload.eure_notional || 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,
submitted_at: toIsoTimestamp(row.submitted_at || payload.submitted_at),
command_at: toIsoTimestamp(row.command_at || payload.command_at),
outcome_status: row.outcome_status || payload.outcome_status || null,
outcome_observed_at: toIsoTimestamp(row.outcome_observed_at || payload.outcome_observed_at),
outcome_source: row.outcome_source || payload.outcome_source || null,
outcome_reason: payload.outcome_reason || null,
attribution_status: row.attribution_status || payload.attribution_status || null,
attribution_method: row.attribution_method || payload.attribution_method || null,
attributed_inventory_delta:
row.attributed_inventory_delta
|| payload.attributed_inventory_delta
|| null,
computed_at: toIsoTimestamp(row.computed_at),
evidence: payload.evidence || null,
};
}
function normalizeRecentQuoteRow(row) { function normalizeRecentQuoteRow(row) {
const payload = row.payload || {}; const payload = row.payload || {};
return { return {
@ -682,6 +919,7 @@ function normalizeSubmissionRow(row) {
const resultPayload = row.result_payload || {}; const resultPayload = row.result_payload || {};
const commandPayload = row.command_payload || {}; const commandPayload = row.command_payload || {};
const decisionPayload = row.decision_payload || {}; const decisionPayload = row.decision_payload || {};
const outcomePayload = row.outcome_payload || {};
return { return {
command_id: resultPayload.command_id || commandPayload.command_id || null, command_id: resultPayload.command_id || commandPayload.command_id || null,
@ -697,6 +935,10 @@ 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,
outcome_status: outcomePayload.outcome_status || null,
outcome_reason: outcomePayload.outcome_reason || null,
attribution_status: outcomePayload.attribution_status || null,
attributed_inventory_delta: outcomePayload.attributed_inventory_delta || null,
request_kind: commandPayload.request_kind || decisionPayload.request_kind || null, request_kind: commandPayload.request_kind || decisionPayload.request_kind || null,
asset_in: commandPayload.asset_in || null, asset_in: commandPayload.asset_in || null,
asset_out: commandPayload.asset_out || null, asset_out: commandPayload.asset_out || null,
@ -733,6 +975,7 @@ function normalizeExecutionResultRow(row) {
const resultPayload = row.result_payload || {}; const resultPayload = row.result_payload || {};
const commandPayload = row.command_payload || {}; const commandPayload = row.command_payload || {};
const decisionPayload = row.decision_payload || {}; const decisionPayload = row.decision_payload || {};
const outcomePayload = row.outcome_payload || {};
return { return {
command_id: resultPayload.command_id || commandPayload.command_id || null, command_id: resultPayload.command_id || commandPayload.command_id || null,
@ -749,15 +992,22 @@ function normalizeExecutionResultRow(row) {
status: resultPayload.status || null, status: resultPayload.status || null,
result_code: resultPayload.result_code || null, result_code: resultPayload.result_code || null,
outcome_status: outcome_status:
resultPayload.outcome_status outcomePayload.outcome_status
|| resultPayload.outcome_status
|| resultPayload.venue_outcome_status || resultPayload.venue_outcome_status
|| resultPayload.trade_outcome_status || resultPayload.trade_outcome_status
|| null, || null,
outcome_reason: outcome_reason:
resultPayload.outcome_reason outcomePayload.outcome_reason
|| resultPayload.outcome_reason
|| resultPayload.venue_outcome_reason || resultPayload.venue_outcome_reason
|| resultPayload.trade_outcome_reason || resultPayload.trade_outcome_reason
|| null, || null,
outcome_source: outcomePayload.outcome_source || null,
attribution_status: outcomePayload.attribution_status || null,
attribution_method: outcomePayload.attribution_method || null,
attributed_inventory_delta: outcomePayload.attributed_inventory_delta || 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.error?.message || null,
note: resultPayload.note || null, note: resultPayload.note || null,

View file

@ -380,13 +380,13 @@ export default function FundsPage({
: 'Baseline anchored before first live trade'; : 'Baseline anchored before first live trade';
const simpleHoldMeta = externalFlowAdjusted const simpleHoldMeta = externalFlowAdjusted
? 'Simple hold includes later credited deposits and completed withdrawals' ? 'Simple hold includes later credited deposits and completed withdrawals'
: 'Trading contribution over simple hold'; : 'Comparison against a no-trade simple-hold baseline';
const marketMoveMeta = externalFlowAdjusted const marketMoveMeta = externalFlowAdjusted
? 'Simple-hold market move on baseline plus later external flows' ? 'Simple-hold market move on baseline plus later external flows'
: 'Baseline mark move only'; : 'Baseline mark move only';
const tradingContributionMeta = externalFlowAdjusted const portfolioVsHoldMeta = externalFlowAdjusted
? 'Current minus cash-flow-adjusted simple hold' ? 'Current minus cash-flow-adjusted simple hold; not realized trade PnL'
: 'Current minus simple hold'; : 'Current minus simple hold; not realized trade PnL';
return ( return (
<> <>
@ -441,10 +441,10 @@ export default function FundsPage({
value={formatEur(profitability.market_move_contribution_eure)} value={formatEur(profitability.market_move_contribution_eure)}
/> />
<MetricCard <MetricCard
label="Trading contribution" label="Portfolio vs simple hold"
meta={tradingContributionMeta} meta={portfolioVsHoldMeta}
signedValue={profitability.trading_contribution_eure} signedValue={profitability.portfolio_vs_simple_hold_eure}
value={formatEur(profitability.trading_contribution_eure)} value={formatEur(profitability.portfolio_vs_simple_hold_eure)}
/> />
<MetricCard <MetricCard
label={SUBMISSION_COPY.recentMetricLabel} label={SUBMISSION_COPY.recentMetricLabel}

View file

@ -97,10 +97,22 @@ function SuccessfulTradesTable({ items }) {
<td className="mono truncate-cell" title={item.pair || ''}>{truncateMiddle(item.pair || '', 32)}</td> <td className="mono truncate-cell" title={item.pair || ''}>{truncateMiddle(item.pair || '', 32)}</td>
<td> <td>
<IdentifierRow label="Quote" value={item.quote_id} /> <IdentifierRow label="Quote" value={item.quote_id} />
<IdentifierRow label="Decision" value={item.decision_id} />
<IdentifierRow label="Command" value={item.command_id} /> <IdentifierRow label="Command" value={item.command_id} />
</td> </td>
<td>{item.reason_text}</td> <td>{item.reason_text}</td>
<td>Linked asset delta not exposed yet</td> <td>
<div>{item.settlement_summary?.text || 'No settled inventory delta is linked to this quote.'}</div>
{item.settlement_summary?.method ? (
<div className="status-subtle mono">{item.settlement_summary.method}</div>
) : null}
{item.settlement_summary?.observed_at ? (
<div className="status-subtle">{`Observed ${formatTimestamp(item.settlement_summary.observed_at)}`}</div>
) : null}
{item.settlement_summary?.caveat ? (
<div className="status-subtle">{item.settlement_summary.caveat}</div>
) : null}
</td>
</tr> </tr>
))} ))}
</tbody> </tbody>

View file

@ -58,6 +58,11 @@ export default function SystemPage({ system, onControl }) {
meta={system.persistence.metrics_error ? 'Metrics error present' : 'Metrics healthy'} meta={system.persistence.metrics_error ? 'Metrics error present' : 'Metrics healthy'}
value={formatTimestamp(system.persistence.last_metrics_at)} value={formatTimestamp(system.persistence.last_metrics_at)}
/> />
<MetricCard
label="Last quote outcome write"
meta={system.persistence.quote_outcomes_error ? 'Outcome attribution error present' : 'Outcome attribution healthy'}
value={formatTimestamp(system.persistence.last_quote_outcomes_at)}
/>
</div> </div>
<TableFrame style={{ marginTop: 14 }}> <TableFrame style={{ marginTop: 14 }}>
<table> <table>

View file

@ -16,6 +16,11 @@ export async function startSolverRelayWs({
let requestId = 1; let requestId = 1;
const pending = new Map(); const pending = new Map();
let readyResolvers = []; let readyResolvers = [];
let reconnectCount = 0;
let lastMessageAt = null;
let lastConnectedAt = null;
let lastDisconnectedAt = null;
let lastReconnectAt = null;
connect(); connect();
@ -55,8 +60,20 @@ export async function startSolverRelayWs({
return { return {
connected, connected,
pending_requests: pending.size, pending_requests: pending.size,
reconnect_count: reconnectCount,
last_message_at: lastMessageAt,
last_connected_at: lastConnectedAt,
last_disconnected_at: lastDisconnectedAt,
last_reconnect_at: lastReconnectAt,
}; };
}, },
reconnect() {
if (socket && socket.readyState <= 1) {
socket.close();
} else {
connect();
}
},
close() { close() {
closed = true; closed = true;
if (reconnectTimer) clearTimeout(reconnectTimer); if (reconnectTimer) clearTimeout(reconnectTimer);
@ -67,6 +84,8 @@ export async function startSolverRelayWs({
function connect() { function connect() {
if (closed) return; if (closed) return;
reconnectCount += 1;
lastReconnectAt = new Date().toISOString();
socket = new WebSocket(wsUrl, { socket = new WebSocket(wsUrl, {
headers: { headers: {
@ -76,6 +95,7 @@ export async function startSolverRelayWs({
socket.addEventListener('open', () => { socket.addEventListener('open', () => {
connected = true; connected = true;
lastConnectedAt = new Date().toISOString();
logger?.info('connection_established', { logger?.info('connection_established', {
venue: 'near-intents', venue: 'near-intents',
}); });
@ -92,6 +112,7 @@ export async function startSolverRelayWs({
}); });
socket.addEventListener('message', (event) => { socket.addEventListener('message', (event) => {
lastMessageAt = new Date().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;
try { try {
@ -116,6 +137,7 @@ export async function startSolverRelayWs({
socket.addEventListener('close', () => { socket.addEventListener('close', () => {
connected = false; connected = false;
lastDisconnectedAt = new Date().toISOString();
rejectAllPending(new Error('Socket disconnected')); rejectAllPending(new Error('Socket disconnected'));
logger?.warn('connection_lost', { logger?.warn('connection_lost', {
venue: 'near-intents', venue: 'near-intents',

View file

@ -38,9 +38,15 @@ export async function startNearIntentsWs({
let lastMatchingQuoteAt = null; let lastMatchingQuoteAt = null;
let lastPublishedAt = null; let lastPublishedAt = null;
let lastPublishedPair = null; let lastPublishedPair = null;
let reconnectCount = 0;
let lastConnectedAt = null;
let lastDisconnectedAt = null;
let lastReconnectAt = null;
function connect() { function connect() {
if (closed) return; if (closed) return;
reconnectCount += 1;
lastReconnectAt = new Date().toISOString();
const ws = new WebSocket(wsUrl, { const ws = new WebSocket(wsUrl, {
headers: { Authorization: `Bearer ${apiKey}` }, headers: { Authorization: `Bearer ${apiKey}` },
@ -49,6 +55,7 @@ export async function startNearIntentsWs({
ws.addEventListener('open', () => { ws.addEventListener('open', () => {
connected = true; connected = true;
lastConnectedAt = new Date().toISOString();
logger?.info('connection_established', { logger?.info('connection_established', {
namespace, namespace,
}); });
@ -133,6 +140,7 @@ export async function startNearIntentsWs({
ws.addEventListener('close', () => { ws.addEventListener('close', () => {
connected = false; connected = false;
activeSocket = null; activeSocket = null;
lastDisconnectedAt = new Date().toISOString();
logger?.warn('connection_lost', { logger?.warn('connection_lost', {
namespace, namespace,
details: { details: {
@ -160,6 +168,7 @@ export async function startNearIntentsWs({
getState() { getState() {
return { return {
connected, connected,
reconnect_count: reconnectCount,
frames_received: framesReceived, frames_received: framesReceived,
quote_frames_received: quoteFramesReceived, quote_frames_received: quoteFramesReceived,
filtered_count: filteredCount, filtered_count: filteredCount,
@ -170,10 +179,20 @@ export async function startNearIntentsWs({
last_matching_quote_at: lastMatchingQuoteAt, last_matching_quote_at: lastMatchingQuoteAt,
last_published_at: lastPublishedAt, last_published_at: lastPublishedAt,
last_published_pair: lastPublishedPair, last_published_pair: lastPublishedPair,
last_connected_at: lastConnectedAt,
last_disconnected_at: lastDisconnectedAt,
last_reconnect_at: lastReconnectAt,
raw_topic: rawTopic, raw_topic: rawTopic,
normalized_topic: normalizedTopic, normalized_topic: normalizedTopic,
}; };
}, },
reconnect() {
if (activeSocket && activeSocket.readyState <= 1) {
activeSocket.close();
} else {
connect();
}
},
close() { close() {
closed = true; closed = true;
if (reconnectTimer) clearTimeout(reconnectTimer); if (reconnectTimer) clearTimeout(reconnectTimer);

View file

@ -6,17 +6,37 @@ import path from 'node:path';
import { createExecutorStateStore } from '../src/core/executor-state-store.mjs'; import { createExecutorStateStore } from '../src/core/executor-state-store.mjs';
test('executor state store persists processing and completion state', () => { test('executor state store persists processing and submission state', () => {
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), 'unrip-executor-')); const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), 'unrip-executor-'));
const store = createExecutorStateStore({ stateDir }); const store = createExecutorStateStore({ stateDir });
store.markProcessing('cmd-1', { quote_id: 'quote-1' }); store.markProcessing('cmd-1', { quote_id: 'quote-1' });
assert.equal(store.get('cmd-1').status, 'processing'); assert.equal(store.get('cmd-1').status, 'processing');
store.markCompleted('cmd-1', { result_event_id: 'result-1' }); store.markSubmitted('cmd-1', { result_event_id: 'result-1' });
assert.equal(store.get('cmd-1').status, 'completed'); assert.equal(store.get('cmd-1').status, 'submitted');
const reloaded = createExecutorStateStore({ stateDir }); const reloaded = createExecutorStateStore({ stateDir });
assert.equal(reloaded.get('cmd-1').status, 'completed'); assert.equal(reloaded.get('cmd-1').status, 'submitted');
assert.equal(reloaded.get('cmd-1').result_event_id, 'result-1'); assert.equal(reloaded.get('cmd-1').result_event_id, 'result-1');
}); });
test('executor state store normalizes legacy completed markers to submitted', () => {
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), 'unrip-executor-'));
const statePath = path.join(stateDir, 'trade-executor-commands.json');
fs.writeFileSync(
statePath,
JSON.stringify({
commands: {
'cmd-legacy': {
status: 'completed',
result_event_id: 'result-legacy',
},
},
}),
);
const store = createExecutorStateStore({ stateDir });
assert.equal(store.get('cmd-legacy').status, 'submitted');
assert.equal(store.getState().commands['cmd-legacy'].status, 'submitted');
});

View file

@ -42,7 +42,7 @@ function buildConfig() {
}; };
} }
test('profitability summary separates baseline, hold, market move, and trading contribution', () => { test('profitability summary separates baseline, hold, market move, and portfolio comparison', () => {
const summary = buildProfitabilitySummary({ const summary = buildProfitabilitySummary({
metric: { metric: {
computed_at: '2026-04-04T09:05:00.000Z', computed_at: '2026-04-04T09:05:00.000Z',
@ -63,7 +63,9 @@ test('profitability summary separates baseline, hold, market move, and trading c
assert.equal(summary.pnl_vs_deposit_baseline_eure, '10'); assert.equal(summary.pnl_vs_deposit_baseline_eure, '10');
assert.equal(summary.pnl_vs_simple_hold_eure, '5'); assert.equal(summary.pnl_vs_simple_hold_eure, '5');
assert.equal(summary.market_move_contribution_eure, '5'); assert.equal(summary.market_move_contribution_eure, '5');
assert.equal(summary.trading_contribution_eure, '5'); assert.equal(summary.portfolio_vs_simple_hold_eure, '5');
assert.equal(summary.trading_contribution_eure, null);
assert.match(summary.caveats.join(' '), /not realized per-trade PnL/i);
assert.equal(summary.computed_at, '2026-04-04T09:05:00.000Z'); assert.equal(summary.computed_at, '2026-04-04T09:05:00.000Z');
assert.equal(summary.recent_submission_count, 4); assert.equal(summary.recent_submission_count, 4);
assert.equal(summary.last_submission_at, '2026-04-04T09:00:00.000Z'); assert.equal(summary.last_submission_at, '2026-04-04T09:00:00.000Z');
@ -489,7 +491,7 @@ test('bootstrap aggregation keeps Funds as default and carries live control stat
paused: false, paused: false,
draining: false, draining: false,
in_flight_count: 0, in_flight_count: 0,
completed_count: 1, submitted_count: 1,
}, },
}, },
{ {
@ -591,7 +593,7 @@ test('bootstrap normalizes actionable decision vocabulary before exposing it to
assert.doesNotMatch(JSON.stringify(bootstrap), /Actionable/); assert.doesNotMatch(JSON.stringify(bootstrap), /Actionable/);
}); });
test('completed lifecycle evidence is the only source of successful trade rows', () => { test('submitted lifecycle evidence never becomes completed by itself', () => {
const rows = deriveQuoteLifecycleRows({ const rows = deriveQuoteLifecycleRows({
recentExecutionResults: [ recentExecutionResults: [
{ {
@ -601,25 +603,125 @@ test('completed lifecycle evidence is the only source of successful trade rows',
status: 'submitted', status: 'submitted',
result_code: 'quote_response_ok', result_code: 'quote_response_ok',
}, },
{
command_id: 'cmd-completed',
quote_id: 'quote-completed',
result_at: '2026-04-09T09:01:00.000Z',
status: 'submitted',
result_code: 'quote_response_ok',
outcome_status: 'completed',
outcome_reason: 'settled',
},
], ],
}); });
const completed = rows.filter((row) => row.lifecycle_state === 'completed'); const completed = rows.filter((row) => row.lifecycle_state === 'completed');
const submitted = rows.filter((row) => row.lifecycle_state === 'submitted'); const submitted = rows.filter((row) => row.lifecycle_state === 'submitted');
assert.equal(completed.length, 1); assert.equal(completed.length, 0);
assert.equal(completed[0].quote_id, 'quote-completed');
assert.equal(submitted.length, 1); assert.equal(submitted.length, 1);
assert.equal(submitted[0].quote_id, 'quote-submitted'); assert.equal(submitted[0].quote_id, 'quote-submitted');
assert.equal(submitted[0].has_settlement_evidence, false);
assert.doesNotMatch(`${submitted[0].lifecycle_label} ${submitted[0].reason_text}`, /completed|successful trade|asset delta/i);
});
test('successful trade rows require completed outcome with linked settled inventory evidence', () => {
const config = buildConfig();
const bootstrap = buildDashboardBootstrap({
config,
auth: {
authenticated: true,
subject: 'local-operator',
mode: 'stub',
roles: ['operator'],
},
portfolioMetric: null,
inventorySnapshot: null,
marketPrice: null,
recentQuotes: [],
submissionPage: { page: 1, page_size: 20, total: 0, total_pages: 1, items: [] },
submissionSummary: { total: 0, last_submission_at: null },
fundingObservations: [],
recentDepositStatuses: [],
recentTradeDecisions: [],
recentExecuteTradeCommands: [],
recentExecutionResults: [{
command_id: 'cmd-submitted',
quote_id: 'quote-submitted',
result_at: '2026-04-09T09:00:00.000Z',
status: 'submitted',
result_code: 'quote_response_ok',
}],
recentQuoteOutcomes: [
{
command_id: 'cmd-completed',
quote_id: 'quote-completed',
pair: config.activePair,
outcome_status: 'completed',
outcome_reason: 'matched_inventory_delta',
outcome_observed_at: '2026-04-09T09:01:00.000Z',
outcome_source: 'intent_inventory_spendable_delta',
attribution_status: 'heuristic_match',
attribution_method: 'exact_asset_delta_within_window',
attributed_inventory_delta: {
observed_at: '2026-04-09T09:01:00.000Z',
delta_units: {
[config.tradingBtc.assetId]: '37014',
[config.tradingEure.assetId]: '-21000021200021200022',
},
},
},
{
command_id: 'cmd-completed-no-delta',
quote_id: 'quote-completed-no-delta',
pair: config.activePair,
outcome_status: 'completed',
outcome_reason: 'settled',
outcome_observed_at: '2026-04-09T09:02:00.000Z',
outcome_source: 'venue_status_without_settlement',
attribution_status: 'unattributed',
attribution_method: null,
attributed_inventory_delta: null,
},
],
recentAlertTransitions: [],
serviceSnapshots: [],
});
const funnel = bootstrap.strategy.strategy_state.trade_funnel;
assert.equal(funnel.successful_trade_count, 1);
assert.equal(funnel.successful_trades[0].quote_id, 'quote-completed');
assert.match(funnel.successful_trades[0].settlement_summary.text, /\+0\.00037014 BTC/);
assert.equal(funnel.counts.submitted, 1);
assert.equal(funnel.counts.completed, 2);
});
test('executor blocking is distinct from strategy rejection', () => {
const [row] = deriveQuoteLifecycleRows({
recentTradeDecisions: [{
observed_at: '2026-04-09T09:00:01.000Z',
payload: {
decision_id: 'decision-1',
quote_id: 'quote-1',
pair: 'btc->eure',
decision: 'actionable',
decision_reason: 'actionable',
},
}],
recentExecuteTradeCommands: [{
observed_at: '2026-04-09T09:00:02.000Z',
command_id: 'cmd-1',
decision_id: 'decision-1',
quote_id: 'quote-1',
pair: 'btc->eure',
}],
recentExecutionResults: [{
command_id: 'cmd-1',
decision_id: 'decision-1',
quote_id: 'quote-1',
pair: 'btc->eure',
result_at: '2026-04-09T09:00:03.000Z',
status: 'rejected',
result_code: 'executor_disarmed',
note: 'executor is disarmed',
}],
});
assert.equal(row.lifecycle_state, 'blocked');
assert.equal(row.lifecycle_label, 'Blocked before submit');
assert.equal(row.reason_code, 'executor_disarmed');
assert.notEqual(row.lifecycle_label, 'Rejected by strategy');
}); });
test('system service state ignores sentinel alert severity and keeps alert surfaces empty', () => { test('system service state ignores sentinel alert severity and keeps alert surfaces empty', () => {
@ -1002,7 +1104,7 @@ test('funding summary includes credited bridge deposits without observer-backed
paused: false, paused: false,
draining: false, draining: false,
in_flight_count: 0, in_flight_count: 0,
completed_count: 0, submitted_count: 0,
}, },
}, },
{ {

View file

@ -15,7 +15,7 @@ const eureAsset = {
decimals: 18, decimals: 18,
}; };
test('portfolio metrics compute trade pnl and mark-to-market pnl from baseline funding inventory', () => { test('portfolio metrics compute portfolio comparison and mark-to-market pnl from baseline funding inventory', () => {
const metric = computePortfolioMetric({ const metric = computePortfolioMetric({
baseline: { baseline: {
anchor: 'latest_inventory_before_first_command', anchor: 'latest_inventory_before_first_command',
@ -55,9 +55,10 @@ test('portfolio metrics compute trade pnl and mark-to-market pnl from baseline f
assert.equal(metric.baseline_status, 'active'); assert.equal(metric.baseline_status, 'active');
assert.equal(metric.current_portfolio_value_eure, '118.576613887978799978'); assert.equal(metric.current_portfolio_value_eure, '118.576613887978799978');
assert.equal(metric.trade_pnl_eure, '0.391183707978799978'); assert.equal(metric.portfolio_vs_simple_hold_eure, '0.497413887978799978');
assert.equal(metric.mark_to_market_pnl_eure, '0.497413887978799978'); assert.equal(metric.trade_pnl_eure, null);
assert.equal(metric.price_move_pnl_eure, '0.10623018'); assert.equal(metric.mark_to_market_pnl_eure, '0.784413887978799978');
assert.equal(metric.price_move_pnl_eure, '0.287');
assert.deepEqual(metric.inventory_delta, { assert.deepEqual(metric.inventory_delta, {
btc_units: '37014', btc_units: '37014',
btc: '0.00037014', btc: '0.00037014',
@ -90,11 +91,79 @@ test('portfolio metrics stay available before the first live execution', () => {
assert.equal(metric.baseline_status, 'awaiting_first_execution'); assert.equal(metric.baseline_status, 'awaiting_first_execution');
assert.equal(metric.current_portfolio_value_eure, '118.0792'); assert.equal(metric.current_portfolio_value_eure, '118.0792');
assert.equal(metric.portfolio_vs_simple_hold_eure, null);
assert.equal(metric.trade_pnl_eure, null); assert.equal(metric.trade_pnl_eure, null);
assert.equal(metric.mark_to_market_pnl_eure, null); assert.equal(metric.mark_to_market_pnl_eure, null);
assert.equal(metric.price_move_pnl_eure, null); assert.equal(metric.price_move_pnl_eure, null);
}); });
test('portfolio metrics treat later deposits and withdrawals as external cash flows instead of PnL', () => {
const metric = computePortfolioMetric({
baseline: {
anchor: 'latest_inventory_before_first_command',
command_at: '2026-04-02T18:10:43.569Z',
inventory: {
inventory_id: 'baseline-2',
synced_at: '2026-04-02T18:10:33.381Z',
spendable: {
'nep141:btc.omft.near': '100000',
'nep141:eure.omft.near': '60000000000000000000',
},
},
price: {
price_id: 'price-baseline-2',
observed_at: '2026-04-02T18:10:30.109Z',
eure_per_btc: '57792.20000000',
},
},
currentInventory: {
inventory_id: 'current-3',
synced_at: '2026-04-07T15:43:30.463Z',
spendable: {
'nep141:btc.omft.near': '137014',
'nep141:eure.omft.near': '63999978599978799978',
},
},
currentPrice: {
price_id: 'price-current-3',
observed_at: '2026-04-07T15:43:29.885Z',
eure_per_btc: '58845.90000000',
},
externalFlows: [
{
flow_id: 'withdrawal-1',
kind: 'withdrawal',
asset_id: eureAsset.assetId,
effective_at: '2026-04-02T10:52:27.863Z',
signed_units: '-1000000000000000000',
reference_price_eure_per_btc_at_flow_time: null,
},
{
flow_id: 'deposit-1',
kind: 'deposit',
asset_id: eureAsset.assetId,
effective_at: '2026-04-07T15:20:54.757Z',
signed_units: '24999999800000000000',
reference_price_eure_per_btc_at_flow_time: null,
},
],
btcAsset,
eureAsset,
commandCount: 7,
resultCount: 7,
});
assert.equal(metric.external_cash_flows.flow_count, 2);
assert.equal(metric.external_cash_flows.net_eure, '23.9999998');
assert.equal(metric.external_cash_flows.net_value_eure_at_flow_time, '23.9999998');
assert.equal(metric.baseline_portfolio_value_eure_at_baseline_price, '141.7921998');
assert.equal(metric.baseline_portfolio_value_eure_at_current_price, '142.8458998');
assert.equal(metric.mark_to_market_pnl_eure, '2.834900225978799978');
assert.equal(metric.price_move_pnl_eure, '1.0537');
assert.equal(metric.portfolio_vs_simple_hold_eure, '1.781200225978799978');
assert.equal(metric.trade_pnl_eure, null);
});
test('portfolio metric id keys off the baseline and current snapshots', () => { test('portfolio metric id keys off the baseline and current snapshots', () => {
const metricId = buildPortfolioMetricId({ const metricId = buildPortfolioMetricId({
baselineInventoryId: 'baseline-1', baselineInventoryId: 'baseline-1',

View file

@ -0,0 +1,166 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { deriveQuoteOutcomeRecords } from '../src/core/quote-outcomes.mjs';
const BTC = {
assetId: 'nep141:btc.omft.near',
symbol: 'BTC',
decimals: 8,
};
const EURE = {
assetId: 'nep141:eure.omft.near',
symbol: 'EURe',
decimals: 18,
};
function submittedResult(quoteId, observedAt = '2026-04-02T18:13:30.000Z') {
return {
observed_at: observedAt,
payload: {
quote_id: quoteId,
command_id: `cmd-${quoteId}`,
decision_id: `decision-${quoteId}`,
status: 'submitted',
result_code: 'quote_response_ok',
},
};
}
function exactOutCommand(quoteId) {
return {
observed_at: '2026-04-02T18:13:29.000Z',
payload: {
quote_id: quoteId,
command_id: `cmd-${quoteId}`,
decision_id: `decision-${quoteId}`,
pair: `${BTC.assetId}->${EURE.assetId}`,
direction: 'eure_to_btc',
request_kind: 'exact_out',
asset_in: BTC.assetId,
asset_out: EURE.assetId,
amount_out: '21000021200021200022',
quote_output: {
amount_in: '37014',
},
min_deadline_ms: 60000,
},
};
}
function inventorySnapshot(observedAt, spendable) {
return {
observed_at: observedAt,
payload: {
inventory_id: `inventory-${observedAt}`,
synced_at: observedAt,
spendable,
},
};
}
test('quote outcome completes only when exact submitted quote delta is observed', () => {
const [outcome] = deriveQuoteOutcomeRecords({
submissions: [submittedResult('quote-settled')],
commands: [exactOutCommand('quote-settled')],
inventorySnapshots: [
inventorySnapshot('2026-04-02T18:13:00.000Z', {
[BTC.assetId]: '0',
[EURE.assetId]: '100000000000000000000',
}),
inventorySnapshot('2026-04-02T18:13:33.000Z', {
[BTC.assetId]: '37014',
[EURE.assetId]: '78999978799978799978',
}),
],
btcAsset: BTC,
eureAsset: EURE,
now: '2026-04-02T18:14:00.000Z',
});
assert.equal(outcome.outcome_status, 'completed');
assert.equal(outcome.attribution_status, 'heuristic_match');
assert.equal(outcome.attributed_inventory_delta.delta_units[BTC.assetId], '37014');
assert.equal(outcome.attributed_inventory_delta.delta_units[EURE.assetId], '-21000021200021200022');
});
test('submitted quote without settlement stays submitted before the deadline window expires', () => {
const [outcome] = deriveQuoteOutcomeRecords({
submissions: [submittedResult('quote-submitted', '2026-04-02T18:13:30.000Z')],
commands: [exactOutCommand('quote-submitted')],
inventorySnapshots: [
inventorySnapshot('2026-04-02T18:13:00.000Z', {
[BTC.assetId]: '0',
[EURE.assetId]: '100000000000000000000',
}),
],
btcAsset: BTC,
eureAsset: EURE,
now: '2026-04-02T18:13:45.000Z',
});
assert.equal(outcome.outcome_status, 'submitted');
assert.equal(outcome.attribution_status, 'unattributed');
assert.equal(outcome.attributed_inventory_delta, null);
});
test('submitted quote without settlement becomes not filled only after deadline and later inventory evidence', () => {
const [outcome] = deriveQuoteOutcomeRecords({
submissions: [submittedResult('quote-not-filled', '2026-04-02T18:13:30.000Z')],
commands: [exactOutCommand('quote-not-filled')],
inventorySnapshots: [
inventorySnapshot('2026-04-02T18:13:00.000Z', {
[BTC.assetId]: '0',
[EURE.assetId]: '100000000000000000000',
}),
inventorySnapshot('2026-04-02T18:15:40.000Z', {
[BTC.assetId]: '0',
[EURE.assetId]: '100000000000000000000',
}),
],
btcAsset: BTC,
eureAsset: EURE,
now: '2026-04-02T18:15:40.000Z',
});
assert.equal(outcome.outcome_status, 'not_filled');
assert.equal(outcome.outcome_reason, 'deadline_elapsed_without_settlement');
assert.equal(outcome.outcome_observed_at, '2026-04-02T18:15:40.000Z');
assert.equal(outcome.payload.evidence.latest_inventory_observed_at, '2026-04-02T18:15:40.000Z');
assert.equal(outcome.attributed_inventory_delta, null);
});
test('ambiguous inventory movement is not counted as completed settlement', () => {
const outcomes = deriveQuoteOutcomeRecords({
submissions: [
submittedResult('quote-a', '2026-04-02T18:13:30.000Z'),
submittedResult('quote-b', '2026-04-02T18:13:31.000Z'),
],
commands: [
exactOutCommand('quote-a'),
exactOutCommand('quote-b'),
],
inventorySnapshots: [
inventorySnapshot('2026-04-02T18:13:00.000Z', {
[BTC.assetId]: '0',
[EURE.assetId]: '100000000000000000000',
}),
inventorySnapshot('2026-04-02T18:13:33.000Z', {
[BTC.assetId]: '37014',
[EURE.assetId]: '78999978799978799978',
}),
],
btcAsset: BTC,
eureAsset: EURE,
now: '2026-04-02T18:14:00.000Z',
});
assert.deepEqual(
outcomes.map((entry) => entry.outcome_status),
['awaiting_outcome', 'awaiting_outcome'],
);
assert.deepEqual(
outcomes.map((entry) => entry.attribution_status),
['ambiguous', 'ambiguous'],
);
});