phase-6: typescript introduction

This commit is contained in:
Kazuma
2026-06-04 22:16:48 -04:00
parent 16bd95aa85
commit 69d13c3dbe
69 changed files with 2323 additions and 1036 deletions
+26 -25
View File
@@ -1,14 +1,5 @@
/**
* 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
* bin/finance.ts Personal Finance CLI
*/
import 'dotenv/config';
@@ -18,17 +9,19 @@ import { PersonalFinanceAnalyzer } from '../server/finance/PersonalFinanceAnalyz
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() {
// ── 1. Load portfolio
if (!existsSync(PORTFOLIO_PATH)) {
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'));
const byType = holdings.reduce((acc, h) => {
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;
@@ -39,7 +32,7 @@ async function main() {
.join(', ')}\n`,
);
// ── 2. SimpleFIN accounts (optional)
// ── SimpleFIN accounts (optional)
let personalFinance = null;
if (process.env.SIMPLEFIN_ACCESS_URL || process.env.SIMPLEFIN_SETUP_TOKEN) {
try {
@@ -50,35 +43,43 @@ async function main() {
personalFinance = new PersonalFinanceAnalyzer().analyse(accounts);
process.stdout.write(` ${accounts.length} accounts loaded\n`);
} catch (err) {
process.stdout.write(` skipped — ${err.message}\n`);
process.stdout.write(` skipped — ${(err as Error).message}\n`);
}
} else {
console.log(' Add SIMPLEFIN_SETUP_TOKEN to .env for account balances & spending data\n');
}
// ── 3. Screen stocks & ETFs
// ── Screen stocks & ETFs
const screenableTickers = holdings
.filter((h) => (h.type ?? 'stock') !== 'crypto')
.map((h) => h.ticker.toUpperCase());
let results = { STOCK: [], ETF: [], BOND: [], ERROR: [], marketContext: {} };
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);
results = (await new ScreenerEngine().screenTickers(screenableTickers)) as any;
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);
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.message);
console.error('Failed:', (err as Error).message);
process.exit(1);
});
+12 -11
View File
@@ -1,5 +1,5 @@
/**
* bin/screen.js Market Screener CLI
* bin/screen.ts Market Screener CLI
*
* Fetches today's catalyst tickers from Yahoo Finance news,
* screens them under both Market-Adjusted and Fundamental lenses,
@@ -16,17 +16,14 @@ import { CatalystAnalyst } from '../server/analyst/CatalystAnalyst.js';
import { ScreenerEngine } from '../server/screener/ScreenerEngine.js';
import { HtmlReporter } from '../server/reporters/HtmlReporter.js';
const DEFAULT_WATCHLIST = [
// Stocks
const DEFAULT_WATCHLIST: string[] = [
'PLTR',
'AAPL',
'MSFT',
'TSLA',
'O',
// ETFs
'VOO',
'QQQ',
// Bonds
'BND',
'LQD',
'TLT',
@@ -37,9 +34,9 @@ const DEFAULT_WATCHLIST = [
'MUB',
];
async function main() {
async function main(): Promise<void> {
const args = process.argv.slice(2);
let tickers = [];
let tickers: string[] = [];
if (args.length > 0 && args[0] !== 'watch') {
tickers = args.map((t) => t.toUpperCase());
@@ -50,7 +47,6 @@ async function main() {
} 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;
@@ -64,7 +60,9 @@ async function main() {
console.log(`\n📋 Tickers: ${tickers.join(', ')}\n`);
}
} catch (err) {
console.warn(`⚠ Catalyst analysis failed (${err.message}) — using default watchlist\n`);
console.warn(
`⚠ Catalyst analysis failed (${(err as Error).message}) — using default watchlist\n`,
);
tickers = DEFAULT_WATCHLIST;
}
}
@@ -72,10 +70,13 @@ async function main() {
try {
const { STOCK, ETF, BOND, ERROR, marketContext } =
await new ScreenerEngine().screenWithProgress(tickers);
const reportPath = new HtmlReporter().generate({ STOCK, ETF, BOND, ERROR }, marketContext);
const reportPath = new HtmlReporter().generate(
{ STOCK, ETF, BOND, ERROR } as any,
marketContext,
);
console.log(`\n✅ Done — report saved to: ${reportPath}\n`);
} catch (err) {
console.error('Screener failed:', err.message);
console.error('Screener failed:', (err as Error).message);
process.exit(1);
}
}
View File