Fix maker-side strategy edge semantics
All checks were successful
deploy / deploy (push) Successful in 54s
All checks were successful
deploy / deploy (push) Successful in 54s
Proof: Full npm test passed after adding a regression that USDC -> BTC requester quotes use the BTC -> USDC maker-side edge when we sell BTC; operator dashboard tests verify explicit +/- maker movement text. Assumptions: User explicitly approved overriding the current turn's no-strategy-change constraint to fix a live semantic safety bug; NEAR quote asset_in/asset_out remain request-side venue terms, while maker strategy config is keyed by asset_out->asset_in. Still fake: Fee-complete realized PnL and venue-native terminal fill ids remain unavailable; existing historical rows before this change only have legacy request-side pair fields.
This commit is contained in:
parent
38f58139e1
commit
e5c0a5dc6e
8 changed files with 288 additions and 39 deletions
|
|
@ -9,7 +9,7 @@ import { createLogger, serializeError } from '../core/log.mjs';
|
||||||
import { extendMakerTiming } from '../core/maker-timing.mjs';
|
import { extendMakerTiming } from '../core/maker-timing.mjs';
|
||||||
import { createRecentIdCache } from '../core/recent-id-cache.mjs';
|
import { createRecentIdCache } from '../core/recent-id-cache.mjs';
|
||||||
import { assertInventorySnapshotEvent, assertMarketPriceEvent, assertNormalizedSwapDemand } from '../core/schemas.mjs';
|
import { assertInventorySnapshotEvent, assertMarketPriceEvent, assertNormalizedSwapDemand } from '../core/schemas.mjs';
|
||||||
import { evaluateTradeOpportunity } from '../core/strategy.mjs';
|
import { evaluateTradeOpportunity, makerTradePairKeyForDemand } from '../core/strategy.mjs';
|
||||||
import { loadConfig } from '../lib/config.mjs';
|
import { loadConfig } from '../lib/config.mjs';
|
||||||
import {
|
import {
|
||||||
createPairStrategyConfigVersion,
|
createPairStrategyConfigVersion,
|
||||||
|
|
@ -113,7 +113,9 @@ async function handleDemand(event) {
|
||||||
const tradingConfig = await tradingConfigStore.getConfig();
|
const tradingConfig = await tradingConfigStore.getConfig();
|
||||||
|
|
||||||
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 requestPairKey = event.payload.pair || `${event.payload.asset_in}->${event.payload.asset_out}`;
|
||||||
|
const makerPairKey = makerTradePairKeyForDemand(event.payload);
|
||||||
|
const pair = tradingConfig.pairByKey?.get(makerPairKey) || tradingConfig.pairByKey?.get(requestPairKey);
|
||||||
const strategyConfig = pair?.strategyConfig || null;
|
const strategyConfig = pair?.strategyConfig || null;
|
||||||
const legacyEureNotional = !pair?.priceRoute
|
const legacyEureNotional = !pair?.priceRoute
|
||||||
|| pair.priceRoute.quoteAssetId === tradingConfig.tradingEure?.assetId;
|
|| pair.priceRoute.quoteAssetId === tradingConfig.tradingEure?.assetId;
|
||||||
|
|
@ -121,7 +123,11 @@ async function handleDemand(event) {
|
||||||
decision_id: `duplicate-${event.payload.quote_id}`,
|
decision_id: `duplicate-${event.payload.quote_id}`,
|
||||||
decision_at: strategyReceivedAt,
|
decision_at: strategyReceivedAt,
|
||||||
quote_id: event.payload.quote_id,
|
quote_id: event.payload.quote_id,
|
||||||
pair: event.payload.pair || `${event.payload.asset_in}->${event.payload.asset_out}`,
|
pair: requestPairKey,
|
||||||
|
request_pair: requestPairKey,
|
||||||
|
request_pair_id: tradingConfig.pairByKey?.get(requestPairKey)?.pairId || requestPairKey,
|
||||||
|
maker_trade_pair: makerPairKey,
|
||||||
|
maker_trade_pair_id: pair?.pairId || makerPairKey,
|
||||||
pair_id: pair?.pairId || null,
|
pair_id: pair?.pairId || null,
|
||||||
pair_config_id: strategyConfig?.configId || null,
|
pair_config_id: strategyConfig?.configId || null,
|
||||||
pair_config_version: strategyConfig?.version == null ? null : String(strategyConfig.version),
|
pair_config_version: strategyConfig?.version == null ? null : String(strategyConfig.version),
|
||||||
|
|
@ -135,6 +141,10 @@ async function handleDemand(event) {
|
||||||
max_notional_eure: legacyEureNotional && strategyConfig?.maxNotional != null
|
max_notional_eure: legacyEureNotional && strategyConfig?.maxNotional != null
|
||||||
? String(strategyConfig.maxNotional)
|
? String(strategyConfig.maxNotional)
|
||||||
: null,
|
: null,
|
||||||
|
maker_send_asset_id: event.payload.asset_out || null,
|
||||||
|
maker_receive_asset_id: event.payload.asset_in || null,
|
||||||
|
maker_send_symbol: tradingConfig.assetRegistry?.get(event.payload.asset_out)?.symbol || null,
|
||||||
|
maker_receive_symbol: tradingConfig.assetRegistry?.get(event.payload.asset_in)?.symbol || null,
|
||||||
maker_timing: extendMakerTiming(event.payload.maker_timing, {
|
maker_timing: extendMakerTiming(event.payload.maker_timing, {
|
||||||
strategy_received_at: strategyReceivedAt,
|
strategy_received_at: strategyReceivedAt,
|
||||||
strategy_decided_at: strategyReceivedAt,
|
strategy_decided_at: strategyReceivedAt,
|
||||||
|
|
@ -185,7 +195,8 @@ async function handleDemand(event) {
|
||||||
|
|
||||||
function resolvePriceEventForDemand(event, tradingConfig) {
|
function resolvePriceEventForDemand(event, tradingConfig) {
|
||||||
const key = event.payload.pair || `${event.payload.asset_in}->${event.payload.asset_out}`;
|
const key = event.payload.pair || `${event.payload.asset_in}->${event.payload.asset_out}`;
|
||||||
const pair = tradingConfig.pairByKey?.get(key);
|
const makerKey = makerTradePairKeyForDemand(event.payload);
|
||||||
|
const pair = tradingConfig.pairByKey?.get(makerKey) || tradingConfig.pairByKey?.get(key);
|
||||||
const routeId = pair?.priceRoute?.routeId || null;
|
const routeId = pair?.priceRoute?.routeId || null;
|
||||||
if (routeId) return state.latest_price_events_by_route[routeId] || null;
|
if (routeId) return state.latest_price_events_by_route[routeId] || null;
|
||||||
return state.latest_price_events_by_pair[key] || state.latest_price_event;
|
return state.latest_price_events_by_pair[key] || state.latest_price_event;
|
||||||
|
|
|
||||||
|
|
@ -1192,6 +1192,10 @@ export function deriveQuoteLifecycleRows({
|
||||||
quote_id: decision.quote_id,
|
quote_id: decision.quote_id,
|
||||||
decision_id: decision.decision_id,
|
decision_id: decision.decision_id,
|
||||||
pair: decision.pair,
|
pair: decision.pair,
|
||||||
|
request_pair: decision.request_pair,
|
||||||
|
request_pair_id: decision.request_pair_id,
|
||||||
|
maker_trade_pair: decision.maker_trade_pair,
|
||||||
|
maker_trade_pair_id: decision.maker_trade_pair_id,
|
||||||
pair_id: decision.pair_id,
|
pair_id: decision.pair_id,
|
||||||
pair_config_id: decision.pair_config_id,
|
pair_config_id: decision.pair_config_id,
|
||||||
pair_config_version: decision.pair_config_version,
|
pair_config_version: decision.pair_config_version,
|
||||||
|
|
@ -1204,6 +1208,10 @@ export function deriveQuoteLifecycleRows({
|
||||||
inventory_available: decision.inventory_available,
|
inventory_available: decision.inventory_available,
|
||||||
inventory_spendable: decision.inventory_spendable,
|
inventory_spendable: decision.inventory_spendable,
|
||||||
inventory_pending_outbound: decision.inventory_pending_outbound,
|
inventory_pending_outbound: decision.inventory_pending_outbound,
|
||||||
|
maker_send_asset_id: decision.maker_send_asset_id,
|
||||||
|
maker_receive_asset_id: decision.maker_receive_asset_id,
|
||||||
|
maker_send_amount_units: decision.maker_send_amount_units,
|
||||||
|
maker_receive_amount_units: decision.maker_receive_amount_units,
|
||||||
pending_inbound: decision.pending_inbound,
|
pending_inbound: decision.pending_inbound,
|
||||||
pending_outbound: decision.pending_outbound,
|
pending_outbound: decision.pending_outbound,
|
||||||
notional: decision.notional,
|
notional: decision.notional,
|
||||||
|
|
@ -1232,6 +1240,10 @@ export function deriveQuoteLifecycleRows({
|
||||||
decision_id: command.decision_id,
|
decision_id: command.decision_id,
|
||||||
command_id: command.command_id,
|
command_id: command.command_id,
|
||||||
pair: command.pair,
|
pair: command.pair,
|
||||||
|
request_pair: command.request_pair,
|
||||||
|
request_pair_id: command.request_pair_id,
|
||||||
|
maker_trade_pair: command.maker_trade_pair,
|
||||||
|
maker_trade_pair_id: command.maker_trade_pair_id,
|
||||||
pair_id: command.pair_id,
|
pair_id: command.pair_id,
|
||||||
pair_config_id: command.pair_config_id,
|
pair_config_id: command.pair_config_id,
|
||||||
pair_config_version: command.pair_config_version,
|
pair_config_version: command.pair_config_version,
|
||||||
|
|
@ -1242,6 +1254,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,
|
||||||
|
maker_send_asset_id: command.maker_send_asset_id || null,
|
||||||
|
maker_receive_asset_id: command.maker_receive_asset_id || null,
|
||||||
|
maker_send_amount_units: command.maker_send_amount_units || null,
|
||||||
|
maker_receive_amount_units: command.maker_receive_amount_units || null,
|
||||||
notional: command.notional || null,
|
notional: command.notional || null,
|
||||||
notional_asset_id: command.notional_asset_id || null,
|
notional_asset_id: command.notional_asset_id || null,
|
||||||
notional_symbol: command.notional_symbol || null,
|
notional_symbol: command.notional_symbol || null,
|
||||||
|
|
@ -1302,6 +1318,10 @@ function ensureLifecycleRow(rowsByKey, key) {
|
||||||
decision_id: null,
|
decision_id: null,
|
||||||
command_id: null,
|
command_id: null,
|
||||||
pair: null,
|
pair: null,
|
||||||
|
request_pair: null,
|
||||||
|
request_pair_id: null,
|
||||||
|
maker_trade_pair: null,
|
||||||
|
maker_trade_pair_id: null,
|
||||||
pair_id: null,
|
pair_id: null,
|
||||||
pair_config_id: null,
|
pair_config_id: null,
|
||||||
pair_config_version: null,
|
pair_config_version: null,
|
||||||
|
|
@ -1326,6 +1346,10 @@ function ensureLifecycleRow(rowsByKey, key) {
|
||||||
asset_out: null,
|
asset_out: null,
|
||||||
amount_in: null,
|
amount_in: null,
|
||||||
amount_out: null,
|
amount_out: null,
|
||||||
|
maker_send_asset_id: null,
|
||||||
|
maker_receive_asset_id: null,
|
||||||
|
maker_send_amount_units: null,
|
||||||
|
maker_receive_amount_units: null,
|
||||||
command_at: null,
|
command_at: null,
|
||||||
execution_result_at: null,
|
execution_result_at: null,
|
||||||
outcome_observed_at: null,
|
outcome_observed_at: null,
|
||||||
|
|
@ -1562,6 +1586,10 @@ function normalizeCommand(command) {
|
||||||
execution_key: command.execution_key || null,
|
execution_key: command.execution_key || null,
|
||||||
quote_id: command.quote_id || null,
|
quote_id: command.quote_id || null,
|
||||||
pair: command.pair || null,
|
pair: command.pair || null,
|
||||||
|
request_pair: command.request_pair || null,
|
||||||
|
request_pair_id: command.request_pair_id || null,
|
||||||
|
maker_trade_pair: command.maker_trade_pair || null,
|
||||||
|
maker_trade_pair_id: command.maker_trade_pair_id || null,
|
||||||
pair_id: command.pair_id || null,
|
pair_id: command.pair_id || null,
|
||||||
pair_config_id: command.pair_config_id || null,
|
pair_config_id: command.pair_config_id || null,
|
||||||
pair_config_version: command.pair_config_version || null,
|
pair_config_version: command.pair_config_version || null,
|
||||||
|
|
@ -1576,6 +1604,12 @@ function normalizeCommand(command) {
|
||||||
asset_out: command.asset_out || null,
|
asset_out: command.asset_out || null,
|
||||||
amount_in: command.amount_in ?? null,
|
amount_in: command.amount_in ?? null,
|
||||||
amount_out: command.amount_out ?? null,
|
amount_out: command.amount_out ?? null,
|
||||||
|
maker_send_asset_id: command.maker_send_asset_id || null,
|
||||||
|
maker_receive_asset_id: command.maker_receive_asset_id || null,
|
||||||
|
maker_send_symbol: command.maker_send_symbol || null,
|
||||||
|
maker_receive_symbol: command.maker_receive_symbol || null,
|
||||||
|
maker_send_amount_units: command.maker_send_amount_units || null,
|
||||||
|
maker_receive_amount_units: command.maker_receive_amount_units || null,
|
||||||
maker_timing: command.maker_timing || null,
|
maker_timing: command.maker_timing || null,
|
||||||
quote_age_at_command_ms: command.quote_age_at_command_ms ?? null,
|
quote_age_at_command_ms: command.quote_age_at_command_ms ?? null,
|
||||||
command_at: command.command_at || command.observed_at || command.ingested_at || null,
|
command_at: command.command_at || command.observed_at || command.ingested_at || null,
|
||||||
|
|
@ -2107,7 +2141,7 @@ function enrichLifecycleRowForUi({ config, row }) {
|
||||||
}),
|
}),
|
||||||
maker_terms: buildMakerLifecycleTerms({
|
maker_terms: buildMakerLifecycleTerms({
|
||||||
config,
|
config,
|
||||||
terms: row.command || row.execution || null,
|
terms: row.command || row.execution || row.decision || row,
|
||||||
}),
|
}),
|
||||||
inventory_check: buildInventoryCheck({
|
inventory_check: buildInventoryCheck({
|
||||||
config,
|
config,
|
||||||
|
|
@ -2145,6 +2179,31 @@ function buildLifecycleTerms({ config, terms }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildMakerLifecycleTerms({ config, terms }) {
|
function buildMakerLifecycleTerms({ config, terms }) {
|
||||||
|
if (terms?.maker_send_asset_id || terms?.maker_receive_asset_id) {
|
||||||
|
const sendAsset = config.assetRegistry.get(terms.maker_send_asset_id);
|
||||||
|
const receiveAsset = config.assetRegistry.get(terms.maker_receive_asset_id);
|
||||||
|
const sendAmount = terms.maker_send_amount_units ?? null;
|
||||||
|
const receiveAmount = terms.maker_receive_amount_units ?? null;
|
||||||
|
const sendDisplay = sendAmount == null ? null : formatUnits(sendAmount, sendAsset?.decimals || 0);
|
||||||
|
const receiveDisplay = receiveAmount == null ? null : formatUnits(receiveAmount, receiveAsset?.decimals || 0);
|
||||||
|
const sendSymbol = sendAsset?.symbol || terms.maker_send_symbol || terms.maker_send_asset_id || null;
|
||||||
|
const receiveSymbol = receiveAsset?.symbol || terms.maker_receive_symbol || terms.maker_receive_asset_id || null;
|
||||||
|
return {
|
||||||
|
receive_asset: terms.maker_receive_asset_id || null,
|
||||||
|
receive_asset_symbol: receiveSymbol,
|
||||||
|
receive_amount_units: receiveAmount,
|
||||||
|
receive_amount: receiveDisplay,
|
||||||
|
send_asset: terms.maker_send_asset_id || null,
|
||||||
|
send_asset_symbol: sendSymbol,
|
||||||
|
send_amount_units: sendAmount,
|
||||||
|
send_amount: sendDisplay,
|
||||||
|
delta_text: [
|
||||||
|
sendSymbol ? `-${sendDisplay || ''} ${sendSymbol}`.trim() : null,
|
||||||
|
receiveSymbol ? `+${receiveDisplay || ''} ${receiveSymbol}`.trim() : null,
|
||||||
|
].filter(Boolean).join(' / '),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const relayTerms = buildLifecycleTerms({ config, terms });
|
const relayTerms = buildLifecycleTerms({ config, terms });
|
||||||
if (!relayTerms?.asset_in && !relayTerms?.asset_out) return null;
|
if (!relayTerms?.asset_in && !relayTerms?.asset_out) return null;
|
||||||
return {
|
return {
|
||||||
|
|
@ -2156,6 +2215,10 @@ function buildMakerLifecycleTerms({ config, terms }) {
|
||||||
send_asset_symbol: relayTerms.asset_out_symbol,
|
send_asset_symbol: relayTerms.asset_out_symbol,
|
||||||
send_amount_units: relayTerms.amount_out_units,
|
send_amount_units: relayTerms.amount_out_units,
|
||||||
send_amount: relayTerms.amount_out,
|
send_amount: relayTerms.amount_out,
|
||||||
|
delta_text: [
|
||||||
|
relayTerms.asset_out ? `-${relayTerms.amount_out || ''} ${relayTerms.asset_out_symbol || relayTerms.asset_out}`.trim() : null,
|
||||||
|
relayTerms.asset_in ? `+${relayTerms.amount_in || ''} ${relayTerms.asset_in_symbol || relayTerms.asset_in}`.trim() : null,
|
||||||
|
].filter(Boolean).join(' / '),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2391,6 +2454,10 @@ function normalizeDecision(decision) {
|
||||||
decision_at: decision.decision_at || null,
|
decision_at: decision.decision_at || null,
|
||||||
quote_id: decision.quote_id || null,
|
quote_id: decision.quote_id || null,
|
||||||
pair: decision.pair || null,
|
pair: decision.pair || null,
|
||||||
|
request_pair: decision.request_pair || null,
|
||||||
|
request_pair_id: decision.request_pair_id || null,
|
||||||
|
maker_trade_pair: decision.maker_trade_pair || null,
|
||||||
|
maker_trade_pair_id: decision.maker_trade_pair_id || null,
|
||||||
pair_id: decision.pair_id || null,
|
pair_id: decision.pair_id || null,
|
||||||
pair_config_id: decision.pair_config_id || null,
|
pair_config_id: decision.pair_config_id || null,
|
||||||
pair_config_version: decision.pair_config_version || null,
|
pair_config_version: decision.pair_config_version || null,
|
||||||
|
|
@ -2409,6 +2476,12 @@ function normalizeDecision(decision) {
|
||||||
inventory_available: decision.inventory_available || null,
|
inventory_available: decision.inventory_available || null,
|
||||||
inventory_spendable: decision.inventory_spendable || null,
|
inventory_spendable: decision.inventory_spendable || null,
|
||||||
inventory_pending_outbound: decision.inventory_pending_outbound || null,
|
inventory_pending_outbound: decision.inventory_pending_outbound || null,
|
||||||
|
maker_send_asset_id: decision.maker_send_asset_id || null,
|
||||||
|
maker_receive_asset_id: decision.maker_receive_asset_id || null,
|
||||||
|
maker_send_symbol: decision.maker_send_symbol || null,
|
||||||
|
maker_receive_symbol: decision.maker_receive_symbol || null,
|
||||||
|
maker_send_amount_units: decision.maker_send_amount_units || null,
|
||||||
|
maker_receive_amount_units: decision.maker_receive_amount_units || null,
|
||||||
pending_inbound: decision.pending_inbound || null,
|
pending_inbound: decision.pending_inbound || null,
|
||||||
pending_outbound: decision.pending_outbound || null,
|
pending_outbound: decision.pending_outbound || null,
|
||||||
notional: decision.notional || null,
|
notional: decision.notional || null,
|
||||||
|
|
@ -2569,6 +2642,10 @@ function normalizeLiveExecutionResult(payload, event) {
|
||||||
execution_key: payload.execution_key || null,
|
execution_key: payload.execution_key || null,
|
||||||
quote_id: payload.quote_id || null,
|
quote_id: payload.quote_id || null,
|
||||||
pair: payload.pair || null,
|
pair: payload.pair || null,
|
||||||
|
request_pair: payload.request_pair || null,
|
||||||
|
request_pair_id: payload.request_pair_id || null,
|
||||||
|
maker_trade_pair: payload.maker_trade_pair || null,
|
||||||
|
maker_trade_pair_id: payload.maker_trade_pair_id || null,
|
||||||
result_at: event.observed_at || event.ingested_at || new Date().toISOString(),
|
result_at: event.observed_at || event.ingested_at || new Date().toISOString(),
|
||||||
status: payload.status || null,
|
status: payload.status || null,
|
||||||
result_code: payload.result_code || null,
|
result_code: payload.result_code || null,
|
||||||
|
|
@ -2582,6 +2659,10 @@ function normalizeLiveExecutionResult(payload, event) {
|
||||||
error_message: payload.relay_error_message || payload.error?.message || null,
|
error_message: payload.relay_error_message || payload.error?.message || null,
|
||||||
note: payload.note || null,
|
note: payload.note || null,
|
||||||
timing: payload.executor_timing || null,
|
timing: payload.executor_timing || null,
|
||||||
|
maker_send_asset_id: payload.maker_send_asset_id || null,
|
||||||
|
maker_receive_asset_id: payload.maker_receive_asset_id || null,
|
||||||
|
maker_send_amount_units: payload.maker_send_amount_units || null,
|
||||||
|
maker_receive_amount_units: payload.maker_receive_amount_units || null,
|
||||||
maker_timing: payload.maker_timing || null,
|
maker_timing: payload.maker_timing || null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,11 @@ import {
|
||||||
|
|
||||||
const EDGE_EPSILON_PCT = 1e-9;
|
const EDGE_EPSILON_PCT = 1e-9;
|
||||||
|
|
||||||
|
export function makerTradePairKeyForDemand(payload = {}) {
|
||||||
|
if (!payload?.asset_in || !payload?.asset_out) return null;
|
||||||
|
return pairKey(payload.asset_out, payload.asset_in);
|
||||||
|
}
|
||||||
|
|
||||||
export function evaluateTradeOpportunity({
|
export function evaluateTradeOpportunity({
|
||||||
demandEvent,
|
demandEvent,
|
||||||
priceEvent,
|
priceEvent,
|
||||||
|
|
@ -31,6 +36,8 @@ export function evaluateTradeOpportunity({
|
||||||
maxNotional = config.strategyMaxNotional ?? config.strategyMaxNotionalEure,
|
maxNotional = config.strategyMaxNotional ?? config.strategyMaxNotionalEure,
|
||||||
}) {
|
}) {
|
||||||
const payload = demandEvent.payload;
|
const payload = demandEvent.payload;
|
||||||
|
const requestPairKey = pairKey(payload.asset_in, payload.asset_out);
|
||||||
|
const makerTradePairKey = makerTradePairKeyForDemand(payload);
|
||||||
const pairRuntime = resolvePairRuntime({ payload, config, thresholdPct, maxNotional });
|
const pairRuntime = resolvePairRuntime({ payload, config, thresholdPct, maxNotional });
|
||||||
const effectiveThresholdPct = pairRuntime.thresholdPct ?? thresholdPct;
|
const effectiveThresholdPct = pairRuntime.thresholdPct ?? thresholdPct;
|
||||||
const effectiveMaxNotional = pairRuntime.maxNotional ?? maxNotional;
|
const effectiveMaxNotional = pairRuntime.maxNotional ?? maxNotional;
|
||||||
|
|
@ -46,7 +53,11 @@ export function evaluateTradeOpportunity({
|
||||||
decision_id: decisionId,
|
decision_id: decisionId,
|
||||||
decision_at: decisionAt,
|
decision_at: decisionAt,
|
||||||
quote_id: payload.quote_id,
|
quote_id: payload.quote_id,
|
||||||
pair: pairKey(payload.asset_in, payload.asset_out),
|
pair: requestPairKey,
|
||||||
|
request_pair: requestPairKey,
|
||||||
|
request_pair_id: pairRuntime.requestPair?.pairId || requestPairKey,
|
||||||
|
maker_trade_pair: pairRuntime.makerTradePairKey || makerTradePairKey,
|
||||||
|
maker_trade_pair_id: pairRuntime.makerTradePair?.pairId || pairRuntime.pair?.pairId || makerTradePairKey,
|
||||||
pair_id: pairRuntime.pair?.pairId || null,
|
pair_id: pairRuntime.pair?.pairId || null,
|
||||||
pair_config_id: pairRuntime.strategyConfig?.configId || null,
|
pair_config_id: pairRuntime.strategyConfig?.configId || null,
|
||||||
pair_config_version: pairRuntime.strategyConfig?.version == null
|
pair_config_version: pairRuntime.strategyConfig?.version == null
|
||||||
|
|
@ -69,6 +80,10 @@ export function evaluateTradeOpportunity({
|
||||||
maker_timing: makerTiming,
|
maker_timing: makerTiming,
|
||||||
quote_age_at_decision_ms: makerTiming.quote_age_at_decision_ms,
|
quote_age_at_decision_ms: makerTiming.quote_age_at_decision_ms,
|
||||||
strategy_armed: armed,
|
strategy_armed: armed,
|
||||||
|
maker_send_asset_id: payload.asset_out,
|
||||||
|
maker_receive_asset_id: payload.asset_in,
|
||||||
|
maker_send_symbol: pairRuntime.assetOut?.symbol || null,
|
||||||
|
maker_receive_symbol: pairRuntime.assetIn?.symbol || null,
|
||||||
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,
|
||||||
usdc_per_usd: pairRuntime.priceRoute?.source === 'btc_usdc_reference' ? '1' : null,
|
usdc_per_usd: pairRuntime.priceRoute?.source === 'btc_usdc_reference' ? '1' : null,
|
||||||
|
|
@ -140,10 +155,16 @@ export function evaluateTradeOpportunity({
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!buildResult.ok) {
|
if (!buildResult.ok) {
|
||||||
|
const makerTerms = buildMakerTradeTerms({
|
||||||
|
demand: payload,
|
||||||
|
proposedAmountIn: buildResult.details?.proposed_amount_in ?? null,
|
||||||
|
proposedAmountOut: buildResult.details?.proposed_amount_out ?? null,
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
decision: {
|
decision: {
|
||||||
...baseDecision,
|
...baseDecision,
|
||||||
...buildResult.details,
|
...buildResult.details,
|
||||||
|
...makerTerms,
|
||||||
price_freshness_ms: String(priceAgeMs),
|
price_freshness_ms: String(priceAgeMs),
|
||||||
inventory_freshness_ms: String(inventoryAgeMs),
|
inventory_freshness_ms: String(inventoryAgeMs),
|
||||||
decision_reason: buildResult.reason,
|
decision_reason: buildResult.reason,
|
||||||
|
|
@ -151,9 +172,16 @@ export function evaluateTradeOpportunity({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const makerTerms = buildMakerTradeTerms({
|
||||||
|
demand: payload,
|
||||||
|
quoteOutput: buildResult.quoteOutput,
|
||||||
|
proposedAmountIn: buildResult.details.proposed_amount_in ?? null,
|
||||||
|
proposedAmountOut: buildResult.details.proposed_amount_out ?? null,
|
||||||
|
});
|
||||||
const decision = {
|
const decision = {
|
||||||
...baseDecision,
|
...baseDecision,
|
||||||
...buildResult.details,
|
...buildResult.details,
|
||||||
|
...makerTerms,
|
||||||
price_freshness_ms: String(priceAgeMs),
|
price_freshness_ms: String(priceAgeMs),
|
||||||
inventory_freshness_ms: String(inventoryAgeMs),
|
inventory_freshness_ms: String(inventoryAgeMs),
|
||||||
decision: armed ? 'actionable' : 'rejected',
|
decision: armed ? 'actionable' : 'rejected',
|
||||||
|
|
@ -206,6 +234,7 @@ 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,
|
||||||
|
...makerTerms,
|
||||||
expected_inventory_delta_units: buildExpectedMakerInventoryDelta({
|
expected_inventory_delta_units: buildExpectedMakerInventoryDelta({
|
||||||
demand: payload,
|
demand: payload,
|
||||||
quoteOutput: buildResult.quoteOutput,
|
quoteOutput: buildResult.quoteOutput,
|
||||||
|
|
@ -563,13 +592,35 @@ function buildExpectedMakerInventoryDelta({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildMakerTradeTerms({
|
||||||
|
demand,
|
||||||
|
quoteOutput = {},
|
||||||
|
proposedAmountIn = null,
|
||||||
|
proposedAmountOut = null,
|
||||||
|
} = {}) {
|
||||||
|
if (!demand?.asset_in || !demand?.asset_out || !demand?.request_kind) return {};
|
||||||
|
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;
|
||||||
|
return compact({
|
||||||
|
maker_send_asset_id: demand.asset_out,
|
||||||
|
maker_receive_asset_id: demand.asset_in,
|
||||||
|
maker_send_amount_units: sendAmount == null ? null : String(sendAmount),
|
||||||
|
maker_receive_amount_units: receiveAmount == null ? null : String(receiveAmount),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function resolvePairRuntime({
|
function resolvePairRuntime({
|
||||||
payload,
|
payload,
|
||||||
config,
|
config,
|
||||||
thresholdPct,
|
thresholdPct,
|
||||||
maxNotional,
|
maxNotional,
|
||||||
}) {
|
}) {
|
||||||
const key = pairKey(payload.asset_in, payload.asset_out);
|
const requestKey = pairKey(payload.asset_in, payload.asset_out);
|
||||||
|
const makerTradeKey = makerTradePairKeyForDemand(payload);
|
||||||
const requiresDb = config.requireDbTradingConfig === true || config.tradingConfigLoaded === true;
|
const requiresDb = config.requireDbTradingConfig === true || config.tradingConfigLoaded === true;
|
||||||
|
|
||||||
if (!requiresDb) {
|
if (!requiresDb) {
|
||||||
|
|
@ -619,18 +670,21 @@ function resolvePairRuntime({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const pair = config.pairByKey?.get(key) || null;
|
const requestPair = config.pairByKey?.get(requestKey) || null;
|
||||||
if (!pair || !pair.enabled || !pair.observeEnabled) {
|
if (!requestPair || !requestPair.enabled || !requestPair.observeEnabled) {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
reason: pair ? 'pair_disabled' : 'unsupported_pair',
|
reason: requestPair ? 'pair_disabled' : 'unsupported_pair',
|
||||||
direction: 'unsupported',
|
direction: 'unsupported',
|
||||||
pair,
|
pair: null,
|
||||||
strategyConfig: pair?.strategyConfig || null,
|
requestPair,
|
||||||
priceRoute: pair?.priceRoute || null,
|
makerTradePair: null,
|
||||||
|
makerTradePairKey: makerTradeKey,
|
||||||
|
strategyConfig: null,
|
||||||
|
priceRoute: requestPair?.priceRoute || null,
|
||||||
assetRegistry: config.assetRegistry,
|
assetRegistry: config.assetRegistry,
|
||||||
assetIn: pair?.assetIn || config.assetRegistry?.get(payload.asset_in) || null,
|
assetIn: config.assetRegistry?.get(payload.asset_in) || null,
|
||||||
assetOut: pair?.assetOut || config.assetRegistry?.get(payload.asset_out) || null,
|
assetOut: config.assetRegistry?.get(payload.asset_out) || null,
|
||||||
thresholdPct,
|
thresholdPct,
|
||||||
maxNotional,
|
maxNotional,
|
||||||
priceMaxAgeMs: config.strategyPriceMaxAgeMs,
|
priceMaxAgeMs: config.strategyPriceMaxAgeMs,
|
||||||
|
|
@ -638,24 +692,34 @@ function resolvePairRuntime({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!pair.makerEnabled) return blockedPairRuntime(pair, config, 'pair_not_maker_enabled');
|
const pair = config.pairByKey?.get(makerTradeKey) || null;
|
||||||
if (pair.blockReason) return blockedPairRuntime(pair, config, pair.blockReason);
|
if (!pair) {
|
||||||
if (!pair.strategyConfig) return blockedPairRuntime(pair, config, 'pair_strategy_config_missing');
|
return blockedPairRuntime(null, config, 'maker_trade_pair_unavailable', { payload, requestPair, makerTradePairKey: makerTradeKey });
|
||||||
if (!pair.priceRoute) return blockedPairRuntime(pair, config, 'price_route_missing');
|
}
|
||||||
|
if (!pair.makerEnabled) return blockedPairRuntime(pair, config, 'pair_not_maker_enabled', { payload, requestPair, makerTradePairKey: makerTradeKey });
|
||||||
|
if (pair.blockReason) return blockedPairRuntime(pair, config, pair.blockReason, { payload, requestPair, makerTradePairKey: makerTradeKey });
|
||||||
|
if (!pair.strategyConfig) return blockedPairRuntime(pair, config, 'pair_strategy_config_missing', { payload, requestPair, makerTradePairKey: makerTradeKey });
|
||||||
|
const priceRoute = pair.priceRoute || requestPair.priceRoute || null;
|
||||||
|
if (!priceRoute) return blockedPairRuntime(pair, config, 'price_route_missing', { payload, requestPair, makerTradePairKey: makerTradeKey });
|
||||||
|
|
||||||
const direction = classifyPriceRouteDirection({ payload, priceRoute: pair.priceRoute });
|
const direction = classifyPriceRouteDirection({ payload, priceRoute });
|
||||||
if (direction === 'unsupported') return blockedPairRuntime(pair, config, 'price_route_missing');
|
if (direction === 'unsupported') {
|
||||||
|
return blockedPairRuntime(pair, config, 'price_route_missing', { payload, requestPair, makerTradePairKey: makerTradeKey, priceRoute });
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
reason: null,
|
reason: null,
|
||||||
direction,
|
direction,
|
||||||
pair,
|
pair,
|
||||||
|
requestPair,
|
||||||
|
makerTradePair: pair,
|
||||||
|
makerTradePairKey: makerTradeKey,
|
||||||
strategyConfig: pair.strategyConfig,
|
strategyConfig: pair.strategyConfig,
|
||||||
priceRoute: pair.priceRoute,
|
priceRoute,
|
||||||
assetRegistry: config.assetRegistry,
|
assetRegistry: config.assetRegistry,
|
||||||
assetIn: pair.assetIn,
|
assetIn: config.assetRegistry?.get(payload.asset_in) || null,
|
||||||
assetOut: pair.assetOut,
|
assetOut: config.assetRegistry?.get(payload.asset_out) || null,
|
||||||
thresholdPct: Number(pair.strategyConfig.edgeBps) / 100,
|
thresholdPct: Number(pair.strategyConfig.edgeBps) / 100,
|
||||||
maxNotional: Number(pair.strategyConfig.maxNotional),
|
maxNotional: Number(pair.strategyConfig.maxNotional),
|
||||||
minNotional: Number(pair.strategyConfig.minNotional ?? 0),
|
minNotional: Number(pair.strategyConfig.minNotional ?? 0),
|
||||||
|
|
@ -664,20 +728,35 @@ function resolvePairRuntime({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function blockedPairRuntime(pair, config, reason) {
|
function blockedPairRuntime(pair, config, reason, {
|
||||||
|
payload = null,
|
||||||
|
requestPair = null,
|
||||||
|
makerTradePairKey = pair?.key || pair?.pairId || null,
|
||||||
|
priceRoute = null,
|
||||||
|
} = {}) {
|
||||||
|
const assetIn = payload?.asset_in
|
||||||
|
? config.assetRegistry?.get(payload.asset_in) || null
|
||||||
|
: pair?.assetIn || null;
|
||||||
|
const assetOut = payload?.asset_out
|
||||||
|
? config.assetRegistry?.get(payload.asset_out) || null
|
||||||
|
: pair?.assetOut || null;
|
||||||
|
const effectivePriceRoute = priceRoute || pair?.priceRoute || requestPair?.priceRoute || null;
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
reason,
|
reason,
|
||||||
direction: classifyPriceRouteDirection({
|
direction: classifyPriceRouteDirection({
|
||||||
payload: { asset_in: pair?.asset_in, asset_out: pair?.asset_out },
|
payload: payload || { asset_in: pair?.asset_in, asset_out: pair?.asset_out },
|
||||||
priceRoute: pair?.priceRoute,
|
priceRoute: effectivePriceRoute,
|
||||||
}),
|
}),
|
||||||
pair,
|
pair,
|
||||||
|
requestPair,
|
||||||
|
makerTradePair: pair || null,
|
||||||
|
makerTradePairKey,
|
||||||
strategyConfig: pair?.strategyConfig || null,
|
strategyConfig: pair?.strategyConfig || null,
|
||||||
priceRoute: pair?.priceRoute || null,
|
priceRoute: effectivePriceRoute,
|
||||||
assetRegistry: config.assetRegistry,
|
assetRegistry: config.assetRegistry,
|
||||||
assetIn: pair?.assetIn || null,
|
assetIn,
|
||||||
assetOut: pair?.assetOut || null,
|
assetOut,
|
||||||
thresholdPct: pair?.strategyConfig ? Number(pair.strategyConfig.edgeBps) / 100 : config.strategyGrossThresholdPct,
|
thresholdPct: pair?.strategyConfig ? Number(pair.strategyConfig.edgeBps) / 100 : config.strategyGrossThresholdPct,
|
||||||
maxNotional: pair?.strategyConfig
|
maxNotional: pair?.strategyConfig
|
||||||
? Number(pair.strategyConfig.maxNotional)
|
? Number(pair.strategyConfig.maxNotional)
|
||||||
|
|
|
||||||
|
|
@ -104,13 +104,14 @@ function formatTerms(terms) {
|
||||||
|
|
||||||
function formatMakerTerms(terms) {
|
function formatMakerTerms(terms) {
|
||||||
if (!terms) return null;
|
if (!terms) return null;
|
||||||
|
if (terms.delta_text) return `For us: ${terms.delta_text}`;
|
||||||
const send = terms.send_amount
|
const send = terms.send_amount
|
||||||
? `${terms.send_amount} ${terms.send_asset_symbol || ''}`.trim()
|
? `${terms.send_amount} ${terms.send_asset_symbol || ''}`.trim()
|
||||||
: terms.send_asset_symbol || terms.send_asset || 'send unavailable';
|
: terms.send_asset_symbol || terms.send_asset || 'send unavailable';
|
||||||
const receive = terms.receive_amount
|
const receive = terms.receive_amount
|
||||||
? `${terms.receive_amount} ${terms.receive_asset_symbol || ''}`.trim()
|
? `${terms.receive_amount} ${terms.receive_asset_symbol || ''}`.trim()
|
||||||
: terms.receive_asset_symbol || terms.receive_asset || 'receive unavailable';
|
: terms.receive_asset_symbol || terms.receive_asset || 'receive unavailable';
|
||||||
return `Maker sends ${send}; receives ${receive}`;
|
return `For us: -${send}; +${receive}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatInventoryCheck(check) {
|
function formatInventoryCheck(check) {
|
||||||
|
|
@ -301,6 +302,22 @@ function pairDisplayLabel(pairId, pairConfig) {
|
||||||
return `${pair.asset_in_symbol || pair.asset_in || pair.assetIn} -> ${pair.asset_out_symbol || pair.asset_out || pair.assetOut}`;
|
return `${pair.asset_in_symbol || pair.asset_in || pair.assetIn} -> ${pair.asset_out_symbol || pair.asset_out || pair.assetOut}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pairAssetInSymbol(pair) {
|
||||||
|
return pair.asset_in_symbol || pair.assetInSymbol || pair.asset_in || pair.assetIn || 'send asset';
|
||||||
|
}
|
||||||
|
|
||||||
|
function pairAssetOutSymbol(pair) {
|
||||||
|
return pair.asset_out_symbol || pair.assetOutSymbol || pair.asset_out || pair.assetOut || 'receive asset';
|
||||||
|
}
|
||||||
|
|
||||||
|
function makerPairMovementLabel(pair) {
|
||||||
|
return `For us: -${pairAssetInSymbol(pair)} / +${pairAssetOutSymbol(pair)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makerPairRequestLabel(pair) {
|
||||||
|
return `Requests: ${pairAssetOutSymbol(pair)} -> ${pairAssetInSymbol(pair)}`;
|
||||||
|
}
|
||||||
|
|
||||||
function fixedRows(items, count) {
|
function fixedRows(items, count) {
|
||||||
const rows = (items || []).slice(0, count);
|
const rows = (items || []).slice(0, count);
|
||||||
while (rows.length < count) rows.push(null);
|
while (rows.length < count) rows.push(null);
|
||||||
|
|
@ -760,8 +777,11 @@ function QuoteLifecycleTable({ items }) {
|
||||||
<td><IdentifierRow label="Quote" value={item.quote_id} /></td>
|
<td><IdentifierRow label="Quote" value={item.quote_id} /></td>
|
||||||
<td>
|
<td>
|
||||||
<div className="quote-lifecycle-cell">
|
<div className="quote-lifecycle-cell">
|
||||||
<div className="lifecycle-line lifecycle-clamp-two">{formatTerms(item.request_terms || item.submitted_terms)}</div>
|
<div className="lifecycle-line lifecycle-clamp-two">{formatMakerTerms(item.maker_terms) || formatTerms(item.request_terms || item.submitted_terms)}</div>
|
||||||
<div className="status-subtle mono lifecycle-line lifecycle-clamp-one">{truncateMiddle(item.pair || '', 34)}</div>
|
<div className="status-subtle lifecycle-line lifecycle-clamp-two">
|
||||||
|
{item.maker_terms ? `Request: ${formatTerms(item.request_terms || item.submitted_terms)}` : ''}
|
||||||
|
</div>
|
||||||
|
<div className="status-subtle mono lifecycle-line lifecycle-clamp-one">{truncateMiddle(item.maker_trade_pair || item.pair || '', 34)}</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>{responseLabel(item)}</td>
|
<td>{responseLabel(item)}</td>
|
||||||
|
|
@ -852,7 +872,8 @@ function SuccessfulTradesTable({ items }) {
|
||||||
<div className="status-subtle">Estimate from edge x notional, not realized PnL.</div>
|
<div className="status-subtle">Estimate from edge x notional, not realized PnL.</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div>{item.settlement_summary?.text || 'No settled inventory delta is linked to this quote.'}</div>
|
<div>{formatMakerTerms(item.maker_terms) || item.settlement_summary?.text || 'No settled inventory delta is linked to this quote.'}</div>
|
||||||
|
{item.maker_terms ? <div className="status-subtle">{item.settlement_summary?.text || 'No settled inventory delta is linked to this quote.'}</div> : null}
|
||||||
{item.settlement_summary?.method ? <div className="status-subtle mono">{item.settlement_summary.method}</div> : null}
|
{item.settlement_summary?.method ? <div className="status-subtle mono">{item.settlement_summary.method}</div> : null}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
@ -1261,7 +1282,9 @@ function PairConfigSection({ assetCatalog, pairConfig, onControl }) {
|
||||||
return (
|
return (
|
||||||
<tr key={pairId}>
|
<tr key={pairId}>
|
||||||
<td>
|
<td>
|
||||||
<div>{pair.asset_in_symbol || pair.asset_in} {'->'} {pair.asset_out_symbol || pair.asset_out}</div>
|
<div>{pairAssetInSymbol(pair)} {'->'} {pairAssetOutSymbol(pair)}</div>
|
||||||
|
<div className="status-subtle">{makerPairMovementLabel(pair)}</div>
|
||||||
|
<div className="status-subtle">{makerPairRequestLabel(pair)}</div>
|
||||||
<div className="status-subtle mono">{truncateMiddle(pairId, 42)}</div>
|
<div className="status-subtle mono">{truncateMiddle(pairId, 42)}</div>
|
||||||
</td>
|
</td>
|
||||||
<td><Pill label={pair.mode || pair.status} stateLabel={pair.canTrade || pair.can_trade ? 'healthy' : 'warning'} /></td>
|
<td><Pill label={pair.mode || pair.status} stateLabel={pair.canTrade || pair.can_trade ? 'healthy' : 'warning'} /></td>
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,9 @@ test('strategy page owns consolidated quote lifecycle and successful trade table
|
||||||
assert.match(strategySource, /Timing:/);
|
assert.match(strategySource, /Timing:/);
|
||||||
assert.match(strategySource, /item\.execution\?\.status === 'submitted'/);
|
assert.match(strategySource, /item\.execution\?\.status === 'submitted'/);
|
||||||
assert.match(strategySource, /Submitted means the relay accepted the response; it does not prove a trade\./);
|
assert.match(strategySource, /Submitted means the relay accepted the response; it does not prove a trade\./);
|
||||||
assert.match(strategySource, /Maker sends/);
|
assert.match(strategySource, /For us:/);
|
||||||
|
assert.match(strategySource, /makerPairMovementLabel/);
|
||||||
|
assert.match(strategySource, /makerPairRequestLabel/);
|
||||||
assert.match(strategySource, /formatInventoryCheck/);
|
assert.match(strategySource, /formatInventoryCheck/);
|
||||||
assert.doesNotMatch(strategySource, /Actionable|actionable/);
|
assert.doesNotMatch(strategySource, /Actionable|actionable/);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1846,6 +1846,7 @@ test('bootstrap lifecycle rows preserve quote terms, submitted terms, and gross
|
||||||
assert.equal(row.maker_terms.send_asset_symbol, 'EURe');
|
assert.equal(row.maker_terms.send_asset_symbol, 'EURe');
|
||||||
assert.equal(row.maker_terms.receive_amount, '0.00123208');
|
assert.equal(row.maker_terms.receive_amount, '0.00123208');
|
||||||
assert.equal(row.maker_terms.receive_asset_symbol, 'BTC');
|
assert.equal(row.maker_terms.receive_asset_symbol, 'BTC');
|
||||||
|
assert.equal(row.maker_terms.delta_text, '-76 EURe / +0.00123208 BTC');
|
||||||
assert.equal(row.inventory_check.required, '76');
|
assert.equal(row.inventory_check.required, '76');
|
||||||
assert.equal(row.inventory_check.available, '79');
|
assert.equal(row.inventory_check.available, '79');
|
||||||
assert.equal(row.inventory_check.spendable, '80');
|
assert.equal(row.inventory_check.spendable, '80');
|
||||||
|
|
|
||||||
|
|
@ -228,6 +228,50 @@ test('strategy uses BTC inventory, not USDC inventory, for USDC -> BTC maker res
|
||||||
assert.equal(result.command.expected_inventory_delta_units[config.tradingUsdc.assetId], '8000000');
|
assert.equal(result.command.expected_inventory_delta_units[config.tradingUsdc.assetId], '8000000');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('USDC -> BTC requester quotes use BTC -> USDC maker-side edge config when we sell BTC', () => {
|
||||||
|
const config = makeBtcUsdcDbConfig({
|
||||||
|
strategyConfigOverrides: { edgeBps: 400 },
|
||||||
|
reverseStrategyConfigOverrides: { edgeBps: 1 },
|
||||||
|
});
|
||||||
|
const requestPair = `${config.tradingUsdc.assetId}->${config.tradingBtc.assetId}`;
|
||||||
|
const makerTradePair = `${config.tradingBtc.assetId}->${config.tradingUsdc.assetId}`;
|
||||||
|
const result = evaluateTradeOpportunity({
|
||||||
|
demandEvent: {
|
||||||
|
payload: {
|
||||||
|
quote_id: 'quote-usdc-to-btc-maker-edge',
|
||||||
|
pair: requestPair,
|
||||||
|
asset_in: config.tradingUsdc.assetId,
|
||||||
|
asset_out: config.tradingBtc.assetId,
|
||||||
|
request_kind: 'exact_in',
|
||||||
|
amount_in: '8000000',
|
||||||
|
min_deadline_ms: '60000',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
priceEvent: makeBtcUsdcPriceEvent(),
|
||||||
|
inventoryEvent: makeBtcUsdcInventoryEvent({
|
||||||
|
spendable: {
|
||||||
|
[config.tradingBtc.assetId]: '1000000',
|
||||||
|
[config.tradingUsdc.assetId]: '0',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
config,
|
||||||
|
armed: true,
|
||||||
|
now: Date.parse('2026-04-02T10:00:05.000Z'),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.decision.decision, 'actionable');
|
||||||
|
assert.equal(result.decision.request_pair, requestPair);
|
||||||
|
assert.equal(result.decision.maker_trade_pair, makerTradePair);
|
||||||
|
assert.equal(result.decision.pair_id, makerTradePair);
|
||||||
|
assert.equal(result.decision.edge_bps, '400');
|
||||||
|
assert.equal(result.decision.threshold_pct, '4');
|
||||||
|
assert.equal(result.command.pair_config_id, `${makerTradePair}:v1`);
|
||||||
|
assert.equal(result.command.maker_send_asset_id, config.tradingBtc.assetId);
|
||||||
|
assert.equal(result.command.maker_receive_asset_id, config.tradingUsdc.assetId);
|
||||||
|
assert.equal(result.command.expected_inventory_delta_units[config.tradingBtc.assetId].startsWith('-'), true);
|
||||||
|
assert.equal(result.command.expected_inventory_delta_units[config.tradingUsdc.assetId], '8000000');
|
||||||
|
});
|
||||||
|
|
||||||
test('strategy subtracts pending outbound from maker inventory before approving USDC -> BTC responses', () => {
|
test('strategy subtracts pending outbound from maker inventory before approving USDC -> BTC responses', () => {
|
||||||
const config = makeBtcUsdcDbConfig({ includeReversePair: true });
|
const config = makeBtcUsdcDbConfig({ includeReversePair: true });
|
||||||
const reversePair = `${config.tradingUsdc.assetId}->${config.tradingBtc.assetId}`;
|
const reversePair = `${config.tradingUsdc.assetId}->${config.tradingBtc.assetId}`;
|
||||||
|
|
@ -528,7 +572,11 @@ test('disabled maker response age policy preserves current BTC -> USDC actionabi
|
||||||
assert.ok(result.command);
|
assert.ok(result.command);
|
||||||
});
|
});
|
||||||
|
|
||||||
function makeBtcUsdcDbConfig({ strategyConfigOverrides = {}, includeReversePair = false } = {}) {
|
function makeBtcUsdcDbConfig({
|
||||||
|
strategyConfigOverrides = {},
|
||||||
|
reverseStrategyConfigOverrides = {},
|
||||||
|
includeReversePair = true,
|
||||||
|
} = {}) {
|
||||||
const tradingBtc = {
|
const tradingBtc = {
|
||||||
assetId: 'nep141:nbtc.bridge.near',
|
assetId: 'nep141:nbtc.bridge.near',
|
||||||
symbol: 'BTC',
|
symbol: 'BTC',
|
||||||
|
|
@ -581,6 +629,7 @@ function makeBtcUsdcDbConfig({ strategyConfigOverrides = {}, includeReversePair
|
||||||
const reverseStrategyConfig = {
|
const reverseStrategyConfig = {
|
||||||
...strategyConfig,
|
...strategyConfig,
|
||||||
configId: `${reversePair}:v1`,
|
configId: `${reversePair}:v1`,
|
||||||
|
...reverseStrategyConfigOverrides,
|
||||||
};
|
};
|
||||||
const reversePairConfig = {
|
const reversePairConfig = {
|
||||||
...pair,
|
...pair,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { evaluateTradeOpportunity } from '../src/core/strategy.mjs';
|
||||||
import {
|
import {
|
||||||
CURRENT_EURE_ASSET_ID,
|
CURRENT_EURE_ASSET_ID,
|
||||||
CURRENT_NBTC_ASSET_ID,
|
CURRENT_NBTC_ASSET_ID,
|
||||||
|
CURRENT_REVERSE_PAIR_KEY,
|
||||||
CURRENT_USDC_ASSET_ID,
|
CURRENT_USDC_ASSET_ID,
|
||||||
GNOSIS_USDC_ASSET_ID,
|
GNOSIS_USDC_ASSET_ID,
|
||||||
LEGACY_OMFT_BTC_ASSET_ID,
|
LEGACY_OMFT_BTC_ASSET_ID,
|
||||||
|
|
@ -555,7 +556,7 @@ test('pair mode activation requires both assets to be registered', async () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('strategy uses DB pair config for current pair and persists config version', async () => {
|
test('strategy uses DB maker-side pair config and persists config version', async () => {
|
||||||
const pool = createMemoryPool();
|
const pool = createMemoryPool();
|
||||||
const snapshot = await seedTradingConfig(pool);
|
const snapshot = await seedTradingConfig(pool);
|
||||||
const result = evaluateTradeOpportunity({
|
const result = evaluateTradeOpportunity({
|
||||||
|
|
@ -578,10 +579,12 @@ test('strategy uses DB pair config for current pair and persists config version'
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(result.decision.decision, 'actionable');
|
assert.equal(result.decision.decision, 'actionable');
|
||||||
|
assert.equal(result.decision.request_pair, `${CURRENT_NBTC_ASSET_ID}->${CURRENT_EURE_ASSET_ID}`);
|
||||||
|
assert.equal(result.decision.maker_trade_pair, CURRENT_REVERSE_PAIR_KEY);
|
||||||
assert.equal(result.decision.edge_bps, '49');
|
assert.equal(result.decision.edge_bps, '49');
|
||||||
assert.equal(result.decision.pair_config_version, '1');
|
assert.equal(result.decision.pair_config_version, '1');
|
||||||
assert.equal(result.command.quote_output.amount_out, '4975500000000000000');
|
assert.equal(result.command.quote_output.amount_out, '4975500000000000000');
|
||||||
assert.equal(result.command.pair_config_id, `${CURRENT_NBTC_ASSET_ID}->${CURRENT_EURE_ASSET_ID}:v1`);
|
assert.equal(result.command.pair_config_id, `${CURRENT_REVERSE_PAIR_KEY}:v1`);
|
||||||
});
|
});
|
||||||
|
|
||||||
function token(assetId, symbol, decimals) {
|
function token(assetId, symbol, decimals) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue