fix: backfill gross edge pct in outcomes and add fallback in UI
All checks were successful
deploy / deploy (push) Successful in 50s
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:
parent
3f17d6b9af
commit
3ee02bde17
5 changed files with 83 additions and 7 deletions
|
|
@ -2349,10 +2349,17 @@ function normalizeTradeForUi({ config, trade }) {
|
|||
};
|
||||
}
|
||||
|
||||
function enrichLifecycleRowForUi({ config, row }) {
|
||||
export function enrichLifecycleRowForUi({ config, 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,
|
||||
gross_edge_pct: edgePct,
|
||||
notional_display: notionalDisplay,
|
||||
request_terms: buildLifecycleTerms({
|
||||
config,
|
||||
|
|
@ -2370,8 +2377,12 @@ function enrichLifecycleRowForUi({ config, row }) {
|
|||
config,
|
||||
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({
|
||||
config,
|
||||
delta: row.attributed_inventory_delta,
|
||||
|
|
@ -2476,7 +2487,13 @@ function estimateGrossEdgeValueEure(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);
|
||||
if (!Number.isFinite(edge) || !Number.isFinite(notional)) return null;
|
||||
const value = (notional * edge) / 100;
|
||||
|
|
|
|||
|
|
@ -44,6 +44,13 @@ export function deriveQuoteOutcomeRecords({
|
|||
...expectedDeltasByQuote.values(),
|
||||
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({
|
||||
inventorySnapshots,
|
||||
activeAssetIds,
|
||||
|
|
@ -360,6 +367,15 @@ function movementMatchesExpectedDelta({
|
|||
if (movementTs < submittedTs) 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)) {
|
||||
const actual = safeBigInt(movement.delta_units?.[assetId]);
|
||||
if (expected > 0n && actual < expected) {
|
||||
|
|
|
|||
|
|
@ -6458,6 +6458,15 @@ async function ensureExpressionIndex(pool, { name, table, expression }) {
|
|||
}
|
||||
|
||||
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();
|
||||
try {
|
||||
await client.query(`SET lock_timeout = '5s'`);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
buildProfitabilitySummary,
|
||||
createDashboardLiveState,
|
||||
deriveQuoteLifecycleRows,
|
||||
enrichLifecycleRowForUi,
|
||||
isDashboardWebSocketBackpressured,
|
||||
resolveDashboardControl,
|
||||
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');
|
||||
});
|
||||
|
||||
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', () => {
|
||||
const config = buildConfig();
|
||||
const bootstrap = buildDashboardBootstrap({
|
||||
|
|
|
|||
|
|
@ -79,8 +79,9 @@ test('quote outcome refresh bounds source queries and joins by recent quote ids'
|
|||
};
|
||||
}
|
||||
if (sql.includes('FROM intent_inventory_snapshots')) {
|
||||
assert.match(sql, /LIMIT \$1/);
|
||||
assert.equal(params[0], 3);
|
||||
assert.match(sql, /BETWEEN \$1 AND \$2/);
|
||||
assert.ok(params[0]);
|
||||
assert.ok(params[1]);
|
||||
return {
|
||||
rows: [
|
||||
eventRow({
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue