// finance.js — Personal Finance Entry Point // // Runs independently of the news screener. // Fetches your accounts from SimpleFIN, screens your portfolio holdings, // and generates a finance-report.html with: // // 1. Net worth + account overview (SimpleFIN) // 2. Portfolio positions with 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 } from './src/finance/SimpleFINClient.js'; import { PersonalFinanceAnalyzer } from './src/finance/PersonalFinanceAnalyzer.js'; import { PortfolioAdvisor } from './src/finance/PortfolioAdvisor.js'; import { ScreenerEngine } from './src/core/engine/ScreenerEngine.js'; import { FinanceReporter } from './src/reporters/FinanceReporter.js'; async function main() { // ── 1. Load portfolio ────────────────────────────────────────────────────── if (!existsSync('./portfolio.json')) { console.error('portfolio.json not found. Edit it with your holdings and re-run.'); process.exit(1); } const { holdings } = JSON.parse(readFileSync('./portfolio.json', '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. Fetch SimpleFIN data ──────────────────────────────────────────────── let personalFinance = null; if (process.env.SIMPLEFIN_ACCESS_URL || process.env.SIMPLEFIN_SETUP_TOKEN) { try { process.stdout.write('💰 Fetching accounts from SimpleFIN...'); const client = new SimpleFINClient(); 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( 'ℹ SimpleFIN not configured — add SIMPLEFIN_SETUP_TOKEN to .env for account data\n', ); } // ── 3. Screen stocks + ETFs (crypto handled separately by PortfolioAdvisor) ─ 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. Fetch crypto prices + generate advice ─────────────────────────────── process.stdout.write('💡 Generating advice...'); const advice = await new PortfolioAdvisor().advise(holdings, results); process.stdout.write(' done\n'); // ── 5. Generate report ───────────────────────────────────────────────────── const reportPath = new FinanceReporter().generate(advice, personalFinance, results.marketContext); console.log(`\n✅ Finance report saved to: ${reportPath}\n`); } main().catch((err) => console.error('Failed:', err.message));