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; }