Versie 0.2.0
This commit is contained in:
commit
e77d8fd373
495 files changed
+81026
No files matched your search
@@ -0,0 +1,142 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user