Value tracked USDC in portfolio metrics
Some checks failed
deploy / deploy (push) Failing after 40s

Proof: Dashboard portfolio metrics now include DB-tracked USDC balances valued from live BTC/EUR and BTC/USDC reference prices, with regression coverage for the observed USDC inventory case.

Assumptions: USDC is cash-equivalent for valuation when a fresh BTC/USDC reference event is available; live trading safety remains governed by pair config and price route checks.

Still fake: Portfolio valuation still does not provide fee-complete realized PnL or generalized valuation for every imported non-stable asset.
This commit is contained in:
philipp 2026-05-18 17:48:18 +02:00
parent 8def832c5e
commit 8f109a7463
7 changed files with 455 additions and 16 deletions

View file

@ -384,6 +384,7 @@ async function refreshPortfolioMetrics() {
btcAsset: tradingConfig.tradingBtc, btcAsset: tradingConfig.tradingBtc,
btcAssets: tradingConfig.tradingBtcAssets, btcAssets: tradingConfig.tradingBtcAssets,
eureAsset: tradingConfig.tradingEure, eureAsset: tradingConfig.tradingEure,
trackedAssets: tradingConfig.trackedAssets,
}); });
const payload = computePortfolioMetric({ const payload = computePortfolioMetric({
baseline: inputs.baseline, baseline: inputs.baseline,
@ -393,6 +394,7 @@ async function refreshPortfolioMetrics() {
btcAsset: tradingConfig.tradingBtc, btcAsset: tradingConfig.tradingBtc,
btcAssets: tradingConfig.tradingBtcAssets, btcAssets: tradingConfig.tradingBtcAssets,
eureAsset: tradingConfig.tradingEure, eureAsset: tradingConfig.tradingEure,
valuationAssets: inputs.valuationAssets || [],
commandCount: inputs.commandCount, commandCount: inputs.commandCount,
resultCount: inputs.resultCount, resultCount: inputs.resultCount,
}); });
@ -402,7 +404,10 @@ async function refreshPortfolioMetrics() {
const metricId = buildPortfolioMetricId({ const metricId = buildPortfolioMetricId({
baselineInventoryId: inputs.baseline?.inventory?.inventory_id || null, baselineInventoryId: inputs.baseline?.inventory?.inventory_id || null,
currentInventoryId: inputs.currentInventory?.payload?.inventory_id || null, currentInventoryId: inputs.currentInventory?.payload?.inventory_id || null,
currentPriceId: inputs.currentPrice?.payload?.price_id || null, currentPriceId: [
inputs.currentPrice?.payload?.price_id,
...(inputs.valuationAssets || []).map((asset) => asset.priceId),
].filter(Boolean).join('+') || null,
}); });
await upsertPortfolioMetric(pool, { await upsertPortfolioMetric(pool, {
metricId, metricId,

View file

@ -551,6 +551,7 @@ export function buildDashboardBootstrap({
inventorySnapshot, inventorySnapshot,
marketPrice, marketPrice,
config, config,
portfolioMetric,
}); });
const funding = buildFundingSummary({ const funding = buildFundingSummary({
config, config,
@ -771,7 +772,7 @@ function buildStatusBar({
}; };
} }
function buildBalanceSummary({ inventorySnapshot, marketPrice, config }) { function buildBalanceSummary({ inventorySnapshot, marketPrice, config, portfolioMetric = null }) {
const inventory = inventorySnapshot?.payload || {}; const inventory = inventorySnapshot?.payload || {};
const spendable = inventory.spendable || {}; const spendable = inventory.spendable || {};
const pendingInbound = inventory.pending_inbound || {}; const pendingInbound = inventory.pending_inbound || {};
@ -779,6 +780,11 @@ function buildBalanceSummary({ inventorySnapshot, marketPrice, config }) {
const balanceAssets = config.trackedAssets?.length const balanceAssets = config.trackedAssets?.length
? config.trackedAssets ? config.trackedAssets
: [...config.assetRegistry.values()]; : [...config.assetRegistry.values()];
const metricValuationsByAssetId = new Map(
(portfolioMetric?.payload?.current_inventory?.valued_assets || [])
.filter((asset) => asset?.asset_id)
.map((asset) => [asset.asset_id, asset]),
);
return { return {
synced_at: inventory.synced_at || inventorySnapshot?.ingested_at || null, synced_at: inventory.synced_at || inventorySnapshot?.ingested_at || null,
@ -787,6 +793,7 @@ function buildBalanceSummary({ inventorySnapshot, marketPrice, config }) {
const spendableUnits = String(spendable[asset.assetId] || '0'); const spendableUnits = String(spendable[asset.assetId] || '0');
const pendingInboundUnits = String(pendingInbound[asset.assetId] || '0'); const pendingInboundUnits = String(pendingInbound[asset.assetId] || '0');
const pendingOutboundUnits = String(pendingOutbound[asset.assetId] || '0'); const pendingOutboundUnits = String(pendingOutbound[asset.assetId] || '0');
const metricValuation = metricValuationsByAssetId.get(asset.assetId);
return { return {
asset_id: asset.assetId, asset_id: asset.assetId,
symbol: asset.symbol, symbol: asset.symbol,
@ -799,11 +806,13 @@ function buildBalanceSummary({ inventorySnapshot, marketPrice, config }) {
pending_inbound: formatUnits(pendingInboundUnits, asset.decimals), pending_inbound: formatUnits(pendingInboundUnits, asset.decimals),
pending_outbound_units: pendingOutboundUnits, pending_outbound_units: pendingOutboundUnits,
pending_outbound: formatUnits(pendingOutboundUnits, asset.decimals), pending_outbound: formatUnits(pendingOutboundUnits, asset.decimals),
eur_value_eure: valueAssetInEur({ eur_value_eure: metricValuation?.value_eure
|| valueAssetInEur({
asset, asset,
units: spendableUnits, units: spendableUnits,
marketPrice: marketPrice?.payload || marketPrice || null, marketPrice: marketPrice?.payload || marketPrice || null,
}), }),
eur_value_source: metricValuation?.valuation_source || null,
}; };
}), }),
}; };

View file

@ -9,6 +9,7 @@ export function computePortfolioMetric({
btcAsset, btcAsset,
btcAssets = null, btcAssets = null,
eureAsset, eureAsset,
valuationAssets = [],
commandCount = 0, commandCount = 0,
resultCount = 0, resultCount = 0,
} = {}) { } = {}) {
@ -24,10 +25,20 @@ export function computePortfolioMetric({
const currentEure = unitsToScaledDecimal(currentEureUnits, eureAsset.decimals); const currentEure = unitsToScaledDecimal(currentEureUnits, eureAsset.decimals);
const currentPriceScaled = parseScaledDecimal(currentPrice.eure_per_btc); const currentPriceScaled = parseScaledDecimal(currentPrice.eure_per_btc);
const currentBtcMarkValue = multiplyScaled(currentBtc, currentPriceScaled); const currentBtcMarkValue = multiplyScaled(currentBtc, currentPriceScaled);
const currentPortfolioValue = currentEure + currentBtcMarkValue; const effectiveValuationAssets = normalizeValuationAssets({
valuationAssets,
btcAssets: effectiveBtcAssets,
eureAsset,
});
const currentValuedAssets = valueValuationAssets({
inventory: currentInventory,
valuationAssets: effectiveValuationAssets,
priceField: 'currentUnitValueEure',
});
const currentPortfolioValue = currentEure + currentBtcMarkValue + currentValuedAssets.total;
const payload = { const payload = {
metric_version: 2, metric_version: 3,
baseline_status: baseline ? 'active' : 'awaiting_first_execution', baseline_status: baseline ? 'active' : 'awaiting_first_execution',
command_count: commandCount, command_count: commandCount,
result_count: resultCount, result_count: resultCount,
@ -41,10 +52,12 @@ export function computePortfolioMetric({
btcAsset: displayBtcAsset, btcAsset: displayBtcAsset,
btcAssets: effectiveBtcAssets, btcAssets: effectiveBtcAssets,
eureAsset, eureAsset,
valuationAssets: effectiveValuationAssets,
}), }),
current_portfolio_value_eure: formatScaledDecimal(currentPortfolioValue), current_portfolio_value_eure: formatScaledDecimal(currentPortfolioValue),
current_btc_mark_value_eure: formatScaledDecimal(currentBtcMarkValue), current_btc_mark_value_eure: formatScaledDecimal(currentBtcMarkValue),
current_eure_cash_value_eure: formatScaledDecimal(currentEure), current_eure_cash_value_eure: formatScaledDecimal(currentEure),
current_valued_asset_value_eure: formatScaledDecimal(currentValuedAssets.total),
portfolio_vs_simple_hold_eure: null, portfolio_vs_simple_hold_eure: null,
trade_pnl_eure: null, trade_pnl_eure: null,
mark_to_market_pnl_eure: null, mark_to_market_pnl_eure: null,
@ -63,6 +76,7 @@ export function computePortfolioMetric({
net_btc: '0', net_btc: '0',
net_eure_units: '0', net_eure_units: '0',
net_eure: '0', net_eure: '0',
net_valued_assets: [],
net_value_eure_at_flow_time: '0', net_value_eure_at_flow_time: '0',
net_value_eure_at_current_price: '0', net_value_eure_at_current_price: '0',
}, },
@ -79,15 +93,37 @@ export function computePortfolioMetric({
const baselineBtc = sumAssetScaled(baseline.inventory, effectiveBtcAssets); const baselineBtc = sumAssetScaled(baseline.inventory, effectiveBtcAssets);
const baselineEure = unitsToScaledDecimal(baselineEureUnits, eureAsset.decimals); const baselineEure = unitsToScaledDecimal(baselineEureUnits, eureAsset.decimals);
const baselinePriceScaled = parseScaledDecimal(baseline.price.eure_per_btc); const baselinePriceScaled = parseScaledDecimal(baseline.price.eure_per_btc);
const baselinePortfolioAtBaselinePrice = baselineEure + multiplyScaled(baselineBtc, baselinePriceScaled); const baselineValuedAssetsAtBaselinePrice = valueValuationAssets({
const baselinePortfolioAtCurrentPrice = baselineEure + multiplyScaled(baselineBtc, currentPriceScaled); inventory: baseline.inventory,
const currentPortfolioAtBaselinePrice = currentEure + multiplyScaled(currentBtc, baselinePriceScaled); valuationAssets: effectiveValuationAssets,
priceField: 'baselineUnitValueEure',
});
const baselineValuedAssetsAtCurrentPrice = valueValuationAssets({
inventory: baseline.inventory,
valuationAssets: effectiveValuationAssets,
priceField: 'currentUnitValueEure',
});
const currentValuedAssetsAtBaselinePrice = valueValuationAssets({
inventory: currentInventory,
valuationAssets: effectiveValuationAssets,
priceField: 'baselineUnitValueEure',
});
const baselinePortfolioAtBaselinePrice = baselineEure
+ multiplyScaled(baselineBtc, baselinePriceScaled)
+ baselineValuedAssetsAtBaselinePrice.total;
const baselinePortfolioAtCurrentPrice = baselineEure
+ multiplyScaled(baselineBtc, currentPriceScaled)
+ baselineValuedAssetsAtCurrentPrice.total;
const currentPortfolioAtBaselinePrice = currentEure
+ multiplyScaled(currentBtc, baselinePriceScaled)
+ currentValuedAssetsAtBaselinePrice.total;
const externalFlowSummary = summarizeExternalFlows({ const externalFlowSummary = summarizeExternalFlows({
externalFlows, externalFlows,
currentPriceScaled, currentPriceScaled,
btcAsset: displayBtcAsset, btcAsset: displayBtcAsset,
btcAssets: effectiveBtcAssets, btcAssets: effectiveBtcAssets,
eureAsset, eureAsset,
valuationAssets: effectiveValuationAssets,
}); });
const fundedPortfolioAtFlowTime = baselinePortfolioAtBaselinePrice const fundedPortfolioAtFlowTime = baselinePortfolioAtBaselinePrice
+ externalFlowSummary.netValueEureAtFlowTime; + externalFlowSummary.netValueEureAtFlowTime;
@ -131,6 +167,10 @@ export function computePortfolioMetric({
externalFlowSummary.netEureUnits.toString(), externalFlowSummary.netEureUnits.toString(),
eureAsset.decimals, eureAsset.decimals,
)), )),
net_valued_assets: buildNetValuedAssetFlowView({
netValuedAssetUnits: externalFlowSummary.netValuedAssetUnits,
valuationAssets: effectiveValuationAssets,
}),
net_value_eure_at_flow_time: formatScaledDecimal(externalFlowSummary.netValueEureAtFlowTime), net_value_eure_at_flow_time: formatScaledDecimal(externalFlowSummary.netValueEureAtFlowTime),
net_value_eure_at_current_price: formatScaledDecimal(externalFlowSummary.netValueEureAtCurrentPrice), net_value_eure_at_current_price: formatScaledDecimal(externalFlowSummary.netValueEureAtCurrentPrice),
}; };
@ -140,6 +180,13 @@ export function computePortfolioMetric({
eure_units: (BigInt(currentEureUnits) - BigInt(baselineEureUnits)).toString(), eure_units: (BigInt(currentEureUnits) - BigInt(baselineEureUnits)).toString(),
eure: formatScaledDecimal(currentEure - baselineEure), eure: formatScaledDecimal(currentEure - baselineEure),
}; };
if (effectiveValuationAssets.length) {
payload.inventory_delta.valued_assets = buildValuedAssetInventoryDelta({
currentInventory,
baselineInventory: baseline.inventory,
valuationAssets: effectiveValuationAssets,
});
}
payload.baseline = { payload.baseline = {
anchor: baseline.anchor || 'latest_inventory_before_first_command', anchor: baseline.anchor || 'latest_inventory_before_first_command',
command_at: baseline.command_at || null, command_at: baseline.command_at || null,
@ -153,12 +200,49 @@ export function computePortfolioMetric({
btcAsset: displayBtcAsset, btcAsset: displayBtcAsset,
btcAssets: effectiveBtcAssets, btcAssets: effectiveBtcAssets,
eureAsset, eureAsset,
valuationAssets: effectiveValuationAssets,
}), }),
}; };
return payload; return payload;
} }
export function buildCashEquivalentValuationAssets({
trackedAssets = [],
btcAsset = null,
btcAssets = null,
eureAsset = null,
priceEvents = [],
} = {}) {
const effectiveBtcAssets = normalizeBtcAssets({ btcAsset, btcAssets });
const excludedAssetIds = new Set(
[...effectiveBtcAssets.map((asset) => asset.assetId), eureAsset?.assetId].filter(Boolean),
);
const payloads = (Array.isArray(priceEvents) ? priceEvents : [priceEvents])
.map((event) => event?.payload || event)
.filter(Boolean);
const usdcPrice = payloads.find((event) => event?.eure_per_btc && event?.usdc_per_btc);
const usdcUnitValueEure = usdcPrice
? divideScaledDecimalStrings(usdcPrice.eure_per_btc, usdcPrice.usdc_per_btc)
: null;
return (trackedAssets || [])
.filter((asset) => asset?.assetId && !excludedAssetIds.has(asset.assetId))
.map((asset) => {
if (asset.symbol !== 'USDC' || !usdcUnitValueEure) return null;
return {
asset,
assetId: asset.assetId,
currentUnitValueEure: usdcUnitValueEure,
baselineUnitValueEure: usdcUnitValueEure,
valuationSource: 'btc_usdc_reference',
priceId: usdcPrice.price_id || null,
observedAt: usdcPrice.observed_at || usdcPrice.ingested_at || null,
};
})
.filter(Boolean);
}
export function buildPortfolioMetricId({ baselineInventoryId, currentInventoryId, currentPriceId }) { export function buildPortfolioMetricId({ baselineInventoryId, currentInventoryId, currentPriceId }) {
return [ return [
'portfolio-metric', 'portfolio-metric',
@ -168,11 +252,27 @@ export function buildPortfolioMetricId({ baselineInventoryId, currentInventoryId
].join(':'); ].join(':');
} }
function buildInventoryView({ inventory, btcAsset, btcAssets = null, eureAsset }) { function buildInventoryView({
inventory,
btcAsset,
btcAssets = null,
eureAsset,
valuationAssets = [],
}) {
const effectiveBtcAssets = normalizeBtcAssets({ btcAsset, btcAssets }); const effectiveBtcAssets = normalizeBtcAssets({ btcAsset, btcAssets });
const effectiveValuationAssets = normalizeValuationAssets({
valuationAssets,
btcAssets: effectiveBtcAssets,
eureAsset,
});
const spendable = inventory?.spendable || {}; const spendable = inventory?.spendable || {};
const btcUnits = sumAssetUnits(inventory, effectiveBtcAssets).toString(); const btcUnits = sumAssetUnits(inventory, effectiveBtcAssets).toString();
const eureUnits = String(spendable[eureAsset.assetId] || '0'); const eureUnits = String(spendable[eureAsset.assetId] || '0');
const valuedAssets = valueValuationAssets({
inventory,
valuationAssets: effectiveValuationAssets,
priceField: 'currentUnitValueEure',
});
return { return {
inventory_id: inventory?.inventory_id || null, inventory_id: inventory?.inventory_id || null,
@ -190,6 +290,8 @@ function buildInventoryView({ inventory, btcAsset, btcAssets = null, eureAsset }
}), }),
eure_units: eureUnits, eure_units: eureUnits,
eure: formatAssetUnits(eureUnits, eureAsset.decimals), eure: formatAssetUnits(eureUnits, eureAsset.decimals),
valued_assets: valuedAssets.items,
valued_assets_value_eure: formatScaledDecimal(valuedAssets.total),
}; };
} }
@ -199,14 +301,21 @@ function summarizeExternalFlows({
btcAsset, btcAsset,
btcAssets = null, btcAssets = null,
eureAsset, eureAsset,
valuationAssets = [],
}) { }) {
const effectiveBtcAssets = normalizeBtcAssets({ btcAsset, btcAssets }); const effectiveBtcAssets = normalizeBtcAssets({ btcAsset, btcAssets });
const effectiveValuationAssets = normalizeValuationAssets({
valuationAssets,
btcAssets: effectiveBtcAssets,
eureAsset,
});
let flowCount = 0; let flowCount = 0;
let depositCount = 0; let depositCount = 0;
let withdrawalCount = 0; let withdrawalCount = 0;
let latestEffectiveAt = null; let latestEffectiveAt = null;
let netBtcUnits = 0n; let netBtcUnits = 0n;
let netEureUnits = 0n; let netEureUnits = 0n;
const netValuedAssetUnits = new Map();
let netValueEureAtFlowTime = 0n; let netValueEureAtFlowTime = 0n;
let netValueEureAtCurrentPrice = 0n; let netValueEureAtCurrentPrice = 0n;
@ -237,6 +346,24 @@ function summarizeExternalFlows({
const eureAmount = unitsToScaledDecimal(signedUnits.toString(), eureAsset.decimals); const eureAmount = unitsToScaledDecimal(signedUnits.toString(), eureAsset.decimals);
netValueEureAtFlowTime += eureAmount; netValueEureAtFlowTime += eureAmount;
netValueEureAtCurrentPrice += eureAmount; netValueEureAtCurrentPrice += eureAmount;
} else {
const valuedAsset = effectiveValuationAssets.find((asset) => asset.assetId === flow.asset_id);
if (!valuedAsset) continue;
netValuedAssetUnits.set(
valuedAsset.assetId,
(netValuedAssetUnits.get(valuedAsset.assetId) || 0n) + signedUnits,
);
const amount = unitsToScaledDecimal(signedUnits.toString(), valuedAsset.decimals);
const flowUnitValue = parseScaledDecimal(
flow.reference_price_eure_per_unit_at_flow_time
|| valuedAsset.baselineUnitValueEure
|| valuedAsset.currentUnitValueEure,
);
netValueEureAtFlowTime += multiplyScaled(amount, flowUnitValue);
netValueEureAtCurrentPrice += multiplyScaled(
amount,
parseScaledDecimal(valuedAsset.currentUnitValueEure),
);
} }
} }
@ -247,11 +374,114 @@ function summarizeExternalFlows({
latestEffectiveAt, latestEffectiveAt,
netBtcUnits, netBtcUnits,
netEureUnits, netEureUnits,
netValuedAssetUnits,
netValueEureAtFlowTime, netValueEureAtFlowTime,
netValueEureAtCurrentPrice, netValueEureAtCurrentPrice,
}; };
} }
function normalizeValuationAssets({ valuationAssets = [], btcAssets = [], eureAsset = null } = {}) {
const excludedAssetIds = new Set([
...normalizeBtcAssets({ btcAssets }).map((asset) => asset.assetId),
eureAsset?.assetId,
].filter(Boolean));
return (valuationAssets || [])
.map((entry) => {
const asset = entry.asset || entry;
const assetId = entry.assetId || entry.asset_id || asset.assetId || asset.asset_id || null;
const currentUnitValueEure =
entry.currentUnitValueEure
?? entry.current_unit_value_eure
?? entry.valueEurePerUnit
?? entry.value_eure_per_unit
?? null;
const baselineUnitValueEure =
entry.baselineUnitValueEure
?? entry.baseline_unit_value_eure
?? currentUnitValueEure;
if (!assetId || excludedAssetIds.has(assetId) || currentUnitValueEure == null) return null;
return {
assetId,
symbol: asset.symbol || entry.symbol || assetId,
label: asset.label || entry.label || asset.symbol || entry.symbol || assetId,
decimals: Number(asset.decimals ?? entry.decimals ?? 0),
currentUnitValueEure: String(currentUnitValueEure),
baselineUnitValueEure: String(baselineUnitValueEure),
valuationSource: entry.valuationSource || entry.valuation_source || null,
priceId: entry.priceId || entry.price_id || null,
observedAt: entry.observedAt || entry.observed_at || null,
};
})
.filter((asset) => (
asset
&& Number.isInteger(asset.decimals)
&& asset.decimals >= 0
&& asset.decimals <= VALUE_SCALE
));
}
function valueValuationAssets({ inventory, valuationAssets, priceField }) {
const spendable = inventory?.spendable || {};
let total = 0n;
const items = [];
for (const asset of valuationAssets || []) {
const units = String(spendable[asset.assetId] || '0');
const amount = unitsToScaledDecimal(units, asset.decimals);
const unitValue = parseScaledDecimal(asset[priceField] || asset.currentUnitValueEure);
const value = multiplyScaled(amount, unitValue);
total += value;
items.push({
asset_id: asset.assetId,
symbol: asset.symbol,
label: asset.label,
units,
amount: formatAssetUnits(units, asset.decimals),
value_eure: formatScaledDecimal(value),
unit_value_eure: formatScaledDecimal(unitValue),
valuation_source: asset.valuationSource,
price_id: asset.priceId,
observed_at: asset.observedAt,
});
}
return { total, items };
}
function buildValuedAssetInventoryDelta({
currentInventory,
baselineInventory,
valuationAssets,
}) {
const currentSpendable = currentInventory?.spendable || {};
const baselineSpendable = baselineInventory?.spendable || {};
return (valuationAssets || []).map((asset) => {
const currentUnits = BigInt(currentSpendable[asset.assetId] || '0');
const baselineUnits = BigInt(baselineSpendable[asset.assetId] || '0');
const deltaUnits = currentUnits - baselineUnits;
return {
asset_id: asset.assetId,
symbol: asset.symbol,
units: deltaUnits.toString(),
amount: formatAssetUnits(deltaUnits.toString(), asset.decimals),
};
});
}
function buildNetValuedAssetFlowView({ netValuedAssetUnits, valuationAssets }) {
return (valuationAssets || []).map((asset) => {
const units = netValuedAssetUnits?.get(asset.assetId) || 0n;
return {
asset_id: asset.assetId,
symbol: asset.symbol,
units: units.toString(),
amount: formatAssetUnits(units.toString(), asset.decimals),
};
});
}
function normalizeBtcAssets({ btcAsset, btcAssets = null }) { function normalizeBtcAssets({ btcAsset, btcAssets = null }) {
const assets = btcAssets?.length ? btcAssets : [btcAsset]; const assets = btcAssets?.length ? btcAssets : [btcAsset];
const byId = new Map(); const byId = new Map();
@ -300,6 +530,15 @@ function multiplyScaled(left, right) {
return (left * right) / VALUE_FACTOR; return (left * right) / VALUE_FACTOR;
} }
function divideScaled(left, right) {
if (right === 0n) throw new Error('cannot divide by zero scaled decimal');
return (left * VALUE_FACTOR) / right;
}
function divideScaledDecimalStrings(left, right) {
return formatScaledDecimal(divideScaled(parseScaledDecimal(left), parseScaledDecimal(right)));
}
function formatScaledDecimal(value) { function formatScaledDecimal(value) {
const negative = value < 0n; const negative = value < 0n;
const absolute = negative ? -value : value; const absolute = negative ? -value : value;

View file

@ -1,6 +1,7 @@
import { Pool } from 'pg'; import { Pool } from 'pg';
import { deriveIntentRequestOutcomeRecords } from '../core/intent-request-outcomes.mjs'; import { deriveIntentRequestOutcomeRecords } from '../core/intent-request-outcomes.mjs';
import { buildCashEquivalentValuationAssets } from '../core/portfolio-metrics.mjs';
import { deriveQuoteOutcomeRecords } from '../core/quote-outcomes.mjs'; import { deriveQuoteOutcomeRecords } from '../core/quote-outcomes.mjs';
import { import {
CURRENT_NBTC_ASSET_ID, CURRENT_NBTC_ASSET_ID,
@ -2003,11 +2004,17 @@ export async function loadPortfolioMetricInputs(pool, {
btcAsset = null, btcAsset = null,
btcAssets = null, btcAssets = null,
eureAsset = null, eureAsset = null,
trackedAssets = [],
} = {}) { } = {}) {
const effectiveBtcAssets = normalizeBtcAssets({ btcAsset, btcAssets }); const effectiveBtcAssets = normalizeBtcAssets({ btcAsset, btcAssets });
const [currentInventory, currentPrice, commandAggregate, resultAggregate] = await Promise.all([ const [currentInventory, currentPrice, currentPriceEvents, commandAggregate, resultAggregate] = await Promise.all([
loadLatestEventPayload(pool, 'intent_inventory_snapshots'), loadLatestEventPayload(pool, 'intent_inventory_snapshots'),
loadLatestEventPayload(pool, 'market_price_events'), loadLatestEventPayload(
pool,
'market_price_events',
"WHERE COALESCE(payload->>'eure_per_btc', '') <> '' ORDER BY COALESCE(observed_at, ingested_at) DESC LIMIT 1",
),
loadLatestMarketPricePayloadsByRoute(pool),
pool.query(` pool.query(`
SELECT SELECT
MIN(ingested_at) AS first_command_at, MIN(ingested_at) AS first_command_at,
@ -2023,11 +2030,20 @@ export async function loadPortfolioMetricInputs(pool, {
const firstCommandAt = commandAggregate.rows[0]?.first_command_at || null; const firstCommandAt = commandAggregate.rows[0]?.first_command_at || null;
const commandCount = Number(commandAggregate.rows[0]?.command_count || 0); const commandCount = Number(commandAggregate.rows[0]?.command_count || 0);
const resultCount = Number(resultAggregate.rows[0]?.result_count || 0); const resultCount = Number(resultAggregate.rows[0]?.result_count || 0);
const valuationAssets = buildCashEquivalentValuationAssets({
trackedAssets,
btcAsset: effectiveBtcAssets[0],
btcAssets: effectiveBtcAssets,
eureAsset,
priceEvents: currentPriceEvents.map((entry) => entry.payload),
});
if (!firstCommandAt) { if (!firstCommandAt) {
return { return {
currentInventory, currentInventory,
currentPrice, currentPrice,
currentPriceEvents,
valuationAssets,
baseline: null, baseline: null,
commandCount, commandCount,
resultCount, resultCount,
@ -2052,12 +2068,15 @@ export async function loadPortfolioMetricInputs(pool, {
btcAsset: effectiveBtcAssets[0], btcAsset: effectiveBtcAssets[0],
btcAssets: effectiveBtcAssets, btcAssets: effectiveBtcAssets,
eureAsset, eureAsset,
valuationAssets,
}) })
: []; : [];
return { return {
currentInventory, currentInventory,
currentPrice, currentPrice,
currentPriceEvents,
valuationAssets,
baseline: baselineInventory && baselinePrice ? { baseline: baselineInventory && baselinePrice ? {
anchor: 'latest_inventory_before_first_command', anchor: 'latest_inventory_before_first_command',
command_at: new Date(firstCommandAt).toISOString(), command_at: new Date(firstCommandAt).toISOString(),
@ -2070,6 +2089,28 @@ export async function loadPortfolioMetricInputs(pool, {
}; };
} }
async function loadLatestMarketPricePayloadsByRoute(pool) {
const result = await pool.query(`
SELECT DISTINCT ON (payload->>'price_route_id')
observed_at,
ingested_at,
payload
FROM market_price_events
WHERE COALESCE(payload->>'price_route_id', '') <> ''
ORDER BY payload->>'price_route_id', COALESCE(observed_at, ingested_at) DESC
`);
return result.rows.map((row) => ({
observed_at: toIsoTimestamp(row.observed_at),
ingested_at: toIsoTimestamp(row.ingested_at),
payload: {
...(row.payload || {}),
observed_at: row.payload?.observed_at || toIsoTimestamp(row.observed_at),
ingested_at: row.payload?.ingested_at || toIsoTimestamp(row.ingested_at),
},
}));
}
export async function upsertPortfolioMetric(pool, { export async function upsertPortfolioMetric(pool, {
metricId, metricId,
computedAt, computedAt,
@ -3081,6 +3122,7 @@ async function loadExternalAssetFlowsSince(pool, {
btcAsset, btcAsset,
btcAssets = null, btcAssets = null,
eureAsset, eureAsset,
valuationAssets = [],
} = {}) { } = {}) {
const effectiveBtcAssets = normalizeBtcAssets({ btcAsset, btcAssets }); const effectiveBtcAssets = normalizeBtcAssets({ btcAsset, btcAssets });
const [depositRows, withdrawalRows] = await Promise.all([ const [depositRows, withdrawalRows] = await Promise.all([
@ -3098,6 +3140,7 @@ async function loadExternalAssetFlowsSince(pool, {
btcAsset: effectiveBtcAssets[0], btcAsset: effectiveBtcAssets[0],
btcAssets: effectiveBtcAssets, btcAssets: effectiveBtcAssets,
eureAsset, eureAsset,
valuationAssets,
})); }));
} }
@ -3109,6 +3152,7 @@ async function loadExternalAssetFlowsSince(pool, {
btcAsset: effectiveBtcAssets[0], btcAsset: effectiveBtcAssets[0],
btcAssets: effectiveBtcAssets, btcAssets: effectiveBtcAssets,
eureAsset, eureAsset,
valuationAssets,
})); }));
} }
@ -3167,6 +3211,7 @@ async function normalizeExternalFlowRow(pool, {
btcAsset, btcAsset,
btcAssets = null, btcAssets = null,
eureAsset, eureAsset,
valuationAssets = [],
} = {}) { } = {}) {
const payload = row?.payload || {}; const payload = row?.payload || {};
const details = payload.details || {}; const details = payload.details || {};
@ -3177,12 +3222,16 @@ async function normalizeExternalFlowRow(pool, {
const effectiveAt = toIsoTimestamp(details.created_at || row.observed_at || row.ingested_at); const effectiveAt = toIsoTimestamp(details.created_at || row.observed_at || row.ingested_at);
const signedUnits = (sign * BigInt(amount)).toString(); const signedUnits = (sign * BigInt(amount)).toString();
let referencePriceAtFlowTime = null; let referencePriceAtFlowTime = null;
let referencePriceEurePerUnitAtFlowTime = null;
const effectiveBtcAssets = normalizeBtcAssets({ btcAsset, btcAssets }); const effectiveBtcAssets = normalizeBtcAssets({ btcAsset, btcAssets });
const flowBtcAsset = effectiveBtcAssets.find((asset) => asset.assetId === assetId); const flowBtcAsset = effectiveBtcAssets.find((asset) => asset.assetId === assetId);
const valuationAsset = valuationAssets.find((asset) => asset.assetId === assetId);
if (flowBtcAsset) { if (flowBtcAsset) {
const nearestPrice = await loadNearestPricePayload(pool, effectiveAt); const nearestPrice = await loadNearestPricePayload(pool, effectiveAt);
referencePriceAtFlowTime = nearestPrice?.payload?.eure_per_btc || null; referencePriceAtFlowTime = nearestPrice?.payload?.eure_per_btc || null;
} else if (valuationAsset) {
referencePriceEurePerUnitAtFlowTime = valuationAsset.currentUnitValueEure || null;
} else if (assetId !== eureAsset?.assetId) { } else if (assetId !== eureAsset?.assetId) {
return null; return null;
} }
@ -3199,6 +3248,7 @@ async function normalizeExternalFlowRow(pool, {
tx_hash: details.tx_hash || null, tx_hash: details.tx_hash || null,
withdrawal_hash: details.withdrawal_hash || null, withdrawal_hash: details.withdrawal_hash || null,
reference_price_eure_per_btc_at_flow_time: referencePriceAtFlowTime, reference_price_eure_per_btc_at_flow_time: referencePriceAtFlowTime,
reference_price_eure_per_unit_at_flow_time: referencePriceEurePerUnitAtFlowTime,
}; };
} }

View file

@ -8,3 +8,9 @@ test('history writer replays durable topics but joins the raw quote firehose liv
assert.match(source, /fromBeginning:\s*topic !== config\.kafkaTopicRawNearIntentsQuote/); assert.match(source, /fromBeginning:\s*topic !== config\.kafkaTopicRawNearIntentsQuote/);
assert.match(source, /Raw quote volume is a live firehose/); assert.match(source, /Raw quote volume is a live firehose/);
}); });
test('history writer passes tracked assets into portfolio valuation', () => {
assert.match(source, /trackedAssets:\s*tradingConfig\.trackedAssets/);
assert.match(source, /valuationAssets:\s*inputs\.valuationAssets \|\| \[\]/);
assert.match(source, /inputs\.valuationAssets[\s\S]+asset\.priceId/);
});

View file

@ -909,6 +909,75 @@ test('bootstrap balances exclude imported catalog assets that are not inventory-
); );
}); });
test('bootstrap balances reuse portfolio metric valuation for tracked USDC', () => {
const config = buildDualBtcConfig();
const usdcAsset = {
assetId: 'nep141:eth-0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.omft.near',
symbol: 'USDC',
label: 'USDC',
decimals: 6,
chain: 'eth:1',
enabledForInventory: true,
};
config.trackedAssets = [...config.trackedAssets, usdcAsset];
config.assetRegistry.set(usdcAsset.assetId, usdcAsset);
const bootstrap = buildDashboardBootstrap({
config,
portfolioMetric: {
computed_at: '2026-05-18T15:49:19.436Z',
baseline_anchor_at: '2026-04-02T18:10:43.569Z',
baseline_status: 'active',
payload: {
current_portfolio_value_eure: '344.233277302186441562',
current_inventory: {
valued_assets: [{
asset_id: usdcAsset.assetId,
amount: '366.39517',
value_eure: '314.632199818186441562',
valuation_source: 'btc_usdc_reference',
}],
},
},
},
inventorySnapshot: {
ingested_at: '2026-05-18T15:49:19.000Z',
payload: {
synced_at: '2026-05-18T15:49:19.000Z',
reconciliation_status: 'ok',
spendable: {
[config.tradingBtc.assetId]: '45186',
[config.tradingEure.assetId]: '0',
[usdcAsset.assetId]: '366395170',
},
pending_inbound: {},
pending_outbound: {},
},
},
marketPrice: {
payload: {
observed_at: '2026-05-18T15:49:19.000Z',
eure_per_btc: '65509.40000000',
},
},
recentQuotes: [],
submissionPage: { page: 1, page_size: 20, total: 0, total_pages: 1, items: [] },
submissionSummary: { total: 0 },
fundingObservations: [],
recentTradeDecisions: [],
recentExecuteTradeCommands: [],
recentExecutionResults: [],
recentAlertTransitions: [],
serviceSnapshots: [],
});
const usdcRow = bootstrap.funds.balances.items.find((item) => item.asset_id === usdcAsset.assetId);
assert.equal(usdcRow.spendable, '366.39517');
assert.equal(usdcRow.eur_value_eure, '314.632199818186441562');
assert.equal(usdcRow.eur_value_source, 'btc_usdc_reference');
assert.equal(bootstrap.status_bar.current_total_portfolio_value_eure, '344.233277302186441562');
});
test('bootstrap keeps no-trade counts without shipping non-rendered row payloads', () => { test('bootstrap keeps no-trade counts without shipping non-rendered row payloads', () => {
const config = buildConfig(); const config = buildConfig();
const bootstrap = buildDashboardBootstrap({ const bootstrap = buildDashboardBootstrap({

View file

@ -1,7 +1,11 @@
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { buildPortfolioMetricId, computePortfolioMetric } from '../src/core/portfolio-metrics.mjs'; import {
buildCashEquivalentValuationAssets,
buildPortfolioMetricId,
computePortfolioMetric,
} from '../src/core/portfolio-metrics.mjs';
const btcAsset = { const btcAsset = {
assetId: 'nep141:btc.omft.near', assetId: 'nep141:btc.omft.near',
@ -22,6 +26,12 @@ const eureAsset = {
decimals: 18, decimals: 18,
}; };
const usdcAsset = {
assetId: 'nep141:eth-0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.omft.near',
symbol: 'USDC',
decimals: 6,
};
test('portfolio metrics compute portfolio comparison and mark-to-market pnl from baseline funding inventory', () => { test('portfolio metrics compute portfolio comparison and mark-to-market pnl from baseline funding inventory', () => {
const metric = computePortfolioMetric({ const metric = computePortfolioMetric({
baseline: { baseline: {
@ -135,6 +145,57 @@ test('portfolio metrics value both tracked BTC wrappers as BTC-equivalent invent
]); ]);
}); });
test('portfolio metrics include tracked USDC valued from live BTC/USDC reference', () => {
const currentPrice = {
price_id: 'price-usdc-current',
observed_at: '2026-05-18T15:41:08.837Z',
eure_per_btc: '65495.20000000',
usdc_per_btc: '76316.32000000',
};
const valuationAssets = buildCashEquivalentValuationAssets({
trackedAssets: [nbtcAsset, btcAsset, eureAsset, usdcAsset],
btcAssets: [nbtcAsset, btcAsset],
eureAsset,
priceEvents: [currentPrice],
});
const metric = computePortfolioMetric({
baseline: null,
currentInventory: {
inventory_id: 'current-usdc',
synced_at: '2026-05-18T15:40:38.976Z',
spendable: {
'nep141:nbtc.bridge.near': '45186',
[usdcAsset.assetId]: '366395170',
[eureAsset.assetId]: '0',
},
},
currentPrice,
btcAsset: nbtcAsset,
btcAssets: [nbtcAsset, btcAsset],
eureAsset,
valuationAssets,
});
assert.equal(valuationAssets[0].currentUnitValueEure, '0.858206999498927621');
assert.equal(metric.current_btc_mark_value_eure, '29.594661072');
assert.equal(metric.current_valued_asset_value_eure, '314.442899476599500513');
assert.equal(metric.current_portfolio_value_eure, '344.037560548599500513');
assert.deepEqual(metric.current_inventory.valued_assets.map((asset) => ({
asset_id: asset.asset_id,
amount: asset.amount,
value_eure: asset.value_eure,
valuation_source: asset.valuation_source,
})), [
{
asset_id: usdcAsset.assetId,
amount: '366.39517',
value_eure: '314.442899476599500513',
valuation_source: 'btc_usdc_reference',
},
]);
});
test('portfolio metrics treat later deposits and withdrawals as external cash flows instead of PnL', () => { test('portfolio metrics treat later deposits and withdrawals as external cash flows instead of PnL', () => {
const metric = computePortfolioMetric({ const metric = computePortfolioMetric({
baseline: { baseline: {