Files
2026-10-30 12:28:48 +01:00

132 lines
6.1 KiB
JavaScript

// Generated, do not edit. sha256:c7cd149bf7a3cd10c21ce3f8a14833aa6c606b7953811447bfbf09af17e22088
import { mkdirSync } from 'node:fs';
import { join } from 'node:path';
import { CONFIG } from '../shared/config.js';
import { priceFromTotal } from '../shared/units.js';
import { toSearchName } from '../shared/search-name.js';
import { initDatabase, closeDatabase, one } from './db.js';
import { portfolioDir, listPortfolios } from './portfolios.mjs';
import { hasFeature } from './license.mjs';
import { completeSetup } from './data/onboarding.js';
import { createEvent } from './data/events.js';
import { createValuation } from './data/valuations.js';
export const DEMO_MONTHS = 30;
const BTC_EUR = {
'2023-01': 21000, '2023-04': 27000, '2023-07': 26500, '2023-10': 32000,
'2024-01': 39000, '2024-02': 48000, '2024-03': 63000, '2024-04': 59000, '2024-05': 62000, '2024-06': 58000,
'2024-07': 59000, '2024-08': 54000, '2024-09': 57000, '2024-10': 63000, '2024-11': 88000, '2024-12': 92000,
'2025-01': 97000, '2025-02': 84000, '2025-03': 78000, '2025-04': 82000, '2025-05': 92000, '2025-06': 91000,
'2025-07': 100000, '2025-08': 97000, '2025-09': 96000, '2025-10': 98000, '2025-11': 82000, '2025-12': 76000,
'2026-01': 79000, '2026-02': 74000, '2026-03': 72000, '2026-04': 78000, '2026-05': 84000, '2026-06': 88000,
'2026-07': 92000, '2026-08': 90000, '2026-09': 94000, '2026-10': 96000,
};
const MONTHS = Object.keys(BTC_EUR).sort();
const SAT = 1e8;
const CENT = 100;
const ACCOUNTS = Object.freeze({
exchange: { name: 'Exchange', kind: 'exchange' },
wallet: { name: 'Hardware wallet', kind: 'wallet' },
bank: { name: 'Bank', kind: 'bank' },
});
const iso = (y, m, day) => new Date(Date.UTC(y, m, day, 10, 0, 0)).toISOString();
function priceOn(at) {
const key = at.slice(0, 7);
const idx = MONTHS.findIndex(k => k >= key);
const here = BTC_EUR[MONTHS[idx < 0 ? MONTHS.length - 1 : idx]];
const next = BTC_EUR[MONTHS[Math.min((idx < 0 ? MONTHS.length - 1 : idx) + 1, MONTHS.length - 1)]];
const frac = (Number(at.slice(8, 10)) - 1) / 30;
return Math.round(here + (next - here) * frac);
}
export function demoRows(today = new Date()) {
const rows = [];
const pos = { exchange: 0, wallet: 0, bank: 0 };
const nu = today.toISOString();
const start = new Date(Date.UTC(today.getUTCFullYear(), today.getUTCMonth() - (DEMO_MONTHS - 1), 1));
const add = (r) => { if (r.at <= nu) rows.push(r); return r.at <= nu; };
for (let i = 0; i < DEMO_MONTHS; i++) {
const y = start.getUTCFullYear(), m = start.getUTCMonth() + i;
if (i === 0 && add({ kind: 'event', type: 'buy', asset: 'EUR', to: 'bank', at: iso(y, m, 1), amount: 4000 * CENT, fiatPrice: 1 })) pos.bank += 4000 * CENT;
if (add({ kind: 'event', type: 'buy', asset: 'EUR', to: 'bank', at: iso(y, m, 1), amount: 900 * CENT, fiatPrice: 1 })) pos.bank += 900 * CENT;
for (const day of [2, 9, 16, 23]) {
const at = iso(y, m, day);
const price = priceOn(at);
const fee = 0.4;
const amount = Math.round((75 - fee) / price * SAT);
if (add({ kind: 'event', type: 'buy', asset: 'BTC', to: 'exchange', at, amount, fiatPrice: priceFromTotal(75, amount, 8, fee, 'buy'), fiatFee: fee })) {
pos.exchange += amount;
}
}
if (i % 4 === 3) {
const amount = pos.exchange - 100_000;
if (amount > 0 && add({ kind: 'event', type: 'transfer', asset: 'BTC', from: 'exchange', to: 'wallet', at: iso(y, m, 26), amount })) {
pos.exchange -= amount; pos.wallet += amount;
}
}
if (i === 20) {
const amount = 2_000_000;
if (add({ kind: 'event', type: 'transfer', asset: 'BTC', from: 'wallet', to: 'exchange', at: iso(y, m, 10), amount })) {
pos.wallet -= amount; pos.exchange += amount;
const at = iso(y, m, 11);
if (add({ kind: 'event', type: 'sell', asset: 'BTC', from: 'exchange', at, amount, fiatPrice: priceOn(at), fiatFee: 2 })) pos.exchange -= amount;
}
}
if (m % 3 === 2) {
const amount = Math.round(pos.bank * 0.015 / 4);
if (amount > 0 && add({ kind: 'event', type: 'interest', asset: 'EUR', to: 'bank', at: iso(y, m, 28), amount, fiatPrice: 1 })) pos.bank += amount;
}
const peil = (account, asset) => {
const at = new Date(Date.UTC(y, m, 28, 22, 0, 0)).toISOString();
add({ kind: 'valuation', asset, account, at, valuedOn: at.slice(0, 10), balance: pos[account],
fiatRate: asset === 'EUR' ? 1 : priceOn(at) });
};
if (m % 6 === 5) { peil('bank', 'EUR'); if (pos.wallet > 0) peil('wallet', 'BTC'); }
if (m % 12 === 11 && pos.exchange > 0) peil('exchange', 'BTC');
}
return rows;
}
export async function seedDemo(today = new Date()) {
const accounts = Object.values(ACCOUNTS);
await completeSetup({ assets: ['BTC'], persona: 'broad', accounts, widgets: ['standing', 'spot'] });
const assetId = (symbol) => one(`SELECT id FROM Asset WHERE symbol = ?`, [symbol]).id;
const accountId = (key) => one(`SELECT id FROM Account WHERE searchName = ?`, [toSearchName(ACCOUNTS[key].name)]).id;
const ids = { BTC: assetId('BTC'), EUR: assetId('EUR') };
for (const r of demoRows(today)) {
if (r.kind === 'event') {
await createEvent({
assetId: ids[r.asset], type: r.type, occurredAt: r.at, amount: r.amount, fiatPrice: r.fiatPrice ?? null,
fiatFee: r.fiatFee ?? null, fromAccountId: r.from ? accountId(r.from) : null,
toAccountId: r.to ? accountId(r.to) : null, source: 'manual',
});
} else {
await createValuation({ assetId: ids[r.asset], accountId: accountId(r.account), balance: r.balance,
fiatRate: r.fiatRate, valuedOn: r.valuedOn, rateAt: r.at, source: 'manual' });
}
}
}
export async function createDemoPortfolio(today = new Date()) {
const map = portfolioDir(CONFIG.PORTFOLIO.DEMO_NAME);
mkdirSync(map, { recursive: true });
initDatabase(join(map, 'db.sqlite'));
try {
await seedDemo(today);
} finally {
closeDatabase();
}
}
export async function ensureDemo(layout) {
if (!layout.fresh || hasFeature('portfolios') || listPortfolios().some(p => p.demo)) return false;
await createDemoPortfolio();
return true;
}