Files
AtreidesAssetsFree/server/mcp/wealth-tool.mjs
T
2026-10-29 11:57:29 +01:00

165 lines
7.1 KiB
JavaScript

// Generated, do not edit. sha256:a35de9757194e9058e50a8d62faae8adcfd1b35f42f9b9b14cece1c6cab04e85
import { accountHoldings, positionsByAsset } from '../data/positions.js';
import { listAccounts } from '../data/accounts.js';
import { isOwnWealth } from '../../shared/wealth-scope.js';
import { latestPricePoint } from '../data/price-history.js';
import { assertScope } from '../ai/gate.js';
import { limitedList } from '../ai/limits.js';
import { shareBucket, shareLabel } from '../ai/buckets.js';
import { shareDisclosure } from '../ai/precision.js';
import { vertalerVoor } from './session.mjs';
import { assetMetrics } from '../../shared/wealth-metrics.js';
import { isMetal, metalFactor } from '../../shared/units.js';
import { getBaseCurrency } from '../data/portfolio.js';
function verdelingsblok(rijen, totaal, t) {
if (!(totaal > 0)) return [];
const vorm = shareDisclosure(t.precision);
if (vorm === 'omit') {
return [
'De verdeling over de assets komt niet mee: met een exact totaal én exacte percentages ligt de '
+ 'waarde per asset vast, en de bedragen per regel zijn juist wél in marges. Zet één van beide '
+ 'terug op marges (of zet onbewerkt delen aan) om hem te zien.',
'',
];
}
const pctUit = pct => (vorm === 'exact' ? `${pct.toFixed(1)}%` : shareLabel(pct));
const posten = [];
let overigPct = 0;
let overigN = 0;
let zonderKoers = 0;
for (const { p, m } of rijen) {
if (m.value == null) { zonderKoers += 1; continue; }
const pct = (m.value / totaal) * 100;
const b = shareBucket(pct);
if (!b || b.below) { overigPct += pct; overigN += 1; continue; }
posten.push({ symbool: t.toAi(p, { entity: 'asset' }).symbol, pct });
}
if (!posten.length && !overigN) return [];
posten.sort((a, b) => b.pct - a.pct);
const regels = limitedList(posten, x => `- ${x.symbool}: ${pctUit(x.pct)}`).lines;
if (overigN) {
regels.push(`- overig (${overigN} ${overigN === 1 ? 'asset' : 'assets'}): ${pctUit(overigPct)}`);
}
return [
vorm === 'exact'
? 'Verdeling over de portefeuille (samen dekken ze het geheel):'
: 'Verdeling over de portefeuille (marges; samen dekken ze het geheel):',
...regels,
...(zonderKoers ? [`(${zonderKoers} zonder koers, dus niet in deze verdeling.)`] : []),
'',
];
}
function buitenTotalen(decimalen) {
const buiten = listAccounts().filter(a => !isOwnWealth(a));
if (!buiten.length) return [];
const soort = new Map(buiten.map(a => [a.id, a.wealthScope]));
const som = new Map([...new Set(buiten.map(a => a.wealthScope))].map(s => [s, { waarde: 0, zonderKoers: 0 }]));
for (const r of accountHoldings(buiten.map(a => a.id))) {
const punt = latestPricePoint(r.assetId);
const post = som.get(soort.get(r.accountId));
if (!punt || !decimalen.has(r.assetId)) { post.zonderKoers += 1; continue; }
post.waarde += (r.saldo / Math.pow(10, decimalen.get(r.assetId))) * punt.fiatPrice;
}
return ['managed', 'none'].filter(s => som.has(s)).map(s => ({ soort: s, ...som.get(s) }));
}
const BUITEN_LABEL = { managed: 'vermogen van derden', none: 'telt niet mee' };
const wealthSummary = Object.freeze({
name: 'wealth_summary',
title: 'Vermogen in cijfers',
description:
'Geeft wat de portefeuille van AssetApp nu waard is: het totaal, en per asset de hoeveelheid, '
+ 'koers, waarde, inleg en het ongerealiseerde resultaat. Staat onbewerkt delen uit, dan komen '
+ 'het totaal en de verdeling per asset terug als marges, zonder bedrag per asset. De koers is '
+ 'het laatst opgeslagen punt en geen verse marktkoers. Wijzigt niets.',
inputSchema: Object.freeze({ type: 'object', properties: {}, additionalProperties: false }),
scope: 'read',
async run(_args, context) {
assertScope(wealthSummary.scope);
const t = await vertalerVoor(context);
t.beginCall();
const onbewerkt = t.precision.rawValues;
const posities = positionsByAsset(undefined, { ownOnly: true });
const cur = getBaseCurrency();
const money = n => `${cur} ${n.toFixed(2)}`;
const totaalTekst = n => {
const uit = t.toAi({ fiatValue: n }, { entity: 'wealth', role: 'total' }).fiatValue;
return typeof uit === 'number' ? money(uit) : `${cur} ${uit}`;
};
const buiten = buitenTotalen(new Map(posities.map(p => [p.id, p.decimals])));
const buitenRegel = !buiten.length ? [] : [
`Niet in dit totaal (Vermogen op het account): ${buiten.map(b => `${BUITEN_LABEL[b.soort]} ${totaalTekst(b.waarde)}`
+ (b.zonderKoers ? ` (${b.zonderKoers} zonder koers)` : '')).join('; ')}.`,
'',
];
const buitenVelden = buiten.length ? ['wealthScope'] : [];
let totaal = 0;
let zonderKoers = 0;
const rijen = [];
for (const p of posities) {
if (p.net === 0 && p.buy_events === 0) continue;
const punt = latestPricePoint(p.id);
const m = assetMetrics(p, punt ? punt.fiatPrice : null);
if (m.value != null) totaal += m.value; else zonderKoers += 1;
rijen.push({ p, m, punt });
}
if (!rijen.length) {
if (buiten.length) {
return { text: ['Deze portefeuille heeft geen eigen bezit.', '', ...buitenRegel].join('\n').trim(),
fieldsShared: ['baseCurrency', 'fiatValue', ...buitenVelden] };
}
return { text: 'Deze portefeuille heeft nog geen bezit: er is niets om een waarde van te geven.',
fieldsShared: [] };
}
const kop = `Totale waarde nu: ${totaalTekst(totaal)}`
+ ` over ${rijen.length} ${rijen.length === 1 ? 'asset' : 'assets'}`
+ (zonderKoers ? `, waarvan ${zonderKoers} zonder koers en dus zonder waarde` : '')
+ '.';
if (!onbewerkt) {
return {
text: [kop, '', ...buitenRegel, ...verdelingsblok(rijen, totaal, t),
'Per asset komen de BEDRAGEN niet mee zolang onbewerkt delen uit staat: een waarde per asset '
+ 'met de koers erbij legt de hoeveelheid vast. Zet die schuif aan voor een portefeuille '
+ 'zonder echte gegevens.',
].join('\n'),
fieldsShared: ['baseCurrency', 'fiatValue', 'symbol', ...buitenVelden],
};
}
const regels = limitedList(rijen, ({ p, m, punt }) => {
const v = t.toAi(p, { entity: 'asset' });
const f = isMetal(p) ? metalFactor(p.unit) : 1;
const stuk = [`${v.symbol}: ${+(m.units / f).toFixed(8)} ${v.unit}`];
if (m.value != null) stuk.push(`waarde ${money(m.value)} tegen ${money(punt.fiatPrice * f)} per ${v.unit}`);
else stuk.push('geen koers, dus geen waarde');
stuk.push(`ingelegd ${money(m.invested)}`);
if (m.unrealized != null) stuk.push(`ongerealiseerd ${money(m.unrealized)}`);
if (m.avgPrice != null) stuk.push(`gemiddelde aankoopkoers ${money(m.avgPrice * f)}`);
return `- ${stuk.join(', ')}`;
}).lines;
return {
text: [kop, '', ...buitenRegel, 'Per asset:', ...regels, '',
'De koers is het laatst opgeslagen koerspunt; dit proces haalt geen verse koersen op.',
].join('\n'),
fieldsShared: ['baseCurrency', 'symbol', 'unit', 'decimals', 'amount', 'balance', 'fiatPrice', 'fiatFee',
'fiatValue', ...buitenVelden],
};
},
});
export const WEALTH_TOOLS = Object.freeze([wealthSummary]);