const VALUE_SCALE = 18; const VALUE_FACTOR = 10n ** BigInt(VALUE_SCALE); export function computePortfolioMetric({ baseline = null, currentInventory, currentPrice, btcAsset, eureAsset, commandCount = 0, resultCount = 0, } = {}) { if (!currentInventory || !currentPrice || !btcAsset?.assetId || !eureAsset?.assetId) { return null; } const currentBtcUnits = String(currentInventory.spendable?.[btcAsset.assetId] || '0'); const currentEureUnits = String(currentInventory.spendable?.[eureAsset.assetId] || '0'); const currentBtc = unitsToScaledDecimal(currentBtcUnits, btcAsset.decimals); 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: 1, 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, eureAsset, }), current_portfolio_value_eure: formatScaledDecimal(currentPortfolioValue), current_btc_mark_value_eure: formatScaledDecimal(currentBtcMarkValue), current_eure_cash_value_eure: formatScaledDecimal(currentEure), 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, inventory_delta: null, baseline: null, }; if (!baseline?.inventory || !baseline?.price) { return payload; } const baselineBtcUnits = String(baseline.inventory.spendable?.[btcAsset.assetId] || '0'); const baselineEureUnits = String(baseline.inventory.spendable?.[eureAsset.assetId] || '0'); const baselineBtc = unitsToScaledDecimal(baselineBtcUnits, btcAsset.decimals); 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 tradePnl = currentPortfolioAtBaselinePrice - baselinePortfolioAtBaselinePrice; const markToMarketPnl = currentPortfolioValue - baselinePortfolioAtCurrentPrice; const priceMovePnl = markToMarketPnl - tradePnl; payload.trade_pnl_eure = formatScaledDecimal(tradePnl); payload.mark_to_market_pnl_eure = formatScaledDecimal(markToMarketPnl); payload.price_move_pnl_eure = formatScaledDecimal(priceMovePnl); payload.baseline_portfolio_value_eure_at_baseline_price = formatScaledDecimal( baselinePortfolioAtBaselinePrice, ); payload.baseline_portfolio_value_eure_at_current_price = formatScaledDecimal( baselinePortfolioAtCurrentPrice, ); payload.current_portfolio_value_eure_at_baseline_price = formatScaledDecimal( currentPortfolioAtBaselinePrice, ); 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, 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, eureAsset }) { const spendable = inventory?.spendable || {}; const btcUnits = String(spendable[btcAsset.assetId] || '0'); 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), eure_units: eureUnits, eure: formatAssetUnits(eureUnits, eureAsset.decimals), }; } 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}`; }