Some checks failed
deploy / deploy (push) Failing after 37s
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.
318 lines
12 KiB
JavaScript
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;
|
|
}
|