// Generated, do not edit. sha256:d62e710a5bda41c8a7b1a7ff304738dfd9331056f2936a826970a6092995f68b import { CONFIG, SOURCE_CAPABILITIES } from '../../shared/config.js'; import { listAssets } from '../data/assets.js'; import { listPriceHistory, listPriceGaps, listIgnoredPriceGaps } from '../data/price-history.js'; import { listProviders, getProvider } from '../data/providers.js'; import { listPriceSource } from '../data/price-source.js'; import { apiMetricsTotals } from '../data/api-metrics.js'; import { dailyProvenance } from '../../shared/price-provenance.js'; import { diagnoseSources } from '../../shared/source-diagnosis.js'; import { serviceCoveredAssets } from './service-cover.mjs'; import { priceFromBase, priceUnitLabel } from '../../shared/units.js'; import { assertScope } from '../ai/gate.js'; import { limitedList } from '../ai/limits.js'; import { vertalerVoor } from './session.mjs'; import { getBaseCurrency } from '../data/portfolio.js'; const DAG = /^\d{4}-\d{2}-\d{2}$/; const DAG_MS = 86400000; const plusDagen = (dag, n) => new Date(Date.parse(`${dag}T00:00:00Z`) + n * DAG_MS).toISOString().slice(0, 10); const dagenTussen = (a, b) => Math.round((Date.parse(`${b}T00:00:00Z`) - Date.parse(`${a}T00:00:00Z`)) / DAG_MS); const STANDAARD_DAGEN = 14; const MAX_DAGEN = 92; const SOORT_TEKST = Object.freeze({ close: 'slotkoers', manual: 'met de hand', live: 'vandaag, beweegt nog', spot: 'afgeleid uit een spot', carried: 'doorgetrokken', none: 'geen koers', }); function getal(n) { if (n == null || !Number.isFinite(n)) return '?'; const d = Math.abs(n) >= 100 ? 2 : Math.abs(n) >= 1 ? 4 : 6; return n.toLocaleString('nl-NL', { maximumFractionDigits: d, useGrouping: false }); } function assetVoorSymbool(gevraagd) { const gezocht = String(gevraagd ?? '').trim().toUpperCase(); return gezocht ? listAssets().find(a => String(a.symbol).toUpperCase() === gezocht) || null : null; } const priceData = Object.freeze({ name: 'price_data', title: 'Koersen van een asset', description: 'Alle koersinformatie van één asset: per dag de gebruikte koers met herkomst en de koers per bron ' + '(days), de gaten (gaps), of de bronnen met keuze, munt, limiet en verbruik (sources). ' + 'Wijzigt niets.', inputSchema: Object.freeze({ type: 'object', properties: { asset: { type: 'string', maxLength: 16, description: 'Symbool, zoals XAG.' }, view: { type: 'string', enum: ['days', 'gaps', 'sources'] }, from: { type: 'string', pattern: '^\\d{4}-\\d{2}-\\d{2}$' }, until: { type: 'string', pattern: '^\\d{4}-\\d{2}-\\d{2}$' }, }, required: ['asset'], additionalProperties: false, }), scope: 'read', async run(args, context) { assertScope(priceData.scope); const t = await vertalerVoor(context); t.beginCall(); const asset = assetVoorSymbool(args?.asset); if (!asset) { const symbolen = listAssets().map(a => a.symbol).join(', '); return { text: `Geen asset met symbool '${String(args?.asset ?? '')}'. Er zijn: ${symbolen}.`, fieldsShared: ['symbol'] }; } const view = args?.view || 'days'; const sleutelVan = providerSleutels(t); if (view === 'gaps') return gatenAntwoord(asset, t); if (view === 'sources') return bronnenAntwoord(asset, t, sleutelVan, (await serviceCoveredAssets()).has(asset.id)); return dagenAntwoord(asset, args, t, sleutelVan); }, }); function providerSleutels(t) { const m = new Map(listProviders().map(p => [p.id, t.toAi({ key: p.key }, { entity: 'provider' }).key])); return id => m.get(id) ?? `bron ${id}`; } function dagenAntwoord(asset, args, t, sleutelVan) { const vandaag = new Date().toISOString().slice(0, 10); let until = DAG.test(String(args?.until ?? '')) ? args.until : vandaag; let from = DAG.test(String(args?.from ?? '')) ? args.from : plusDagen(until, -(STANDAARD_DAGEN - 1)); if (from > until) [from, until] = [until, from]; let ingekort = false; if (dagenTussen(from, until) + 1 > MAX_DAGEN) { from = plusDagen(until, -(MAX_DAGEN - 1)); ingekort = true; } const rijen = listPriceHistory(asset.id, plusDagen(from, -31)).filter(r => r.observedAt.slice(0, 10) <= until); const manual = listProviders().find(p => p.key === 'manual'); const dagen = dailyProvenance(rijen, { assetKind: asset.kind, manualProviderId: manual ? manual.id : null, today: vandaag, from, until }); const eenheid = priceUnitLabel(asset); const prijs = (fiatPrice) => getal(priceFromBase(t.toAi({ fiatPrice },{ entity: 'priceHistory' }).fiatPrice, asset)); const grens = CONFIG.HYGIENE.SOURCES_DISAGREE_SHARE; const perDag = new Map(); for (const r of rijen) { const dag = r.observedAt.slice(0, 10); if (dag < from) continue; if (!perDag.has(dag)) perDag.set(dag, []); perDag.get(dag).push(r); } const regels = limitedList(dagen.length ? dagen : [], d => { const gebruikt = d.kind === 'none' ? 'geen koers' : `${prijs(d.fiatPrice)} (${SOORT_TEKST[d.kind] || d.kind}${d.row ? `, ${sleutelVan(d.row.providerId)}` : ''}` + `${d.kind === 'carried' ? ` van ${d.row.observedAt.slice(0, 10)}, ${d.ageDays} dagen` : ''})`; const bronnen = (perDag.get(d.day) || []).map(r => { const prijsTekst = r.fiatPrice != null && r.fiatPrice > 0 ? prijs(r.fiatPrice) : 'leeg'; const moment = r.resolution === 'timestamp' ? ` spot ${r.observedAt.slice(11, 16)} UTC` : ''; return `${sleutelVan(r.providerId)}${moment} ${prijsTekst}`; }); const dagrijen = (perDag.get(d.day) || []).filter(r => r.resolution === 'day' && r.fiatPrice > 0); let oneens = ''; if (new Set(dagrijen.map(r => r.providerId)).size > 1) { const laag = Math.min(...dagrijen.map(r => r.fiatPrice)), hoog = Math.max(...dagrijen.map(r => r.fiatPrice)); const spreiding = hoog / laag - 1; if (spreiding > grens) oneens = ` · BRONNEN ONEENS: ${getal(Math.round(spreiding * 1000) / 10)}%`; } return `${d.day}: ${gebruikt}${bronnen.length ? ` · ${bronnen.join('; ')}` : ''}${oneens}`; }).lines; return { text: [ `Koersen van ${t.toAi(asset, { entity: 'asset' }).symbol} in ${getBaseCurrency()} per ${eenheid}, ${from} t/m ${until}` + `${ingekort ? ` (ingekort tot ${MAX_DAGEN} dagen)` : ''}. Per dag: de koers die de app gebruikt met` + ' herkomst en bron, daarna elke opgeslagen koers per bron. Een spot is een moment, geen slot.', ...(regels.length ? regels : ['Geen koersen in dit venster, en geen die ervoor doorgetrokken kan worden.']), ].join('\n'), fieldsShared: [...new Set([...t.fieldsShared(), 'baseCurrency', 'symbol', 'kind', 'unit', 'observedAt', 'fiatPrice', 'resolution', 'key', 'updatedAt'])].sort(), }; } function gatenAntwoord(asset, t) { const open = listPriceGaps().rows.filter(g => g.assetId === asset.id); const genegeerd = listIgnoredPriceGaps().filter(g => g.assetId === asset.id); const symbool = t.toAi(asset, { entity: 'asset' }).symbol; return { text: [ `Gaten in de koershistorie van ${symbool} (dagen zonder koers, vanaf de eerste boeking t/m gisteren,` + ` langer dan ${CONFIG.HYGIENE.PRICE_GAP_DAYS - 1} dagen):`, ...(open.length ? limitedList(open, g => `- ${g.from} t/m ${g.until}, ${g.days} dagen`).lines : ['- geen']), 'Door de gebruiker genegeerd (telt als gedekt):', ...(genegeerd.length ? limitedList(genegeerd, g => `- ${g.fromDay} t/m ${g.untilDay}`).lines : ['- geen']), ].join('\n'), fieldsShared: ['symbol', 'observedAt', 'fromDay', 'untilDay'], }; } function bronnenAntwoord(asset, t, sleutelVan, uitDienst = false) { const maand = `${new Date().toISOString().slice(0, 7)}-01T00:00`; const etmaal = new Date(Date.now() - DAG_MS).toISOString().slice(0, 16); const providers = listProviders().map(p => getProvider(p.key)).filter(Boolean); const usage = {}; for (const p of providers) if (p.callsPerMonth) usage[p.id] = apiMetricsTotals(p.id, maand).calls; const cellen = listPriceSource().filter(c => c.assetId === asset.id); const rijen = listPriceHistory(asset.id); const keten = SOURCE_CAPABILITIES.map(cap => { const k = cellen.filter(c => c.capability === cap && c.rank != null).sort((a, b) => a.rank - b.rank) .map(c => { const v = t.toAi({ rank: c.rank, status: c.status }, { entity: 'priceSource' }); return `${v.rank}. ${sleutelVan(c.providerId)} (${v.status})`; }); return `${cap === 'spot' ? 'Spot' : 'Historie'}: ${k.length ? k.join(', ') : 'geen bron gekozen'}`; }); const dekkend = providers.filter(p => (p.coverage || []).includes(asset.symbol) || p.key === 'manual'); const regels = limitedList(dekkend, p => { const rauw = (p.symbols || []).find(s => !s.isCatchAll && s.symbol === asset.symbol) || (p.symbols || []).find(s => s.isCatchAll) || null; const kaart = rauw ? t.toAi({ externalId: rauw.externalId, quoteUnit: rauw.quoteUnit }, { entity: 'provider' }) : null; const eindpunten = (p.endpoints || []).map(e => { const v = t.toAi({ capability: e.capability, quoteCurrency: e.quoteCurrency }, { entity: 'provider' }); return `${v.capability}${v.quoteCurrency ? ` in ${v.quoteCurrency}` : ''}`; }); const eigen = rijen.filter(r => r.providerId === p.id); const laatste = eigen[0] ? eigen[0].observedAt.slice(0, 10) : null; const eerste = eigen.length ? eigen[eigen.length - 1].observedAt.slice(0, 10) : null; const m = apiMetricsTotals(p.id, etmaal); const delen = [ p.isActive ? 'actief' : 'uit', eindpunten.length ? eindpunten.join(', ') : 'geen eindpunten', ...(kaart ? [`invoer ${kaart.externalId}${kaart.quoteUnit ? `, per ${kaart.quoteUnit}` : ''}`] : []), ...(p.callsPerMonth ? [`${usage[p.id] ?? 0} van ${p.callsPerMonth} verzoeken deze maand`] : []), `${m.calls} aanroepen in 24 uur, ${m.fail} mislukt`, eigen.length ? `${eigen.length} koersen, ${eerste} t/m ${laatste}` : 'geen koersen opgeslagen', ]; return `- ${sleutelVan(p.id)}: ${delen.join('; ')}`; }).lines; const historyChain = cellen.filter(c => c.capability === 'history' && c.rank != null) .sort((a, b) => a.rank - b.rank).map(c => providers.find(p => p.id === c.providerId)?.key).filter(Boolean); const bevindingen = diagnoseSources(asset, { providers, historyChain, usage, serviceCovers: uitDienst }) .map(b => `- ${b.code}${(b.names || []).length ? ` (${b.names.join(', ')})` : ''}` + `${b.proposal ? `, voorstel: ${b.proposal.providerKey}` : ''}`); return { text: [ `Bronnen van ${t.toAi(asset, { entity: 'asset' }).symbol} (koersen worden per gram opgeslagen bij metaal):`, ...keten, ...(uitDienst ? ['Dienst: historie uit Atreides Services (stand niet uit, symbool gedekt).'] : []), 'Bronnen die dit symbool dekken:', ...(regels.length ? regels : ['- geen']), 'Diagnose van de bronkeuze:', ...(bevindingen.length ? bevindingen : ['- in orde']), ].join('\n'), fieldsShared: [...new Set([...t.fieldsShared(), 'symbol', 'key', 'isActive', 'capability', 'rank', 'status', 'quoteCurrency', 'quoteUnit', 'externalId', 'callsPerMonth', 'okCount', 'failCount', 'observedAt', ...(uitDienst ? ['value'] : [])])].sort(), }; } export const PRICE_TOOLS = Object.freeze([priceData]);