Versie 0.2.0
This commit is contained in:
commit
e77d8fd373
495 files changed
+81026
No files matched your search
@@ -0,0 +1,233 @@
|
||||
// Generated, do not edit. sha256:c2a988d6416e302a0ad7224bc98a3cd265bc5cc9cadb0dce809af179af082299
|
||||
import * as db from '../db.js';
|
||||
import { nowIso, todayIso, daysBetween, isoToDate, dateToIso, addDaysIso, weekdayOf, clampDom } from './_shared.js';
|
||||
import { createEvent, eventDaysByAccount } from './events.js';
|
||||
import { listSelectableAccounts } from './accounts.js';
|
||||
import { positionAt } from './positions.js';
|
||||
import { listSchedulesForAccount, listPeriods, realizedForSchedule } from './interest-schedules.js';
|
||||
import { anchorDaysBetween } from './valuations.js';
|
||||
import { toUtcIso } from '../../shared/time.js';
|
||||
import { fail } from '../../shared/app-error.js';
|
||||
|
||||
export function realizedInterestSub(accountId, assetId, fromIso, toIso) {
|
||||
const r = db.one(`SELECT COALESCE(SUM(amount),0) AS s FROM Event
|
||||
WHERE type = 'interest' AND toAccountId = ? AND assetId = ? AND occurredAt >= ? AND occurredAt < ?`,
|
||||
[accountId, assetId, fromIso, toIso]);
|
||||
return r ? r.s : 0;
|
||||
}
|
||||
|
||||
function weeklyDates(startIso, endIso, weekday) {
|
||||
const out = [];
|
||||
let cur = addDaysIso(startIso, 1), guard = 0;
|
||||
while (weekdayOf(cur) !== weekday && guard++ < 7) cur = addDaysIso(cur, 1);
|
||||
for (; cur <= endIso; cur = addDaysIso(cur, 7)) out.push(cur);
|
||||
return out;
|
||||
}
|
||||
function monthlyDates(startIso, endIso, dom) {
|
||||
const out = [];
|
||||
const s = isoToDate(startIso);
|
||||
let y = s.getUTCFullYear(), m = s.getUTCMonth();
|
||||
for (;;) {
|
||||
const iso = dateToIso(new Date(Date.UTC(y, m, clampDom(y, m, dom))));
|
||||
if (iso > endIso) break;
|
||||
if (iso > startIso) out.push(iso);
|
||||
if (++m > 11) { m = 0; y++; }
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function buildPeriods(dates, startIso) {
|
||||
const periods = [];
|
||||
let prev = startIso;
|
||||
for (const d of dates) { periods.push({ periodStart: prev, periodEnd: d }); prev = d; }
|
||||
return periods;
|
||||
}
|
||||
|
||||
function allCreditingPeriods(s, horizonIso = todayIso()) {
|
||||
const periods = cadencePeriods(s, horizonIso);
|
||||
if (s.endsOn && s.creditCadence !== 'term_end') {
|
||||
const last = periods.length ? periods[periods.length - 1].periodEnd : s.startsOn;
|
||||
if (last < s.endsOn) periods.push({ periodStart: last, periodEnd: s.endsOn });
|
||||
}
|
||||
return periods;
|
||||
}
|
||||
|
||||
function cadencePeriods(s, horizonIso) {
|
||||
const eind = s.endsOn || horizonIso;
|
||||
if (s.creditCadence === 'term_end') return s.endsOn ? [{ periodStart: s.startsOn, periodEnd: s.endsOn }] : [];
|
||||
if (s.creditCadence === 'annual') {
|
||||
const periods = [];
|
||||
const startYear = Number(s.startsOn.slice(0, 4));
|
||||
for (let y = startYear; ; y++) {
|
||||
const periodEnd = `${y + 1}-01-01`;
|
||||
if (periodEnd > eind) break;
|
||||
periods.push({ periodStart: y === startYear ? s.startsOn : `${y}-01-01`, periodEnd: periodEnd });
|
||||
}
|
||||
return periods;
|
||||
}
|
||||
if (s.creditCadence === 'quarterly') {
|
||||
const periods = [];
|
||||
const s0 = isoToDate(s.startsOn);
|
||||
let y = s0.getUTCFullYear();
|
||||
let m = Math.floor(s0.getUTCMonth() / 3) * 3;
|
||||
let prev = s.startsOn;
|
||||
for (;;) {
|
||||
m += 3;
|
||||
if (m > 11) { m -= 12; y++; }
|
||||
const periodEnd = dateToIso(new Date(Date.UTC(y, m, 1)));
|
||||
if (periodEnd > eind) break;
|
||||
periods.push({ periodStart: prev, periodEnd });
|
||||
prev = periodEnd;
|
||||
}
|
||||
return periods;
|
||||
}
|
||||
const dates = s.creditCadence === 'weekly'
|
||||
? weeklyDates(s.startsOn, eind, s.creditWeekday)
|
||||
: monthlyDates(s.startsOn, eind, s.creditDayOfMonth);
|
||||
return buildPeriods(dates, s.startsOn);
|
||||
}
|
||||
|
||||
export function scheduleForecast(s, asofIso = todayIso()) {
|
||||
const realizedSub = realizedForSchedule(s, asofIso);
|
||||
const sameAsset = s.principalAssetId === s.payoutAssetId;
|
||||
const periods = allCreditingPeriods(s, addDaysIso(asofIso, 400));
|
||||
const amount = periodAmounts(s, periods);
|
||||
const later = periods.filter(p => p.periodEnd > asofIso);
|
||||
const upcoming = (s.endsOn ? later : later.slice(0, 1)).map(p => ({ ...p, projectedSub: amount(p) }));
|
||||
|
||||
if (!s.endsOn || s.principalAmount == null) {
|
||||
return { termTotalSub: null, realizedSub, remainingSub: null, remainingCount: null, sameAsset, upcoming };
|
||||
}
|
||||
|
||||
const termTotalSub = periods.reduce((n, p) => n + amount(p), 0);
|
||||
const remainingSub = Math.max(0, termTotalSub - realizedSub);
|
||||
return { termTotalSub, realizedSub, remainingSub, remainingCount: upcoming.length, sameAsset, upcoming };
|
||||
}
|
||||
|
||||
function periodAmounts(s, periods) {
|
||||
if (!s.compounding) return p => projectedSubForPeriod(s, p);
|
||||
const byEnd = new Map();
|
||||
let accrued = 0;
|
||||
for (const p of periods) {
|
||||
const sub = projectedSubForPeriod(s, p, accrued);
|
||||
byEnd.set(p.periodEnd, sub);
|
||||
accrued += sub;
|
||||
}
|
||||
return p => byEnd.get(p.periodEnd);
|
||||
}
|
||||
|
||||
function creditingPeriods(s, asofIso) {
|
||||
return allCreditingPeriods(s).filter(p => p.periodEnd <= asofIso);
|
||||
}
|
||||
function rateSlices(s, vanIso, totIso) {
|
||||
const perioden = listPeriods(s.id);
|
||||
const wissels = perioden.filter(p => p.startsOn > vanIso && p.startsOn < totIso);
|
||||
const eerder = perioden.filter(p => p.startsOn <= vanIso);
|
||||
let rate = eerder.length ? eerder[eerder.length - 1].annualRate : s.annualRate;
|
||||
|
||||
const uit = [];
|
||||
let van = vanIso;
|
||||
for (const w of wissels) {
|
||||
uit.push({ van, tot: w.startsOn, rate });
|
||||
van = w.startsOn;
|
||||
rate = w.annualRate;
|
||||
}
|
||||
uit.push({ van, tot: totIso, rate });
|
||||
return uit;
|
||||
}
|
||||
|
||||
function principalMainVoor(s, opDag) {
|
||||
if (s.principalAmount != null) return s.principalAmount / Math.pow(10, s.principal_decimals);
|
||||
const moment = toUtcIso(`${opDag}T23:59:59`);
|
||||
return positionAt(s.accountId, s.principalAssetId, moment) / Math.pow(10, s.principal_decimals);
|
||||
}
|
||||
|
||||
function saldoDagen(s, vanDag, totDag) {
|
||||
const dagen = new Set([
|
||||
...eventDaysByAccount(s.accountId, s.principalAssetId, vanDag, totDag),
|
||||
...anchorDaysBetween(s.accountId, s.principalAssetId, vanDag, totDag),
|
||||
]);
|
||||
return [...dagen].sort();
|
||||
}
|
||||
|
||||
function projectedSubForPeriod(s, p, accruedSub = 0) {
|
||||
const stukken = [];
|
||||
for (const deel of rateSlices(s, p.periodStart, p.periodEnd)) {
|
||||
if (s.principalAmount != null) { stukken.push(deel); continue; }
|
||||
let van = deel.van;
|
||||
for (const dag of saldoDagen(s, deel.van, deel.tot)) {
|
||||
stukken.push({ van, tot: dag, rate: deel.rate });
|
||||
van = dag;
|
||||
}
|
||||
stukken.push({ van, tot: deel.tot, rate: deel.rate });
|
||||
}
|
||||
const accrued = accruedSub / Math.pow(10, s.payout_decimals);
|
||||
const som = stukken.reduce((n, deel) =>
|
||||
n + (principalMainVoor(s, deel.van) + accrued) * deel.rate * Math.max(0, daysBetween(deel.van, deel.tot) / 365), 0);
|
||||
return Math.round(som * Math.pow(10, s.payout_decimals));
|
||||
}
|
||||
function getCredit(scheduleId, periodEnd) {
|
||||
return db.one(`SELECT * FROM InterestCredit WHERE scheduleId = ? AND periodEnd = ?`, [scheduleId, periodEnd]) || null;
|
||||
}
|
||||
|
||||
export function dueCreditsForAccount(accountId) {
|
||||
const today = todayIso();
|
||||
const out = [];
|
||||
for (const s of listSchedulesForAccount(accountId)) {
|
||||
if (s.rateKind !== 'fixed') continue;
|
||||
const periods = creditingPeriods(s, today);
|
||||
const amount = periodAmounts(s, periods);
|
||||
for (const p of periods) {
|
||||
if (getCredit(s.id, p.periodEnd)) continue;
|
||||
if (realizedForSchedule(s, p.periodEnd, addDaysIso(p.periodStart, 1)) > 0) continue;
|
||||
const projected = amount(p);
|
||||
if (projected === 0) continue;
|
||||
out.push({
|
||||
scheduleId: s.id, accountId: accountId, payoutAssetId: s.payoutAssetId,
|
||||
payout_symbol: s.payout_symbol, payout_decimals: s.payout_decimals,
|
||||
payout_unit: s.payout_unit, payout_subunit: s.payout_subunit, payout_display_unit: s.payout_display_unit,
|
||||
periodStart: p.periodStart, periodEnd: p.periodEnd, projected_sub: projected,
|
||||
});
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function dueCreditsAll() {
|
||||
const out = [];
|
||||
for (const a of listSelectableAccounts()) {
|
||||
for (const d of dueCreditsForAccount(a.id)) out.push({ ...d, account_name: a.name });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function olderOpenDue(due) {
|
||||
return dueCreditsForAccount(due.accountId)
|
||||
.filter(d => d.periodEnd < due.periodEnd)
|
||||
.sort((a, b) => a.periodEnd.localeCompare(b.periodEnd))[0] || null;
|
||||
}
|
||||
|
||||
export async function bookDueCredit(due, amountSub) {
|
||||
const ouder = olderOpenDue(due);
|
||||
if (ouder) {
|
||||
fail('interest.bookOlderFirst', { start: ouder.periodStart, eind: ouder.periodEnd },
|
||||
`Book the credit of ${ouder.periodStart} to ${ouder.periodEnd} first: each credit raises the `
|
||||
+ 'balance the next one is computed on.');
|
||||
}
|
||||
const eventId = await createEvent({
|
||||
assetId: due.payoutAssetId, type: 'interest', occurredAt: toUtcIso(`${due.periodEnd}T00:00:00`),
|
||||
amount: amountSub, fiatPrice: null, fiatFee: null, unitFee: null,
|
||||
fromAccountId: null, toAccountId: due.accountId, source: 'manual',
|
||||
importReference: null, externalAddress: null, note: null,
|
||||
});
|
||||
db.run(`INSERT INTO InterestCredit (scheduleId, periodStart, periodEnd, eventId, status, createdAt)
|
||||
VALUES (?,?,?,?, 'booked', ?)`,
|
||||
[due.scheduleId, due.periodStart, due.periodEnd, eventId, nowIso()]);
|
||||
await db.persist();
|
||||
return eventId;
|
||||
}
|
||||
export async function dismissDueCredit(due) {
|
||||
db.run(`INSERT INTO InterestCredit (scheduleId, periodStart, periodEnd, eventId, status, createdAt)
|
||||
VALUES (?,?,?, NULL, 'dismissed', ?)`,
|
||||
[due.scheduleId, due.periodStart, due.periodEnd, nowIso()]);
|
||||
await db.persist();
|
||||
}
|
||||
Reference in new issue
Block a user