phase-1: optimize code
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 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 '../src/finance/clients/SimpleFINClient.js';
|
||||
import { PersonalFinanceAnalyzer } from '../src/finance/PersonalFinanceAnalyzer.js';
|
||||
import { PortfolioAdvisor } from '../src/finance/PortfolioAdvisor.js';
|
||||
import { ScreenerEngine } from '../src/screener/ScreenerEngine.js';
|
||||
import { FinanceReporter } from '../src/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);
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* bin/screen.js — Market Screener CLI
|
||||
*
|
||||
* Fetches today's catalyst tickers from Yahoo Finance news,
|
||||
* screens them under both Market-Adjusted and Fundamental lenses,
|
||||
* and saves a full HTML report.
|
||||
*
|
||||
* Usage:
|
||||
* npm start → Yahoo news → catalyst tickers → screen
|
||||
* npm start -- watch → default watchlist
|
||||
* npm start -- AAPL MSFT VOO → specific tickers
|
||||
*/
|
||||
|
||||
import 'dotenv/config';
|
||||
import { CatalystAnalyst } from '../src/analyst/CatalystAnalyst.js';
|
||||
import { ScreenerEngine } from '../src/screener/ScreenerEngine.js';
|
||||
import { HtmlReporter } from '../src/reporters/HtmlReporter.js';
|
||||
|
||||
const DEFAULT_WATCHLIST = [
|
||||
// Stocks
|
||||
'PLTR',
|
||||
'AAPL',
|
||||
'MSFT',
|
||||
'TSLA',
|
||||
'O',
|
||||
// ETFs
|
||||
'VOO',
|
||||
'QQQ',
|
||||
// Bonds
|
||||
'BND',
|
||||
'LQD',
|
||||
'TLT',
|
||||
'IEF',
|
||||
'SHY',
|
||||
'GOVT',
|
||||
'AGG',
|
||||
'MUB',
|
||||
];
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
let tickers = [];
|
||||
|
||||
if (args.length > 0 && args[0] !== 'watch') {
|
||||
tickers = args.map((t) => t.toUpperCase());
|
||||
console.log(`📋 Screening: ${tickers.join(', ')}\n`);
|
||||
} else if (args[0] === 'watch') {
|
||||
tickers = DEFAULT_WATCHLIST;
|
||||
console.log(`📋 Screening default watchlist (${tickers.length} tickers)\n`);
|
||||
} else {
|
||||
try {
|
||||
const { tickers: newsTickers, stories } = await new CatalystAnalyst().run();
|
||||
|
||||
if (newsTickers.length === 0) {
|
||||
console.warn("⚠ No tickers in today's news — using default watchlist\n");
|
||||
tickers = DEFAULT_WATCHLIST;
|
||||
} else {
|
||||
tickers = newsTickers;
|
||||
console.log("\n📰 Stories driving today's screen:");
|
||||
stories.slice(0, 5).forEach((s) => {
|
||||
const tags = s.relatedTickers.slice(0, 3).join(', ');
|
||||
console.log(` • ${s.title}${tags ? ` [${tags}]` : ''}`);
|
||||
});
|
||||
console.log(`\n📋 Tickers: ${tickers.join(', ')}\n`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`⚠ Catalyst analysis failed (${err.message}) — using default watchlist\n`);
|
||||
tickers = DEFAULT_WATCHLIST;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const { STOCK, ETF, BOND, ERROR, marketContext } =
|
||||
await new ScreenerEngine().screenWithProgress(tickers);
|
||||
const reportPath = new HtmlReporter().generate({ STOCK, ETF, BOND, ERROR }, marketContext);
|
||||
console.log(`\n✅ Done — report saved to: ${reportPath}\n`);
|
||||
} catch (err) {
|
||||
console.error('Screener failed:', err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,14 @@
|
||||
import 'dotenv/config';
|
||||
import { buildApp } from '../src/server/app.js';
|
||||
|
||||
const PORT = process.env.PORT ?? 3000;
|
||||
const HOST = process.env.HOST ?? '0.0.0.0';
|
||||
|
||||
const app = await buildApp();
|
||||
|
||||
try {
|
||||
await app.listen({ port: Number(PORT), host: HOST });
|
||||
} catch (err) {
|
||||
app.log.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user