Fix pair-native runtime validation gaps
Some checks failed
deploy / deploy (push) Failing after 40s

Proof: targeted pair-native strategy, preflight, outcome, dashboard, and ops tests pass; full npm test passes 237/237; operator dashboard production bundle builds; ops watcher Python test passes.

Assumptions: DB asset, pair, strategy config, and price route rows remain canonical; legacy EURe fields stay only for old-row/API compatibility; local shell has no Kubernetes context for direct live namespace recheck.

Still fake: venue-native terminal fill ids and realized fee/PnL attribution remain unavailable; live deployment verification must happen through the repo workflow because manual cluster repair is out of scope.
This commit is contained in:
philipp 2026-05-18 19:44:54 +02:00
parent 729d2ade0e
commit fdeb1287b4
11 changed files with 137 additions and 13 deletions

View file

@ -22,6 +22,11 @@ ASSETS = {
symbol="EURe", symbol="EURe",
decimals=18, decimals=18,
), ),
"nep141:usdc.omft.near": watch_live_mm.AssetMeta(
asset_id="nep141:usdc.omft.near",
symbol="USDC",
decimals=6,
),
} }
@ -99,6 +104,7 @@ class WatchLiveMmTests(unittest.TestCase):
"spendable": { "spendable": {
"nep141:btc.omft.near": "137014", "nep141:btc.omft.near": "137014",
"nep141:eure": "38999978799978799978", "nep141:eure": "38999978799978799978",
"nep141:usdc.omft.near": "1234567",
} }
} }
}, },
@ -132,6 +138,38 @@ class WatchLiveMmTests(unittest.TestCase):
self.assertEqual(state["mark_to_market_pnl_eure"], "0.497413887978799978") self.assertEqual(state["mark_to_market_pnl_eure"], "0.497413887978799978")
self.assertEqual(state["btc_spendable"], "0.00137014 BTC") self.assertEqual(state["btc_spendable"], "0.00137014 BTC")
self.assertEqual(state["eure_spendable"], "38.9999788 EURe") self.assertEqual(state["eure_spendable"], "38.9999788 EURe")
self.assertIn("1.234567 USDC", state["spendable_assets"])
def test_current_state_fingerprint_does_not_require_eure_asset(self) -> None:
assets = {
key: value
for key, value in ASSETS.items()
if value.symbol != "EURe"
}
responses = {
"near-intents-ingest": {"ingest": {"published_count": 1}},
"inventory-sync": {
"last_snapshot": {
"spendable": {
"nep141:btc.omft.near": "100000",
"nep141:usdc.omft.near": "5000000",
}
}
},
"history-writer": {"latest_portfolio_metrics": {}},
"strategy-engine": {"armed": False, "latest_decision": {}},
"trade-executor": {"armed": False, "signer_registered": True},
}
with mock.patch.object(
watch_live_mm,
"fetch_service_state",
side_effect=lambda *, namespace, deployment: responses[deployment],
):
state = watch_live_mm.current_state_fingerprint(namespace="unrip", assets=assets)
self.assertEqual(state["eure_spendable"], "untracked")
self.assertIn("5 USDC", state["spendable_assets"])
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -219,6 +219,7 @@ def current_state_fingerprint(*, namespace: str, assets: dict[str, AssetMeta]) -
spendable = snapshot.get("spendable") or {} spendable = snapshot.get("spendable") or {}
latest_decision = strategy.get("latest_decision") or {} latest_decision = strategy.get("latest_decision") or {}
latest_metrics = history.get("latest_portfolio_metrics") or {} latest_metrics = history.get("latest_portfolio_metrics") or {}
spendable_assets = format_spendable_assets(spendable=spendable, assets=assets)
return { return {
"ts": now_string(), "ts": now_string(),
@ -227,8 +228,9 @@ def current_state_fingerprint(*, namespace: str, assets: dict[str, AssetMeta]) -
"strategy_armed": str(bool(strategy.get("armed"))).lower(), "strategy_armed": str(bool(strategy.get("armed"))).lower(),
"executor_armed": str(bool(executor.get("armed"))).lower(), "executor_armed": str(bool(executor.get("armed"))).lower(),
"signer_registered": str(executor.get("signer_registered")), "signer_registered": str(executor.get("signer_registered")),
"btc_spendable": format_amount(spendable.get(find_symbol_asset_id(assets, "BTC"), "0"), assets.get(find_symbol_asset_id(assets, "BTC"))), "spendable_assets": ", ".join(spendable_assets) if spendable_assets else "none",
"eure_spendable": format_amount(spendable.get(find_symbol_asset_id(assets, "EURe"), "0"), assets.get(find_symbol_asset_id(assets, "EURe"))), "btc_spendable": format_symbol_amount(spendable=spendable, assets=assets, symbol="BTC"),
"eure_spendable": format_symbol_amount(spendable=spendable, assets=assets, symbol="EURe"),
"latest_reason": str(latest_decision.get("decision_reason") or "none"), "latest_reason": str(latest_decision.get("decision_reason") or "none"),
"latest_quote_id": str(latest_decision.get("quote_id") or "none"), "latest_quote_id": str(latest_decision.get("quote_id") or "none"),
"trade_pnl_eure": str(latest_metrics.get("trade_pnl_eure") or "-"), "trade_pnl_eure": str(latest_metrics.get("trade_pnl_eure") or "-"),
@ -293,6 +295,7 @@ def print_snapshot(*, namespace: str, assets: dict[str, AssetMeta]) -> None:
f" signer_registered={state['signer_registered']}" f" signer_registered={state['signer_registered']}"
f" btc={state['btc_spendable']}" f" btc={state['btc_spendable']}"
f" eure={state['eure_spendable']}" f" eure={state['eure_spendable']}"
f" balances=[{state['spendable_assets']}]"
f" trade_pnl_eure={state['trade_pnl_eure']}" f" trade_pnl_eure={state['trade_pnl_eure']}"
f" mtm_pnl_eure={state['mark_to_market_pnl_eure']}" f" mtm_pnl_eure={state['mark_to_market_pnl_eure']}"
f" latest_reason={state['latest_reason']}" f" latest_reason={state['latest_reason']}"
@ -308,6 +311,7 @@ def print_state_line(state: dict[str, str]) -> None:
f" signer_registered={state['signer_registered']}" f" signer_registered={state['signer_registered']}"
f" btc={state['btc_spendable']}" f" btc={state['btc_spendable']}"
f" eure={state['eure_spendable']}" f" eure={state['eure_spendable']}"
f" balances=[{state['spendable_assets']}]"
f" trade_pnl_eure={state['trade_pnl_eure']}" f" trade_pnl_eure={state['trade_pnl_eure']}"
f" mtm_pnl_eure={state['mark_to_market_pnl_eure']}" f" mtm_pnl_eure={state['mark_to_market_pnl_eure']}"
f" last_matching_quote_at={state['last_matching_quote_at']}" f" last_matching_quote_at={state['last_matching_quote_at']}"
@ -404,6 +408,22 @@ def find_symbol_asset_id(assets: dict[str, AssetMeta], symbol: str) -> str:
raise KeyError(f"missing asset for symbol {symbol}") raise KeyError(f"missing asset for symbol {symbol}")
def format_symbol_amount(*, spendable: dict[str, str], assets: dict[str, AssetMeta], symbol: str) -> str:
for asset_id, asset in assets.items():
if asset.symbol == symbol:
return format_amount(spendable.get(asset_id, "0"), asset)
return "untracked"
def format_spendable_assets(*, spendable: dict[str, str], assets: dict[str, AssetMeta]) -> list[str]:
rows: list[str] = []
for asset_id, asset in sorted(assets.items(), key=lambda entry: (entry[1].symbol, entry[0])):
rows.append(format_amount(spendable.get(asset_id, "0"), asset))
for asset_id in sorted(set(spendable) - set(assets)):
rows.append(f"{spendable.get(asset_id, '0')} {asset_id}")
return rows
def format_amount(raw_amount: str | None, asset: AssetMeta | None, *, max_places: int = 8) -> str: def format_amount(raw_amount: str | None, asset: AssetMeta | None, *, max_places: int = 8) -> str:
if raw_amount in (None, ""): if raw_amount in (None, ""):
return "-" return "-"

View file

@ -157,7 +157,7 @@ async function handleDemand(event) {
source: 'strategy-engine', source: 'strategy-engine',
venue: 'near-intents', venue: 'near-intents',
eventType: 'execute_trade', eventType: 'execute_trade',
observedAt: event.observed_at, observedAt: evaluation.command.decision_at || event.observed_at || event.ingested_at,
payload: evaluation.command, payload: evaluation.command,
}); });
await producer.sendJson(config.kafkaTopicCmdExecuteTrade, commandEvent, { key: evaluation.command.execution_key }); await producer.sendJson(config.kafkaTopicCmdExecuteTrade, commandEvent, { key: evaluation.command.execution_key });

View file

@ -49,11 +49,11 @@ export function createIntentRequestController({
const destinationAsset = requestPair.destinationAsset; const destinationAsset = requestPair.destinationAsset;
const sourceAmount = String( const sourceAmount = String(
body.source_amount body.source_amount
|| body.amount ?? body.amount
|| body.amount_eure ?? 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 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);
@ -95,7 +95,12 @@ export function createIntentRequestController({
sourceAsset.decimals, sourceAsset.decimals,
{ field: 'intent_request_max_notional' }, { field: 'intent_request_max_notional' },
); );
try {
sourceAmountUnits = parseDecimalToUnits(sourceAmount, sourceAsset.decimals, { field: 'source_amount' }); sourceAmountUnits = parseDecimalToUnits(sourceAmount, sourceAsset.decimals, { field: 'source_amount' });
} catch (error) {
blockedBeforeQuote = true;
throw codedError('invalid_source_amount', error.message || 'Source amount is invalid.');
}
if (maxAmountUnits != null && BigInt(sourceAmountUnits) > BigInt(maxAmountUnits)) { if (maxAmountUnits != null && BigInt(sourceAmountUnits) > BigInt(maxAmountUnits)) {
blockedBeforeQuote = true; blockedBeforeQuote = true;
throw codedError( throw codedError(
@ -198,7 +203,10 @@ export function createIntentRequestController({
throw codedError('solver_quote_unanswered', 'The relay returned no solver quotes for this request.'); throw codedError('solver_quote_unanswered', 'The relay returned no solver quotes for this request.');
} }
if (!selectedQuote) { if (!selectedQuote) {
throw codedError('quote_below_min_receive', 'Solver quotes were below the explicit minimum BTC receive amount.'); throw codedError(
'quote_below_min_receive',
`Solver quotes were below the explicit minimum ${destinationAsset.symbol || 'destination'} receive amount.`,
);
} }
state = 'draft'; state = 'draft';

View file

@ -1001,6 +1001,8 @@ function normalizeIntentRequestForUi({ config, request }) {
const destinationAsset = config.assetRegistry.get(request.destination_asset_id) || config.tradingBtc; const destinationAsset = config.assetRegistry.get(request.destination_asset_id) || config.tradingBtc;
return { return {
...request, ...request,
source_symbol: request.source_symbol || sourceAsset?.symbol || 'Source',
destination_symbol: request.destination_symbol || destinationAsset?.symbol || 'Destination',
source_amount: formatUnits(request.source_amount_units || '0', sourceAsset?.decimals || 0), source_amount: formatUnits(request.source_amount_units || '0', sourceAsset?.decimals || 0),
expected_destination_amount: formatUnits( expected_destination_amount: formatUnits(
request.expected_destination_amount_units || '0', request.expected_destination_amount_units || '0',

View file

@ -29,8 +29,10 @@ export function evaluateTradeOpportunity({
const effectiveMaxNotional = pairRuntime.maxNotional ?? maxNotional; const effectiveMaxNotional = pairRuntime.maxNotional ?? maxNotional;
const legacyEureNotional = isLegacyEureNotional({ pairRuntime, config }); const legacyEureNotional = isLegacyEureNotional({ pairRuntime, config });
const decisionId = crypto.randomUUID(); const decisionId = crypto.randomUUID();
const decisionAt = new Date(now).toISOString();
const baseDecision = { const baseDecision = {
decision_id: decisionId, decision_id: decisionId,
decision_at: decisionAt,
quote_id: payload.quote_id, quote_id: payload.quote_id,
pair: pairKey(payload.asset_in, payload.asset_out), pair: pairKey(payload.asset_in, payload.asset_out),
pair_id: pairRuntime.pair?.pairId || null, pair_id: pairRuntime.pair?.pairId || null,
@ -143,11 +145,13 @@ export function evaluateTradeOpportunity({
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,
edge_bps: decision.edge_bps, edge_bps: decision.edge_bps,
decision_at: decision.decision_at,
max_notional: decision.max_notional, max_notional: decision.max_notional,
min_notional: decision.min_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,
direction: decision.direction,
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,

View file

@ -382,6 +382,7 @@ function IntentRequestForm({ summary, onControl }) {
} }
function IntentRequestLifecycle({ item }) { function IntentRequestLifecycle({ item }) {
const destinationSymbol = item.destination_symbol || 'destination';
return ( return (
<div className="lifecycle-detail-panel"> <div className="lifecycle-detail-panel">
<div className="lifecycle-stage-grid"> <div className="lifecycle-stage-grid">
@ -400,7 +401,11 @@ function IntentRequestLifecycle({ item }) {
<div className="stage-status">{`${item.solver_quote_count || 0} quote(s)`}</div> <div className="stage-status">{`${item.solver_quote_count || 0} quote(s)`}</div>
<div className="stage-body"> <div className="stage-body">
<IdentifierLine label="Quote hash" value={item.quote_hash} /> <IdentifierLine label="Quote hash" value={item.quote_hash} />
<div className="status-subtle">{item.quoted_destination_amount ? `Quoted ${item.quoted_destination_amount} BTC` : 'No usable quote stored'}</div> <div className="status-subtle">
{item.quoted_destination_amount
? `Quoted ${item.quoted_destination_amount} ${destinationSymbol}`
: 'No usable quote stored'}
</div>
</div> </div>
</div> </div>
<div className="lifecycle-stage-card"> <div className="lifecycle-stage-card">
@ -470,14 +475,16 @@ function IntentRequestsTable({ items, executorArmed, onControl }) {
const rowKey = item.request_id || item.idempotency_key || String(index); const rowKey = item.request_id || item.idempotency_key || String(index);
const isExpanded = expanded.has(rowKey); const isExpanded = expanded.has(rowKey);
const canSubmit = item.live_submit_capable && executorArmed === true; const canSubmit = item.live_submit_capable && executorArmed === true;
const sourceSymbol = item.source_symbol || 'source';
const destinationSymbol = item.destination_symbol || 'destination';
return ( return (
<Fragment key={rowKey}> <Fragment key={rowKey}>
<tr> <tr>
<td>{formatTimestamp(item.resolved_at || item.submitted_at || item.created_at)}</td> <td>{formatTimestamp(item.resolved_at || item.submitted_at || item.created_at)}</td>
<td><IdentifierLine label="Request" value={item.request_id} /></td> <td><IdentifierLine label="Request" value={item.request_id} /></td>
<td> <td>
<div className="mono">{`${item.source_amount} ${item.source_symbol || 'EURe'}`}</div> <div className="mono">{`${item.source_amount} ${sourceSymbol}`}</div>
<div className="status-subtle">{`Min ${item.min_destination_amount} ${item.destination_symbol || 'BTC'}`}</div> <div className="status-subtle">{`Min ${item.min_destination_amount} ${destinationSymbol}`}</div>
<div className="status-subtle">{`${item.slippage_bps ?? 'n/a'} bps slippage`}</div> <div className="status-subtle">{`${item.slippage_bps ?? 'n/a'} bps slippage`}</div>
</td> </td>
<td> <td>

View file

@ -452,6 +452,34 @@ test('nBTC/USDC preflight blocks insufficient source inventory with source-speci
assert.equal(relay.quoteCalls, 0); assert.equal(relay.quoteCalls, 0);
}); });
test('pair-native preflight treats explicit zero source_amount as invalid instead of using default', async () => {
const relay = buildRelay();
const pair = buildPair({
sourceAsset: USDC,
destinationAsset: BTC,
source: 'btc_usdc_reference',
routeId: 'btc-usdc-route',
});
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: 0 });
assert.equal(preflight.state, 'blocked');
assert.equal(preflight.reason_code, 'invalid_source_amount');
assert.equal(preflight.source_amount, '0');
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

@ -36,8 +36,12 @@ test('request UI uses pair-native source amount fields and copy', () => {
assert.match(fundsSource, /name="source_amount"/); assert.match(fundsSource, /name="source_amount"/);
assert.match(fundsSource, /pair_id: form\.pair_id/); assert.match(fundsSource, /pair_id: form\.pair_id/);
assert.match(fundsSource, /Pair request creation/); assert.match(fundsSource, /Pair request creation/);
assert.match(fundsSource, /Quoted \$\{item\.quoted_destination_amount\} \$\{destinationSymbol\}/);
assert.doesNotMatch(fundsSource, /EURe to BTC request creation/); assert.doesNotMatch(fundsSource, /EURe to BTC request creation/);
assert.doesNotMatch(fundsSource, /EURe-to-BTC/); assert.doesNotMatch(fundsSource, /EURe-to-BTC/);
assert.doesNotMatch(fundsSource, /Quoted \$\{item\.quoted_destination_amount\} BTC/);
assert.doesNotMatch(fundsSource, /source_symbol \|\| 'EURe'/);
assert.doesNotMatch(fundsSource, /destination_symbol \|\| 'BTC'/);
}); });
test('funds balance rows expose unvalued valuation reasons', () => { test('funds balance rows expose unvalued valuation reasons', () => {

View file

@ -10,3 +10,10 @@ test('strategy duplicate quote tracking is bounded and state-safe', () => {
assert.match(source, /seenQuotes\.getState\(\)/); assert.match(source, /seenQuotes\.getState\(\)/);
assert.doesNotMatch(source, /seen_quotes:\s*\{\}/); assert.doesNotMatch(source, /seen_quotes:\s*\{\}/);
}); });
test('strategy execute commands use decision timestamp as durable observed time', () => {
assert.match(
source,
/observedAt:\s*evaluation\.command\.decision_at\s*\|\|\s*event\.observed_at\s*\|\|\s*event\.ingested_at/,
);
});

View file

@ -87,8 +87,11 @@ test('strategy emits actionable exact-in BTC -> EURe command when armed and inve
}); });
assert.equal(result.decision.decision, 'actionable'); assert.equal(result.decision.decision, 'actionable');
assert.equal(result.decision.decision_at, '2026-04-02T10:00:05.000Z');
assert.equal(result.decision.decision_reason, 'actionable'); assert.equal(result.decision.decision_reason, 'actionable');
assert.ok(result.command); assert.ok(result.command);
assert.equal(result.command.decision_at, '2026-04-02T10:00:05.000Z');
assert.equal(result.command.direction, 'btc_to_eure');
assert.equal(result.command.quote_output.amount_out, '4900000000000000000'); assert.equal(result.command.quote_output.amount_out, '4900000000000000000');
}); });
@ -180,6 +183,9 @@ test('strategy emits actionable exact-in BTC -> USDC command from DB price route
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.decision.eure_notional, null);
assert.equal(result.command.direction, 'base_to_quote');
assert.equal(result.command.decision_at, '2026-04-02T10:00:05.000Z');
assert.equal(result.command.max_notional_eure, 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.notional_symbol, 'USDC');
assert.equal(result.command.expected_inventory_delta_units[config.tradingBtc.assetId], '10000'); assert.equal(result.command.expected_inventory_delta_units[config.tradingBtc.assetId], '10000');