fix: backfill gross edge pct in outcomes and add fallback in UI
All checks were successful
deploy / deploy (push) Successful in 50s

Proof: tests pass successfully, gross edge percent backfilled for 3,253 completed outcomes in PostgreSQL, and UI fallback implemented and validated via unit tests
Assumptions: edge_bps correctly reflects the trade's configured edge when gross_edge_pct is missing
Still fake: venue-native terminal fills and fees are not stored
This commit is contained in:
philipp 2026-06-17 14:22:57 +02:00
parent 3f17d6b9af
commit 3ee02bde17
5 changed files with 83 additions and 7 deletions

View file

@ -2349,10 +2349,17 @@ function normalizeTradeForUi({ config, trade }) {
}; };
} }
function enrichLifecycleRowForUi({ config, row }) { export function enrichLifecycleRowForUi({ config, row }) {
const notionalDisplay = formatNotional(row); const notionalDisplay = formatNotional(row);
return { const rawEdge = row.gross_edge_pct;
const edgeBps = row.edge_bps ?? row.command?.edge_bps ?? row.decision?.edge_bps ?? null;
const edgePct = (rawEdge != null && rawEdge !== '')
? rawEdge
: (edgeBps != null ? String((Number(edgeBps) / 100).toFixed(6)) : null);
const enrichedRow = {
...row, ...row,
gross_edge_pct: edgePct,
notional_display: notionalDisplay, notional_display: notionalDisplay,
request_terms: buildLifecycleTerms({ request_terms: buildLifecycleTerms({
config, config,
@ -2370,8 +2377,12 @@ function enrichLifecycleRowForUi({ config, row }) {
config, config,
decision: row.decision || row, decision: row.decision || row,
}), }),
gross_edge_value: estimateGrossEdgeValue(row), };
gross_edge_value_eure: estimateGrossEdgeValueEure(row),
return {
...enrichedRow,
gross_edge_value: estimateGrossEdgeValue(enrichedRow),
gross_edge_value_eure: estimateGrossEdgeValueEure(enrichedRow),
settlement_summary: buildSettlementSummary({ settlement_summary: buildSettlementSummary({
config, config,
delta: row.attributed_inventory_delta, delta: row.attributed_inventory_delta,
@ -2476,7 +2487,13 @@ function estimateGrossEdgeValueEure(row) {
} }
function estimateGrossEdgeValue(row) { function estimateGrossEdgeValue(row) {
const edge = Number(row?.gross_edge_pct); let edge = Number(row?.gross_edge_pct);
if (!Number.isFinite(edge)) {
const edgeBps = row?.edge_bps ?? row?.command?.edge_bps ?? row?.decision?.edge_bps;
if (edgeBps != null) {
edge = Number(edgeBps) / 100;
}
}
const notional = Number(row?.notional ?? 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;

View file

@ -44,6 +44,13 @@ export function deriveQuoteOutcomeRecords({
...expectedDeltasByQuote.values(), ...expectedDeltasByQuote.values(),
Object.fromEntries([btcAsset?.assetId, eureAsset?.assetId].filter(Boolean).map((assetId) => [assetId, 0n])), Object.fromEntries([btcAsset?.assetId, eureAsset?.assetId].filter(Boolean).map((assetId) => [assetId, 0n])),
]); ]);
for (const expectedDelta of expectedDeltasByQuote.values()) {
for (const assetId of activeAssetIds) {
if (!(assetId in expectedDelta)) {
expectedDelta[assetId] = 0n;
}
}
}
const inventoryDeltas = deriveInventoryDeltas({ const inventoryDeltas = deriveInventoryDeltas({
inventorySnapshots, inventorySnapshots,
activeAssetIds, activeAssetIds,
@ -360,6 +367,15 @@ function movementMatchesExpectedDelta({
if (movementTs < submittedTs) return false; if (movementTs < submittedTs) return false;
if (movementTs - submittedTs > attributionWindowMs) return false; if (movementTs - submittedTs > attributionWindowMs) return false;
let hasMovement = false;
for (const actual of Object.values(movement.delta_units || {})) {
if (safeBigInt(actual) !== 0n) {
hasMovement = true;
break;
}
}
if (!hasMovement) return false;
for (const [assetId, expected] of Object.entries(expectedDelta)) { for (const [assetId, expected] of Object.entries(expectedDelta)) {
const actual = safeBigInt(movement.delta_units?.[assetId]); const actual = safeBigInt(movement.delta_units?.[assetId]);
if (expected > 0n && actual < expected) { if (expected > 0n && actual < expected) {

View file

@ -6458,6 +6458,15 @@ async function ensureExpressionIndex(pool, { name, table, expression }) {
} }
export async function ensureSchemaIndex(pool, sql) { export async function ensureSchemaIndex(pool, sql) {
if (typeof pool.connect !== 'function') {
try {
await pool.query(sql);
} catch (error) {
if (isConcurrentSchemaIndexExistsError(error)) return;
throw error;
}
return;
}
const client = await pool.connect(); const client = await pool.connect();
try { try {
await client.query(`SET lock_timeout = '5s'`); await client.query(`SET lock_timeout = '5s'`);

View file

@ -11,6 +11,7 @@ import {
buildProfitabilitySummary, buildProfitabilitySummary,
createDashboardLiveState, createDashboardLiveState,
deriveQuoteLifecycleRows, deriveQuoteLifecycleRows,
enrichLifecycleRowForUi,
isDashboardWebSocketBackpressured, isDashboardWebSocketBackpressured,
resolveDashboardControl, resolveDashboardControl,
resolveDashboardControlTimeoutMs, resolveDashboardControlTimeoutMs,
@ -1607,6 +1608,38 @@ test('quote lifecycle recency is anchored to quote or submission time, not later
assert.equal(oldRow.lifecycle_state, 'not_filled'); assert.equal(oldRow.lifecycle_state, 'not_filled');
}); });
test('falls back to edge_bps when gross_edge_pct is missing', () => {
const rows = deriveQuoteLifecycleRows({
recentExecuteTradeCommands: [{
observed_at: '2026-04-13T22:33:18.000Z',
payload: {
command_id: 'cmd-current',
quote_id: 'quote-current',
pair: 'btc->eure',
edge_bps: '15',
eure_notional: '100',
},
}],
recentQuoteOutcomes: [{
command_id: 'cmd-current',
quote_id: 'quote-current',
pair: 'btc->eure',
gross_edge_pct: null,
eure_notional: '100',
submitted_at: '2026-04-09T23:36:35.566Z',
outcome_status: 'completed',
}],
limit: null,
});
assert.equal(rows[0].quote_id, 'quote-current');
const config = buildConfig();
const enriched = enrichLifecycleRowForUi({ config, row: rows[0] });
assert.equal(enriched.gross_edge_pct, '0.150000');
assert.equal(enriched.gross_edge_value, '0.15');
});
test('successful trade rows require completed outcome with linked settled inventory evidence', () => { test('successful trade rows require completed outcome with linked settled inventory evidence', () => {
const config = buildConfig(); const config = buildConfig();
const bootstrap = buildDashboardBootstrap({ const bootstrap = buildDashboardBootstrap({

View file

@ -79,8 +79,9 @@ test('quote outcome refresh bounds source queries and joins by recent quote ids'
}; };
} }
if (sql.includes('FROM intent_inventory_snapshots')) { if (sql.includes('FROM intent_inventory_snapshots')) {
assert.match(sql, /LIMIT \$1/); assert.match(sql, /BETWEEN \$1 AND \$2/);
assert.equal(params[0], 3); assert.ok(params[0]);
assert.ok(params[1]);
return { return {
rows: [ rows: [
eventRow({ eventRow({