// Generated, do not edit. sha256:5f58c2b253a83045479ff5ac37e60e082b447d2cdda1c0769094b6a2ac261676 import * as db from '../db.js'; import { getBaseCurrency } from './portfolio.js'; import { marketConfigured, marketRegister, marketSeries, marketHistory, marketSpots, isRetryLater } from '../market-service.mjs'; import { CONFIG } from '../../shared/config.js'; const DAY_MS = 86_400_000; const unitFactor = (entry) => (entry.unit == null ? 1 : CONFIG.METALS.UNITS[entry.unit] ?? null); const perStorageUnit = (entry, price) => price / unitFactor(entry); const usable = register => (register || []).filter(i => i && typeof i.key === 'string' && unitFactor(i) != null); function registerEntry(asset, register) { const key = `${asset.kind}:${String(asset.symbol || '').toUpperCase()}`; return usable(register).find(i => i.key === key) || null; } function minuteEntry(asset, register) { const hit = registerEntry(asset, register); return hit && Array.isArray(hit.resolutions) && hit.resolutions.includes('minute') ? hit : null; } function unavailable(e) { if (typeof e?.key === 'string' && e.key.startsWith('marketService.')) { return { status: 'unavailable', retryLater: isRetryLater(e), key: e.key, params: e.params ?? null, ...(e.requestId ? { requestId: e.requestId } : {}) }; } throw e; } const assetRow = (assetId) => { const asset = db.one(`SELECT id, kind, symbol FROM Asset WHERE id = ?`, [Number(assetId)]); if (!asset) throw new Error(`Unknown asset id ${assetId}.`); return asset; }; export async function intradaySeries(assetId, sinceMs) { const asset = assetRow(assetId); const now = Date.now(); const from = Number(sinceMs); if (!Number.isFinite(from) || from >= now || now - from > CONFIG.MARKET_SERVICE.INTRADAY_MAX_DAYS * DAY_MS) { throw new Error('sinceMs has to lie in the past, within the intraday limit.'); } if (!(await marketConfigured())) return { status: 'off' }; try { const entry = minuteEntry(asset, await marketRegister()); if (!entry) return { status: 'notCovered' }; const s = await marketSeries({ instrument: entry.key, quote: getBaseCurrency(), resolution: 'minute', from, to: now }); const named = src => s.sourceLabels?.[src] ?? src; return { status: 'ok', instrument: entry.key, currency: s.currency, source: s.source, sourceLabel: s.sourceLabel ?? null, points: s.points.map(([t, p, src]) => (src ? [t, perStorageUnit(entry, p), named(src)] : [t, perStorageUnit(entry, p)])) }; } catch (e) { return unavailable(e); } } export async function priceAtMoment(assetId, atIso) { const asset = assetRow(assetId); const at = Date.parse(String(atIso)); const now = Date.now(); if (!Number.isFinite(at) || at > now) throw new Error(`A moment in the past is needed: '${atIso}'.`); if (!(await marketConfigured())) return { status: 'off' }; const window = CONFIG.MARKET_SERVICE.MOMENT_WINDOW_MS; try { const entry = minuteEntry(asset, await marketRegister()); if (!entry) return { status: 'notCovered' }; const s = await marketSeries({ instrument: entry.key, quote: getBaseCurrency(), resolution: 'minute', from: at - window, to: Math.min(at + window, now) }); let best = null; for (const [t, p, src] of s.points || []) { if (!Number.isFinite(t) || !Number.isFinite(p) || Math.abs(t - at) > window) continue; if (!best || Math.abs(t - at) < Math.abs(best[0] - at)) best = [t, p, src]; } if (!best) return { status: 'missing' }; return { status: 'ok', price: perStorageUnit(entry, best[1]), currency: s.currency, at: best[0], source: best[2] || s.source }; } catch (e) { return unavailable(e); } } export async function serviceDayHistory(assetId, sinceMs) { const asset = assetRow(assetId); const now = Date.now(); const from = Number(sinceMs); if (!Number.isFinite(from) || from >= now || now - from > CONFIG.MARKET_SERVICE.HISTORY_MAX_DAYS * DAY_MS + DAY_MS) { throw new Error('sinceMs has to lie in the past, within the history limit.'); } if (!(await marketConfigured())) return { status: 'off' }; try { const entry = registerEntry(asset, await marketRegister()); if (!entry || !Array.isArray(entry.kinds) || !entry.kinds.includes('history')) return { status: 'notCovered' }; const h = await marketHistory({ instrument: entry.key, quote: getBaseCurrency(), from, to: now }); const today = Math.floor(now / DAY_MS) * DAY_MS; const named = src => h.sourceLabels?.[src] ?? src; const days = h.points.filter(([t, , resolution]) => resolution === 'day' && t < today) .map(([t, p, , src]) => (src ? [Math.floor(t / DAY_MS) * DAY_MS, perStorageUnit(entry, p), named(src)] : [Math.floor(t / DAY_MS) * DAY_MS, perStorageUnit(entry, p)])); return { status: 'ok', instrument: entry.key, currency: h.currency, source: h.source, days }; } catch (e) { return unavailable(e); } } export async function serviceCoverage() { if (!(await marketConfigured())) return { status: 'off' }; try { const register = await marketRegister(); const assetIds = db.all(`SELECT id, kind, symbol FROM Asset ORDER BY id`) .filter(a => registerEntry(a, register)) .map(a => a.id); return { status: 'ok', assetIds }; } catch (e) { return unavailable(e); } } export async function serviceSpot(assetId) { const asset = assetRow(assetId); if (!(await marketConfigured())) return { status: 'off' }; const today = new Date().toISOString().slice(0, 10); const manual = db.one( `SELECT 1 AS x FROM PriceHistory JOIN Provider ON Provider.id = PriceHistory.providerId WHERE PriceHistory.assetId = ? AND Provider.key = 'manual' AND substr(PriceHistory.observedAt, 1, 10) = ?`, [asset.id, today]); if (manual) return { status: 'local' }; try { const register = await marketRegister(); const entry = registerEntry(asset, register); if (!entry) return { status: 'notCovered' }; const r = await marketSpots(usable(register).map(i => i.key), getBaseCurrency()); const hit = r.spot.find(s => s.instrument === entry.key); if (hit) return { status: 'ok', ...hit, price: perStorageUnit(entry, hit.price) }; if (r.unreachable.includes(entry.key)) { return { status: 'unavailable', retryLater: true, key: 'marketService.spotUnreachable', params: null }; } return { status: 'missing' }; } catch (e) { return unavailable(e); } }