unrip/src/core/portfolio-metrics.mjs
philipp ab078d976a
Some checks failed
deploy / deploy (push) Failing after 37s
Track nBTC reserve and legacy BTC asset
Proof: npm test (138 passing); npm run operator-dashboard:build; git diff --cached --check.

Assumptions: Bridge deposits expose near_token_id/intents_token_id for credited asset attribution; nBTC is the solver trading reserve while btc.omft.near remains a tracked legacy BTC wrapper.

Still fake: No live asset migration was submitted; existing btc.omft.near balance is only tracked and withdrawable until a separately approved conversion or withdrawal is executed.
2026-05-07 16:06:26 +02:00

318 lines
12 KiB
JavaScript

const VALUE_SCALE = 18;
const VALUE_FACTOR = 10n ** BigInt(VALUE_SCALE);
export function computePortfolioMetric({
baseline = null,
currentInventory,
currentPrice,
externalFlows = [],
btcAsset,
btcAssets = null,
eureAsset,
commandCount = 0,
resultCount = 0,
} = {}) {
const effectiveBtcAssets = normalizeBtcAssets({ btcAsset, btcAssets });
const displayBtcAsset = effectiveBtcAssets[0];
if (!currentInventory || !currentPrice || !displayBtcAsset?.assetId || !eureAsset?.assetId) {
return null;
}
const currentBtcUnits = sumAssetUnits(currentInventory, effectiveBtcAssets).toString();
const currentEureUnits = String(currentInventory.spendable?.[eureAsset.assetId] || '0');
const currentBtc = sumAssetScaled(currentInventory, effectiveBtcAssets);
const currentEure = unitsToScaledDecimal(currentEureUnits, eureAsset.decimals);
const currentPriceScaled = parseScaledDecimal(currentPrice.eure_per_btc);
const currentBtcMarkValue = multiplyScaled(currentBtc, currentPriceScaled);
const currentPortfolioValue = currentEure + currentBtcMarkValue;
const payload = {
metric_version: 2,
baseline_status: baseline ? 'active' : 'awaiting_first_execution',
command_count: commandCount,
result_count: resultCount,
current_price: {
price_id: currentPrice.price_id || null,
observed_at: currentPrice.observed_at || null,
eure_per_btc: String(currentPrice.eure_per_btc),
},
current_inventory: buildInventoryView({
inventory: currentInventory,
btcAsset: displayBtcAsset,
btcAssets: effectiveBtcAssets,
eureAsset,
}),
current_portfolio_value_eure: formatScaledDecimal(currentPortfolioValue),
current_btc_mark_value_eure: formatScaledDecimal(currentBtcMarkValue),
current_eure_cash_value_eure: formatScaledDecimal(currentEure),
portfolio_vs_simple_hold_eure: null,
trade_pnl_eure: null,
mark_to_market_pnl_eure: null,
price_move_pnl_eure: null,
baseline_portfolio_value_eure_at_baseline_price: null,
baseline_portfolio_value_eure_at_current_price: null,
current_portfolio_value_eure_at_baseline_price: null,
initial_baseline_portfolio_value_eure_at_baseline_price: null,
initial_baseline_portfolio_value_eure_at_current_price: null,
external_cash_flows: {
flow_count: 0,
deposit_count: 0,
withdrawal_count: 0,
latest_effective_at: null,
net_btc_units: '0',
net_btc: '0',
net_eure_units: '0',
net_eure: '0',
net_value_eure_at_flow_time: '0',
net_value_eure_at_current_price: '0',
},
inventory_delta: null,
baseline: null,
};
if (!baseline?.inventory || !baseline?.price) {
return payload;
}
const baselineBtcUnits = sumAssetUnits(baseline.inventory, effectiveBtcAssets).toString();
const baselineEureUnits = String(baseline.inventory.spendable?.[eureAsset.assetId] || '0');
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 externalFlowSummary = summarizeExternalFlows({
externalFlows,
currentPriceScaled,
btcAsset: displayBtcAsset,
btcAssets: effectiveBtcAssets,
eureAsset,
});
const fundedPortfolioAtFlowTime = baselinePortfolioAtBaselinePrice
+ externalFlowSummary.netValueEureAtFlowTime;
const simpleHoldAtCurrentPrice = baselinePortfolioAtCurrentPrice
+ externalFlowSummary.netValueEureAtCurrentPrice;
const portfolioVsSimpleHold = currentPortfolioValue - simpleHoldAtCurrentPrice;
const markToMarketPnl = currentPortfolioValue - fundedPortfolioAtFlowTime;
const priceMovePnl = simpleHoldAtCurrentPrice - fundedPortfolioAtFlowTime;
payload.portfolio_vs_simple_hold_eure = formatScaledDecimal(portfolioVsSimpleHold);
payload.trade_pnl_eure = null;
payload.mark_to_market_pnl_eure = formatScaledDecimal(markToMarketPnl);
payload.price_move_pnl_eure = formatScaledDecimal(priceMovePnl);
payload.baseline_portfolio_value_eure_at_baseline_price = formatScaledDecimal(
fundedPortfolioAtFlowTime,
);
payload.baseline_portfolio_value_eure_at_current_price = formatScaledDecimal(
simpleHoldAtCurrentPrice,
);
payload.current_portfolio_value_eure_at_baseline_price = formatScaledDecimal(
currentPortfolioAtBaselinePrice,
);
payload.initial_baseline_portfolio_value_eure_at_baseline_price = formatScaledDecimal(
baselinePortfolioAtBaselinePrice,
);
payload.initial_baseline_portfolio_value_eure_at_current_price = formatScaledDecimal(
baselinePortfolioAtCurrentPrice,
);
payload.external_cash_flows = {
flow_count: externalFlowSummary.flowCount,
deposit_count: externalFlowSummary.depositCount,
withdrawal_count: externalFlowSummary.withdrawalCount,
latest_effective_at: externalFlowSummary.latestEffectiveAt,
net_btc_units: externalFlowSummary.netBtcUnits.toString(),
net_btc: formatScaledDecimal(unitsToScaledDecimal(
externalFlowSummary.netBtcUnits.toString(),
displayBtcAsset.decimals,
)),
net_eure_units: externalFlowSummary.netEureUnits.toString(),
net_eure: formatScaledDecimal(unitsToScaledDecimal(
externalFlowSummary.netEureUnits.toString(),
eureAsset.decimals,
)),
net_value_eure_at_flow_time: formatScaledDecimal(externalFlowSummary.netValueEureAtFlowTime),
net_value_eure_at_current_price: formatScaledDecimal(externalFlowSummary.netValueEureAtCurrentPrice),
};
payload.inventory_delta = {
btc_units: (BigInt(currentBtcUnits) - BigInt(baselineBtcUnits)).toString(),
btc: formatScaledDecimal(currentBtc - baselineBtc),
eure_units: (BigInt(currentEureUnits) - BigInt(baselineEureUnits)).toString(),
eure: formatScaledDecimal(currentEure - baselineEure),
};
payload.baseline = {
anchor: baseline.anchor || 'latest_inventory_before_first_command',
command_at: baseline.command_at || null,
price: {
price_id: baseline.price.price_id || null,
observed_at: baseline.price.observed_at || null,
eure_per_btc: String(baseline.price.eure_per_btc),
},
inventory: buildInventoryView({
inventory: baseline.inventory,
btcAsset: displayBtcAsset,
btcAssets: effectiveBtcAssets,
eureAsset,
}),
};
return payload;
}
export function buildPortfolioMetricId({ baselineInventoryId, currentInventoryId, currentPriceId }) {
return [
'portfolio-metric',
baselineInventoryId || 'no-baseline',
currentInventoryId || 'no-current-inventory',
currentPriceId || 'no-current-price',
].join(':');
}
function buildInventoryView({ inventory, btcAsset, btcAssets = null, eureAsset }) {
const effectiveBtcAssets = normalizeBtcAssets({ btcAsset, btcAssets });
const spendable = inventory?.spendable || {};
const btcUnits = sumAssetUnits(inventory, effectiveBtcAssets).toString();
const eureUnits = String(spendable[eureAsset.assetId] || '0');
return {
inventory_id: inventory?.inventory_id || null,
synced_at: inventory?.synced_at || null,
btc_units: btcUnits,
btc: formatAssetUnits(btcUnits, btcAsset.decimals),
btc_assets: effectiveBtcAssets.map((asset) => {
const units = String(spendable[asset.assetId] || '0');
return {
asset_id: asset.assetId,
label: asset.label || asset.symbol,
units,
amount: formatAssetUnits(units, asset.decimals),
};
}),
eure_units: eureUnits,
eure: formatAssetUnits(eureUnits, eureAsset.decimals),
};
}
function summarizeExternalFlows({
externalFlows,
currentPriceScaled,
btcAsset,
btcAssets = null,
eureAsset,
}) {
const effectiveBtcAssets = normalizeBtcAssets({ btcAsset, btcAssets });
let flowCount = 0;
let depositCount = 0;
let withdrawalCount = 0;
let latestEffectiveAt = null;
let netBtcUnits = 0n;
let netEureUnits = 0n;
let netValueEureAtFlowTime = 0n;
let netValueEureAtCurrentPrice = 0n;
for (const flow of externalFlows || []) {
if (!flow?.asset_id || !flow?.signed_units) continue;
const signedUnits = BigInt(flow.signed_units);
if (signedUnits === 0n) continue;
flowCount += 1;
if (flow.kind === 'deposit') depositCount += 1;
if (flow.kind === 'withdrawal') withdrawalCount += 1;
if (timestampValue(flow.effective_at) > timestampValue(latestEffectiveAt)) {
latestEffectiveAt = flow.effective_at || null;
}
const flowBtcAsset = effectiveBtcAssets.find((asset) => asset.assetId === flow.asset_id);
if (flowBtcAsset) {
netBtcUnits += signedUnits;
const btcAmount = unitsToScaledDecimal(signedUnits.toString(), flowBtcAsset.decimals);
const flowPriceScaled = parseScaledDecimal(
flow.reference_price_eure_per_btc_at_flow_time || '0',
);
netValueEureAtFlowTime += multiplyScaled(btcAmount, flowPriceScaled);
netValueEureAtCurrentPrice += multiplyScaled(btcAmount, currentPriceScaled);
} else if (flow.asset_id === eureAsset.assetId) {
netEureUnits += signedUnits;
const eureAmount = unitsToScaledDecimal(signedUnits.toString(), eureAsset.decimals);
netValueEureAtFlowTime += eureAmount;
netValueEureAtCurrentPrice += eureAmount;
}
}
return {
flowCount,
depositCount,
withdrawalCount,
latestEffectiveAt,
netBtcUnits,
netEureUnits,
netValueEureAtFlowTime,
netValueEureAtCurrentPrice,
};
}
function normalizeBtcAssets({ btcAsset, btcAssets = null }) {
const assets = btcAssets?.length ? btcAssets : [btcAsset];
const byId = new Map();
for (const asset of assets) {
if (!asset?.assetId) continue;
byId.set(asset.assetId, asset);
}
return [...byId.values()];
}
function sumAssetUnits(inventory, assets) {
const spendable = inventory?.spendable || {};
return assets.reduce(
(total, asset) => total + BigInt(spendable[asset.assetId] || '0'),
0n,
);
}
function sumAssetScaled(inventory, assets) {
const spendable = inventory?.spendable || {};
return assets.reduce((total, asset) => (
total + unitsToScaledDecimal(String(spendable[asset.assetId] || '0'), asset.decimals)
), 0n);
}
function unitsToScaledDecimal(units, decimals) {
return BigInt(units || '0') * 10n ** BigInt(VALUE_SCALE - decimals);
}
function formatAssetUnits(units, decimals) {
return formatScaledDecimal(BigInt(units || '0') * 10n ** BigInt(VALUE_SCALE - decimals));
}
function parseScaledDecimal(value) {
const normalized = String(value ?? '0').trim();
const negative = normalized.startsWith('-');
const unsigned = normalized.replace(/^[+-]/, '');
const [wholePart, fractionalPart = ''] = unsigned.split('.');
const whole = BigInt(wholePart || '0');
const fractional = BigInt((fractionalPart.padEnd(VALUE_SCALE, '0')).slice(0, VALUE_SCALE) || '0');
const scaled = (whole * VALUE_FACTOR) + fractional;
return negative ? -scaled : scaled;
}
function multiplyScaled(left, right) {
return (left * right) / VALUE_FACTOR;
}
function formatScaledDecimal(value) {
const negative = value < 0n;
const absolute = negative ? -value : value;
const whole = absolute / VALUE_FACTOR;
const fractional = absolute % VALUE_FACTOR;
if (fractional === 0n) {
return `${negative ? '-' : ''}${whole}`;
}
const fractionalText = fractional.toString().padStart(VALUE_SCALE, '0').replace(/0+$/, '');
return `${negative ? '-' : ''}${whole}.${fractionalText}`;
}
function timestampValue(value) {
const parsed = Date.parse(value || '');
return Number.isFinite(parsed) ? parsed : -Infinity;
}