phase-6: typescript introduction
This commit is contained in:
committed by
saikiranvella
parent
57625c27d7
commit
c160e65bd6
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
Reference in New Issue
Block a user