Files
market_screener/bin/finance.ts
T
2026-06-06 22:55:43 -04:00

86 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* bin/finance.ts — Personal Finance CLI
*/
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';
import type { PortfolioHolding } from '../server/types.js';
const PORTFOLIO_PATH = './portfolio.json';
async function main(): Promise<void> {
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')) as {
holdings: PortfolioHolding[];
};
const byType = holdings.reduce<Record<string, number>>((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`,
);
// ── 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 as Error).message}\n`);
}
} else {
console.log(' Add SIMPLEFIN_SETUP_TOKEN to .env for account balances & spending data\n');
}
// ── Screen stocks & ETFs
const screenableTickers = holdings
.filter((h) => (h.type ?? 'stock') !== 'crypto')
.map((h) => h.ticker.toUpperCase());
let results = {
STOCK: [] as any[],
ETF: [] as any[],
BOND: [] as any[],
ERROR: [] as any[],
marketContext: {} as any,
};
if (screenableTickers.length > 0) {
process.stdout.write(`📊 Screening ${screenableTickers.length} stock/ETF positions...`);
results = (await new ScreenerEngine().screenTickers(screenableTickers)) as any;
process.stdout.write(' done\n');
}
process.stdout.write('💡 Generating portfolio advice...');
const advice = await new PortfolioAdvisor().advise(holdings, results);
process.stdout.write(' done\n');
const reportPath = new FinanceReporter().generate(
advice as any,
personalFinance,
results.marketContext,
);
console.log(`\n✅ Finance report: ${reportPath}\n`);
}
main().catch((err) => {
console.error('Failed:', (err as Error).message);
process.exit(1);
});