Versie 0.2.0

This commit is contained in:
Atreides Assets committed 2026-10-29 11:57:29 +01:00
commit e77d8fd373
495 files changed
+81026

No files matched your search

+152
View File
@@ -0,0 +1,152 @@
// Generated, do not edit. sha256:4249f91eb8232d410f76bd1c74b9d6d6491317d71d4cb8ae4ce4ed0b5bdfa51b
import { CONFIG } from './config.js';
const DAY_MS = 24 * 60 * 60 * 1000;
export const PROVENANCE_KINDS = Object.freeze(['close', 'manual', 'live', 'spot', 'carried', 'none']);
const dayOf = iso => String(iso).slice(0, 10);
const msOf = day => Date.parse(`${day}T00:00:00Z`);
const addDays = (day, n) => new Date(msOf(day) + n * DAY_MS).toISOString().slice(0, 10);
export function dailyProvenance(rows, opts = {}) {
const { assetKind = 'crypto', manualProviderId = null } = opts;
const today = opts.today || new Date().toISOString().slice(0, 10);
const perDay = new Map();
for (const r of rows || []) {
if (r == null || r.fiatPrice == null || !(r.fiatPrice > 0)) continue;
const day = dayOf(r.observedAt);
const cur = perDay.get(day);
perDay.set(day, cur && beats(cur, r, day === today) ? cur : r);
}
if (perDay.size === 0) return [];
const days = [...perDay.keys()].sort();
const from = opts.from || days[0];
const until = opts.until || today;
if (from > until) return [];
const kindOf = (r, day) => kindFor(r, day, manualProviderId, today);
const limit = CONFIG.PRICE_HISTORY.CARRY_FORWARD_DAYS[assetKind];
const out = [];
let last = null;
for (const d of days) if (d < from) last = { day: d, row: perDay.get(d) };
for (let day = from; day <= until; day = addDays(day, 1)) {
const row = perDay.get(day);
if (row) {
last = { day, row };
out.push({ day, fiatPrice:row.fiatPrice, kind: kindOf(row, day), row, ageDays: 0, deviation: null });
continue;
}
const age = last ? Math.round((msOf(day) - msOf(last.day)) / DAY_MS) : null;
if (last && (limit == null || age <= limit)) {
out.push({ day, fiatPrice:last.row.fiatPrice, kind: 'carried', row: last.row, ageDays: age, deviation: null });
} else {
out.push({ day, fiatPrice:null, kind: 'none', row: null, ageDays: age, deviation: null });
}
}
addDeviations(out);
return out;
}
function kindFor(r, day, manualProviderId, today) {
if (manualProviderId != null && r.providerId === manualProviderId) return 'manual';
if (day === today) return 'live';
return r.resolution !== 'timestamp' && !isProvisional(r) ? 'close' : 'spot';
}
export function provenanceOfRow(row, opts = {}) {
if (!row || !(row.fiatPrice > 0)) return null;
const today = opts.today || new Date().toISOString().slice(0, 10);
const voor = opts.day || today;
const dag = dayOf(row.observedAt);
const ageDays = Math.max(0, Math.round((msOf(voor) - msOf(dag)) / DAY_MS));
const kind = ageDays > 0 ? 'carried'
: (row.derivedFrom === 'spot' && dag !== today ? 'spot' : kindFor(row, dag, opts.manualProviderId ?? null, today));
return { day: voor, fiatPrice:row.fiatPrice, kind, row, ageDays, deviation: null };
}
export function provenanceOn(rows, day, opts = {}) {
const today = opts.today || new Date().toISOString().slice(0, 10);
const tot = addDays(day, 31) < today ? addDays(day, 31) : today;
const d = dailyProvenance(rows, { ...opts, today, from: addDays(day, -31), until: tot < day ? day : tot })
.find(x => x.day === day) || null;
const used = opts.usedPrice;
if (d && d.fiatPrice == null && used != null) {
const eerder = (rows || []).filter(r => r && r.fiatPrice > 0 && dayOf(r.observedAt) <= day && gelijk(r.fiatPrice, used))
.sort((a, b) => (a.observedAt < b.observedAt ? 1 : -1))[0];
if (!eerder) return d;
return { day, fiatPrice:eerder.fiatPrice, kind: 'carried', row: eerder, deviation: null,
ageDays: Math.round((msOf(day) - msOf(dayOf(eerder.observedAt))) / DAY_MS) };
}
if (!d || used == null || d.fiatPrice == null || gelijk(d.fiatPrice, used)) return d;
const bronDag = dayOf(d.row.observedAt);
const alt = (rows || []).find(r => r && r.fiatPrice > 0 && dayOf(r.observedAt) === bronDag && gelijk(r.fiatPrice, used));
if (!alt) return d;
const kind = d.kind === 'carried' ? 'carried' : kindFor(alt, day, opts.manualProviderId ?? null, today);
return { ...d, fiatPrice:alt.fiatPrice, row: alt, kind };
}
const gelijk = (a, b) => Math.abs(a - b) <= 1e-9 * Math.max(1, Math.abs(a), Math.abs(b));
function isProvisional(r) {
const written = r.updatedAt || r.createdAt;
return !!written && dayOf(written) <= dayOf(r.observedAt);
}
const seenAt = r => (r.resolution === 'timestamp' ? r.observedAt : (r.updatedAt || r.createdAt || ''));
function beats(cur, cand, isToday = false) {
if (isToday) return seenAt(cur) >= seenAt(cand);
const curDay = cur.resolution !== 'timestamp', candDay = cand.resolution !== 'timestamp';
if (curDay !== candDay) return curDay;
if (curDay) return (cur.id ?? 0) >= (cand.id ?? 0);
return cur.observedAt >= cand.observedAt;
}
function addDeviations(out) {
const real = k => k === 'close' || k === 'manual';
let before = null;
const pct = (v, ref) => (ref ? Math.round(((v - ref) / ref) * 10000) / 100 : null);
for (const d of out) {
if (real(d.kind)) before = d.fiatPrice;
else if (d.kind === 'spot') d.deviation = { before: pct(d.fiatPrice, before), after: null };
}
let after = null;
for (let i = out.length - 1; i >= 0; i--) {
const d = out[i];
if (real(d.kind)) after = d.fiatPrice;
else if (d.kind === 'spot') d.deviation.after = pct(d.fiatPrice, after);
}
}
export function provenanceRuns(days) {
const runs = [];
for (const d of days || []) {
const last = runs[runs.length - 1];
if (last && last.kind === d.kind) { last.until = d.day; last.days += 1; }
else runs.push({ kind: d.kind, from: d.day, until: d.day, days: 1 });
}
return runs;
}
export function originFrom(d, names = new Map()) {
if (!d || d.fiatPrice == null || !d.row) return null;
const r = d.row;
return {
day: d.day, kind: d.kind, fiatPrice:d.fiatPrice, ageDays: d.ageDays, deviation: d.deviation,
source: names.get(r.providerId) || null,
sourceDay: dayOf(r.observedAt),
moment: d.kind === 'spot' || d.kind === 'live'
? (r.resolution === 'timestamp' ? r.observedAt : (r.updatedAt || r.createdAt || null)) : null,
};
}
export function liveOrigin(fiatPrice, source = null, now = new Date()) {
if (fiatPrice == null) return null;
const nu = now.toISOString();
return { day: nu.slice(0, 10), kind: 'live', fiatPrice, ageDays: 0, deviation: null, source, sourceDay: null, moment: nu };
}
export function provenanceCounts(days) {
const counts = Object.fromEntries(PROVENANCE_KINDS.map(k => [k, 0]));
for (const d of days || []) counts[d.kind] += 1;
return counts;
}