From 1d66ae208f25b90705df9103870e14e0dd783a63 Mon Sep 17 00:00:00 2001 From: philipp Date: Tue, 19 May 2026 15:45:30 +0200 Subject: [PATCH] Expose maker edge competitiveness Proof: Maker competitiveness now persists edge_bps into quote outcome payloads, groups summaries by edge, and shows the edge in the operator dashboard so filled versus not-filled responses can be compared against configured strategy edge. Assumptions: Edge bps remains DB-owned pair strategy config; this change is observational and does not change live pair enablement, notional limits, inventory checks, response policy, or relay submission behavior. Still fake: Venue-native terminal fill ids and fee-complete realized PnL remain unavailable; relay acceptance is still only submission evidence. --- src/core/maker-competitiveness.mjs | 3 ++ src/core/operator-dashboard.mjs | 1 + src/core/quote-outcomes.mjs | 3 ++ src/lib/postgres.mjs | 1 + .../static/pages/StrategyPage.jsx | 6 ++-- test/maker-timing-competitiveness.test.mjs | 32 +++++++++++++++++++ test/operator-dashboard-ui-static.test.mjs | 1 + test/postgres-quote-outcomes-refresh.test.mjs | 2 ++ 8 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/core/maker-competitiveness.mjs b/src/core/maker-competitiveness.mjs index 152f098..cbc06e1 100644 --- a/src/core/maker-competitiveness.mjs +++ b/src/core/maker-competitiveness.mjs @@ -103,6 +103,7 @@ export function normalizeCompetitivenessEntry(row = {}) { pair_config_id: row.pair_config_id || decision.pair_config_id || execution.pair_config_id || null, pair_config_version: row.pair_config_version || decision.pair_config_version || execution.pair_config_version || null, + edge_bps: row.edge_bps ?? decision.edge_bps ?? row.command?.edge_bps ?? execution.edge_bps ?? null, direction: row.direction || decision.direction || row.command?.direction || 'unknown', request_kind: row.request_kind || decision.request_kind || row.command?.request_kind || 'unknown', result_code: resultCode || 'no_result', @@ -154,6 +155,7 @@ function groupEntries(entries) { entry.pair || 'unknown', entry.direction || 'unknown', entry.request_kind || 'unknown', + entry.edge_bps ?? 'unknown', entry.result_code || 'no_result', entry.failure_category || 'none', entry.quote_age_bucket, @@ -173,6 +175,7 @@ function summarizeGroup(entries) { pair: first.pair || null, direction: first.direction || 'unknown', request_kind: first.request_kind || 'unknown', + edge_bps: first.edge_bps ?? null, result_code: first.result_code || 'no_result', failure_category: first.failure_category || null, quote_age_bucket: first.quote_age_bucket, diff --git a/src/core/operator-dashboard.mjs b/src/core/operator-dashboard.mjs index 4ba9ab6..5102f2c 100644 --- a/src/core/operator-dashboard.mjs +++ b/src/core/operator-dashboard.mjs @@ -1248,6 +1248,7 @@ export function deriveQuoteLifecycleRows({ pair: outcome?.pair || null, direction: outcome?.direction || null, request_kind: outcome?.request_kind || null, + edge_bps: outcome?.edge_bps || null, gross_edge_pct: outcome?.gross_edge_pct || null, notional: outcome?.notional || null, notional_asset_id: outcome?.notional_asset_id || null, diff --git a/src/core/quote-outcomes.mjs b/src/core/quote-outcomes.mjs index b6f5485..e5e4519 100644 --- a/src/core/quote-outcomes.mjs +++ b/src/core/quote-outcomes.mjs @@ -311,6 +311,7 @@ function baseOutcomeRecord({ pair: command?.pair || decision?.pair || submission.pair || null, direction: decision?.direction || command?.direction || null, request_kind: command?.request_kind || decision?.request_kind || null, + edge_bps: command?.edge_bps || decision?.edge_bps || submission.edge_bps || null, gross_edge_pct: decision?.gross_edge_pct || null, notional: decision?.notional || command?.notional || null, notional_asset_id: decision?.notional_asset_id || command?.notional_asset_id || null, @@ -443,6 +444,7 @@ function normalizeCommand(entry) { pair: payload.pair || null, direction: payload.direction || null, request_kind: payload.request_kind || null, + edge_bps: payload.edge_bps || null, notional: payload.notional || null, notional_asset_id: payload.notional_asset_id || null, notional_symbol: payload.notional_symbol || null, @@ -476,6 +478,7 @@ function normalizeDecision(entry) { pair: payload.pair || null, direction: payload.direction || null, request_kind: payload.request_kind || null, + edge_bps: payload.edge_bps || null, gross_edge_pct: payload.gross_edge_pct || null, notional: payload.notional || null, notional_asset_id: payload.notional_asset_id || null, diff --git a/src/lib/postgres.mjs b/src/lib/postgres.mjs index a138255..8c2306c 100644 --- a/src/lib/postgres.mjs +++ b/src/lib/postgres.mjs @@ -3390,6 +3390,7 @@ function normalizeQuoteOutcomeRow(row) { pair: payload.pair || null, direction: payload.direction || null, request_kind: payload.request_kind || null, + edge_bps: payload.edge_bps || null, gross_edge_pct: payload.gross_edge_pct || null, notional: payload.notional || null, notional_asset_id: payload.notional_asset_id || null, diff --git a/src/operator-dashboard/static/pages/StrategyPage.jsx b/src/operator-dashboard/static/pages/StrategyPage.jsx index f4c63cc..efe7cb4 100644 --- a/src/operator-dashboard/static/pages/StrategyPage.jsx +++ b/src/operator-dashboard/static/pages/StrategyPage.jsx @@ -293,6 +293,7 @@ function MakerCompetitivenessSection({ summary, pairConfig }) { Pair Direction Request + Edge Result Age / notional Outcome @@ -304,13 +305,14 @@ function MakerCompetitivenessSection({ summary, pairConfig }) { {groups.length ? groups.slice(0, 12).map((group, index) => { const quoteToRelay = group.latency_stages?.find((stage) => stage.stage === 'quote_to_relay_result_ms'); return ( - +
{pairDisplayLabel(group.pair, pairConfig)}
{truncateMiddle(group.pair || '', 42)}
{plainCodeLabel(group.direction)} {plainCodeLabel(group.request_kind)} + {group.edge_bps == null ? 'Unavailable' : `${group.edge_bps} bps`}
{plainCodeLabel(group.result_code)}
{group.failure_category ?
{plainCodeLabel(group.failure_category)}
: null} @@ -331,7 +333,7 @@ function MakerCompetitivenessSection({ summary, pairConfig }) { ); }) : ( - No competitiveness rows are available yet. + No competitiveness rows are available yet. )} diff --git a/test/maker-timing-competitiveness.test.mjs b/test/maker-timing-competitiveness.test.mjs index e9155c1..288968d 100644 --- a/test/maker-timing-competitiveness.test.mjs +++ b/test/maker-timing-competitiveness.test.mjs @@ -77,6 +77,7 @@ test('maker competitiveness aggregates pair, direction, request kind, result, fa pair: `${nbtc}->${eure}`, direction: 'base_to_quote', request_kind: 'exact_in', + edge_bps: '49', notional: '5', notional_symbol: 'EURe', outcome_status: 'submitted', @@ -98,6 +99,7 @@ test('maker competitiveness aggregates pair, direction, request kind, result, fa pair: `${nbtc}->${usdc}`, direction: 'base_to_quote', request_kind: 'exact_in', + edge_bps: '20', notional: '8', notional_symbol: 'USDC', execution_result_at: '2026-05-18T10:00:00.400Z', @@ -120,6 +122,7 @@ test('maker competitiveness aggregates pair, direction, request kind, result, fa pair: `${nbtc}->${usdc}`, direction: 'base_to_quote', request_kind: 'exact_in', + edge_bps: '20', notional: '12', notional_symbol: 'USDC', maker_timing: extendMakerTiming(baseTiming, { @@ -145,6 +148,7 @@ test('maker competitiveness aggregates pair, direction, request kind, result, fa assert.ok(summary.groups.some((group) => ( group.pair === `${nbtc}->${usdc}` && group.request_kind === 'exact_in' + && group.edge_bps === '20' && group.result_code === 'submission_failed' && group.failure_category === 'quote_not_found_or_finished' && group.quote_age_bucket === '250-500ms' @@ -159,3 +163,31 @@ test('maker competitiveness aggregates pair, direction, request kind, result, fa assert.equal(summary.latest_errors[0].error_message, 'quote not found or already finished'); assert.equal(summary.policy_skips[0].reason_code, 'maker_quote_too_old'); }); + +test('maker competitiveness keeps edge bps as a grouping dimension', () => { + const nbtc = 'nep141:nbtc.bridge.near'; + const usdc = 'nep141:usdc.omft.near'; + const base = { + pair: `${nbtc}->${usdc}`, + direction: 'base_to_quote', + request_kind: 'exact_in', + notional: '8', + notional_symbol: 'USDC', + outcome_status: 'not_filled', + execution: { + status: 'submitted', + result_code: 'quote_response_ok', + }, + }; + + const summary = buildMakerCompetitivenessSummary({ + lifecycleRows: [ + { ...base, quote_id: 'quote-edge-1', edge_bps: '1' }, + { ...base, quote_id: 'quote-edge-20', edge_bps: '20' }, + ], + }); + + const edgeGroups = summary.groups.filter((group) => group.pair === `${nbtc}->${usdc}`); + assert.equal(edgeGroups.length, 2); + assert.deepEqual(edgeGroups.map((group) => group.edge_bps).sort(), ['1', '20']); +}); diff --git a/test/operator-dashboard-ui-static.test.mjs b/test/operator-dashboard-ui-static.test.mjs index 5b3d2e9..8c47bd3 100644 --- a/test/operator-dashboard-ui-static.test.mjs +++ b/test/operator-dashboard-ui-static.test.mjs @@ -127,6 +127,7 @@ test('strategy page exposes maker timing waterfall and competitiveness summaries assert.match(strategySource, /quote_not_found_or_finished/); assert.match(strategySource, /maker_competitiveness/); assert.match(strategySource, /pairDisplayLabel/); + assert.match(strategySource, /group\.edge_bps/); assert.match(stylesSource, /\.two-column-grid/); assert.match(stylesSource, /\.timing-waterfall-table/); }); diff --git a/test/postgres-quote-outcomes-refresh.test.mjs b/test/postgres-quote-outcomes-refresh.test.mjs index a7a6f83..9103860 100644 --- a/test/postgres-quote-outcomes-refresh.test.mjs +++ b/test/postgres-quote-outcomes-refresh.test.mjs @@ -48,6 +48,7 @@ test('quote outcome refresh bounds source queries and joins by recent quote ids' command_id: 'cmd-1', decision_id: 'decision-1', quote_id: 'quote-1', + edge_bps: '10', min_deadline_ms: '15000', asset_in: eureAsset.assetId, asset_out: btcAsset.assetId, @@ -124,6 +125,7 @@ test('quote outcome refresh bounds source queries and joins by recent quote ids' assert.equal(records.length, 1); assert.equal(records[0].quote_id, 'quote-1'); + assert.equal(records[0].payload.edge_bps, '10'); assert.equal(queries.filter((entry) => entry.sql.includes('INSERT INTO quote_outcome_attributions')).length, 1); });