Files

142 lines
6.3 KiB
JavaScript
Raw Permalink Normal View History

2026-10-29 11:57:29 +01:00
// 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);
}
}