/** * bin/finance.js — Personal Finance CLI * * Fetches your accounts from SimpleFIN, screens your portfolio holdings, * and saves a finance-report.html with: * 1. Net worth + account overview (SimpleFIN) * 2. Portfolio hold/sell/add advice (screener + crypto prices) * 3. Spending breakdown (SimpleFIN) * * Usage: * npm run finance */ import 'dotenv/config'; import { readFileSync, existsSync } from 'fs'; import { SimpleFINClient, saveAccessUrlToEnv } from '../server/finance/clients/SimpleFINClient.js'; import { PersonalFinanceAnalyzer } from '../server/finance/PersonalFinanceAnalyzer.js'; import { PortfolioAdvisor } from '../server/finance/PortfolioAdvisor.js'; import { ScreenerEngine } from '../server/screener/ScreenerEngine.js'; import { FinanceReporter } from '../server/reporters/FinanceReporter.js'; const PORTFOLIO_PATH = './portfolio.json'; async function main() { // ── 1. Load portfolio if (!existsSync(PORTFOLIO_PATH)) { throw new Error('portfolio.json not found — edit it with your holdings and re-run.'); } const { holdings } = JSON.parse(readFileSync(PORTFOLIO_PATH, 'utf8')); const byType = holdings.reduce((acc, h) => { const t = h.type ?? 'stock'; acc[t] = (acc[t] ?? 0) + 1; return acc; }, {}); console.log( `šŸ“‹ Portfolio: ${holdings.length} positions — ${Object.entries(byType) .map(([t, n]) => `${n} ${t}`) .join(', ')}\n`, ); // ── 2. SimpleFIN accounts (optional) let personalFinance = null; if (process.env.SIMPLEFIN_ACCESS_URL || process.env.SIMPLEFIN_SETUP_TOKEN) { try { process.stdout.write('šŸ’° Fetching SimpleFIN accounts...'); const client = new SimpleFINClient({ onAccessUrlClaimed: saveAccessUrlToEnv }); await client.init(); const { accounts } = await client.getAccounts(); personalFinance = new PersonalFinanceAnalyzer().analyse(accounts); process.stdout.write(` ${accounts.length} accounts loaded\n`); } catch (err) { process.stdout.write(` skipped — ${err.message}\n`); } } else { console.log('ℹ Add SIMPLEFIN_SETUP_TOKEN to .env for account balances & spending data\n'); } // ── 3. Screen stocks & ETFs const screenableTickers = holdings .filter((h) => (h.type ?? 'stock') !== 'crypto') .map((h) => h.ticker.toUpperCase()); let results = { STOCK: [], ETF: [], BOND: [], ERROR: [], marketContext: {} }; if (screenableTickers.length > 0) { process.stdout.write(`šŸ“Š Screening ${screenableTickers.length} stock/ETF positions...`); results = await new ScreenerEngine().screenTickers(screenableTickers); process.stdout.write(' done\n'); } // ── 4. Portfolio advice + crypto prices process.stdout.write('šŸ’” Generating portfolio advice...'); const advice = await new PortfolioAdvisor().advise(holdings, results); process.stdout.write(' done\n'); // ── 5. Report const reportPath = new FinanceReporter().generate(advice, personalFinance, results.marketContext); console.log(`\nāœ… Finance report: ${reportPath}\n`); } main().catch((err) => { console.error('Failed:', err.message); process.exit(1); });