Implement pair-native trade semantics
Some checks failed
deploy / deploy (push) Failing after 43s

Proof: Pair-native trade semantics and multi-asset outcome truth; strategy, request preflight, outcome attribution, valuation visibility, dashboard labels, alerts, and ops watch paths now use DB pair/asset/route metadata with nBTC/EURe compatibility and nBTC/USDC regressions covered.

Assumptions: Postgres asset, pair, strategy config, and price route rows remain canonical; supported reference adapters remain BTC/EUR and BTC/USDC; deployment is push-driven through the existing Forgejo workflow.

Still fake: Arbitrary multi-hop valuation, new execution venues, fee-complete realized PnL, venue-native terminal fill ingestion, and autonomous optimization remain unbuilt.
This commit is contained in:
philipp 2026-05-18 18:52:18 +02:00
parent 8f109a7463
commit 729d2ade0e
30 changed files with 1504 additions and 163 deletions

View file

@ -11,7 +11,7 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent)) sys.path.insert(0, str(Path(__file__).resolve().parent))
from common import DEFAULT_NAMESPACE, config_value, kubectl from common import DEFAULT_NAMESPACE, kubectl
CONTROL_PORTS = { CONTROL_PORTS = {
@ -120,19 +120,15 @@ def main() -> int:
def load_asset_registry(*, namespace: str) -> dict[str, AssetMeta]: def load_asset_registry(*, namespace: str) -> dict[str, AssetMeta]:
btc = AssetMeta( rows = query_rows(ASSET_QUERY, namespace=namespace, columns=ASSET_COLUMNS)
asset_id=config_value("TRADING_BTC_ASSET_ID", namespace=namespace),
symbol=config_value("TRADING_BTC_SYMBOL", namespace=namespace) or "BTC",
decimals=int(config_value("TRADING_BTC_DECIMALS", namespace=namespace) or "8"),
)
eure = AssetMeta(
asset_id=config_value("TRADING_EURE_ASSET_ID", namespace=namespace),
symbol=config_value("TRADING_EURE_SYMBOL", namespace=namespace) or "EURe",
decimals=int(config_value("TRADING_EURE_DECIMALS", namespace=namespace) or "18"),
)
return { return {
btc.asset_id: btc, row["asset_id"]: AssetMeta(
eure.asset_id: eure, asset_id=row["asset_id"],
symbol=row["symbol"] or row["asset_id"],
decimals=int(row["decimals"] or "0"),
)
for row in rows
if row["asset_id"]
} }
@ -358,7 +354,8 @@ def render_decision_row(row: dict[str, str], assets: dict[str, AssetMeta]) -> st
f" direction={row['direction']}" f" direction={row['direction']}"
f" decision={row['decision']}" f" decision={row['decision']}"
f" reason={row['decision_reason']}" f" reason={row['decision_reason']}"
f" notional_eure={row['eure_notional'] or '-'}" f" notional={row.get('notional') or row.get('eure_notional') or '-'}"
f" notional_symbol={row.get('notional_symbol') or ('EURe' if row.get('eure_notional') else '-')}"
f" gross_edge_pct={row['gross_edge_pct'] or '-'}" f" gross_edge_pct={row['gross_edge_pct'] or '-'}"
f" need={need}" f" need={need}"
f" have={have}" f" have={have}"
@ -451,12 +448,39 @@ DECISION_COLUMNS = [
"decision", "decision",
"decision_reason", "decision_reason",
"gross_edge_pct", "gross_edge_pct",
"notional",
"notional_symbol",
"eure_notional", "eure_notional",
"inventory_asset", "inventory_asset",
"inventory_available", "inventory_available",
"inventory_required", "inventory_required",
] ]
ASSET_COLUMNS = [
"asset_id",
"symbol",
"decimals",
]
ASSET_QUERY = """
select
asset_id,
coalesce(symbol, ''),
coalesce(decimals::text, '')
from trading_assets
where enabled_for_inventory = true
or asset_id in (
select payload->>'asset_in' from trade_decisions where payload ? 'asset_in'
union
select payload->>'asset_out' from trade_decisions where payload ? 'asset_out'
union
select payload->>'source_asset_id' from intent_request_preflights where payload ? 'source_asset_id'
union
select payload->>'destination_asset_id' from intent_request_preflights where payload ? 'destination_asset_id'
)
order by symbol, asset_id;
"""
COMMAND_COLUMNS = [ COMMAND_COLUMNS = [
"event_id", "event_id",
"ingested_at", "ingested_at",
@ -505,6 +529,8 @@ select
coalesce(payload->>'decision', ''), coalesce(payload->>'decision', ''),
coalesce(payload->>'decision_reason', ''), coalesce(payload->>'decision_reason', ''),
coalesce(payload->>'gross_edge_pct', ''), coalesce(payload->>'gross_edge_pct', ''),
coalesce(payload->>'notional', ''),
coalesce(payload->>'notional_symbol', ''),
coalesce(payload->>'eure_notional', ''), coalesce(payload->>'eure_notional', ''),
coalesce(payload->>'inventory_asset', ''), coalesce(payload->>'inventory_asset', ''),
coalesce(payload->>'inventory_available', ''), coalesce(payload->>'inventory_available', ''),

View file

@ -464,8 +464,8 @@ async function refreshQuoteOutcomeAttributions() {
async function requireTradingConfig() { async function requireTradingConfig() {
const tradingConfig = await tradingConfigStore.getConfig(); const tradingConfig = await tradingConfigStore.getConfig();
if (!tradingConfig.ok || !tradingConfig.tradingBtc || !tradingConfig.tradingEure) { if (!tradingConfig.ok) {
throw new Error(`trading config unavailable: ${tradingConfig.blockReason || 'missing current assets'}`); throw new Error(`trading config unavailable: ${tradingConfig.blockReason || 'unavailable'}`);
} }
return tradingConfig; return tradingConfig;
} }

View file

@ -271,6 +271,8 @@ function buildBtcEurPriceEvent(now, {
price_route_id: referencePair.priceRoute.routeId, price_route_id: referencePair.priceRoute.routeId,
base_asset_id: referencePair.priceRoute.baseAssetId, base_asset_id: referencePair.priceRoute.baseAssetId,
quote_asset_id: referencePair.priceRoute.quoteAssetId, quote_asset_id: referencePair.priceRoute.quoteAssetId,
quote_per_base: eurPerBtc.toFixed(8),
base_per_quote: btcPerEur.toFixed(12),
reference_pair: 'BTC/EUR', reference_pair: 'BTC/EUR',
eur_per_btc: eurPerBtc.toFixed(8), eur_per_btc: eurPerBtc.toFixed(8),
eure_per_btc: eurPerBtc.toFixed(8), eure_per_btc: eurPerBtc.toFixed(8),
@ -308,6 +310,8 @@ function buildBtcUsdcPriceEvent(now, {
price_route_id: referencePair.priceRoute.routeId, price_route_id: referencePair.priceRoute.routeId,
base_asset_id: referencePair.priceRoute.baseAssetId, base_asset_id: referencePair.priceRoute.baseAssetId,
quote_asset_id: referencePair.priceRoute.quoteAssetId, quote_asset_id: referencePair.priceRoute.quoteAssetId,
quote_per_base: usdcPerBtc.toFixed(8),
base_per_quote: btcPerUsdc.toFixed(12),
reference_pair: 'BTC/USDC', reference_pair: 'BTC/USDC',
eur_per_btc: eurPerBtc.toFixed(8), eur_per_btc: eurPerBtc.toFixed(8),
eure_per_btc: eurPerBtc.toFixed(8), eure_per_btc: eurPerBtc.toFixed(8),

View file

@ -112,6 +112,14 @@ const state = {
const alertEngine = createAlertEngine({ const alertEngine = createAlertEngine({
activePair: config.activePair, activePair: config.activePair,
activePairs: (config.observedPairs || config.pairs || []).map((pair) => pair.key || pair.pairId).filter(Boolean),
priceRoutes: (config.pairs || [])
.filter((pair) => pair.priceRoute?.routeId)
.map((pair) => ({
pair: pair.key || pair.pairId,
price_route_id: pair.priceRoute.routeId,
reference_pair: pair.priceRoute.routeConfig?.reference_pair || pair.priceRoute.source || null,
})),
priceStaleMs: config.opsSentinelPriceStaleMs, priceStaleMs: config.opsSentinelPriceStaleMs,
inventoryStaleMs: config.opsSentinelInventoryStaleMs, inventoryStaleMs: config.opsSentinelInventoryStaleMs,
fundingCreditPendingMs: config.opsSentinelFundingCreditPendingMs, fundingCreditPendingMs: config.opsSentinelFundingCreditPendingMs,
@ -290,6 +298,7 @@ async function evaluateRuntimeHealthLoop() {
state.service_health = [...evaluateRuntimeHealth({ state.service_health = [...evaluateRuntimeHealth({
servicesByName, servicesByName,
activePair: config.activePair, activePair: config.activePair,
activePairs: (config.observedPairs || config.pairs || []).map((pair) => pair.key || pair.pairId).filter(Boolean),
activeAlerts: desiredRuntimeAlerts, activeAlerts: desiredRuntimeAlerts,
now, now,
}).values()]; }).values()];

View file

@ -113,6 +113,8 @@ async function handleDemand(event) {
if (seenQuotes.has(event.payload.quote_id)) { if (seenQuotes.has(event.payload.quote_id)) {
const pair = tradingConfig.pairByKey?.get(event.payload.pair || `${event.payload.asset_in}->${event.payload.asset_out}`); const pair = tradingConfig.pairByKey?.get(event.payload.pair || `${event.payload.asset_in}->${event.payload.asset_out}`);
const strategyConfig = pair?.strategyConfig || null; const strategyConfig = pair?.strategyConfig || null;
const legacyEureNotional = !pair?.priceRoute
|| pair.priceRoute.quoteAssetId === tradingConfig.tradingEure?.assetId;
await publishDecision({ await publishDecision({
decision_id: `duplicate-${event.payload.quote_id}`, decision_id: `duplicate-${event.payload.quote_id}`,
quote_id: event.payload.quote_id, quote_id: event.payload.quote_id,
@ -126,7 +128,10 @@ async function handleDemand(event) {
decision: 'rejected', decision: 'rejected',
decision_reason: 'duplicate_quote_id', decision_reason: 'duplicate_quote_id',
threshold_pct: strategyConfig?.edgeBps == null ? null : String(Number(strategyConfig.edgeBps) / 100), threshold_pct: strategyConfig?.edgeBps == null ? null : String(Number(strategyConfig.edgeBps) / 100),
max_notional_eure: strategyConfig?.maxNotional == null ? null : String(strategyConfig.maxNotional), max_notional: strategyConfig?.maxNotional == null ? null : String(strategyConfig.maxNotional),
max_notional_eure: legacyEureNotional && strategyConfig?.maxNotional != null
? String(strategyConfig.maxNotional)
: null,
strategy_armed: state.armed, strategy_armed: state.armed,
}); });
return; return;

View file

@ -26,6 +26,7 @@ import {
loadLatestIntentRequestSubmission, loadLatestIntentRequestSubmission,
loadLatestInventorySnapshot, loadLatestInventorySnapshot,
loadLatestMarketPrice, loadLatestMarketPrice,
loadLatestMarketPriceForRoute,
refreshIntentRequestOutcomes, refreshIntentRequestOutcomes,
seedTradingConfig, seedTradingConfig,
} from '../lib/postgres.mjs'; } from '../lib/postgres.mjs';
@ -452,7 +453,11 @@ const controlApi = startControlApi({
function createIntentRequestStore() { function createIntentRequestStore() {
return { return {
loadLatestInventorySnapshot: () => loadLatestInventorySnapshot(requestPool), loadLatestInventorySnapshot: () => loadLatestInventorySnapshot(requestPool),
loadLatestMarketPrice: () => loadLatestMarketPrice(requestPool), loadLatestMarketPrice: ({ priceRouteId } = {}) => (
priceRouteId
? loadLatestMarketPriceForRoute(requestPool, { priceRouteId })
: loadLatestMarketPrice(requestPool)
),
findPreflight: ({ requestId = null, idempotencyKey = null } = {}) => ( findPreflight: ({ requestId = null, idempotencyKey = null } = {}) => (
loadIntentRequestPreflightByIdOrKey(requestPool, { requestId, idempotencyKey }) loadIntentRequestPreflightByIdOrKey(requestPool, { requestId, idempotencyKey })
), ),
@ -504,8 +509,8 @@ function createIntentRequestStore() {
}, },
refreshOutcomes: async () => { refreshOutcomes: async () => {
const tradingConfig = await tradingConfigStore.getConfig(); const tradingConfig = await tradingConfigStore.getConfig();
if (!tradingConfig.ok || !tradingConfig.tradingBtc || !tradingConfig.tradingEure) { if (!tradingConfig.ok) {
throw new Error(`trading config unavailable: ${tradingConfig.blockReason || 'missing current assets'}`); throw new Error(`trading config unavailable: ${tradingConfig.blockReason || 'unavailable'}`);
} }
return refreshIntentRequestOutcomes(requestPool, { return refreshIntentRequestOutcomes(requestPool, {
btcAsset: tradingConfig.tradingBtc, btcAsset: tradingConfig.tradingBtc,

View file

@ -2,6 +2,8 @@ const DEFAULT_RECENT_LIMIT = 50;
export function createAlertEngine({ export function createAlertEngine({
activePair, activePair,
activePairs = activePair ? [activePair] : [],
priceRoutes = [],
priceStaleMs, priceStaleMs,
inventoryStaleMs, inventoryStaleMs,
fundingCreditPendingMs, fundingCreditPendingMs,
@ -11,6 +13,7 @@ export function createAlertEngine({
}) { }) {
const state = { const state = {
latest_price: null, latest_price: null,
latest_prices_by_route: {},
latest_inventory: null, latest_inventory: null,
latest_liquidity_action: null, latest_liquidity_action: null,
latest_trade_result: null, latest_trade_result: null,
@ -26,6 +29,7 @@ export function createAlertEngine({
switch (topic) { switch (topic) {
case 'ref.market_price': case 'ref.market_price':
state.latest_price = payload; state.latest_price = payload;
if (payload?.price_route_id) state.latest_prices_by_route[payload.price_route_id] = payload;
break; break;
case 'state.intent_inventory': case 'state.intent_inventory':
state.latest_inventory = payload; state.latest_inventory = payload;
@ -48,6 +52,8 @@ export function createAlertEngine({
return evaluateAlerts({ return evaluateAlerts({
state, state,
activePair, activePair,
activePairs,
priceRoutes,
priceStaleMs, priceStaleMs,
inventoryStaleMs, inventoryStaleMs,
fundingCreditPendingMs, fundingCreditPendingMs,
@ -60,6 +66,8 @@ export function createAlertEngine({
return evaluateAlerts({ return evaluateAlerts({
state, state,
activePair, activePair,
activePairs,
priceRoutes,
priceStaleMs, priceStaleMs,
inventoryStaleMs, inventoryStaleMs,
fundingCreditPendingMs, fundingCreditPendingMs,
@ -79,6 +87,8 @@ export function createAlertEngine({
getState(now = new Date().toISOString()) { getState(now = new Date().toISOString()) {
return summarizeState({ return summarizeState({
state, state,
activePairs,
priceRoutes,
evaluationIntervalMs, evaluationIntervalMs,
now, now,
}); });
@ -89,6 +99,8 @@ export function createAlertEngine({
function evaluateAlerts({ function evaluateAlerts({
state, state,
activePair, activePair,
activePairs = activePair ? [activePair] : [],
priceRoutes = [],
priceStaleMs, priceStaleMs,
inventoryStaleMs, inventoryStaleMs,
fundingCreditPendingMs, fundingCreditPendingMs,
@ -99,26 +111,41 @@ function evaluateAlerts({
const desired = new Map(); const desired = new Map();
const nowValue = timestampValue(now); const nowValue = timestampValue(now);
const priceAgeMs = ageMs(state.latest_price?.observed_at || state.latest_price?.ingested_at, nowValue); const routesToCheck = priceRoutes.length
if (priceAgeMs == null || priceAgeMs > priceStaleMs) { ? priceRoutes
: [{
pair: activePair,
price_route_id: state.latest_price?.price_route_id || null,
reference_pair: state.latest_price?.reference_pair || null,
}];
for (const route of routesToCheck) {
const latestPrice = route.price_route_id
? state.latest_prices_by_route[route.price_route_id]
: state.latest_price;
const priceAgeMs = ageMs(latestPrice?.observed_at || latestPrice?.ingested_at, nowValue);
if (priceAgeMs != null && priceAgeMs <= priceStaleMs) continue;
desired.set( desired.set(
buildAlertKey({ buildAlertKey({
alertCode: 'reference_price_stale', alertCode: 'reference_price_stale',
serviceScope: 'market-reference-ingest', serviceScope: 'market-reference-ingest',
pair: activePair, pair: route.pair || activePair,
assetId: route.price_route_id || null,
}), }),
{ {
alert_code: 'reference_price_stale', alert_code: 'reference_price_stale',
severity: 'warning', severity: 'warning',
reason: priceAgeMs == null reason: priceAgeMs == null
? 'no reference price has been observed' ? `no reference price has been observed for ${route.reference_pair || route.price_route_id || 'route'}`
: `reference price age ${priceAgeMs}ms exceeds ${priceStaleMs}ms`, : `reference price age ${priceAgeMs}ms exceeds ${priceStaleMs}ms for ${route.reference_pair || route.price_route_id || 'route'}`,
service_scope: 'market-reference-ingest', service_scope: 'market-reference-ingest',
pair: activePair, pair: route.pair || activePair,
asset_id: null, asset_id: null,
tx_hash: null, tx_hash: null,
details: { details: {
last_price_at: state.latest_price?.observed_at || state.latest_price?.ingested_at || null, price_route_id: route.price_route_id || null,
reference_pair: route.reference_pair || null,
active_pairs: activePairs,
last_price_at: latestPrice?.observed_at || latestPrice?.ingested_at || null,
age_ms: priceAgeMs, age_ms: priceAgeMs,
stale_after_ms: priceStaleMs, stale_after_ms: priceStaleMs,
}, },
@ -411,7 +438,7 @@ function reconcileRuntimeAlertState({
return transitions; return transitions;
} }
function summarizeState({ state, evaluationIntervalMs, now }) { function summarizeState({ state, activePairs = [], priceRoutes = [], evaluationIntervalMs, now }) {
const activeAlerts = Object.values(state.active_alerts) const activeAlerts = Object.values(state.active_alerts)
.sort((left, right) => timestampValue(right.first_raised_at) - timestampValue(left.first_raised_at)); .sort((left, right) => timestampValue(right.first_raised_at) - timestampValue(left.first_raised_at));
const nowValue = timestampValue(now); const nowValue = timestampValue(now);
@ -421,8 +448,16 @@ function summarizeState({ state, evaluationIntervalMs, now }) {
recent_transitions: state.recent_transitions, recent_transitions: state.recent_transitions,
last_evaluated_at: state.last_evaluated_at, last_evaluated_at: state.last_evaluated_at,
stale: ageMs(state.last_evaluated_at, nowValue) > (evaluationIntervalMs * 2), stale: ageMs(state.last_evaluated_at, nowValue) > (evaluationIntervalMs * 2),
active_pairs: activePairs,
price_routes: priceRoutes,
latest_inputs: { latest_inputs: {
market_price_at: state.latest_price?.observed_at || state.latest_price?.ingested_at || null, market_price_at: state.latest_price?.observed_at || state.latest_price?.ingested_at || null,
market_prices_by_route: Object.fromEntries(
Object.entries(state.latest_prices_by_route || {}).map(([routeId, price]) => [
routeId,
price?.observed_at || price?.ingested_at || null,
]),
),
inventory_at: state.latest_inventory?.synced_at || state.latest_inventory?.ingested_at || null, inventory_at: state.latest_inventory?.synced_at || state.latest_inventory?.ingested_at || null,
liquidity_action_at: state.latest_liquidity_action?.observed_at || null, liquidity_action_at: state.latest_liquidity_action?.observed_at || null,
trade_result_at: state.latest_trade_result?.ingested_at || null, trade_result_at: state.latest_trade_result?.ingested_at || null,

View file

@ -4,7 +4,7 @@ import { serializeError } from './log.mjs';
import { import {
applySlippageBps, applySlippageBps,
buildSolverQuoteRequest, buildSolverQuoteRequest,
computeBtcReceiveUnitsFromEure, formatUnitsDecimal,
futureIso, futureIso,
isExpired, isExpired,
normalizeRelayPublishResponse, normalizeRelayPublishResponse,
@ -12,6 +12,12 @@ import {
parseDecimalToUnits, parseDecimalToUnits,
selectBestSolverQuote, selectBestSolverQuote,
} from './intent-requests.mjs'; } from './intent-requests.mjs';
import {
classifyRouteDirection,
computeDestinationAmountUnitsFromRoute,
isSupportedPriceRouteSource,
resolveRouteRates,
} from './route-rates.mjs';
import { buildIntentRequestSubmission } from '../venues/near-intents/signing.mjs'; import { buildIntentRequestSubmission } from '../venues/near-intents/signing.mjs';
export function createIntentRequestController({ export function createIntentRequestController({
@ -41,28 +47,25 @@ export function createIntentRequestController({
const requestPair = await resolveIntentRequestPair({ body, config, getTradingConfig }); const requestPair = await resolveIntentRequestPair({ body, config, getTradingConfig });
const sourceAsset = requestPair.sourceAsset; const sourceAsset = requestPair.sourceAsset;
const destinationAsset = requestPair.destinationAsset; const destinationAsset = requestPair.destinationAsset;
const amountEure = String( const sourceAmount = String(
body.amount_eure body.source_amount
|| body.amount || body.amount
|| body.amount_eure
|| requestPair.requestDefaultNotional || requestPair.requestDefaultNotional
|| config.intentRequestDefaultAmountEure || config.intentRequestDefaultAmountEure
|| '5', || '5',
); );
const legacyAmountEure = body.amount_eure == null ? null : String(body.amount_eure);
const slippageBps = Number(body.slippage_bps ?? requestPair.slippageBps ?? config.intentRequestDefaultSlippageBps ?? 200); const slippageBps = Number(body.slippage_bps ?? requestPair.slippageBps ?? config.intentRequestDefaultSlippageBps ?? 200);
const minDeadlineMs = Number(body.min_deadline_ms || requestPair.minDeadlineMs || config.intentRequestMinDeadlineMs || 60_000); const minDeadlineMs = Number(body.min_deadline_ms || requestPair.minDeadlineMs || config.intentRequestMinDeadlineMs || 60_000);
const maxAmountUnits = requestPair.requestMaxNotional == null
? null
: parseDecimalToUnits(
String(requestPair.requestMaxNotional),
sourceAsset.decimals,
{ field: 'intent_request_max_notional' },
);
let sourceAmountUnits = '0'; let sourceAmountUnits = '0';
let expectedDestinationAmountUnits = '0'; let expectedDestinationAmountUnits = '0';
let minDestinationAmountUnits = '0'; let minDestinationAmountUnits = '0';
let inventorySnapshot = null; let inventorySnapshot = null;
let marketPrice = null; let marketPrice = null;
let routeDirection = null;
let routeRates = null;
let signerRegistered = null; let signerRegistered = null;
let solverQuoteResponse = null; let solverQuoteResponse = null;
let solverQuotes = []; let solverQuotes = [];
@ -81,12 +84,23 @@ export function createIntentRequestController({
requestPair.reasonText, requestPair.reasonText,
); );
} }
sourceAmountUnits = parseDecimalToUnits(amountEure, sourceAsset.decimals, { field: 'amount_eure' }); if (!Number.isInteger(sourceAsset?.decimals) || !Number.isInteger(destinationAsset?.decimals)) {
blockedBeforeQuote = true;
throw codedError('asset_decimals_missing', 'Source and destination asset decimals are required.');
}
const maxAmountUnits = requestPair.requestMaxNotional == null
? null
: parseDecimalToUnits(
String(requestPair.requestMaxNotional),
sourceAsset.decimals,
{ field: 'intent_request_max_notional' },
);
sourceAmountUnits = parseDecimalToUnits(sourceAmount, sourceAsset.decimals, { field: 'source_amount' });
if (maxAmountUnits != null && BigInt(sourceAmountUnits) > BigInt(maxAmountUnits)) { if (maxAmountUnits != null && BigInt(sourceAmountUnits) > BigInt(maxAmountUnits)) {
blockedBeforeQuote = true; blockedBeforeQuote = true;
throw codedError( throw codedError(
'amount_exceeds_request_limit', 'amount_exceeds_request_limit',
`Requested ${amountEure} ${sourceAsset.symbol} exceeds configured live request limit ${requestPair.requestMaxNotional} ${sourceAsset.symbol}.`, `Requested ${sourceAmount} ${sourceAsset.symbol} exceeds configured live request limit ${requestPair.requestMaxNotional} ${sourceAsset.symbol}.`,
); );
} }
if (!Number.isInteger(slippageBps) || slippageBps < 0) { if (!Number.isInteger(slippageBps) || slippageBps < 0) {
@ -106,7 +120,7 @@ export function createIntentRequestController({
[inventorySnapshot, marketPrice, signerRegistered] = await Promise.all([ [inventorySnapshot, marketPrice, signerRegistered] = await Promise.all([
store.loadLatestInventorySnapshot(), store.loadLatestInventorySnapshot(),
store.loadLatestMarketPrice(), store.loadLatestMarketPrice({ priceRouteId: requestPair.priceRoute?.routeId || null }),
verifierClient.isPublicKeyRegistered({ accountId: config.nearIntentsAccountId }), verifierClient.isPublicKeyRegistered({ accountId: config.nearIntentsAccountId }),
]); ]);
@ -120,9 +134,22 @@ export function createIntentRequestController({
blockedBeforeQuote = true; blockedBeforeQuote = true;
throw codedError('stale_inventory', 'Inventory snapshot is too stale for request creation.'); throw codedError('stale_inventory', 'Inventory snapshot is too stale for request creation.');
} }
if (!marketPrice?.payload?.eure_per_btc) { routeDirection = classifyRouteDirection({
sourceAssetId: sourceAsset.assetId,
destinationAssetId: destinationAsset.assetId,
priceRoute: requestPair.priceRoute,
});
routeRates = resolveRouteRates({
price: marketPrice?.payload || null,
priceRoute: requestPair.priceRoute,
direction: routeDirection,
});
if (!routeRates.ok) {
blockedBeforeQuote = true; blockedBeforeQuote = true;
throw codedError('reference_price_unavailable', 'No BTC/EUR reference price is available.'); throw codedError(
routeRates.reason === 'unsupported_price_route' ? 'unsupported_price_route' : 'reference_price_unavailable',
`No usable reference price is available for route ${requestPair.priceRoute?.routeId || 'unknown'}.`,
);
} }
if (!isFresh(priceObservedAt, requestPair.priceMaxAgeMs ?? config.intentRequestPriceMaxAgeMs ?? config.strategyPriceMaxAgeMs, now())) { if (!isFresh(priceObservedAt, requestPair.priceMaxAgeMs ?? config.intentRequestPriceMaxAgeMs ?? config.strategyPriceMaxAgeMs, now())) {
blockedBeforeQuote = true; blockedBeforeQuote = true;
@ -136,14 +163,19 @@ export function createIntentRequestController({
const spendableUnits = String(inventorySnapshot.payload.spendable[sourceAsset.assetId] || '0'); const spendableUnits = String(inventorySnapshot.payload.spendable[sourceAsset.assetId] || '0');
if (BigInt(spendableUnits) < BigInt(sourceAmountUnits)) { if (BigInt(spendableUnits) < BigInt(sourceAmountUnits)) {
blockedBeforeQuote = true; blockedBeforeQuote = true;
throw codedError('insufficient_spendable_eure', 'Spendable EURe is below the requested amount.'); throw codedError(
insufficientInventoryCode(sourceAsset),
`Spendable ${sourceAsset.symbol || sourceAsset.assetId} is below the requested amount.`,
);
} }
expectedDestinationAmountUnits = computeBtcReceiveUnitsFromEure({ expectedDestinationAmountUnits = computeDestinationAmountUnitsFromRoute({
eureUnits: sourceAmountUnits, sourceAmountUnits,
eurPerBtc: marketPrice.payload.eure_per_btc, sourceDecimals: sourceAsset.decimals,
eureDecimals: sourceAsset.decimals, destinationDecimals: destinationAsset.decimals,
btcDecimals: destinationAsset.decimals, direction: routeDirection,
quotePerBase: routeRates.quotePerBase,
basePerQuote: routeRates.basePerQuote,
}); });
minDestinationAmountUnits = applySlippageBps(expectedDestinationAmountUnits, slippageBps); minDestinationAmountUnits = applySlippageBps(expectedDestinationAmountUnits, slippageBps);
@ -182,6 +214,14 @@ export function createIntentRequestController({
}); });
} }
const preflightNotional = buildPreflightNotional({
sourceAsset,
destinationAsset,
sourceAmount,
sourceAmountUnits,
expectedDestinationAmountUnits,
priceRoute: requestPair.priceRoute,
});
const payload = { const payload = {
request_id: requestId, request_id: requestId,
idempotency_key: idempotencyKey, idempotency_key: idempotencyKey,
@ -207,14 +247,22 @@ export function createIntentRequestController({
? null ? null
: Number(requestPair.requestMaxSlippageBps), : Number(requestPair.requestMaxSlippageBps),
price_route_id: requestPair.priceRoute?.routeId || null, price_route_id: requestPair.priceRoute?.routeId || null,
reference_price_id: routeRates?.referencePriceId || marketPrice?.payload?.price_id || null,
route_direction: routeDirection,
source_asset_id: sourceAsset.assetId, source_asset_id: sourceAsset.assetId,
source_symbol: sourceAsset.symbol, source_symbol: sourceAsset.symbol,
source_decimals: sourceAsset.decimals, source_decimals: sourceAsset.decimals,
destination_asset_id: destinationAsset.assetId, destination_asset_id: destinationAsset.assetId,
destination_symbol: destinationAsset.symbol, destination_symbol: destinationAsset.symbol,
destination_decimals: destinationAsset.decimals, destination_decimals: destinationAsset.decimals,
source_amount: sourceAmount,
source_amount_units: sourceAmountUnits, source_amount_units: sourceAmountUnits,
amount_eure: amountEure, destination_amount_units: expectedDestinationAmountUnits,
notional: preflightNotional.notional,
notional_asset_id: preflightNotional.notionalAssetId,
notional_symbol: preflightNotional.notionalSymbol,
amount_eure: legacyAmountEure
?? (sourceAsset.assetId === config.tradingEure?.assetId ? sourceAmount : null),
expected_destination_amount_units: expectedDestinationAmountUnits, expected_destination_amount_units: expectedDestinationAmountUnits,
min_destination_amount_units: minDestinationAmountUnits, min_destination_amount_units: minDestinationAmountUnits,
quoted_destination_amount_units: selectedQuote?.amount_out || null, quoted_destination_amount_units: selectedQuote?.amount_out || null,
@ -235,7 +283,14 @@ export function createIntentRequestController({
market_price: marketPrice ? { market_price: marketPrice ? {
ingested_at: marketPrice.ingested_at, ingested_at: marketPrice.ingested_at,
observed_at: marketPrice.payload?.observed_at || null, observed_at: marketPrice.payload?.observed_at || null,
price_route_id: marketPrice.payload?.price_route_id || requestPair.priceRoute?.routeId || null,
reference_pair: marketPrice.payload?.reference_pair || requestPair.priceRoute?.routeConfig?.reference_pair || null,
base_asset_id: marketPrice.payload?.base_asset_id || requestPair.priceRoute?.baseAssetId || null,
quote_asset_id: marketPrice.payload?.quote_asset_id || requestPair.priceRoute?.quoteAssetId || null,
quote_per_base: routeRates?.quotePerBase == null ? null : String(routeRates.quotePerBase),
base_per_quote: routeRates?.basePerQuote == null ? null : String(routeRates.basePerQuote),
eure_per_btc: marketPrice.payload?.eure_per_btc || null, eure_per_btc: marketPrice.payload?.eure_per_btc || null,
usdc_per_btc: marketPrice.payload?.usdc_per_btc || null,
price_id: marketPrice.payload?.price_id || null, price_id: marketPrice.payload?.price_id || null,
} : null, } : null,
solver_quote_count: solverQuotes.length, solver_quote_count: solverQuotes.length,
@ -313,10 +368,14 @@ export function createIntentRequestController({
const latestInventory = await store.loadLatestInventorySnapshot(); const latestInventory = await store.loadLatestInventorySnapshot();
const spendableUnits = String(latestInventory?.payload?.spendable?.[preflight.source_asset_id] || '0'); const spendableUnits = String(latestInventory?.payload?.spendable?.[preflight.source_asset_id] || '0');
if (BigInt(spendableUnits) < BigInt(preflight.source_amount_units)) { if (BigInt(spendableUnits) < BigInt(preflight.source_amount_units)) {
const sourceAssetForReason = {
assetId: preflight.source_asset_id,
symbol: preflight.source_symbol,
};
const blocked = await recordSubmissionResult(preflight, { const blocked = await recordSubmissionResult(preflight, {
status: 'blocked', status: 'blocked',
result_code: 'insufficient_spendable_eure', result_code: insufficientInventoryCode(sourceAssetForReason),
result_text: 'Spendable EURe changed below the requested amount before submit.', result_text: `Spendable ${preflight.source_symbol || preflight.source_asset_id} changed below the requested amount before submit.`,
}); });
return { preflight, submission_result: blocked }; return { preflight, submission_result: blocked };
} }
@ -458,9 +517,14 @@ export function createIntentRequestController({
max_notional: preflight.max_notional || null, max_notional: preflight.max_notional || null,
request_max_notional: preflight.request_max_notional || null, request_max_notional: preflight.request_max_notional || null,
price_route_id: preflight.price_route_id || null, price_route_id: preflight.price_route_id || null,
reference_price_id: preflight.reference_price_id || null,
notional: preflight.notional || null,
notional_asset_id: preflight.notional_asset_id || null,
notional_symbol: preflight.notional_symbol || null,
source_asset_id: preflight.source_asset_id, source_asset_id: preflight.source_asset_id,
destination_asset_id: preflight.destination_asset_id, destination_asset_id: preflight.destination_asset_id,
source_amount_units: preflight.source_amount_units, source_amount_units: preflight.source_amount_units,
destination_amount_units: preflight.destination_amount_units || null,
min_destination_amount_units: preflight.min_destination_amount_units, min_destination_amount_units: preflight.min_destination_amount_units,
quote_hash: preflight.selected_quote?.quote_hash || extra.quote_hash || null, quote_hash: preflight.selected_quote?.quote_hash || extra.quote_hash || null,
lifecycle: { lifecycle: {
@ -490,7 +554,13 @@ async function resolveIntentRequestPair({ body, config, getTradingConfig }) {
destinationAsset: config.tradingBtc, destinationAsset: config.tradingBtc,
pair: null, pair: null,
strategyConfig: null, strategyConfig: null,
priceRoute: null, priceRoute: {
routeId: 'legacy-btc-eur-reference',
source: 'btc_eur_reference',
baseAssetId: config.tradingBtc?.assetId,
quoteAssetId: config.tradingEure?.assetId,
routeConfig: { reference_pair: 'BTC/EUR' },
},
requestDefaultNotional: config.intentRequestDefaultAmountEure, requestDefaultNotional: config.intentRequestDefaultAmountEure,
requestMaxNotional: config.intentRequestMaxAmountEure, requestMaxNotional: config.intentRequestMaxAmountEure,
slippageBps: config.intentRequestDefaultSlippageBps, slippageBps: config.intentRequestDefaultSlippageBps,
@ -513,9 +583,12 @@ async function resolveIntentRequestPair({ body, config, getTradingConfig }) {
}); });
} }
const requestedSource = body.source_asset_id || body.asset_in || null; const requestedPairId = body.pair_id || null;
const requestedDestination = body.destination_asset_id || body.asset_out || null; const requestedSource = body.source_asset_id || body.asset_in || body.asset_in_id || null;
const pair = requestedSource && requestedDestination const requestedDestination = body.destination_asset_id || body.asset_out || body.asset_out_id || null;
const pair = requestedPairId
? tradingConfig.pairById?.get(requestedPairId) || tradingConfig.pairByKey?.get(requestedPairId)
: requestedSource && requestedDestination
? tradingConfig.pairByKey.get(`${requestedSource}->${requestedDestination}`) ? tradingConfig.pairByKey.get(`${requestedSource}->${requestedDestination}`)
: tradingConfig.defaultTakerPair; : tradingConfig.defaultTakerPair;
@ -548,10 +621,21 @@ async function resolveIntentRequestPair({ body, config, getTradingConfig }) {
}); });
} }
if (pair.priceRoute?.source !== 'btc_eur_reference') { const priceRoute = normalizeRequestPriceRoute(pair);
if (!priceRoute) {
return blockedRequestPair({ return blockedRequestPair({
reasonCode: 'price_route_missing', reasonCode: 'price_route_missing',
reasonText: 'Only the DB-backed BTC/EUR price route is supported for request creation in this turn.', reasonText: 'The selected pair has no DB-backed price route for request creation.',
pair,
sourceAsset: pair.assetIn,
destinationAsset: pair.assetOut,
});
}
if (!isSupportedPriceRouteSource(priceRoute.source)) {
return blockedRequestPair({
reasonCode: 'unsupported_price_route',
reasonText: `The selected pair uses unsupported price route source ${priceRoute.source || 'unknown'}.`,
pair, pair,
sourceAsset: pair.assetIn, sourceAsset: pair.assetIn,
destinationAsset: pair.assetOut, destinationAsset: pair.assetOut,
@ -565,7 +649,7 @@ async function resolveIntentRequestPair({ body, config, getTradingConfig }) {
destinationAsset: pair.assetOut, destinationAsset: pair.assetOut,
pair, pair,
strategyConfig, strategyConfig,
priceRoute: pair.priceRoute, priceRoute,
requestDefaultNotional: requestDefaultNotional:
strategyConfig.requestDefaultNotional || config.intentRequestDefaultAmountEure, strategyConfig.requestDefaultNotional || config.intentRequestDefaultAmountEure,
requestMaxNotional: strategyConfig.requestMaxNotional ?? null, requestMaxNotional: strategyConfig.requestMaxNotional ?? null,
@ -577,6 +661,22 @@ async function resolveIntentRequestPair({ body, config, getTradingConfig }) {
}; };
} }
function normalizeRequestPriceRoute(pair) {
const priceRoute = pair?.priceRoute || null;
if (!priceRoute) return null;
if (priceRoute.baseAssetId && priceRoute.quoteAssetId) return priceRoute;
const assets = [pair.assetIn, pair.assetOut].filter(Boolean);
const baseAsset = assets.find((asset) => asset.symbol === 'BTC') || null;
const quoteAsset = priceRoute.source === 'btc_usdc_reference'
? assets.find((asset) => asset.symbol === 'USDC')
: assets.find((asset) => asset.symbol === 'EURe') || assets.find((asset) => asset.symbol !== 'BTC');
return {
...priceRoute,
baseAssetId: priceRoute.baseAssetId || baseAsset?.assetId || null,
quoteAssetId: priceRoute.quoteAssetId || quoteAsset?.assetId || null,
};
}
function blockedRequestPair({ function blockedRequestPair({
reasonCode, reasonCode,
reasonText, reasonText,
@ -596,6 +696,51 @@ function blockedRequestPair({
}; };
} }
function buildPreflightNotional({
sourceAsset,
destinationAsset,
sourceAmount,
sourceAmountUnits,
expectedDestinationAmountUnits,
priceRoute,
} = {}) {
const quoteAssetId = priceRoute?.quoteAssetId || null;
if (!quoteAssetId) {
return {
notional: null,
notionalAssetId: null,
notionalSymbol: null,
};
}
if (sourceAsset?.assetId === quoteAssetId) {
return {
notional: sourceAmount,
notionalAssetId: sourceAsset.assetId,
notionalSymbol: sourceAsset.symbol || null,
};
}
if (destinationAsset?.assetId === quoteAssetId) {
return {
notional: expectedDestinationAmountUnits
? formatUnitsDecimal(expectedDestinationAmountUnits, destinationAsset.decimals)
: null,
notionalAssetId: destinationAsset.assetId,
notionalSymbol: destinationAsset.symbol || null,
};
}
return {
notional: null,
notionalAssetId: quoteAssetId,
notionalSymbol: null,
};
}
function insufficientInventoryCode(asset) {
const symbol = String(asset?.symbol || '').trim().toLowerCase();
if (/^[a-z0-9]+$/.test(symbol)) return `insufficient_spendable_${symbol}`;
return 'insufficient_source_inventory';
}
function isFresh(timestamp, maxAgeMs, nowMs) { function isFresh(timestamp, maxAgeMs, nowMs) {
const parsed = Date.parse(timestamp || ''); const parsed = Date.parse(timestamp || '');
if (!Number.isFinite(parsed)) return false; if (!Number.isFinite(parsed)) return false;

View file

@ -16,13 +16,21 @@ export function deriveIntentRequestOutcomeRecords({
attributionWindowMs = DEFAULT_ATTRIBUTION_WINDOW_MS, attributionWindowMs = DEFAULT_ATTRIBUTION_WINDOW_MS,
settlementGraceMs = DEFAULT_SETTLEMENT_GRACE_MS, settlementGraceMs = DEFAULT_SETTLEMENT_GRACE_MS,
} = {}) { } = {}) {
const activeAssetIds = [btcAsset?.assetId, eureAsset?.assetId].filter(Boolean);
const preflightsByRequest = new Map( const preflightsByRequest = new Map(
preflights preflights
.map(normalizePreflight) .map(normalizePreflight)
.filter((entry) => entry?.request_id) .filter((entry) => entry?.request_id)
.map((entry) => [entry.request_id, entry]), .map((entry) => [entry.request_id, entry]),
); );
const expectedDeltasByRequest = new Map();
for (const preflight of preflightsByRequest.values()) {
const expectedDelta = buildExpectedRequestDelta(preflight, null);
if (expectedDelta) expectedDeltasByRequest.set(preflight.request_id, expectedDelta);
}
const activeAssetIds = uniqueAssetIds([
...expectedDeltasByRequest.values(),
Object.fromEntries([btcAsset?.assetId, eureAsset?.assetId].filter(Boolean).map((assetId) => [assetId, 0n])),
]);
const latestSubmissionByRequest = new Map(); const latestSubmissionByRequest = new Map();
for (const submission of submissions.map(normalizeSubmission).filter(Boolean)) { for (const submission of submissions.map(normalizeSubmission).filter(Boolean)) {
@ -273,6 +281,7 @@ function deriveOneOutcome({
function buildExpectedRequestDelta(preflight, submission) { function buildExpectedRequestDelta(preflight, submission) {
const destinationAmount = submission?.destination_amount_units const destinationAmount = submission?.destination_amount_units
|| preflight?.destination_amount_units
|| preflight?.selected_quote?.amount_out || preflight?.selected_quote?.amount_out
|| preflight?.quoted_destination_amount_units; || preflight?.quoted_destination_amount_units;
if (!preflight?.source_asset_id || !preflight?.destination_asset_id) return null; if (!preflight?.source_asset_id || !preflight?.destination_asset_id) return null;
@ -302,9 +311,14 @@ function baseOutcomeRecord({
submission_id: submission?.submission_id || null, submission_id: submission?.submission_id || null,
intent_hash: submission?.intent_hash || null, intent_hash: submission?.intent_hash || null,
source_asset_id: preflight.source_asset_id, source_asset_id: preflight.source_asset_id,
source_symbol: preflight.source_symbol || null,
destination_asset_id: preflight.destination_asset_id, destination_asset_id: preflight.destination_asset_id,
destination_symbol: preflight.destination_symbol || null,
source_amount_units: preflight.source_amount_units, source_amount_units: preflight.source_amount_units,
destination_amount_units: submission?.destination_amount_units || null, destination_amount_units: submission?.destination_amount_units || preflight.destination_amount_units || null,
notional: preflight.notional || null,
notional_asset_id: preflight.notional_asset_id || null,
notional_symbol: preflight.notional_symbol || null,
min_destination_amount_units: preflight.min_destination_amount_units, min_destination_amount_units: preflight.min_destination_amount_units,
quote_hash: submission?.quote_hash || preflight.selected_quote?.quote_hash || null, quote_hash: submission?.quote_hash || preflight.selected_quote?.quote_hash || null,
submitted_at: submission?.submitted_at || null, submitted_at: submission?.submitted_at || null,
@ -353,6 +367,10 @@ function movementMatchesExpectedDelta({
for (const [assetId, expected] of Object.entries(expectedDelta)) { for (const [assetId, expected] of Object.entries(expectedDelta)) {
if (safeBigInt(movement.delta_units?.[assetId]) !== expected) return false; if (safeBigInt(movement.delta_units?.[assetId]) !== expected) return false;
} }
for (const [assetId, actual] of Object.entries(movement.delta_units || {})) {
if (Object.prototype.hasOwnProperty.call(expectedDelta, assetId)) continue;
if (safeBigInt(actual) !== 0n) return false;
}
return true; return true;
} }
@ -405,8 +423,14 @@ function normalizePreflight(entry) {
state: payload.state || null, state: payload.state || null,
reason_code: payload.reason_code || null, reason_code: payload.reason_code || null,
source_asset_id: payload.source_asset_id || null, source_asset_id: payload.source_asset_id || null,
source_symbol: payload.source_symbol || null,
destination_asset_id: payload.destination_asset_id || null, destination_asset_id: payload.destination_asset_id || null,
destination_symbol: payload.destination_symbol || null,
source_amount_units: payload.source_amount_units || null, source_amount_units: payload.source_amount_units || null,
destination_amount_units: payload.destination_amount_units || null,
notional: payload.notional || null,
notional_asset_id: payload.notional_asset_id || null,
notional_symbol: payload.notional_symbol || null,
min_destination_amount_units: payload.min_destination_amount_units || null, min_destination_amount_units: payload.min_destination_amount_units || null,
quoted_destination_amount_units: payload.quoted_destination_amount_units || null, quoted_destination_amount_units: payload.quoted_destination_amount_units || null,
selected_quote: payload.selected_quote || null, selected_quote: payload.selected_quote || null,
@ -471,6 +495,16 @@ function payloadOf(entry) {
return entry.payload || entry; return entry.payload || entry;
} }
function uniqueAssetIds(deltaRecords = []) {
const ids = new Set();
for (const record of deltaRecords || []) {
for (const assetId of Object.keys(record || {})) {
if (assetId) ids.add(assetId);
}
}
return [...ids];
}
function safeBigInt(value) { function safeBigInt(value) {
if (value == null || value === '') return 0n; if (value == null || value === '') return 0n;
return BigInt(String(value)); return BigInt(String(value));

View file

@ -103,8 +103,8 @@ const CONTROL_DEFINITIONS = [
action: 'intent-request-preflight', action: 'intent-request-preflight',
method: 'POST', method: 'POST',
path: '/intent-request/preflight', path: '/intent-request/preflight',
label: 'Preflight BTC Request', label: 'Preflight Pair Request',
description: 'Ask solvers for an EURe-to-BTC request quote without submitting live funds.', description: 'Ask solvers for a configured pair request quote without submitting live funds.',
page: 'funds', page: 'funds',
risk_class: 'safe', risk_class: 'safe',
}, },
@ -113,8 +113,8 @@ const CONTROL_DEFINITIONS = [
action: 'intent-request-submit', action: 'intent-request-submit',
method: 'POST', method: 'POST',
path: '/intent-request/submit', path: '/intent-request/submit',
label: 'Submit BTC Request', label: 'Submit Pair Request',
description: 'Submit a previously drafted EURe-to-BTC request. Relay acceptance is not settlement.', description: 'Submit a previously drafted pair request. Relay acceptance is not settlement.',
page: 'funds', page: 'funds',
risk_class: 'live_funds', risk_class: 'live_funds',
}, },
@ -702,12 +702,18 @@ export function buildProfitabilitySummary({ metric, submissionSummary } = {}) {
} }
export function buildLiveStatusBar(state) { export function buildLiveStatusBar(state) {
const referenceLabel = state.latest_market_price?.reference_pair || 'Reference route';
const referenceValue = state.latest_market_price?.quote_per_base
|| state.latest_market_price?.eure_per_btc
|| null;
return { return {
near_intents_upstream_status: state.near_intents_status?.status || null, near_intents_upstream_status: state.near_intents_status?.status || null,
near_intents_upstream_label: state.near_intents_status?.label || null, near_intents_upstream_label: state.near_intents_status?.label || null,
near_intents_upstream_reason: state.near_intents_status?.decisive_reason || null, near_intents_upstream_reason: state.near_intents_status?.decisive_reason || null,
near_intents_upstream_observed_at: state.near_intents_status?.observed_at || null, near_intents_upstream_observed_at: state.near_intents_status?.observed_at || null,
latest_reference_price_eure_per_btc: state.latest_market_price?.eure_per_btc || null, latest_reference_price_eure_per_btc: state.latest_market_price?.eure_per_btc || null,
latest_reference_route_label: referenceLabel,
latest_reference_route_value: referenceValue,
market_observed_at: market_observed_at:
state.latest_market_price?.observed_at state.latest_market_price?.observed_at
|| state.latest_market_price?.ingested_at || state.latest_market_price?.ingested_at
@ -748,13 +754,35 @@ function buildStatusBar({
servicesByName, servicesByName,
nearIntentsStatus = null, nearIntentsStatus = null,
}) { }) {
const activePairs = (config.observedPairs || config.pairs || [])
.filter((pair) => pair.observeEnabled || pair.enabled)
.map((pair) => pair.key || pair.pairId)
.filter(Boolean);
const referenceRoutes = (config.pairs || [])
.filter((pair) => pair.priceRoute?.routeId)
.map((pair) => ({
pair: pair.key || pair.pairId,
price_route_id: pair.priceRoute.routeId,
reference_pair: pair.priceRoute.routeConfig?.reference_pair || pair.priceRoute.source || null,
}));
const referenceLabel = marketPrice?.payload?.reference_pair
|| referenceRoutes[0]?.reference_pair
|| 'Reference route';
const referenceValue = marketPrice?.payload?.quote_per_base
|| marketPrice?.payload?.eure_per_btc
|| null;
return { return {
active_pair: config.activePair, active_pair: config.activePair,
active_pairs: activePairs,
active_pair_count: activePairs.length,
reference_routes: referenceRoutes,
near_intents_upstream_status: nearIntentsStatus?.status || null, near_intents_upstream_status: nearIntentsStatus?.status || null,
near_intents_upstream_label: nearIntentsStatus?.label || null, near_intents_upstream_label: nearIntentsStatus?.label || null,
near_intents_upstream_reason: nearIntentsStatus?.decisive_reason || null, near_intents_upstream_reason: nearIntentsStatus?.decisive_reason || null,
near_intents_upstream_observed_at: nearIntentsStatus?.observed_at || null, near_intents_upstream_observed_at: nearIntentsStatus?.observed_at || null,
latest_reference_price_eure_per_btc: marketPrice?.payload?.eure_per_btc || null, latest_reference_price_eure_per_btc: marketPrice?.payload?.eure_per_btc || null,
latest_reference_route_label: referenceLabel,
latest_reference_route_value: referenceValue,
market_observed_at: marketPrice?.payload?.observed_at || marketPrice?.ingested_at || null, market_observed_at: marketPrice?.payload?.observed_at || marketPrice?.ingested_at || null,
market_freshness_ms: ageMs(marketPrice?.payload?.observed_at || marketPrice?.ingested_at), market_freshness_ms: ageMs(marketPrice?.payload?.observed_at || marketPrice?.ingested_at),
inventory_observed_at: inventory_observed_at:
@ -785,6 +813,11 @@ function buildBalanceSummary({ inventorySnapshot, marketPrice, config, portfolio
.filter((asset) => asset?.asset_id) .filter((asset) => asset?.asset_id)
.map((asset) => [asset.asset_id, asset]), .map((asset) => [asset.asset_id, asset]),
); );
const metricUnvaluedByAssetId = new Map(
(portfolioMetric?.payload?.current_inventory?.unvalued_assets || [])
.filter((asset) => asset?.asset_id)
.map((asset) => [asset.asset_id, asset]),
);
return { return {
synced_at: inventory.synced_at || inventorySnapshot?.ingested_at || null, synced_at: inventory.synced_at || inventorySnapshot?.ingested_at || null,
@ -794,6 +827,13 @@ function buildBalanceSummary({ inventorySnapshot, marketPrice, config, portfolio
const pendingInboundUnits = String(pendingInbound[asset.assetId] || '0'); const pendingInboundUnits = String(pendingInbound[asset.assetId] || '0');
const pendingOutboundUnits = String(pendingOutbound[asset.assetId] || '0'); const pendingOutboundUnits = String(pendingOutbound[asset.assetId] || '0');
const metricValuation = metricValuationsByAssetId.get(asset.assetId); const metricValuation = metricValuationsByAssetId.get(asset.assetId);
const metricUnvalued = metricUnvaluedByAssetId.get(asset.assetId);
const fallbackValue = valueAssetInEur({
asset,
units: spendableUnits,
marketPrice: marketPrice?.payload || marketPrice || null,
});
const value = metricValuation?.value_eure || fallbackValue;
return { return {
asset_id: asset.assetId, asset_id: asset.assetId,
symbol: asset.symbol, symbol: asset.symbol,
@ -806,13 +846,12 @@ function buildBalanceSummary({ inventorySnapshot, marketPrice, config, portfolio
pending_inbound: formatUnits(pendingInboundUnits, asset.decimals), pending_inbound: formatUnits(pendingInboundUnits, asset.decimals),
pending_outbound_units: pendingOutboundUnits, pending_outbound_units: pendingOutboundUnits,
pending_outbound: formatUnits(pendingOutboundUnits, asset.decimals), pending_outbound: formatUnits(pendingOutboundUnits, asset.decimals),
eur_value_eure: metricValuation?.value_eure eur_value_eure: value,
|| valueAssetInEur({
asset,
units: spendableUnits,
marketPrice: marketPrice?.payload || marketPrice || null,
}),
eur_value_source: metricValuation?.valuation_source || null, eur_value_source: metricValuation?.valuation_source || null,
valuation_status: value != null ? 'valued' : 'unvalued',
valuation_reason: value == null
? metricUnvalued?.valuation_reason || 'valuation_route_missing'
: null,
}; };
}), }),
}; };
@ -914,25 +953,46 @@ function buildIntentRequestSummary({ config, intentRequests = [], executorState
const usingDbPairConfig = Boolean(config.tradingConfigLoaded && strategyConfig); const usingDbPairConfig = Boolean(config.tradingConfigLoaded && strategyConfig);
const sourceAsset = defaultPair?.assetIn || config.tradingEure; const sourceAsset = defaultPair?.assetIn || config.tradingEure;
const destinationAsset = defaultPair?.assetOut || config.tradingBtc; const destinationAsset = defaultPair?.assetOut || config.tradingBtc;
const takerPairs = (config.pairs || []).filter((pair) => pair.takerEnabled);
return { return {
defaults: { defaults: {
pair_id: defaultPair?.pairId || null,
pair_label: defaultPair
? `${sourceAsset?.symbol || defaultPair.asset_in} -> ${destinationAsset?.symbol || defaultPair.asset_out}`
: null,
source_symbol: sourceAsset?.symbol || 'Source', source_symbol: sourceAsset?.symbol || 'Source',
destination_symbol: destinationAsset?.symbol || 'Destination', destination_symbol: destinationAsset?.symbol || 'Destination',
amount_eure: String(strategyConfig?.requestDefaultNotional ?? config.intentRequestDefaultAmountEure ?? 5), source_amount: String(strategyConfig?.requestDefaultNotional ?? config.intentRequestDefaultAmountEure ?? 5),
max_amount_eure: usingDbPairConfig max_source_amount: usingDbPairConfig
? strategyConfig.requestMaxNotional ?? null ? strategyConfig.requestMaxNotional ?? null
: String(config.intentRequestMaxAmountEure || 5), : String(config.intentRequestMaxAmountEure || 5),
amount_eure: sourceAsset?.assetId === config.tradingEure?.assetId
? String(strategyConfig?.requestDefaultNotional ?? config.intentRequestDefaultAmountEure ?? 5)
: null,
max_amount_eure: sourceAsset?.assetId === config.tradingEure?.assetId
? (usingDbPairConfig ? strategyConfig.requestMaxNotional ?? null : String(config.intentRequestMaxAmountEure || 5))
: null,
slippage_bps: Number(strategyConfig?.slippageBps ?? config.intentRequestDefaultSlippageBps ?? 200), slippage_bps: Number(strategyConfig?.slippageBps ?? config.intentRequestDefaultSlippageBps ?? 200),
max_slippage_bps: usingDbPairConfig max_slippage_bps: usingDbPairConfig
? strategyConfig.requestMaxSlippageBps ?? null ? strategyConfig.requestMaxSlippageBps ?? null
: Number(config.intentRequestMaxSlippageBps ?? 200), : Number(config.intentRequestMaxSlippageBps ?? 200),
}, },
pairs: takerPairs.map((pair) => ({
pair_id: pair.pairId,
pair: pair.key,
label: `${pair.assetIn?.symbol || pair.asset_in} -> ${pair.assetOut?.symbol || pair.asset_out}`,
mode: pair.mode,
can_trade: pair.canTrade,
block_reason: pair.blockReason || null,
source_symbol: pair.assetIn?.symbol || pair.asset_in,
destination_symbol: pair.assetOut?.symbol || pair.asset_out,
})),
executor_armed: executorState.armed ?? null, executor_armed: executorState.armed ?? null,
executor_paused: executorState.paused ?? null, executor_paused: executorState.paused ?? null,
request_creation_state: executorState.request_creation || null, request_creation_state: executorState.request_creation || null,
items: (intentRequests || []).map((request) => normalizeIntentRequestForUi({ config, request })), items: (intentRequests || []).map((request) => normalizeIntentRequestForUi({ config, request })),
caveat: caveat:
'Own request relay acceptance is not a completed trade. Completed requires durable EURe decrease and BTC increase evidence.', 'Own request relay acceptance is not a completed trade. Completed requires durable source decrease and destination increase evidence.',
}; };
} }
@ -1113,6 +1173,9 @@ export function deriveQuoteLifecycleRows({
direction: decision.direction, direction: decision.direction,
request_kind: decision.request_kind, request_kind: decision.request_kind,
gross_edge_pct: decision.gross_edge_pct, gross_edge_pct: decision.gross_edge_pct,
notional: decision.notional,
notional_asset_id: decision.notional_asset_id,
notional_symbol: decision.notional_symbol,
eure_notional: decision.eure_notional, eure_notional: decision.eure_notional,
decision, decision,
decision_at: decision.decision_at || null, decision_at: decision.decision_at || null,
@ -1145,6 +1208,10 @@ export function deriveQuoteLifecycleRows({
asset_out: command.asset_out || null, asset_out: command.asset_out || null,
amount_in: command.amount_in || null, amount_in: command.amount_in || null,
amount_out: command.amount_out || null, amount_out: command.amount_out || null,
notional: command.notional || null,
notional_asset_id: command.notional_asset_id || null,
notional_symbol: command.notional_symbol || null,
eure_notional: command.eure_notional || null,
command, command,
command_at: command.command_at || null, command_at: command.command_at || null,
}); });
@ -1172,6 +1239,9 @@ export function deriveQuoteLifecycleRows({
direction: outcome?.direction || null, direction: outcome?.direction || null,
request_kind: outcome?.request_kind || null, request_kind: outcome?.request_kind || null,
gross_edge_pct: outcome?.gross_edge_pct || null, gross_edge_pct: outcome?.gross_edge_pct || null,
notional: outcome?.notional || null,
notional_asset_id: outcome?.notional_asset_id || null,
notional_symbol: outcome?.notional_symbol || null,
eure_notional: outcome?.eure_notional || null, eure_notional: outcome?.eure_notional || null,
outcome, outcome,
command_at: outcome?.command_at || null, command_at: outcome?.command_at || null,
@ -1201,6 +1271,9 @@ function ensureLifecycleRow(rowsByKey, key) {
direction: null, direction: null,
request_kind: null, request_kind: null,
gross_edge_pct: null, gross_edge_pct: null,
notional: null,
notional_asset_id: null,
notional_symbol: null,
eure_notional: null, eure_notional: null,
quote_observed_at: null, quote_observed_at: null,
decision_at: null, decision_at: null,
@ -1431,6 +1504,10 @@ function normalizeCommand(command) {
edge_bps: command.edge_bps || null, edge_bps: command.edge_bps || null,
direction: command.direction || null, direction: command.direction || null,
request_kind: command.request_kind || null, request_kind: command.request_kind || null,
notional: command.notional || null,
notional_asset_id: command.notional_asset_id || null,
notional_symbol: command.notional_symbol || null,
eure_notional: command.eure_notional || null,
asset_in: command.asset_in || null, asset_in: command.asset_in || null,
asset_out: command.asset_out || null, asset_out: command.asset_out || null,
amount_in: command.amount_in ?? null, amount_in: command.amount_in ?? null,
@ -1910,8 +1987,10 @@ function normalizeTradeForUi({ config, trade }) {
} }
function enrichLifecycleRowForUi({ config, row }) { function enrichLifecycleRowForUi({ config, row }) {
const notionalDisplay = formatNotional(row);
return { return {
...row, ...row,
notional_display: notionalDisplay,
request_terms: buildLifecycleTerms({ request_terms: buildLifecycleTerms({
config, config,
terms: row.quote || row, terms: row.quote || row,
@ -1920,6 +1999,7 @@ function enrichLifecycleRowForUi({ config, row }) {
config, config,
terms: row.command || row.execution || null, terms: row.command || row.execution || null,
}), }),
gross_edge_value: estimateGrossEdgeValue(row),
gross_edge_value_eure: estimateGrossEdgeValueEure(row), gross_edge_value_eure: estimateGrossEdgeValueEure(row),
settlement_summary: buildSettlementSummary({ settlement_summary: buildSettlementSummary({
config, config,
@ -1951,13 +2031,25 @@ function buildLifecycleTerms({ config, terms }) {
} }
function estimateGrossEdgeValueEure(row) { function estimateGrossEdgeValueEure(row) {
if (row?.notional && row?.notional_symbol && row.notional_symbol !== 'EURe') return null;
return estimateGrossEdgeValue(row);
}
function estimateGrossEdgeValue(row) {
const edge = Number(row?.gross_edge_pct); const edge = Number(row?.gross_edge_pct);
const notional = Number(row?.eure_notional); const notional = Number(row?.notional ?? row?.eure_notional);
if (!Number.isFinite(edge) || !Number.isFinite(notional)) return null; if (!Number.isFinite(edge) || !Number.isFinite(notional)) return null;
const value = (notional * edge) / 100; const value = (notional * edge) / 100;
return value.toFixed(8).replace(/\.?0+$/, ''); return value.toFixed(8).replace(/\.?0+$/, '');
} }
function formatNotional(row) {
const notional = row?.notional ?? row?.eure_notional ?? null;
if (notional == null) return null;
const symbol = row?.notional_symbol || (row?.eure_notional ? 'EURe' : null);
return symbol ? `${notional} ${symbol}` : String(notional);
}
function buildSettlementSummary({ function buildSettlementSummary({
config, config,
delta, delta,
@ -2155,9 +2247,13 @@ function normalizeDecision(decision) {
decision_reason: normalizeDecisionReason(decision.decision_reason), decision_reason: normalizeDecisionReason(decision.decision_reason),
gross_edge_pct: decision.gross_edge_pct || null, gross_edge_pct: decision.gross_edge_pct || null,
threshold_pct: decision.threshold_pct || null, threshold_pct: decision.threshold_pct || null,
max_notional: decision.max_notional || null,
max_notional_eure: decision.max_notional_eure || null, max_notional_eure: decision.max_notional_eure || null,
strategy_armed: decision.strategy_armed ?? null, strategy_armed: decision.strategy_armed ?? null,
inventory_asset: decision.inventory_asset || null, inventory_asset: decision.inventory_asset || null,
notional: decision.notional || null,
notional_asset_id: decision.notional_asset_id || null,
notional_symbol: decision.notional_symbol || null,
eure_notional: decision.eure_notional || null, eure_notional: decision.eure_notional || null,
}; };
} }

View file

@ -30,6 +30,11 @@ export function computePortfolioMetric({
btcAssets: effectiveBtcAssets, btcAssets: effectiveBtcAssets,
eureAsset, eureAsset,
}); });
const currentUnvaluedAssets = normalizeUnvaluedAssets({
valuationAssets,
btcAssets: effectiveBtcAssets,
eureAsset,
});
const currentValuedAssets = valueValuationAssets({ const currentValuedAssets = valueValuationAssets({
inventory: currentInventory, inventory: currentInventory,
valuationAssets: effectiveValuationAssets, valuationAssets: effectiveValuationAssets,
@ -53,6 +58,7 @@ export function computePortfolioMetric({
btcAssets: effectiveBtcAssets, btcAssets: effectiveBtcAssets,
eureAsset, eureAsset,
valuationAssets: effectiveValuationAssets, valuationAssets: effectiveValuationAssets,
unvaluedAssets: currentUnvaluedAssets,
}), }),
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),
@ -124,6 +130,7 @@ export function computePortfolioMetric({
btcAssets: effectiveBtcAssets, btcAssets: effectiveBtcAssets,
eureAsset, eureAsset,
valuationAssets: effectiveValuationAssets, valuationAssets: effectiveValuationAssets,
unvaluedAssets: currentUnvaluedAssets,
}); });
const fundedPortfolioAtFlowTime = baselinePortfolioAtBaselinePrice const fundedPortfolioAtFlowTime = baselinePortfolioAtBaselinePrice
+ externalFlowSummary.netValueEureAtFlowTime; + externalFlowSummary.netValueEureAtFlowTime;
@ -201,6 +208,7 @@ export function computePortfolioMetric({
btcAssets: effectiveBtcAssets, btcAssets: effectiveBtcAssets,
eureAsset, eureAsset,
valuationAssets: effectiveValuationAssets, valuationAssets: effectiveValuationAssets,
unvaluedAssets: currentUnvaluedAssets,
}), }),
}; };
@ -229,12 +237,26 @@ export function buildCashEquivalentValuationAssets({
return (trackedAssets || []) return (trackedAssets || [])
.filter((asset) => asset?.assetId && !excludedAssetIds.has(asset.assetId)) .filter((asset) => asset?.assetId && !excludedAssetIds.has(asset.assetId))
.map((asset) => { .map((asset) => {
if (asset.symbol !== 'USDC' || !usdcUnitValueEure) return null; if (asset.symbol !== 'USDC' || !usdcUnitValueEure) {
return { return {
asset, asset,
assetId: asset.assetId, assetId: asset.assetId,
symbol: asset.symbol || asset.assetId,
label: asset.label || asset.symbol || asset.assetId,
decimals: asset.decimals,
valuationStatus: 'unvalued',
valuationReason: 'valuation_route_missing',
};
}
return {
asset,
assetId: asset.assetId,
symbol: asset.symbol || asset.assetId,
label: asset.label || asset.symbol || asset.assetId,
decimals: asset.decimals,
currentUnitValueEure: usdcUnitValueEure, currentUnitValueEure: usdcUnitValueEure,
baselineUnitValueEure: usdcUnitValueEure, baselineUnitValueEure: usdcUnitValueEure,
valuationStatus: 'valued',
valuationSource: 'btc_usdc_reference', valuationSource: 'btc_usdc_reference',
priceId: usdcPrice.price_id || null, priceId: usdcPrice.price_id || null,
observedAt: usdcPrice.observed_at || usdcPrice.ingested_at || null, observedAt: usdcPrice.observed_at || usdcPrice.ingested_at || null,
@ -258,6 +280,7 @@ function buildInventoryView({
btcAssets = null, btcAssets = null,
eureAsset, eureAsset,
valuationAssets = [], valuationAssets = [],
unvaluedAssets = [],
}) { }) {
const effectiveBtcAssets = normalizeBtcAssets({ btcAsset, btcAssets }); const effectiveBtcAssets = normalizeBtcAssets({ btcAsset, btcAssets });
const effectiveValuationAssets = normalizeValuationAssets({ const effectiveValuationAssets = normalizeValuationAssets({
@ -291,6 +314,7 @@ function buildInventoryView({
eure_units: eureUnits, eure_units: eureUnits,
eure: formatAssetUnits(eureUnits, eureAsset.decimals), eure: formatAssetUnits(eureUnits, eureAsset.decimals),
valued_assets: valuedAssets.items, valued_assets: valuedAssets.items,
unvalued_assets: buildUnvaluedAssetRows({ inventory, unvaluedAssets }),
valued_assets_value_eure: formatScaledDecimal(valuedAssets.total), valued_assets_value_eure: formatScaledDecimal(valuedAssets.total),
}; };
} }
@ -421,6 +445,37 @@ function normalizeValuationAssets({ valuationAssets = [], btcAssets = [], eureAs
)); ));
} }
function normalizeUnvaluedAssets({ valuationAssets = [], btcAssets = [], eureAsset = null } = {}) {
const excludedAssetIds = new Set([
...normalizeBtcAssets({ btcAssets }).map((asset) => asset.assetId),
eureAsset?.assetId,
].filter(Boolean));
return (valuationAssets || [])
.map((entry) => {
const asset = entry.asset || entry;
const assetId = entry.assetId || entry.asset_id || asset.assetId || asset.asset_id || null;
const valuationStatus = entry.valuationStatus || entry.valuation_status || null;
if (!assetId || excludedAssetIds.has(assetId) || valuationStatus !== 'unvalued') return null;
return {
assetId,
symbol: asset.symbol || entry.symbol || assetId,
label: asset.label || entry.label || asset.symbol || entry.symbol || assetId,
decimals: Number(asset.decimals ?? entry.decimals ?? 0),
valuationStatus: 'unvalued',
valuationReason:
entry.valuationReason
|| entry.valuation_reason
|| 'valuation_route_missing',
};
})
.filter((asset) => (
asset
&& Number.isInteger(asset.decimals)
&& asset.decimals >= 0
));
}
function valueValuationAssets({ inventory, valuationAssets, priceField }) { function valueValuationAssets({ inventory, valuationAssets, priceField }) {
const spendable = inventory?.spendable || {}; const spendable = inventory?.spendable || {};
let total = 0n; let total = 0n;
@ -449,6 +504,22 @@ function valueValuationAssets({ inventory, valuationAssets, priceField }) {
return { total, items }; return { total, items };
} }
function buildUnvaluedAssetRows({ inventory, unvaluedAssets }) {
const spendable = inventory?.spendable || {};
return (unvaluedAssets || []).map((asset) => {
const units = String(spendable[asset.assetId] || '0');
return {
asset_id: asset.assetId,
symbol: asset.symbol,
label: asset.label,
units,
amount: formatAssetUnits(units, asset.decimals),
valuation_status: asset.valuationStatus,
valuation_reason: asset.valuationReason,
};
});
}
function buildValuedAssetInventoryDelta({ function buildValuedAssetInventoryDelta({
currentInventory, currentInventory,
baselineInventory, baselineInventory,
@ -512,9 +583,20 @@ function unitsToScaledDecimal(units, decimals) {
} }
function formatAssetUnits(units, decimals) { function formatAssetUnits(units, decimals) {
if (decimals > VALUE_SCALE) return formatRawUnitsDecimal(units, decimals);
return formatScaledDecimal(BigInt(units || '0') * 10n ** BigInt(VALUE_SCALE - decimals)); return formatScaledDecimal(BigInt(units || '0') * 10n ** BigInt(VALUE_SCALE - decimals));
} }
function formatRawUnitsDecimal(units, decimals) {
const raw = String(units ?? '0');
const negative = raw.startsWith('-');
const digits = negative ? raw.slice(1) : raw;
const padded = digits.padStart(decimals + 1, '0');
const whole = padded.slice(0, padded.length - decimals) || '0';
const fraction = decimals > 0 ? padded.slice(-decimals).replace(/0+$/, '') : '';
return `${negative ? '-' : ''}${whole}${fraction ? `.${fraction}` : ''}`;
}
function parseScaledDecimal(value) { function parseScaledDecimal(value) {
const normalized = String(value ?? '0').trim(); const normalized = String(value ?? '0').trim();
const negative = normalized.startsWith('-'); const negative = normalized.startsWith('-');

View file

@ -17,7 +17,6 @@ export function deriveQuoteOutcomeRecords({
attributionWindowMs = DEFAULT_ATTRIBUTION_WINDOW_MS, attributionWindowMs = DEFAULT_ATTRIBUTION_WINDOW_MS,
settlementGraceMs = DEFAULT_SETTLEMENT_GRACE_MS, settlementGraceMs = DEFAULT_SETTLEMENT_GRACE_MS,
} = {}) { } = {}) {
const activeAssetIds = [btcAsset?.assetId, eureAsset?.assetId].filter(Boolean);
const normalizedSubmissions = submissions const normalizedSubmissions = submissions
.map(normalizeSubmission) .map(normalizeSubmission)
.filter((entry) => entry?.quote_id && entry.status === 'submitted'); .filter((entry) => entry?.quote_id && entry.status === 'submitted');
@ -33,6 +32,16 @@ export function deriveQuoteOutcomeRecords({
.filter((entry) => entry?.quote_id) .filter((entry) => entry?.quote_id)
.map((entry) => [entry.quote_id, entry]), .map((entry) => [entry.quote_id, entry]),
); );
const expectedDeltasByQuote = new Map();
for (const submission of normalizedSubmissions) {
const command = commandsByQuote.get(submission.quote_id) || null;
const expectedDelta = buildExpectedMakerDeltas(command);
if (expectedDelta) expectedDeltasByQuote.set(submission.quote_id, expectedDelta);
}
const activeAssetIds = uniqueAssetIds([
...expectedDeltasByQuote.values(),
Object.fromEntries([btcAsset?.assetId, eureAsset?.assetId].filter(Boolean).map((assetId) => [assetId, 0n])),
]);
const inventoryDeltas = deriveInventoryDeltas({ const inventoryDeltas = deriveInventoryDeltas({
inventorySnapshots, inventorySnapshots,
activeAssetIds, activeAssetIds,
@ -42,8 +51,7 @@ export function deriveQuoteOutcomeRecords({
const candidatesByQuote = new Map(); const candidatesByQuote = new Map();
for (const submission of normalizedSubmissions) { for (const submission of normalizedSubmissions) {
const command = commandsByQuote.get(submission.quote_id) || null; const expectedDelta = expectedDeltasByQuote.get(submission.quote_id) || null;
const expectedDelta = buildExpectedMakerDeltas(command);
if (!expectedDelta) continue; if (!expectedDelta) continue;
const matches = inventoryDeltas.filter((movement) => ( const matches = inventoryDeltas.filter((movement) => (
@ -298,7 +306,10 @@ function baseOutcomeRecord({
direction: decision?.direction || command?.direction || null, direction: decision?.direction || command?.direction || null,
request_kind: command?.request_kind || decision?.request_kind || null, request_kind: command?.request_kind || decision?.request_kind || null,
gross_edge_pct: decision?.gross_edge_pct || null, gross_edge_pct: decision?.gross_edge_pct || null,
eure_notional: decision?.eure_notional || null, notional: decision?.notional || command?.notional || null,
notional_asset_id: decision?.notional_asset_id || command?.notional_asset_id || null,
notional_symbol: decision?.notional_symbol || command?.notional_symbol || null,
eure_notional: decision?.eure_notional || command?.eure_notional || null,
execution_result_status: submission.status, execution_result_status: submission.status,
execution_result_code: submission.result_code || null, execution_result_code: submission.result_code || null,
submitted_at: submission.submitted_at, submitted_at: submission.submitted_at,
@ -335,6 +346,10 @@ function movementMatchesExpectedDelta({
for (const [assetId, expected] of Object.entries(expectedDelta)) { for (const [assetId, expected] of Object.entries(expectedDelta)) {
if (safeBigInt(movement.delta_units?.[assetId]) !== expected) return false; if (safeBigInt(movement.delta_units?.[assetId]) !== expected) return false;
} }
for (const [assetId, actual] of Object.entries(movement.delta_units || {})) {
if (Object.prototype.hasOwnProperty.call(expectedDelta, assetId)) continue;
if (safeBigInt(actual) !== 0n) return false;
}
return true; return true;
} }
@ -368,6 +383,8 @@ function getExpiredSettlementWindow({
} }
function buildExpectedMakerDeltas(command) { function buildExpectedMakerDeltas(command) {
const explicitDelta = normalizeExpectedDelta(command?.expected_inventory_delta_units);
if (explicitDelta) return explicitDelta;
if (!command?.asset_in || !command?.asset_out || !command?.request_kind) return null; if (!command?.asset_in || !command?.asset_out || !command?.request_kind) return null;
const receiveAmount = command.request_kind === 'exact_in' const receiveAmount = command.request_kind === 'exact_in'
@ -416,6 +433,10 @@ function normalizeCommand(entry) {
pair: payload.pair || null, pair: payload.pair || null,
direction: payload.direction || null, direction: payload.direction || null,
request_kind: payload.request_kind || null, request_kind: payload.request_kind || null,
notional: payload.notional || null,
notional_asset_id: payload.notional_asset_id || null,
notional_symbol: payload.notional_symbol || null,
eure_notional: payload.eure_notional || null,
asset_in: payload.asset_in || null, asset_in: payload.asset_in || null,
asset_out: payload.asset_out || null, asset_out: payload.asset_out || null,
amount_in: payload.amount_in ?? null, amount_in: payload.amount_in ?? null,
@ -423,6 +444,7 @@ function normalizeCommand(entry) {
quote_output: payload.quote_output || {}, quote_output: payload.quote_output || {},
proposed_amount_in: payload.proposed_amount_in ?? null, proposed_amount_in: payload.proposed_amount_in ?? null,
proposed_amount_out: payload.proposed_amount_out ?? null, proposed_amount_out: payload.proposed_amount_out ?? null,
expected_inventory_delta_units: payload.expected_inventory_delta_units || null,
min_deadline_ms: payload.min_deadline_ms ?? null, min_deadline_ms: payload.min_deadline_ms ?? null,
command_at: toIsoTimestamp( command_at: toIsoTimestamp(
entry?.observed_at entry?.observed_at
@ -444,6 +466,9 @@ function normalizeDecision(entry) {
direction: payload.direction || null, direction: payload.direction || null,
request_kind: payload.request_kind || null, request_kind: payload.request_kind || null,
gross_edge_pct: payload.gross_edge_pct || null, gross_edge_pct: payload.gross_edge_pct || null,
notional: payload.notional || null,
notional_asset_id: payload.notional_asset_id || null,
notional_symbol: payload.notional_symbol || null,
eure_notional: payload.eure_notional || null, eure_notional: payload.eure_notional || null,
}; };
} }
@ -476,6 +501,26 @@ function payloadOf(entry) {
return entry.payload || entry; return entry.payload || entry;
} }
function uniqueAssetIds(deltaRecords = []) {
const ids = new Set();
for (const record of deltaRecords || []) {
for (const assetId of Object.keys(record || {})) {
if (assetId) ids.add(assetId);
}
}
return [...ids];
}
function normalizeExpectedDelta(delta) {
if (!delta || typeof delta !== 'object' || Array.isArray(delta)) return null;
const normalized = {};
for (const [assetId, units] of Object.entries(delta)) {
if (!assetId || units == null || units === '') continue;
normalized[assetId] = safeBigInt(units);
}
return Object.keys(normalized).length ? normalized : null;
}
function safeBigInt(value) { function safeBigInt(value) {
if (value == null || value === '') return 0n; if (value == null || value === '') return 0n;
return BigInt(String(value)); return BigInt(String(value));

144
src/core/route-rates.mjs Normal file
View file

@ -0,0 +1,144 @@
const SUPPORTED_ROUTE_SOURCES = new Set([
'btc_eur_reference',
'btc_usdc_reference',
]);
export function isSupportedPriceRouteSource(source) {
return SUPPORTED_ROUTE_SOURCES.has(source);
}
export function classifyRouteDirection({
sourceAssetId = null,
destinationAssetId = null,
priceRoute = null,
} = {}) {
if (!priceRoute || !isSupportedPriceRouteSource(priceRoute.source)) return 'unsupported';
if (sourceAssetId === priceRoute.baseAssetId && destinationAssetId === priceRoute.quoteAssetId) {
return 'base_to_quote';
}
if (sourceAssetId === priceRoute.quoteAssetId && destinationAssetId === priceRoute.baseAssetId) {
return 'quote_to_base';
}
return 'unsupported';
}
export function resolveRouteRates({
price = null,
priceRoute = null,
direction = null,
} = {}) {
if (!price) return { ok: false, reason: 'reference_price_missing' };
const normalizedDirection = normalizeRouteDirection(direction);
if (!normalizedDirection) return { ok: false, reason: 'unsupported_pair' };
if (priceRoute?.routeId && price.price_route_id && price.price_route_id !== priceRoute.routeId) {
return { ok: false, reason: 'reference_price_missing' };
}
if (priceRoute?.source && !isSupportedPriceRouteSource(priceRoute.source)) {
return { ok: false, reason: 'unsupported_price_route' };
}
const legacyFields = legacyRateFields({
source: priceRoute?.source || inferRouteSourceFromDirection(direction),
direction,
});
let quotePerBase = Number(firstPresent(
price.quote_per_base,
price.quotePerBase,
legacyFields ? price[legacyFields.quotePerBaseField] : null,
));
let basePerQuote = Number(firstPresent(
price.base_per_quote,
price.basePerQuote,
legacyFields ? price[legacyFields.basePerQuoteField] : null,
));
if (!(basePerQuote > 0) && quotePerBase > 0) basePerQuote = 1 / quotePerBase;
if (!(quotePerBase > 0) && basePerQuote > 0) quotePerBase = 1 / basePerQuote;
if (!(quotePerBase > 0) || !(basePerQuote > 0)) {
return { ok: false, reason: 'reference_price_missing' };
}
return {
ok: true,
quotePerBase,
basePerQuote,
baseToQuote: normalizedDirection === 'base_to_quote',
direction: normalizedDirection,
referencePriceId: price.price_id || null,
priceRouteId: price.price_route_id || priceRoute?.routeId || null,
};
}
export function computeDestinationAmountUnitsFromRoute({
sourceAmountUnits,
sourceDecimals,
destinationDecimals,
direction,
quotePerBase,
basePerQuote,
} = {}) {
const normalizedDirection = normalizeRouteDirection(direction);
if (!normalizedDirection) throw new Error('unsupported route direction');
if (!Number.isInteger(sourceDecimals) || sourceDecimals < 0) {
throw new Error('source decimals are required');
}
if (!Number.isInteger(destinationDecimals) || destinationDecimals < 0) {
throw new Error('destination decimals are required');
}
const rate = parsePositiveDecimal(
normalizedDirection === 'base_to_quote' ? quotePerBase : basePerQuote,
{ field: normalizedDirection === 'base_to_quote' ? 'quote_per_base' : 'base_per_quote' },
);
const sourceUnits = BigInt(String(sourceAmountUnits || '0'));
if (sourceUnits <= 0n) throw new Error('sourceAmountUnits must be greater than zero');
const numerator = sourceUnits * (10n ** BigInt(destinationDecimals)) * rate.units;
const denominator = (10n ** BigInt(sourceDecimals)) * rate.scale;
if (denominator <= 0n) throw new Error('invalid route denominator');
return (numerator / denominator).toString();
}
export function normalizeRouteDirection(direction) {
if (direction === 'base_to_quote' || direction === 'quote_to_base') return direction;
if (direction === 'btc_to_eure' || direction === 'btc_to_usdc') return 'base_to_quote';
if (direction === 'eure_to_btc' || direction === 'usdc_to_btc') return 'quote_to_base';
return null;
}
function legacyRateFields({ source, direction } = {}) {
if (source === 'btc_usdc_reference' || direction === 'btc_to_usdc' || direction === 'usdc_to_btc') {
return {
quotePerBaseField: 'usdc_per_btc',
basePerQuoteField: 'btc_per_usdc',
};
}
if (source === 'btc_eur_reference' || direction === 'btc_to_eure' || direction === 'eure_to_btc') {
return {
quotePerBaseField: 'eure_per_btc',
basePerQuoteField: 'btc_per_eure',
};
}
return null;
}
function inferRouteSourceFromDirection(direction) {
if (direction === 'btc_to_usdc' || direction === 'usdc_to_btc') return 'btc_usdc_reference';
if (direction === 'btc_to_eure' || direction === 'eure_to_btc') return 'btc_eur_reference';
return null;
}
function firstPresent(...values) {
return values.find((value) => value != null && value !== '');
}
function parsePositiveDecimal(value, { field }) {
const raw = String(value ?? '').trim();
if (!/^\d+(\.\d+)?$/.test(raw)) throw new Error(`${field} must be a positive decimal`);
const [whole, fraction = ''] = raw.split('.');
const scale = 10n ** BigInt(fraction.length);
const units = BigInt(`${whole}${fraction}`.replace(/^0+/, '') || '0');
if (units <= 0n) throw new Error(`${field} must be greater than zero`);
return { units, scale };
}

View file

@ -33,6 +33,7 @@ export function createRuntimeHealthThresholds(config = {}) {
export function evaluateRuntimeHealth({ export function evaluateRuntimeHealth({
servicesByName, servicesByName,
activePair, activePair,
activePairs = activePair ? [activePair] : [],
activeAlerts = [], activeAlerts = [],
now = new Date().toISOString(), now = new Date().toISOString(),
} = {}) { } = {}) {
@ -45,6 +46,7 @@ export function evaluateRuntimeHealth({
service, service,
snapshot, snapshot,
activePair, activePair,
activePairs,
activeAlerts: alerts, activeAlerts: alerts,
now, now,
})); }));
@ -57,6 +59,7 @@ export function deriveServiceHealth({
service, service,
snapshot, snapshot,
activePair = null, activePair = null,
activePairs = activePair ? [activePair] : [],
activeAlerts = [], activeAlerts = [],
now = new Date().toISOString(), now = new Date().toISOString(),
} = {}) { } = {}) {
@ -149,7 +152,7 @@ export function deriveServiceHealth({
if ( if (
['strategy-engine', 'trade-executor'].includes(service) ['strategy-engine', 'trade-executor'].includes(service)
&& (state.armed ?? false) && (state.armed ?? false)
&& hasCriticalTruthAlert(activeAlerts, activePair) && hasCriticalTruthAlert(activeAlerts, activePairs.length ? activePairs : activePair)
) { ) {
status = escalateHealth(status, 'critical'); status = escalateHealth(status, 'critical');
if (label === 'healthy' || label === 'warning') { if (label === 'healthy' || label === 'warning') {
@ -355,11 +358,12 @@ function indexAlertsByService(activeAlerts) {
} }
function hasCriticalTruthAlert(alerts, activePair) { function hasCriticalTruthAlert(alerts, activePair) {
const activePairSet = new Set(Array.isArray(activePair) ? activePair : [activePair].filter(Boolean));
return (alerts || []).some((alert) => ( return (alerts || []).some((alert) => (
alert.severity === 'critical' alert.severity === 'critical'
&& ( && (
alert.pair == null alert.pair == null
|| alert.pair === activePair || activePairSet.has(alert.pair)
|| alert.alert_code.includes('stale') || alert.alert_code.includes('stale')
|| alert.alert_code.includes('disconnected') || alert.alert_code.includes('disconnected')
) )

View file

@ -8,6 +8,10 @@ import {
pairKey, pairKey,
unitsToNumber, unitsToNumber,
} from './assets.mjs'; } from './assets.mjs';
import {
classifyRouteDirection,
resolveRouteRates,
} from './route-rates.mjs';
export function evaluateTradeOpportunity({ export function evaluateTradeOpportunity({
demandEvent, demandEvent,
@ -17,12 +21,13 @@ export function evaluateTradeOpportunity({
now = Date.now(), now = Date.now(),
armed = false, armed = false,
thresholdPct = config.strategyGrossThresholdPct, thresholdPct = config.strategyGrossThresholdPct,
maxNotionalEure = config.strategyMaxNotionalEure, maxNotional = config.strategyMaxNotional ?? config.strategyMaxNotionalEure,
}) { }) {
const payload = demandEvent.payload; const payload = demandEvent.payload;
const pairRuntime = resolvePairRuntime({ payload, config, thresholdPct, maxNotionalEure }); const pairRuntime = resolvePairRuntime({ payload, config, thresholdPct, maxNotional });
const effectiveThresholdPct = pairRuntime.thresholdPct ?? thresholdPct; const effectiveThresholdPct = pairRuntime.thresholdPct ?? thresholdPct;
const effectiveMaxNotionalEure = pairRuntime.maxNotionalEure ?? maxNotionalEure; const effectiveMaxNotional = pairRuntime.maxNotional ?? maxNotional;
const legacyEureNotional = isLegacyEureNotional({ pairRuntime, config });
const decisionId = crypto.randomUUID(); const decisionId = crypto.randomUUID();
const baseDecision = { const baseDecision = {
decision_id: decisionId, decision_id: decisionId,
@ -36,7 +41,7 @@ export function evaluateTradeOpportunity({
edge_bps: pairRuntime.strategyConfig?.edgeBps == null edge_bps: pairRuntime.strategyConfig?.edgeBps == null
? null ? null
: String(pairRuntime.strategyConfig.edgeBps), : String(pairRuntime.strategyConfig.edgeBps),
max_notional: effectiveMaxNotionalEure == null ? null : String(effectiveMaxNotionalEure), max_notional: effectiveMaxNotional == null ? null : String(effectiveMaxNotional),
min_notional: pairRuntime.strategyConfig?.minNotional == null min_notional: pairRuntime.strategyConfig?.minNotional == null
? null ? null
: String(pairRuntime.strategyConfig.minNotional), : String(pairRuntime.strategyConfig.minNotional),
@ -46,7 +51,9 @@ export function evaluateTradeOpportunity({
decision: 'rejected', decision: 'rejected',
decision_reason: 'unknown', decision_reason: 'unknown',
threshold_pct: String(effectiveThresholdPct), threshold_pct: String(effectiveThresholdPct),
max_notional_eure: String(effectiveMaxNotionalEure), max_notional_eure: legacyEureNotional && effectiveMaxNotional != null
? String(effectiveMaxNotional)
: null,
strategy_armed: armed, strategy_armed: armed,
assumptions: compact({ assumptions: compact({
eure_per_eur: pairRuntime.priceRoute?.source === 'btc_eur_reference' ? '1' : null, eure_per_eur: pairRuntime.priceRoute?.source === 'btc_eur_reference' ? '1' : null,
@ -97,7 +104,7 @@ export function evaluateTradeOpportunity({
config, config,
pairRuntime, pairRuntime,
thresholdPct: effectiveThresholdPct, thresholdPct: effectiveThresholdPct,
maxNotionalEure: effectiveMaxNotionalEure, maxNotional: effectiveMaxNotional,
}); });
if (!buildResult.ok) { if (!buildResult.ok) {
@ -137,12 +144,19 @@ export function evaluateTradeOpportunity({
pair_config_version: decision.pair_config_version, pair_config_version: decision.pair_config_version,
edge_bps: decision.edge_bps, edge_bps: decision.edge_bps,
max_notional: decision.max_notional, max_notional: decision.max_notional,
min_notional: decision.min_notional,
max_notional_eure: decision.max_notional_eure, max_notional_eure: decision.max_notional_eure,
price_route_id: decision.price_route_id, price_route_id: decision.price_route_id,
reference_price_id: buildResult.details.price_id || null, reference_price_id: buildResult.details.price_id || null,
notional: decision.notional, notional: decision.notional,
notional_asset_id: decision.notional_asset_id, notional_asset_id: decision.notional_asset_id,
notional_symbol: decision.notional_symbol, notional_symbol: decision.notional_symbol,
source_asset_id: payload.asset_in,
source_symbol: pairRuntime.assetIn?.symbol || null,
source_decimals: pairRuntime.assetIn?.decimals ?? null,
destination_asset_id: payload.asset_out,
destination_symbol: pairRuntime.assetOut?.symbol || null,
destination_decimals: pairRuntime.assetOut?.decimals ?? null,
asset_in: payload.asset_in, asset_in: payload.asset_in,
asset_out: payload.asset_out, asset_out: payload.asset_out,
asset_in_decimals: pairRuntime.assetIn?.decimals ?? null, asset_in_decimals: pairRuntime.assetIn?.decimals ?? null,
@ -154,6 +168,12 @@ export function evaluateTradeOpportunity({
quote_output: buildResult.quoteOutput, quote_output: buildResult.quoteOutput,
proposed_amount_in: buildResult.details.proposed_amount_in ?? null, proposed_amount_in: buildResult.details.proposed_amount_in ?? null,
proposed_amount_out: buildResult.details.proposed_amount_out ?? null, proposed_amount_out: buildResult.details.proposed_amount_out ?? null,
expected_inventory_delta_units: buildExpectedMakerInventoryDelta({
demand: payload,
quoteOutput: buildResult.quoteOutput,
proposedAmountIn: buildResult.details.proposed_amount_in ?? null,
proposedAmountOut: buildResult.details.proposed_amount_out ?? null,
}),
}, },
}; };
} }
@ -165,7 +185,7 @@ function buildQuote({
config, config,
pairRuntime = null, pairRuntime = null,
thresholdPct, thresholdPct,
maxNotionalEure, maxNotional,
}) { }) {
const direction = pairRuntime?.direction || classifyPairDirection({ const direction = pairRuntime?.direction || classifyPairDirection({
assetIn: demand.asset_in, assetIn: demand.asset_in,
@ -190,11 +210,17 @@ function buildQuote({
const spendAsset = demand.asset_out; const spendAsset = demand.asset_out;
const available = bigintAmount(inventory.spendable?.[spendAsset] || '0'); const available = bigintAmount(inventory.spendable?.[spendAsset] || '0');
const pendingInbound = bigintAmount(inventory.pending_inbound?.[spendAsset] || '0'); const pendingInbound = bigintAmount(inventory.pending_inbound?.[spendAsset] || '0');
const referenceRates = resolveReferenceRates({ direction, price }); const referenceRates = resolveRouteRates({
direction,
price,
priceRoute: pairRuntime?.priceRoute || null,
});
if (!referenceRates.ok) return { ok: false, reason: referenceRates.reason, details: {} }; if (!referenceRates.ok) return { ok: false, reason: referenceRates.reason, details: {} };
const { quotePerBase, basePerQuote, baseToQuote } = referenceRates; const { quotePerBase, basePerQuote, baseToQuote } = referenceRates;
const notionalAssetId = pairRuntime?.priceRoute?.quoteAssetId || assetOut.assetId; const notionalAssetId = pairRuntime?.priceRoute?.quoteAssetId
|| (baseToQuote ? assetOut.assetId : assetIn.assetId);
const notionalAsset = assetRegistry.get(notionalAssetId) || null; const notionalAsset = assetRegistry.get(notionalAssetId) || null;
const legacyEureNotional = notionalAssetId === config.tradingEure?.assetId;
if (demand.request_kind === 'exact_in') { if (demand.request_kind === 'exact_in') {
const amountIn = bigintAmount(demand.amount_in); const amountIn = bigintAmount(demand.amount_in);
@ -232,7 +258,8 @@ function buildQuote({
quoteNotional, quoteNotional,
notionalAssetId, notionalAssetId,
notionalSymbol: notionalAsset?.symbol || null, notionalSymbol: notionalAsset?.symbol || null,
maxNotionalEure, maxNotional,
legacyEureNotional,
proposedAmountOut: proposedOutputUnits, proposedAmountOut: proposedOutputUnits,
impliedRate, impliedRate,
referenceRate, referenceRate,
@ -240,6 +267,10 @@ function buildQuote({
priceId: price.price_id, priceId: price.price_id,
assetInDecimals: assetIn.decimals, assetInDecimals: assetIn.decimals,
assetOutDecimals: assetOut.decimals, assetOutDecimals: assetOut.decimals,
assetInId: assetIn.assetId,
assetOutId: assetOut.assetId,
assetInSymbol: assetIn.symbol,
assetOutSymbol: assetOut.symbol,
}); });
} }
@ -279,7 +310,8 @@ function buildQuote({
quoteNotional, quoteNotional,
notionalAssetId, notionalAssetId,
notionalSymbol: notionalAsset?.symbol || null, notionalSymbol: notionalAsset?.symbol || null,
maxNotionalEure, maxNotional,
legacyEureNotional,
proposedAmountIn: proposedInputUnits, proposedAmountIn: proposedInputUnits,
proposedAmountOut: demand.amount_out, proposedAmountOut: demand.amount_out,
impliedRate, impliedRate,
@ -288,6 +320,10 @@ function buildQuote({
priceId: price.price_id, priceId: price.price_id,
assetInDecimals: assetIn.decimals, assetInDecimals: assetIn.decimals,
assetOutDecimals: assetOut.decimals, assetOutDecimals: assetOut.decimals,
assetInId: assetIn.assetId,
assetOutId: assetOut.assetId,
assetInSymbol: assetIn.symbol,
assetOutSymbol: assetOut.symbol,
}); });
} }
@ -303,7 +339,8 @@ function finalizeQuote({
quoteNotional, quoteNotional,
notionalAssetId = null, notionalAssetId = null,
notionalSymbol = null, notionalSymbol = null,
maxNotionalEure, maxNotional,
legacyEureNotional = false,
proposedAmountIn = null, proposedAmountIn = null,
proposedAmountOut = null, proposedAmountOut = null,
impliedRate, impliedRate,
@ -312,6 +349,10 @@ function finalizeQuote({
priceId, priceId,
assetInDecimals = null, assetInDecimals = null,
assetOutDecimals = null, assetOutDecimals = null,
assetInId = null,
assetOutId = null,
assetInSymbol = null,
assetOutSymbol = null,
}) { }) {
const grossEdgePct = ((referenceRate - impliedRate) / referenceRate) * 100; const grossEdgePct = ((referenceRate - impliedRate) / referenceRate) * 100;
const reasonBase = { const reasonBase = {
@ -327,10 +368,16 @@ function finalizeQuote({
reference_price_id: priceId, reference_price_id: priceId,
asset_in_decimals: assetInDecimals == null ? null : String(assetInDecimals), asset_in_decimals: assetInDecimals == null ? null : String(assetInDecimals),
asset_out_decimals: assetOutDecimals == null ? null : String(assetOutDecimals), asset_out_decimals: assetOutDecimals == null ? null : String(assetOutDecimals),
source_asset_id: assetInId,
source_symbol: assetInSymbol,
source_decimals: assetInDecimals == null ? null : String(assetInDecimals),
destination_asset_id: assetOutId,
destination_symbol: assetOutSymbol,
destination_decimals: assetOutDecimals == null ? null : String(assetOutDecimals),
notional: formatNumber(quoteNotional, 6), notional: formatNumber(quoteNotional, 6),
notional_asset_id: notionalAssetId, notional_asset_id: notionalAssetId,
notional_symbol: notionalSymbol, notional_symbol: notionalSymbol,
eure_notional: formatNumber(quoteNotional, 6), eure_notional: legacyEureNotional ? formatNumber(quoteNotional, 6) : null,
proposed_amount_in: proposedAmountIn, proposed_amount_in: proposedAmountIn,
proposed_amount_out: proposedAmountOut, proposed_amount_out: proposedAmountOut,
}; };
@ -339,7 +386,7 @@ function finalizeQuote({
return { ok: false, reason: 'invalid_pricing', details: reasonBase }; return { ok: false, reason: 'invalid_pricing', details: reasonBase };
} }
if (quoteNotional > maxNotionalEure) { if (quoteNotional > maxNotional) {
return { ok: false, reason: 'max_notional_exceeded', details: reasonBase }; return { ok: false, reason: 'max_notional_exceeded', details: reasonBase };
} }
@ -381,11 +428,31 @@ function withReason(decision, reason) {
}; };
} }
function buildExpectedMakerInventoryDelta({
demand,
quoteOutput = {},
proposedAmountIn = null,
proposedAmountOut = null,
} = {}) {
if (!demand?.asset_in || !demand?.asset_out || !demand?.request_kind) return null;
const receiveAmount = demand.request_kind === 'exact_in'
? demand.amount_in
: quoteOutput?.amount_in || proposedAmountIn;
const sendAmount = demand.request_kind === 'exact_in'
? quoteOutput?.amount_out || proposedAmountOut
: demand.amount_out;
if (receiveAmount == null || sendAmount == null) return null;
return {
[demand.asset_in]: bigintAmount(receiveAmount).toString(),
[demand.asset_out]: (-bigintAmount(sendAmount)).toString(),
};
}
function resolvePairRuntime({ function resolvePairRuntime({
payload, payload,
config, config,
thresholdPct, thresholdPct,
maxNotionalEure, maxNotional,
}) { }) {
const key = pairKey(payload.asset_in, payload.asset_out); const key = pairKey(payload.asset_in, payload.asset_out);
const requiresDb = config.requireDbTradingConfig === true || config.tradingConfigLoaded === true; const requiresDb = config.requireDbTradingConfig === true || config.tradingConfigLoaded === true;
@ -410,7 +477,7 @@ function resolvePairRuntime({
assetIn: config.assetRegistry?.get(payload.asset_in) || null, assetIn: config.assetRegistry?.get(payload.asset_in) || null,
assetOut: config.assetRegistry?.get(payload.asset_out) || null, assetOut: config.assetRegistry?.get(payload.asset_out) || null,
thresholdPct, thresholdPct,
maxNotionalEure, maxNotional,
priceMaxAgeMs: config.strategyPriceMaxAgeMs, priceMaxAgeMs: config.strategyPriceMaxAgeMs,
inventoryMaxAgeMs: config.strategyInventoryMaxAgeMs, inventoryMaxAgeMs: config.strategyInventoryMaxAgeMs,
pair: null, pair: null,
@ -431,7 +498,7 @@ function resolvePairRuntime({
assetIn: null, assetIn: null,
assetOut: null, assetOut: null,
thresholdPct, thresholdPct,
maxNotionalEure, maxNotional,
priceMaxAgeMs: config.strategyPriceMaxAgeMs, priceMaxAgeMs: config.strategyPriceMaxAgeMs,
inventoryMaxAgeMs: config.strategyInventoryMaxAgeMs, inventoryMaxAgeMs: config.strategyInventoryMaxAgeMs,
}; };
@ -450,7 +517,7 @@ function resolvePairRuntime({
assetIn: pair?.assetIn || config.assetRegistry?.get(payload.asset_in) || null, assetIn: pair?.assetIn || config.assetRegistry?.get(payload.asset_in) || null,
assetOut: pair?.assetOut || config.assetRegistry?.get(payload.asset_out) || null, assetOut: pair?.assetOut || config.assetRegistry?.get(payload.asset_out) || null,
thresholdPct, thresholdPct,
maxNotionalEure, maxNotional,
priceMaxAgeMs: config.strategyPriceMaxAgeMs, priceMaxAgeMs: config.strategyPriceMaxAgeMs,
inventoryMaxAgeMs: config.strategyInventoryMaxAgeMs, inventoryMaxAgeMs: config.strategyInventoryMaxAgeMs,
}; };
@ -475,7 +542,7 @@ function resolvePairRuntime({
assetIn: pair.assetIn, assetIn: pair.assetIn,
assetOut: pair.assetOut, assetOut: pair.assetOut,
thresholdPct: Number(pair.strategyConfig.edgeBps) / 100, thresholdPct: Number(pair.strategyConfig.edgeBps) / 100,
maxNotionalEure: Number(pair.strategyConfig.maxNotional), maxNotional: Number(pair.strategyConfig.maxNotional),
priceMaxAgeMs: Number(pair.strategyConfig.priceMaxAgeMs), priceMaxAgeMs: Number(pair.strategyConfig.priceMaxAgeMs),
inventoryMaxAgeMs: Number(pair.strategyConfig.inventoryMaxAgeMs), inventoryMaxAgeMs: Number(pair.strategyConfig.inventoryMaxAgeMs),
}; };
@ -496,43 +563,23 @@ function blockedPairRuntime(pair, config, reason) {
assetIn: pair?.assetIn || null, assetIn: pair?.assetIn || null,
assetOut: pair?.assetOut || null, assetOut: pair?.assetOut || null,
thresholdPct: pair?.strategyConfig ? Number(pair.strategyConfig.edgeBps) / 100 : config.strategyGrossThresholdPct, thresholdPct: pair?.strategyConfig ? Number(pair.strategyConfig.edgeBps) / 100 : config.strategyGrossThresholdPct,
maxNotionalEure: pair?.strategyConfig ? Number(pair.strategyConfig.maxNotional) : config.strategyMaxNotionalEure, maxNotional: pair?.strategyConfig
? Number(pair.strategyConfig.maxNotional)
: (config.strategyMaxNotional ?? config.strategyMaxNotionalEure),
priceMaxAgeMs: pair?.strategyConfig ? Number(pair.strategyConfig.priceMaxAgeMs) : config.strategyPriceMaxAgeMs, priceMaxAgeMs: pair?.strategyConfig ? Number(pair.strategyConfig.priceMaxAgeMs) : config.strategyPriceMaxAgeMs,
inventoryMaxAgeMs: pair?.strategyConfig ? Number(pair.strategyConfig.inventoryMaxAgeMs) : config.strategyInventoryMaxAgeMs, inventoryMaxAgeMs: pair?.strategyConfig ? Number(pair.strategyConfig.inventoryMaxAgeMs) : config.strategyInventoryMaxAgeMs,
}; };
} }
function classifyPriceRouteDirection({ payload, priceRoute }) { function classifyPriceRouteDirection({ payload, priceRoute }) {
if (!priceRoute) return 'unsupported'; return classifyRouteDirection({
if (!['btc_eur_reference', 'btc_usdc_reference'].includes(priceRoute.source)) return 'unsupported'; sourceAssetId: payload.asset_in,
if (payload.asset_in === priceRoute.baseAssetId && payload.asset_out === priceRoute.quoteAssetId) { destinationAssetId: payload.asset_out,
return priceRoute.source === 'btc_usdc_reference' ? 'btc_to_usdc' : 'btc_to_eure'; priceRoute,
} });
if (payload.asset_in === priceRoute.quoteAssetId && payload.asset_out === priceRoute.baseAssetId) {
return priceRoute.source === 'btc_usdc_reference' ? 'usdc_to_btc' : 'eure_to_btc';
}
return 'unsupported';
} }
function resolveReferenceRates({ direction, price }) { function isLegacyEureNotional({ pairRuntime, config }) {
const rateFieldsByDirection = { if (!pairRuntime?.priceRoute) return true;
btc_to_eure: ['eure_per_btc', 'btc_per_eure', true], return pairRuntime.priceRoute.quoteAssetId === config.tradingEure?.assetId;
eure_to_btc: ['eure_per_btc', 'btc_per_eure', false],
btc_to_usdc: ['usdc_per_btc', 'btc_per_usdc', true],
usdc_to_btc: ['usdc_per_btc', 'btc_per_usdc', false],
};
const fields = rateFieldsByDirection[direction];
if (!fields) return { ok: false, reason: 'unsupported_pair' };
const [quotePerBaseField, basePerQuoteField, baseToQuote] = fields;
const quotePerBase = Number(price[quotePerBaseField]);
const basePerQuote = Number(price[basePerQuoteField]);
if (!(quotePerBase > 0) || !(basePerQuote > 0)) {
return { ok: false, reason: 'reference_price_missing' };
}
return {
ok: true,
quotePerBase,
basePerQuote,
baseToQuote,
};
} }

View file

@ -1148,9 +1148,11 @@ function buildTradingConfigSnapshot({
const defaultTakerPair = pairByKey.get('nep141:gnosis-0x420ca0f9b9b604ce0fd9c18ef134c705e5fa3430.omft.near->nep141:nbtc.bridge.near') const defaultTakerPair = pairByKey.get('nep141:gnosis-0x420ca0f9b9b604ce0fd9c18ef134c705e5fa3430.omft.near->nep141:nbtc.bridge.near')
|| pairs.find((pair) => pair.takerEnabled && pair.canTrade) || pairs.find((pair) => pair.takerEnabled && pair.canTrade)
|| null; || null;
const activeAssetIds = preferredActivePair?.assetIn && preferredActivePair?.assetOut const activeAssetIds = [...new Set(
? [preferredActivePair.assetIn.assetId, preferredActivePair.assetOut.assetId] observedPairs
: []; .flatMap((pair) => [pair.assetIn?.assetId, pair.assetOut?.assetId])
.filter(Boolean),
)];
const blockReason = observedPairs.length === 0 const blockReason = observedPairs.length === 0
? 'no_enabled_pairs' ? 'no_enabled_pairs'
: trackedAssets.length === 0 : trackedAssets.length === 0
@ -1231,6 +1233,7 @@ export function summarizeTradingConfigSnapshot(snapshot) {
tracked_asset_count: snapshot.trackedAssets?.length || 0, tracked_asset_count: snapshot.trackedAssets?.length || 0,
enabled_pair_count: snapshot.observedPairs?.length || 0, enabled_pair_count: snapshot.observedPairs?.length || 0,
active_pair: snapshot.activePair || null, active_pair: snapshot.activePair || null,
active_pairs: (snapshot.observedPairs || []).map((pair) => pair.key || pair.pairId),
pairs: (snapshot.pairs || []).map((pair) => ({ pairs: (snapshot.pairs || []).map((pair) => ({
pair_id: pair.pairId, pair_id: pair.pairId,
pair: pair.key, pair: pair.key,
@ -2233,8 +2236,6 @@ export async function refreshQuoteOutcomes(pool, {
submissionLimit = 1000, submissionLimit = 1000,
inventoryLimit = 5000, inventoryLimit = 5000,
} = {}) { } = {}) {
if (!btcAsset?.assetId || !eureAsset?.assetId) return [];
const safeSubmissionLimit = Math.max(1, Number(submissionLimit) || 1000); const safeSubmissionLimit = Math.max(1, Number(submissionLimit) || 1000);
const safeInventoryLimit = Math.max(1, Number(inventoryLimit) || 5000); const safeInventoryLimit = Math.max(1, Number(inventoryLimit) || 5000);
const submissionsResult = await pool.query( const submissionsResult = await pool.query(
@ -2860,6 +2861,21 @@ export async function loadLatestMarketPrice(pool) {
}; };
} }
export async function loadLatestMarketPriceForRoute(pool, { priceRouteId } = {}) {
if (!priceRouteId) return loadLatestMarketPrice(pool);
const latest = await loadLatestEventPayload(
pool,
'market_price_events',
"WHERE payload->>'price_route_id' = $1 ORDER BY COALESCE(observed_at, ingested_at) DESC LIMIT 1",
[priceRouteId],
);
if (!latest) return null;
return {
ingested_at: latest.ingested_at,
payload: latest.payload,
};
}
export async function loadRecentQuotes(pool, { limit = 10 } = {}) { export async function loadRecentQuotes(pool, { limit = 10 } = {}) {
const result = await pool.query( const result = await pool.query(
` `
@ -3281,6 +3297,9 @@ function normalizeQuoteOutcomeRow(row) {
direction: payload.direction || null, direction: payload.direction || null,
request_kind: payload.request_kind || null, request_kind: payload.request_kind || null,
gross_edge_pct: payload.gross_edge_pct || null, gross_edge_pct: payload.gross_edge_pct || null,
notional: payload.notional || null,
notional_asset_id: payload.notional_asset_id || null,
notional_symbol: payload.notional_symbol || null,
eure_notional: payload.eure_notional || null, eure_notional: payload.eure_notional || null,
execution_result_status: row.execution_result_status || payload.execution_result_status || null, execution_result_status: row.execution_result_status || payload.execution_result_status || null,
execution_result_code: row.execution_result_code || payload.execution_result_code || null, execution_result_code: row.execution_result_code || payload.execution_result_code || null,

View file

@ -5,6 +5,10 @@ function statusSubtitle(label, status, websocketState) {
switch (label) { switch (label) {
case 'Pair': case 'Pair':
return `WebSocket ${websocketState}`; return `WebSocket ${websocketState}`;
case 'Pairs':
return status.active_pair_count ? `${status.active_pair_count} enabled` : `WebSocket ${websocketState}`;
case 'Reference Route':
return status.latest_reference_route_label || 'Reference route';
case 'Market Freshness': case 'Market Freshness':
return formatTimestamp(status.market_observed_at); return formatTimestamp(status.market_observed_at);
case 'Inventory Freshness': case 'Inventory Freshness':
@ -29,10 +33,10 @@ export default function StatusBar({ status, websocketState }) {
]] ]]
: []; : [];
const tiles = [ const tiles = [
['Pair', truncateMiddle(status.active_pair, 40), status.active_pair], ['Pairs', truncateMiddle((status.active_pairs || [status.active_pair]).filter(Boolean).join(', '), 40), (status.active_pairs || [status.active_pair]).filter(Boolean).join(', ')],
...nearIntentsTile, ...nearIntentsTile,
['Portfolio', formatEur(status.current_total_portfolio_value_eure)], ['Portfolio', formatEur(status.current_total_portfolio_value_eure)],
['Reference BTC/EUR', formatEur(status.latest_reference_price_eure_per_btc)], ['Reference Route', status.latest_reference_route_value || formatEur(status.latest_reference_price_eure_per_btc), status.latest_reference_route_label],
['Market Freshness', formatAge(status.market_freshness_ms)], ['Market Freshness', formatAge(status.market_freshness_ms)],
['Inventory Freshness', formatAge(status.inventory_freshness_ms)], ['Inventory Freshness', formatAge(status.inventory_freshness_ms)],
['Strategy Armed', formatBoolean(status.strategy_armed)], ['Strategy Armed', formatBoolean(status.strategy_armed)],
@ -50,7 +54,7 @@ export default function StatusBar({ status, websocketState }) {
className={[ className={[
'status-value', 'status-value',
signedClass(value), signedClass(value),
label === 'Pair' ? 'truncate-line mono' : '', label === 'Pairs' ? 'truncate-line mono' : '',
].filter(Boolean).join(' ')} ].filter(Boolean).join(' ')}
title={fullValue || 'Unavailable'} title={fullValue || 'Unavailable'}
> >

View file

@ -44,7 +44,12 @@ function BalancesTable({ items }) {
<td className="mono">{item.spendable}</td> <td className="mono">{item.spendable}</td>
<td className="mono">{item.pending_inbound}</td> <td className="mono">{item.pending_inbound}</td>
<td className="mono">{item.pending_outbound}</td> <td className="mono">{item.pending_outbound}</td>
<td className="mono">{formatEur(item.eur_value_eure)}</td> <td className="mono">
{item.valuation_status === 'unvalued'
? item.valuation_reason || 'valuation_route_missing'
: formatEur(item.eur_value_eure)}
{item.eur_value_source ? <div className="status-subtle">{item.eur_value_source}</div> : null}
</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
@ -279,26 +284,31 @@ function IdentifierLine({ label, value }) {
function IntentRequestForm({ summary, onControl }) { function IntentRequestForm({ summary, onControl }) {
const defaults = summary?.defaults || {}; const defaults = summary?.defaults || {};
const sourceSymbol = defaults.source_symbol || 'EURe'; const pairs = summary?.pairs || [];
const destinationSymbol = defaults.destination_symbol || 'BTC'; const hasAmountCap = defaults.max_source_amount != null && defaults.max_source_amount !== '';
const hasAmountCap = defaults.max_amount_eure != null && defaults.max_amount_eure !== '';
const hasSlippageCap = defaults.max_slippage_bps != null && defaults.max_slippage_bps !== ''; const hasSlippageCap = defaults.max_slippage_bps != null && defaults.max_slippage_bps !== '';
const [form, setForm] = useState({ const [form, setForm] = useState({
amount_eure: defaults.amount_eure || '5', pair_id: defaults.pair_id || '',
source_amount: defaults.source_amount || '5',
slippage_bps: String(defaults.slippage_bps ?? 200), slippage_bps: String(defaults.slippage_bps ?? 200),
}); });
const selectedPair = pairs.find((pair) => pair.pair_id === form.pair_id) || null;
const sourceSymbol = selectedPair?.source_symbol || defaults.source_symbol || 'Source';
const destinationSymbol = selectedPair?.destination_symbol || defaults.destination_symbol || 'Destination';
useEffect(() => { useEffect(() => {
setForm({ setForm({
amount_eure: defaults.amount_eure || '5', pair_id: defaults.pair_id || '',
source_amount: defaults.source_amount || '5',
slippage_bps: String(defaults.slippage_bps ?? 200), slippage_bps: String(defaults.slippage_bps ?? 200),
}); });
}, [defaults.amount_eure, defaults.slippage_bps]); }, [defaults.pair_id, defaults.source_amount, defaults.slippage_bps]);
async function handlePreflight(event) { async function handlePreflight(event) {
event.preventDefault(); event.preventDefault();
await onControl('trade-executor', 'intent-request-preflight', { await onControl('trade-executor', 'intent-request-preflight', {
amount_eure: form.amount_eure, pair_id: form.pair_id || undefined,
source_amount: form.source_amount,
slippage_bps: Number(form.slippage_bps), slippage_bps: Number(form.slippage_bps),
}); });
} }
@ -306,20 +316,37 @@ function IntentRequestForm({ summary, onControl }) {
return ( return (
<form onSubmit={handlePreflight}> <form onSubmit={handlePreflight}>
<div className="form-grid"> <div className="form-grid">
{pairs.length ? (
<div className="field">
<label htmlFor="intent-request-pair">Pair</label>
<select
id="intent-request-pair"
name="pair_id"
onChange={(event) => setForm((current) => ({ ...current, pair_id: event.target.value }))}
value={form.pair_id}
>
{pairs.map((pair) => (
<option disabled={!pair.can_trade} key={pair.pair_id} value={pair.pair_id}>
{pair.label}
</option>
))}
</select>
</div>
) : null}
<div className="field"> <div className="field">
<label htmlFor="intent-request-amount">{`Spend ${sourceSymbol}`}</label> <label htmlFor="intent-request-amount">{`Spend ${sourceSymbol}`}</label>
<input <input
id="intent-request-amount" id="intent-request-amount"
min="0.01" min="0.01"
name="amount_eure" name="source_amount"
onChange={(event) => setForm((current) => ({ ...current, amount_eure: event.target.value }))} onChange={(event) => setForm((current) => ({ ...current, source_amount: event.target.value }))}
step="0.01" step="0.01"
type="number" type="number"
value={form.amount_eure} value={form.source_amount}
{...(hasAmountCap ? { max: defaults.max_amount_eure } : {})} {...(hasAmountCap ? { max: defaults.max_source_amount } : {})}
/> />
<div className="status-subtle"> <div className="status-subtle">
{hasAmountCap ? `Max ${defaults.max_amount_eure} ${sourceSymbol}` : 'No request amount cap'} {hasAmountCap ? `Max ${defaults.max_source_amount} ${sourceSymbol}` : 'No request amount cap'}
</div> </div>
</div> </div>
<div className="field"> <div className="field">
@ -412,7 +439,7 @@ function IntentRequestLifecycle({ item }) {
function IntentRequestsTable({ items, executorArmed, onControl }) { function IntentRequestsTable({ items, executorArmed, onControl }) {
const [expanded, setExpanded] = useState(() => new Set()); const [expanded, setExpanded] = useState(() => new Set());
if (!items?.length) return <EmptyState>No repo-created EURe-to-BTC requests are stored yet.</EmptyState>; if (!items?.length) return <EmptyState>No repo-created pair requests are stored yet.</EmptyState>;
function toggle(rowKey) { function toggle(rowKey) {
setExpanded((current) => { setExpanded((current) => {
@ -679,7 +706,7 @@ export default function FundsPage({
<div className="panel-head"> <div className="panel-head">
<div> <div>
<div className="eyebrow">Own requests</div> <div className="eyebrow">Own requests</div>
<h3>EURe to BTC request creation</h3> <h3>Pair request creation</h3>
<div className="panel-subtitle">Create a solver quote request first, then submit only a drafted request. Completed requires inventory movement, not relay acceptance.</div> <div className="panel-subtitle">Create a solver quote request first, then submit only a drafted request. Completed requires inventory movement, not relay acceptance.</div>
</div> </div>
<div className="pills"> <div className="pills">

View file

@ -70,8 +70,16 @@ function responseLabel(item) {
} }
function grossEdgeEstimate(item) { function grossEdgeEstimate(item) {
if (!item.gross_edge_value_eure) return 'Unavailable'; if (!item.gross_edge_value) return 'Unavailable';
return formatEur(item.gross_edge_value_eure); const symbol = item.notional_symbol || (item.eure_notional ? 'EURe' : null);
if (symbol === 'EURe') return formatEur(item.gross_edge_value);
return symbol ? `${item.gross_edge_value} ${symbol}` : item.gross_edge_value;
}
function notionalLabel(item) {
if (item.notional_display) return item.notional_display;
if (item.notional != null) return `${item.notional}${item.notional_symbol ? ` ${item.notional_symbol}` : ''}`;
return item.eure_notional ? formatEur(item.eure_notional) : 'Notional unavailable';
} }
function plainCodeLabel(value, fallback = 'Unavailable') { function plainCodeLabel(value, fallback = 'Unavailable') {
@ -109,7 +117,7 @@ function LifecycleDetails({ item }) {
<StageCard at={item.decision_at} status={strategyDecisionStatus(item.decision)} title="2. Strategy decided"> <StageCard at={item.decision_at} status={strategyDecisionStatus(item.decision)} title="2. Strategy decided">
<div>{plainCodeLabel(item.decision?.decision_reason || item.reason_code, 'No decision reason recorded')}</div> <div>{plainCodeLabel(item.decision?.decision_reason || item.reason_code, 'No decision reason recorded')}</div>
<div className="status-subtle">{item.gross_edge_pct ? `Edge ${item.gross_edge_pct}%` : 'Edge unavailable'}</div> <div className="status-subtle">{item.gross_edge_pct ? `Edge ${item.gross_edge_pct}%` : 'Edge unavailable'}</div>
<div className="status-subtle">{item.eure_notional ? `Notional ${formatEur(item.eure_notional)}` : 'Notional unavailable'}</div> <div className="status-subtle">{notionalLabel(item)}</div>
</StageCard> </StageCard>
<StageCard at={item.command_at} status={item.command_id ? 'Command recorded' : 'No command'} title="3. Executor command"> <StageCard at={item.command_at} status={item.command_id ? 'Command recorded' : 'No command'} title="3. Executor command">
@ -195,7 +203,7 @@ function QuoteLifecycleTable({ items }) {
</td> </td>
<td> <td>
<div className={Number(item.gross_edge_pct) > 0 ? 'value-positive' : Number(item.gross_edge_pct) < 0 ? 'value-negative' : ''}>{item.gross_edge_pct ? `${item.gross_edge_pct}%` : 'Unavailable'}</div> <div className={Number(item.gross_edge_pct) > 0 ? 'value-positive' : Number(item.gross_edge_pct) < 0 ? 'value-negative' : ''}>{item.gross_edge_pct ? `${item.gross_edge_pct}%` : 'Unavailable'}</div>
<div className="status-subtle">{item.eure_notional ? formatEur(item.eure_notional) : 'Notional unavailable'}</div> <div className="status-subtle">{notionalLabel(item)}</div>
</td> </td>
<td> <td>
<button className="button secondary" onClick={() => toggle(rowKey)} type="button"> <button className="button secondary" onClick={() => toggle(rowKey)} type="button">
@ -261,7 +269,7 @@ function SuccessfulTradesTable({ items }) {
<td><IdentifierRow label="Quote" value={item.quote_id} /></td> <td><IdentifierRow label="Quote" value={item.quote_id} /></td>
<td> <td>
<div>{item.gross_edge_pct ? `${item.gross_edge_pct}%` : 'Unavailable'}</div> <div>{item.gross_edge_pct ? `${item.gross_edge_pct}%` : 'Unavailable'}</div>
<div className="status-subtle">{item.eure_notional ? formatEur(item.eure_notional) : 'Notional unavailable'}</div> <div className="status-subtle">{notionalLabel(item)}</div>
</td> </td>
<td> <td>
<div>{grossEdgeEstimate(item)}</div> <div>{grossEdgeEstimate(item)}</div>

View file

@ -14,6 +14,33 @@ function createEngine() {
}); });
} }
function createMultiRouteEngine() {
return createAlertEngine({
activePair: 'nep141:btc.omft.near->nep141:eure.omft.near',
activePairs: [
'nep141:btc.omft.near->nep141:eure.omft.near',
'nep141:btc.omft.near->nep141:usdc.omft.near',
],
priceRoutes: [
{
pair: 'nep141:btc.omft.near->nep141:eure.omft.near',
price_route_id: 'btc-eur-route',
reference_pair: 'BTC/EUR',
},
{
pair: 'nep141:btc.omft.near->nep141:usdc.omft.near',
price_route_id: 'btc-usdc-route',
reference_pair: 'BTC/USDC',
},
],
priceStaleMs: 30_000,
inventoryStaleMs: 30_000,
fundingCreditPendingMs: 300_000,
fundingStuckMs: 3_600_000,
evaluationIntervalMs: 5_000,
});
}
test('alert engine raises and clears stale state transitions', () => { test('alert engine raises and clears stale state transitions', () => {
const engine = createEngine(); const engine = createEngine();
@ -127,3 +154,36 @@ test('runtime alerts raise and clear independently from event-derived alerts', (
assert.equal(transitions[0].alert_code, 'near_intents_quotes_stale'); assert.equal(transitions[0].alert_code, 'near_intents_quotes_stale');
assert.equal(transitions[0].status, 'cleared'); assert.equal(transitions[0].status, 'cleared');
}); });
test('reference price stale alert is scoped to the affected route and pair', () => {
const engine = createMultiRouteEngine();
const transitions = engine.applyEvent('ref.market_price', {
price_id: 'price-eur-1',
pair: 'nep141:btc.omft.near->nep141:eure.omft.near',
price_route_id: 'btc-eur-route',
reference_pair: 'BTC/EUR',
quote_per_base: '50000',
observed_at: '2026-04-03T08:00:00.000Z',
}, '2026-04-03T08:00:00.000Z');
engine.applyEvent('state.intent_inventory', {
inventory_id: 'inventory-1',
spendable: {},
synced_at: '2026-04-03T08:00:00.000Z',
}, '2026-04-03T08:00:00.000Z');
const stale = transitions.find((entry) => entry.alert_code === 'reference_price_stale');
assert.equal(stale.status, 'raised');
assert.equal(stale.pair, 'nep141:btc.omft.near->nep141:usdc.omft.near');
assert.equal(stale.details.price_route_id, 'btc-usdc-route');
assert.equal(stale.details.reference_pair, 'BTC/USDC');
assert.deepEqual(stale.details.active_pairs, [
'nep141:btc.omft.near->nep141:eure.omft.near',
'nep141:btc.omft.near->nep141:usdc.omft.near',
]);
assert.deepEqual(engine.getState('2026-04-03T08:00:10.000Z').active_pairs, [
'nep141:btc.omft.near->nep141:eure.omft.near',
'nep141:btc.omft.near->nep141:usdc.omft.near',
]);
});

View file

@ -13,6 +13,11 @@ const EURE = {
symbol: 'EURe', symbol: 'EURe',
decimals: 18, decimals: 18,
}; };
const USDC = {
assetId: 'nep141:usdc.omft.near',
symbol: 'USDC',
decimals: 6,
};
function preflight(overrides = {}) { function preflight(overrides = {}) {
return { return {
@ -179,3 +184,49 @@ test('blocked preflight remains blocked and is distinct from request rejection o
assert.notEqual(record.outcome_status, 'completed'); assert.notEqual(record.outcome_status, 'completed');
assert.notEqual(record.outcome_status, 'rejected'); assert.notEqual(record.outcome_status, 'rejected');
}); });
test('completed BTC/USDC request uses preflight source and destination assets', () => {
const [record] = outcomes({
preflights: [preflight({
source_asset_id: USDC.assetId,
source_symbol: 'USDC',
destination_asset_id: BTC.assetId,
destination_symbol: 'BTC',
source_amount_units: '10000000',
destination_amount_units: '20000',
quoted_destination_amount_units: '20000',
notional: '10',
notional_asset_id: USDC.assetId,
notional_symbol: 'USDC',
})],
submissions: [submission({ destination_amount_units: '20000' })],
inventorySnapshots: [
{
inventory_id: 'inventory-before-usdc',
observed_at: '2026-04-12T10:00:00.000Z',
spendable: {
[USDC.assetId]: '10000000',
[BTC.assetId]: '0',
[EURE.assetId]: '1',
},
},
{
inventory_id: 'inventory-after-usdc',
observed_at: '2026-04-12T10:00:15.000Z',
spendable: {
[USDC.assetId]: '0',
[BTC.assetId]: '20000',
[EURE.assetId]: '1',
},
},
],
});
assert.equal(record.outcome_status, 'completed');
assert.equal(record.payload.notional_symbol, 'USDC');
assert.deepEqual(record.attributed_inventory_delta.delta_units, {
[BTC.assetId]: '20000',
[EURE.assetId]: '0',
[USDC.assetId]: '-10000000',
});
});

View file

@ -23,6 +23,11 @@ const EURE = {
symbol: 'EURe', symbol: 'EURe',
decimals: 18, decimals: 18,
}; };
const USDC = {
assetId: 'nep141:usdc.omft.near',
symbol: 'USDC',
decimals: 6,
};
function buildConfig() { function buildConfig() {
return { return {
@ -155,6 +160,43 @@ function buildController({
}; };
} }
function buildPair({
sourceAsset,
destinationAsset,
source,
routeId,
canTrade = true,
blockReason = null,
}) {
const pairId = `${sourceAsset.assetId}->${destinationAsset.assetId}`;
return {
key: pairId,
pairId,
takerEnabled: true,
canTrade,
blockReason,
assetIn: sourceAsset,
assetOut: destinationAsset,
strategyConfig: {
configId: `${pairId}:v1`,
version: 1,
requestDefaultNotional: '5',
requestMaxNotional: null,
slippageBps: 200,
requestMaxSlippageBps: null,
minDeadlineMs: 60_000,
priceMaxAgeMs: 30_000,
inventoryMaxAgeMs: 30_000,
},
priceRoute: source ? {
routeId,
source,
baseAssetId: BTC.assetId,
quoteAssetId: sourceAsset.assetId === BTC.assetId ? destinationAsset.assetId : sourceAsset.assetId,
} : null,
};
}
test('EURe decimal parsing, BTC expected receive, and slippage math are exact enough for request limits', () => { test('EURe decimal parsing, BTC expected receive, and slippage math are exact enough for request limits', () => {
const sourceUnits = parseDecimalToUnits('5', EURE.decimals, { field: 'amount_eure' }); const sourceUnits = parseDecimalToUnits('5', EURE.decimals, { field: 'amount_eure' });
const expectedBtc = computeBtcReceiveUnitsFromEure({ const expectedBtc = computeBtcReceiveUnitsFromEure({
@ -259,6 +301,157 @@ test('DB null request limits allow operator-chosen amount and slippage', async (
assert.equal(relay.quoteCalls, 1); assert.equal(relay.quoteCalls, 1);
}); });
test('nBTC/USDC preflight accepts pair_id and source_amount without EURe price fields', async () => {
const nowIso = '2026-04-12T10:00:00.000Z';
const store = buildStore({ nowIso });
store.loadLatestInventorySnapshot = async () => ({
ingested_at: nowIso,
payload: {
synced_at: nowIso,
spendable: {
[USDC.assetId]: '10000000',
[BTC.assetId]: '0',
},
pending_inbound: {},
},
});
store.loadLatestMarketPrice = async ({ priceRouteId } = {}) => ({
ingested_at: nowIso,
payload: {
observed_at: nowIso,
price_id: 'price-usdc-1',
price_route_id: priceRouteId,
usdc_per_btc: '50000',
},
});
const relay = buildRelay();
relay.quote = async function quote(request) {
this.quoteCalls += 1;
assert.equal(request.defuse_asset_identifier_in, USDC.assetId);
assert.equal(request.defuse_asset_identifier_out, BTC.assetId);
assert.equal(request.exact_amount_in, '10000000');
return [{
quote_hash: 'usdc-quote-hash',
amount_out: '20000',
expiration_time: '2026-04-12T10:01:00.000Z',
}];
};
const pair = buildPair({
sourceAsset: USDC,
destinationAsset: BTC,
source: 'btc_usdc_reference',
routeId: 'btc-usdc-route',
});
const { controller } = buildController({
store,
relay,
getTradingConfig: async () => ({
ok: true,
tradingEure: EURE,
tradingBtc: BTC,
pairByKey: new Map([[pair.key, pair]]),
pairById: new Map([[pair.pairId, pair]]),
defaultTakerPair: pair,
}),
});
const preflight = await controller.preflight({
pair_id: pair.pairId,
source_amount: '10',
slippage_bps: 200,
});
assert.equal(preflight.state, 'draft');
assert.equal(preflight.reason_code, 'quote_available');
assert.equal(preflight.amount_eure, null);
assert.equal(preflight.source_asset_id, USDC.assetId);
assert.equal(preflight.destination_asset_id, BTC.assetId);
assert.equal(preflight.expected_destination_amount_units, '20000');
assert.equal(preflight.min_destination_amount_units, '19600');
assert.equal(preflight.notional, '10');
assert.equal(preflight.notional_symbol, 'USDC');
assert.equal(preflight.reference_price_id, 'price-usdc-1');
assert.equal(relay.quoteCalls, 1);
});
test('nBTC/USDC preflight blocks missing route before solver quote', async () => {
const relay = buildRelay();
const pair = buildPair({
sourceAsset: USDC,
destinationAsset: BTC,
source: null,
routeId: null,
canTrade: false,
blockReason: 'price_route_missing',
});
const { controller } = buildController({
relay,
getTradingConfig: async () => ({
ok: true,
tradingEure: EURE,
tradingBtc: BTC,
pairByKey: new Map([[pair.key, pair]]),
pairById: new Map([[pair.pairId, pair]]),
defaultTakerPair: pair,
}),
});
const preflight = await controller.preflight({ pair_id: pair.pairId, source_amount: '10' });
assert.equal(preflight.state, 'blocked');
assert.equal(preflight.reason_code, 'price_route_missing');
assert.equal(relay.quoteCalls, 0);
});
test('nBTC/USDC preflight blocks insufficient source inventory with source-specific reason', async () => {
const nowIso = '2026-04-12T10:00:00.000Z';
const store = buildStore({ nowIso });
store.loadLatestInventorySnapshot = async () => ({
ingested_at: nowIso,
payload: {
synced_at: nowIso,
spendable: {
[USDC.assetId]: '1',
},
pending_inbound: {},
},
});
store.loadLatestMarketPrice = async ({ priceRouteId } = {}) => ({
ingested_at: nowIso,
payload: {
observed_at: nowIso,
price_id: 'price-usdc-2',
price_route_id: priceRouteId,
usdc_per_btc: '50000',
},
});
const relay = buildRelay();
const pair = buildPair({
sourceAsset: USDC,
destinationAsset: BTC,
source: 'btc_usdc_reference',
routeId: 'btc-usdc-route',
});
const { controller } = buildController({
store,
relay,
getTradingConfig: async () => ({
ok: true,
tradingEure: EURE,
tradingBtc: BTC,
pairByKey: new Map([[pair.key, pair]]),
pairById: new Map([[pair.pairId, pair]]),
defaultTakerPair: pair,
}),
});
const preflight = await controller.preflight({ pair_id: pair.pairId, source_amount: '10' });
assert.equal(preflight.state, 'blocked');
assert.equal(preflight.reason_code, 'insufficient_spendable_usdc');
assert.equal(relay.quoteCalls, 0);
});
test('insufficient spendable EURe blocks before solver quote or signing', async () => { test('insufficient spendable EURe blocks before solver quote or signing', async () => {
const store = buildStore({ inventoryUnits: '0' }); const store = buildStore({ inventoryUnits: '0' });
const relay = buildRelay(); const relay = buildRelay();

View file

@ -32,6 +32,25 @@ test('funds page no longer renders duplicate quote and submission tables', () =>
assert.doesNotMatch(fundsSource, /Durable ledger/); assert.doesNotMatch(fundsSource, /Durable ledger/);
}); });
test('request UI uses pair-native source amount fields and copy', () => {
assert.match(fundsSource, /name="source_amount"/);
assert.match(fundsSource, /pair_id: form\.pair_id/);
assert.match(fundsSource, /Pair request creation/);
assert.doesNotMatch(fundsSource, /EURe to BTC request creation/);
assert.doesNotMatch(fundsSource, /EURe-to-BTC/);
});
test('funds balance rows expose unvalued valuation reasons', () => {
assert.match(fundsSource, /valuation_status === 'unvalued'/);
assert.match(fundsSource, /valuation_route_missing/);
});
test('status bar labels reference routes instead of a single BTC\\/EUR tile', () => {
assert.match(statusBarSource, /Reference Route/);
assert.match(statusBarSource, /active_pairs/);
assert.doesNotMatch(statusBarSource, /Reference BTC\/EUR/);
});
test('dashboard freshness surfaces show age and exact timestamp evidence', () => { test('dashboard freshness surfaces show age and exact timestamp evidence', () => {
assert.match(serviceCardSource, /formatAgeFromTimestamp\(service\.freshness_at, now\)/); assert.match(serviceCardSource, /formatAgeFromTimestamp\(service\.freshness_at, now\)/);
assert.match(serviceCardSource, /formatTimestamp\(service\.freshness_at\)/); assert.match(serviceCardSource, /formatTimestamp\(service\.freshness_at\)/);

View file

@ -32,6 +32,12 @@ const usdcAsset = {
decimals: 6, decimals: 6,
}; };
const nearAsset = {
assetId: 'nep141:wrap.near',
symbol: 'wNEAR',
decimals: 24,
};
test('portfolio metrics compute portfolio comparison 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: {
@ -196,6 +202,51 @@ test('portfolio metrics include tracked USDC valued from live BTC/USDC reference
]); ]);
}); });
test('portfolio metrics keep tracked assets visible when valuation route is missing', () => {
const currentPrice = {
price_id: 'price-current-unvalued',
observed_at: '2026-05-18T15:41:08.837Z',
eure_per_btc: '65495.20000000',
};
const valuationAssets = buildCashEquivalentValuationAssets({
trackedAssets: [nbtcAsset, eureAsset, nearAsset],
btcAssets: [nbtcAsset],
eureAsset,
priceEvents: [currentPrice],
});
const metric = computePortfolioMetric({
baseline: null,
currentInventory: {
inventory_id: 'current-unvalued',
synced_at: '2026-05-18T15:40:38.976Z',
spendable: {
[nbtcAsset.assetId]: '0',
[eureAsset.assetId]: '0',
[nearAsset.assetId]: '1000000000000000000000000',
},
},
currentPrice,
btcAsset: nbtcAsset,
btcAssets: [nbtcAsset],
eureAsset,
valuationAssets,
});
assert.deepEqual(metric.current_inventory.unvalued_assets.map((asset) => ({
asset_id: asset.asset_id,
amount: asset.amount,
valuation_status: asset.valuation_status,
valuation_reason: asset.valuation_reason,
})), [{
asset_id: nearAsset.assetId,
amount: '1',
valuation_status: 'unvalued',
valuation_reason: 'valuation_route_missing',
}]);
assert.equal(metric.current_portfolio_value_eure, '0');
});
test('portfolio metrics treat later deposits and withdrawals as external cash flows instead of PnL', () => { test('portfolio metrics treat later deposits and withdrawals as external cash flows instead of PnL', () => {
const metric = computePortfolioMetric({ const metric = computePortfolioMetric({
baseline: { baseline: {

View file

@ -9,6 +9,8 @@ test('market reference ingest publishes BTC/USDC route-specific prices', () => {
assert.match(marketReferenceSource, /XBTUSDC/); assert.match(marketReferenceSource, /XBTUSDC/);
assert.match(marketReferenceSource, /btc_usdc_reference/); assert.match(marketReferenceSource, /btc_usdc_reference/);
assert.match(marketReferenceSource, /usdc_per_btc/); assert.match(marketReferenceSource, /usdc_per_btc/);
assert.match(marketReferenceSource, /quote_per_base/);
assert.match(marketReferenceSource, /base_per_quote/);
assert.match(marketReferenceSource, /buildPriceId\(now, referencePair\.priceRoute\.routeId\)/); assert.match(marketReferenceSource, /buildPriceId\(now, referencePair\.priceRoute\.routeId\)/);
}); });

View file

@ -13,6 +13,11 @@ const EURE = {
symbol: 'EURe', symbol: 'EURe',
decimals: 18, decimals: 18,
}; };
const USDC = {
assetId: 'nep141:usdc.omft.near',
symbol: 'USDC',
decimals: 6,
};
function submittedResult(quoteId, observedAt = '2026-04-02T18:13:30.000Z') { function submittedResult(quoteId, observedAt = '2026-04-02T18:13:30.000Z') {
return { return {
@ -192,3 +197,96 @@ test('ambiguous inventory movement is not counted as completed settlement', () =
['ambiguous', 'ambiguous'], ['ambiguous', 'ambiguous'],
); );
}); });
test('BTC/USDC maker command completes from pair-native expected deltas', () => {
const quoteId = 'quote-usdc-settled';
const [outcome] = deriveQuoteOutcomeRecords({
submissions: [submittedResult(quoteId)],
commands: [{
observed_at: '2026-04-02T18:13:29.000Z',
payload: {
quote_id: quoteId,
command_id: `cmd-${quoteId}`,
decision_id: `decision-${quoteId}`,
pair: `${BTC.assetId}->${USDC.assetId}`,
direction: 'base_to_quote',
request_kind: 'exact_in',
asset_in: BTC.assetId,
asset_out: USDC.assetId,
amount_in: '10000',
quote_output: { amount_out: '7960800' },
expected_inventory_delta_units: {
[BTC.assetId]: '10000',
[USDC.assetId]: '-7960800',
},
notional: '8',
notional_asset_id: USDC.assetId,
notional_symbol: 'USDC',
min_deadline_ms: 60000,
},
}],
inventorySnapshots: [
inventorySnapshot('2026-04-02T18:13:00.000Z', {
[BTC.assetId]: '0',
[USDC.assetId]: '10000000',
[EURE.assetId]: '1',
}),
inventorySnapshot('2026-04-02T18:13:33.000Z', {
[BTC.assetId]: '10000',
[USDC.assetId]: '2039200',
[EURE.assetId]: '1',
}),
],
btcAsset: BTC,
eureAsset: EURE,
now: '2026-04-02T18:14:00.000Z',
});
assert.equal(outcome.outcome_status, 'completed');
assert.equal(outcome.payload.notional_symbol, 'USDC');
assert.equal(outcome.attributed_inventory_delta.delta_units[USDC.assetId], '-7960800');
});
test('unrelated active-asset movement does not create false maker completion', () => {
const quoteId = 'quote-usdc-extra-move';
const [outcome] = deriveQuoteOutcomeRecords({
submissions: [submittedResult(quoteId)],
commands: [{
observed_at: '2026-04-02T18:13:29.000Z',
payload: {
quote_id: quoteId,
command_id: `cmd-${quoteId}`,
decision_id: `decision-${quoteId}`,
pair: `${BTC.assetId}->${USDC.assetId}`,
request_kind: 'exact_in',
asset_in: BTC.assetId,
asset_out: USDC.assetId,
amount_in: '10000',
quote_output: { amount_out: '7960800' },
expected_inventory_delta_units: {
[BTC.assetId]: '10000',
[USDC.assetId]: '-7960800',
},
min_deadline_ms: 60000,
},
}],
inventorySnapshots: [
inventorySnapshot('2026-04-02T18:13:00.000Z', {
[BTC.assetId]: '0',
[USDC.assetId]: '10000000',
[EURE.assetId]: '1',
}),
inventorySnapshot('2026-04-02T18:13:33.000Z', {
[BTC.assetId]: '10000',
[USDC.assetId]: '2039200',
[EURE.assetId]: '2',
}),
],
btcAsset: BTC,
eureAsset: EURE,
now: '2026-04-02T18:14:00.000Z',
});
assert.equal(outcome.outcome_status, 'submitted');
assert.equal(outcome.attribution_status, 'unattributed');
});

87
test/route-rates.test.mjs Normal file
View file

@ -0,0 +1,87 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
classifyRouteDirection,
computeDestinationAmountUnitsFromRoute,
resolveRouteRates,
} from '../src/core/route-rates.mjs';
const BTC = 'nep141:nbtc.bridge.near';
const EURE = 'nep141:eure.omft.near';
const USDC = 'nep141:usdc.omft.near';
test('generic route math handles BTC/EUR base to quote and quote to base fixtures', () => {
const priceRoute = {
routeId: 'btc-eur-route',
source: 'btc_eur_reference',
baseAssetId: BTC,
quoteAssetId: EURE,
};
const price = {
price_route_id: 'btc-eur-route',
quote_per_base: '50000',
base_per_quote: '0.00002',
};
const sellBtcDirection = classifyRouteDirection({
sourceAssetId: BTC,
destinationAssetId: EURE,
priceRoute,
});
const buyBtcDirection = classifyRouteDirection({
sourceAssetId: EURE,
destinationAssetId: BTC,
priceRoute,
});
assert.equal(sellBtcDirection, 'base_to_quote');
assert.equal(buyBtcDirection, 'quote_to_base');
const rates = resolveRouteRates({ price, priceRoute, direction: sellBtcDirection });
assert.equal(rates.ok, true);
assert.equal(computeDestinationAmountUnitsFromRoute({
sourceAmountUnits: '10000',
sourceDecimals: 8,
destinationDecimals: 18,
direction: sellBtcDirection,
quotePerBase: rates.quotePerBase,
basePerQuote: rates.basePerQuote,
}), '5000000000000000000');
assert.equal(computeDestinationAmountUnitsFromRoute({
sourceAmountUnits: '5000000000000000000',
sourceDecimals: 18,
destinationDecimals: 8,
direction: buyBtcDirection,
quotePerBase: rates.quotePerBase,
basePerQuote: rates.basePerQuote,
}), '10000');
});
test('generic route math handles BTC/USDC legacy adapter fields', () => {
const priceRoute = {
routeId: 'btc-usdc-route',
source: 'btc_usdc_reference',
baseAssetId: BTC,
quoteAssetId: USDC,
};
const rates = resolveRouteRates({
price: {
price_route_id: 'btc-usdc-route',
usdc_per_btc: '80000',
btc_per_usdc: '0.0000125',
},
priceRoute,
direction: 'quote_to_base',
});
assert.equal(rates.ok, true);
assert.equal(computeDestinationAmountUnitsFromRoute({
sourceAmountUnits: '10000000',
sourceDecimals: 6,
destinationDecimals: 8,
direction: 'quote_to_base',
quotePerBase: rates.quotePerBase,
basePerQuote: rates.basePerQuote,
}), '12500');
});

View file

@ -2,6 +2,7 @@ import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { import {
deriveServiceHealth,
shouldContainExecutorForAlerts, shouldContainExecutorForAlerts,
shouldRaiseIngestPublishStale, shouldRaiseIngestPublishStale,
} from '../src/core/runtime-health.mjs'; } from '../src/core/runtime-health.mjs';
@ -47,3 +48,32 @@ test('executor containment stays disabled even for broken truth path alerts', ()
severity: 'critical', severity: 'critical',
}]), false); }]), false);
}); });
test('armed service treats critical truth alerts for any active pair as critical', () => {
const health = deriveServiceHealth({
service: 'strategy-engine',
snapshot: {
reachable: true,
state: {
armed: true,
},
health: {
ok: true,
},
},
activePairs: [
'btc->eure',
'btc->usdc',
],
activeAlerts: [{
alert_code: 'reference_price_stale',
severity: 'critical',
service_scope: 'strategy-engine',
pair: 'btc->usdc',
}],
now: '2026-05-18T10:00:00.000Z',
});
assert.equal(health.status, 'critical');
assert.equal(health.label, 'armed on stale truth');
});

View file

@ -22,3 +22,10 @@ test('kubernetes production config does not carry pair, asset, or edge env vars'
assert.doesNotMatch(manifest, /INTENT_REQUEST_DEFAULT_AMOUNT_EURE/); assert.doesNotMatch(manifest, /INTENT_REQUEST_DEFAULT_AMOUNT_EURE/);
assert.doesNotMatch(manifest, /INTENT_REQUEST_MAX_AMOUNT_EURE/); assert.doesNotMatch(manifest, /INTENT_REQUEST_MAX_AMOUNT_EURE/);
}); });
test('live market-maker watch script reads DB assets instead of removed trading env vars', () => {
const source = readFileSync(new URL('../scripts/ops/watch_live_mm.py', import.meta.url), 'utf8');
assert.match(source, /from trading_assets/);
assert.doesNotMatch(source, /TRADING_BTC_ASSET_ID/);
assert.doesNotMatch(source, /TRADING_EURE_ASSET_ID/);
});

View file

@ -174,12 +174,16 @@ test('strategy emits actionable exact-in BTC -> USDC command from DB price route
}); });
assert.equal(result.decision.decision, 'actionable'); assert.equal(result.decision.decision, 'actionable');
assert.equal(result.decision.direction, 'btc_to_usdc'); assert.equal(result.decision.direction, 'base_to_quote');
assert.equal(result.decision.edge_bps, '49'); assert.equal(result.decision.edge_bps, '49');
assert.equal(result.decision.price_route_id, 'btc-usdc:v1'); assert.equal(result.decision.price_route_id, 'btc-usdc:v1');
assert.equal(result.decision.notional, '8.000000'); assert.equal(result.decision.notional, '8.000000');
assert.equal(result.decision.notional_symbol, 'USDC'); assert.equal(result.decision.notional_symbol, 'USDC');
assert.equal(result.decision.eure_notional, null);
assert.equal(result.command.quote_output.amount_out, '7960800'); assert.equal(result.command.quote_output.amount_out, '7960800');
assert.equal(result.command.notional_symbol, 'USDC');
assert.equal(result.command.expected_inventory_delta_units[config.tradingBtc.assetId], '10000');
assert.equal(result.command.expected_inventory_delta_units[config.tradingUsdc.assetId], '-7960800');
assert.equal(result.command.asset_out_decimals, 6); assert.equal(result.command.asset_out_decimals, 6);
}); });