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,
btcAssets: tradingConfig.tradingBtcAssets,
eureAsset: tradingConfig.tradingEure,
trackedAssets: tradingConfig.trackedAssets,
});
const payload = computePortfolioMetric({
baseline: inputs.baseline,
@ -393,6 +394,7 @@ async function refreshPortfolioMetrics() {
btcAsset: tradingConfig.tradingBtc,
btcAssets: tradingConfig.tradingBtcAssets,
eureAsset: tradingConfig.tradingEure,
valuationAssets: inputs.valuationAssets || [],
commandCount: inputs.commandCount,
resultCount: inputs.resultCount,
});
@ -402,7 +404,10 @@ async function refreshPortfolioMetrics() {
const metricId = buildPortfolioMetricId({
baselineInventoryId: inputs.baseline?.inventory?.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, {
metricId,

View file

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

View file

@ -9,6 +9,7 @@ export function computePortfolioMetric({
btcAsset,
btcAssets = null,
eureAsset,
valuationAssets = [],
commandCount = 0,
resultCount = 0,
} = {}) {
@ -24,10 +25,20 @@ export function computePortfolioMetric({
const currentEure = unitsToScaledDecimal(currentEureUnits, eureAsset.decimals);
const currentPriceScaled = parseScaledDecimal(currentPrice.eure_per_btc);
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 = {
metric_version: 2,
metric_version: 3,
baseline_status: baseline ? 'active' : 'awaiting_first_execution',
command_count: commandCount,
result_count: resultCount,
@ -41,10 +52,12 @@ export function computePortfolioMetric({
btcAsset: displayBtcAsset,
btcAssets: effectiveBtcAssets,
eureAsset,
valuationAssets: effectiveValuationAssets,
}),
current_portfolio_value_eure: formatScaledDecimal(currentPortfolioValue),
current_btc_mark_value_eure: formatScaledDecimal(currentBtcMarkValue),
current_eure_cash_value_eure: formatScaledDecimal(currentEure),
current_valued_asset_value_eure: formatScaledDecimal(currentValuedAssets.total),
portfolio_vs_simple_hold_eure: null,
trade_pnl_eure: null,
mark_to_market_pnl_eure: null,
@ -63,6 +76,7 @@ export function computePortfolioMetric({
net_btc: '0',
net_eure_units: '0',
net_eure: '0',
net_valued_assets: [],
net_value_eure_at_flow_time: '0',
net_value_eure_at_current_price: '0',
},
@ -79,15 +93,37 @@ export function computePortfolioMetric({
const baselineBtc = sumAssetScaled(baseline.inventory, effectiveBtcAssets);
const baselineEure = unitsToScaledDecimal(baselineEureUnits, eureAsset.decimals);
const baselinePriceScaled = parseScaledDecimal(baseline.price.eure_per_btc);
const baselinePortfolioAtBaselinePrice = baselineEure + multiplyScaled(baselineBtc, baselinePriceScaled);
const baselinePortfolioAtCurrentPrice = baselineEure + multiplyScaled(baselineBtc, currentPriceScaled);
const currentPortfolioAtBaselinePrice = currentEure + multiplyScaled(currentBtc, baselinePriceScaled);
const baselineValuedAssetsAtBaselinePrice = valueValuationAssets({
inventory: baseline.inventory,
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({
externalFlows,
currentPriceScaled,
btcAsset: displayBtcAsset,
btcAssets: effectiveBtcAssets,
eureAsset,
valuationAssets: effectiveValuationAssets,
});
const fundedPortfolioAtFlowTime = baselinePortfolioAtBaselinePrice
+ externalFlowSummary.netValueEureAtFlowTime;
@ -131,6 +167,10 @@ export function computePortfolioMetric({
externalFlowSummary.netEureUnits.toString(),
eureAsset.decimals,
)),
net_valued_assets: buildNetValuedAssetFlowView({
netValuedAssetUnits: externalFlowSummary.netValuedAssetUnits,
valuationAssets: effectiveValuationAssets,
}),
net_value_eure_at_flow_time: formatScaledDecimal(externalFlowSummary.netValueEureAtFlowTime),
net_value_eure_at_current_price: formatScaledDecimal(externalFlowSummary.netValueEureAtCurrentPrice),
};
@ -140,6 +180,13 @@ export function computePortfolioMetric({
eure_units: (BigInt(currentEureUnits) - BigInt(baselineEureUnits)).toString(),
eure: formatScaledDecimal(currentEure - baselineEure),
};
if (effectiveValuationAssets.length) {
payload.inventory_delta.valued_assets = buildValuedAssetInventoryDelta({
currentInventory,
baselineInventory: baseline.inventory,
valuationAssets: effectiveValuationAssets,
});
}
payload.baseline = {
anchor: baseline.anchor || 'latest_inventory_before_first_command',
command_at: baseline.command_at || null,
@ -153,12 +200,49 @@ export function computePortfolioMetric({
btcAsset: displayBtcAsset,
btcAssets: effectiveBtcAssets,
eureAsset,
valuationAssets: effectiveValuationAssets,
}),
};
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 }) {
return [
'portfolio-metric',
@ -168,11 +252,27 @@ export function buildPortfolioMetricId({ baselineInventoryId, currentInventoryId
].join(':');
}
function buildInventoryView({ inventory, btcAsset, btcAssets = null, eureAsset }) {
function buildInventoryView({
inventory,
btcAsset,
btcAssets = null,
eureAsset,
valuationAssets = [],
}) {
const effectiveBtcAssets = normalizeBtcAssets({ btcAsset, btcAssets });
const effectiveValuationAssets = normalizeValuationAssets({
valuationAssets,
btcAssets: effectiveBtcAssets,
eureAsset,
});
const spendable = inventory?.spendable || {};
const btcUnits = sumAssetUnits(inventory, effectiveBtcAssets).toString();
const eureUnits = String(spendable[eureAsset.assetId] || '0');
const valuedAssets = valueValuationAssets({
inventory,
valuationAssets: effectiveValuationAssets,
priceField: 'currentUnitValueEure',
});
return {
inventory_id: inventory?.inventory_id || null,
@ -190,6 +290,8 @@ function buildInventoryView({ inventory, btcAsset, btcAssets = null, eureAsset }
}),
eure_units: eureUnits,
eure: formatAssetUnits(eureUnits, eureAsset.decimals),
valued_assets: valuedAssets.items,
valued_assets_value_eure: formatScaledDecimal(valuedAssets.total),
};
}
@ -199,14 +301,21 @@ function summarizeExternalFlows({
btcAsset,
btcAssets = null,
eureAsset,
valuationAssets = [],
}) {
const effectiveBtcAssets = normalizeBtcAssets({ btcAsset, btcAssets });
const effectiveValuationAssets = normalizeValuationAssets({
valuationAssets,
btcAssets: effectiveBtcAssets,
eureAsset,
});
let flowCount = 0;
let depositCount = 0;
let withdrawalCount = 0;
let latestEffectiveAt = null;
let netBtcUnits = 0n;
let netEureUnits = 0n;
const netValuedAssetUnits = new Map();
let netValueEureAtFlowTime = 0n;
let netValueEureAtCurrentPrice = 0n;
@ -237,6 +346,24 @@ function summarizeExternalFlows({
const eureAmount = unitsToScaledDecimal(signedUnits.toString(), eureAsset.decimals);
netValueEureAtFlowTime += 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,
netBtcUnits,
netEureUnits,
netValuedAssetUnits,
netValueEureAtFlowTime,
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 }) {
const assets = btcAssets?.length ? btcAssets : [btcAsset];
const byId = new Map();
@ -300,6 +530,15 @@ function multiplyScaled(left, right) {
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) {
const negative = value < 0n;
const absolute = negative ? -value : value;

View file

@ -1,6 +1,7 @@
import { Pool } from 'pg';
import { deriveIntentRequestOutcomeRecords } from '../core/intent-request-outcomes.mjs';
import { buildCashEquivalentValuationAssets } from '../core/portfolio-metrics.mjs';
import { deriveQuoteOutcomeRecords } from '../core/quote-outcomes.mjs';
import {
CURRENT_NBTC_ASSET_ID,
@ -2003,11 +2004,17 @@ export async function loadPortfolioMetricInputs(pool, {
btcAsset = null,
btcAssets = null,
eureAsset = null,
trackedAssets = [],
} = {}) {
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, '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(`
SELECT
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 commandCount = Number(commandAggregate.rows[0]?.command_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) {
return {
currentInventory,
currentPrice,
currentPriceEvents,
valuationAssets,
baseline: null,
commandCount,
resultCount,
@ -2052,12 +2068,15 @@ export async function loadPortfolioMetricInputs(pool, {
btcAsset: effectiveBtcAssets[0],
btcAssets: effectiveBtcAssets,
eureAsset,
valuationAssets,
})
: [];
return {
currentInventory,
currentPrice,
currentPriceEvents,
valuationAssets,
baseline: baselineInventory && baselinePrice ? {
anchor: 'latest_inventory_before_first_command',
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, {
metricId,
computedAt,
@ -3081,6 +3122,7 @@ async function loadExternalAssetFlowsSince(pool, {
btcAsset,
btcAssets = null,
eureAsset,
valuationAssets = [],
} = {}) {
const effectiveBtcAssets = normalizeBtcAssets({ btcAsset, btcAssets });
const [depositRows, withdrawalRows] = await Promise.all([
@ -3098,6 +3140,7 @@ async function loadExternalAssetFlowsSince(pool, {
btcAsset: effectiveBtcAssets[0],
btcAssets: effectiveBtcAssets,
eureAsset,
valuationAssets,
}));
}
@ -3109,6 +3152,7 @@ async function loadExternalAssetFlowsSince(pool, {
btcAsset: effectiveBtcAssets[0],
btcAssets: effectiveBtcAssets,
eureAsset,
valuationAssets,
}));
}
@ -3167,6 +3211,7 @@ async function normalizeExternalFlowRow(pool, {
btcAsset,
btcAssets = null,
eureAsset,
valuationAssets = [],
} = {}) {
const payload = row?.payload || {};
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 signedUnits = (sign * BigInt(amount)).toString();
let referencePriceAtFlowTime = null;
let referencePriceEurePerUnitAtFlowTime = null;
const effectiveBtcAssets = normalizeBtcAssets({ btcAsset, btcAssets });
const flowBtcAsset = effectiveBtcAssets.find((asset) => asset.assetId === assetId);
const valuationAsset = valuationAssets.find((asset) => asset.assetId === assetId);
if (flowBtcAsset) {
const nearestPrice = await loadNearestPricePayload(pool, effectiveAt);
referencePriceAtFlowTime = nearestPrice?.payload?.eure_per_btc || null;
} else if (valuationAsset) {
referencePriceEurePerUnitAtFlowTime = valuationAsset.currentUnitValueEure || null;
} else if (assetId !== eureAsset?.assetId) {
return null;
}
@ -3199,6 +3248,7 @@ async function normalizeExternalFlowRow(pool, {
tx_hash: details.tx_hash || null,
withdrawal_hash: details.withdrawal_hash || null,
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, /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', () => {
const config = buildConfig();
const bootstrap = buildDashboardBootstrap({

View file

@ -1,7 +1,11 @@
import test from 'node:test';
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 = {
assetId: 'nep141:btc.omft.near',
@ -22,6 +26,12 @@ const eureAsset = {
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', () => {
const metric = computePortfolioMetric({
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', () => {
const metric = computePortfolioMetric({
baseline: {