phase-6: typescript introduction

This commit is contained in:
Kazuma
2026-06-04 22:16:48 -04:00
committed by Kazuma
parent de8427d578
commit 2b785aa861
69 changed files with 2323 additions and 1036 deletions
@@ -1,16 +1,32 @@
import { YahooClient } from '../market/YahooClient.js';
import type { Logger } from '../types.js';
interface Story {
title: string;
publisher: string;
link: string;
relatedTickers: string[];
}
interface CatalystResult {
tickers: string[];
stories: Story[];
}
const NEWS_QUERIES = ['stock market today', 'earnings report', 'market news'];
const MAX_STORIES = 15;
const TICKER_REGEX = /^[A-Z]{1,6}$/;
export class CatalystAnalyst {
constructor({ logger } = {}) {
private client: YahooClient;
private logger: Pick<Logger, 'write'>;
constructor({ logger }: { logger?: Pick<Logger, 'write'> } = {}) {
this.client = new YahooClient();
this.logger = logger ?? { write: (msg) => process.stdout.write(msg) };
this.logger = logger ?? { write: (msg: string) => process.stdout.write(msg) };
}
async run() {
async run(): Promise<CatalystResult> {
this.logger.write('🔍 Fetching market news...');
const stories = await this._fetchNews();
const tickers = this._extractTickers(stories);
@@ -18,12 +34,15 @@ export class CatalystAnalyst {
return { tickers, stories };
}
async _fetchNews() {
const seen = new Map();
private async _fetchNews(): Promise<Story[]> {
const seen = new Map<string, Story>();
for (const query of NEWS_QUERIES) {
try {
const { news = [] } = await this.client.yf.search(query, { newsCount: 8, quotesCount: 0 });
for (const s of news) {
const { news = [] } = await (this.client as any).yf.search(query, {
newsCount: 8,
quotesCount: 0,
});
for (const s of news as any[]) {
if (!seen.has(s.title)) {
seen.set(s.title, {
title: s.title,
@@ -40,8 +59,8 @@ export class CatalystAnalyst {
return [...seen.values()].slice(0, MAX_STORIES);
}
_extractTickers(stories) {
const tickers = new Set();
private _extractTickers(stories: Story[]): string[] {
const tickers = new Set<string>();
for (const { relatedTickers } of stories) {
for (const t of relatedTickers) {
const clean = t.split(':')[0].toUpperCase();
@@ -1,15 +1,10 @@
import Anthropic from '@anthropic-ai/sdk';
import type { Logger, LLMAnalysis } from '../types.js';
// LLMAnalyst — uses Claude Haiku to analyze news catalyst stories.
//
// Given a list of news headlines and the tickers already identified,
// it produces:
// - A concise market summary (2-3 sentences)
// - Industries likely to be affected (beyond the directly mentioned tickers)
// - Up to 5 related tickers worth watching
// - A risk sentiment assessment (BULLISH / NEUTRAL / BEARISH)
//
// Requires ANTHROPIC_API_KEY in environment.
interface Story {
title: string;
publisher?: string;
}
const SYSTEM_PROMPT = `You are a professional equity analyst. You will be given a list of today's market news headlines and the tickers already identified as catalysts.
@@ -32,21 +27,21 @@ Return ONLY valid JSON in this exact shape — no markdown, no explanation:
}`;
export class LLMAnalyst {
constructor({ logger } = {}) {
private logger: Pick<Logger, 'log' | 'warn'>;
private client: Anthropic | null;
constructor({ logger }: { logger?: Pick<Logger, 'log' | 'warn'> } = {}) {
this.logger = logger ?? { log: console.log, warn: console.warn };
this.client = process.env.ANTHROPIC_API_KEY
? new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })
: null;
}
// Analyzes news stories and returns structured market intelligence.
// Returns null if ANTHROPIC_API_KEY is not set (graceful degradation).
async analyze(stories, existingTickers = []) {
async analyze(stories: Story[], existingTickers: string[] = []): Promise<LLMAnalysis | null> {
if (!this.client) {
this.logger.warn('LLMAnalyst: ANTHROPIC_API_KEY not set — skipping analysis');
return null;
}
if (!stories?.length) return null;
const headlines = stories
@@ -64,14 +59,14 @@ export class LLMAnalyst {
messages: [{ role: 'user', content: userMessage }],
});
const raw = response.content[0]?.text ?? '';
const raw = (response.content[0] as { text?: string })?.text ?? '';
const cleaned = raw
.replace(/^```(?:json)?\s*/i, '')
.replace(/```\s*$/i, '')
.trim();
return JSON.parse(cleaned);
return JSON.parse(cleaned) as LLMAnalysis;
} catch (err) {
this.logger.warn('LLMAnalyst: analysis failed —', err.message);
this.logger.warn('LLMAnalyst: analysis failed —', (err as Error).message);
return null;
}
}