phase-2: extract shared utils

This commit is contained in:
Kazuma
2026-06-04 11:06:30 -04:00
parent b75e8bda72
commit 0a0a368b87
49 changed files with 299 additions and 120 deletions
+53
View File
@@ -0,0 +1,53 @@
import { YahooClient } from '../market/YahooClient.js';
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 } = {}) {
this.client = new YahooClient();
this.logger = logger ?? { write: (msg) => process.stdout.write(msg) };
}
async run() {
this.logger.write('🔍 Fetching market news...');
const stories = await this._fetchNews();
const tickers = this._extractTickers(stories);
this.logger.write(` ${stories.length} stories, ${tickers.length} tickers\n`);
return { tickers, stories };
}
async _fetchNews() {
const seen = new Map();
for (const query of NEWS_QUERIES) {
try {
const { news = [] } = await this.client.yf.search(query, { newsCount: 8, quotesCount: 0 });
for (const s of news) {
if (!seen.has(s.title)) {
seen.set(s.title, {
title: s.title,
publisher: s.publisher,
link: s.link,
relatedTickers: s.relatedTickers ?? [],
});
}
}
} catch {
/* skip failed query */
}
}
return [...seen.values()].slice(0, MAX_STORIES);
}
_extractTickers(stories) {
const tickers = new Set();
for (const { relatedTickers } of stories) {
for (const t of relatedTickers) {
const clean = t.split(':')[0].toUpperCase();
if (TICKER_REGEX.test(clean)) tickers.add(clean);
}
}
return [...tickers];
}
}
+78
View File
@@ -0,0 +1,78 @@
import Anthropic from '@anthropic-ai/sdk';
// 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.
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.
Your job is to:
1. Write a 2-3 sentence market summary capturing the dominant theme
2. Identify up to 4 industries that are likely to be secondarily affected (not directly mentioned but impacted by contagion, supply chain, regulation, or macro effects)
3. Suggest up to 5 related ticker symbols worth screening that are NOT already in the provided list
4. Assess overall market sentiment as BULLISH, NEUTRAL, or BEARISH based on the news
Return ONLY valid JSON in this exact shape — no markdown, no explanation:
{
"summary": "string",
"sentiment": "BULLISH" | "NEUTRAL" | "BEARISH",
"affectedIndustries": [
{ "name": "string", "reason": "string (one sentence)" }
],
"relatedTickers": [
{ "ticker": "string", "reason": "string (one sentence)" }
]
}`;
export class LLMAnalyst {
constructor({ logger } = {}) {
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 = []) {
if (!this.client) {
this.logger.warn('LLMAnalyst: ANTHROPIC_API_KEY not set — skipping analysis');
return null;
}
if (!stories?.length) return null;
const headlines = stories
.slice(0, 15)
.map((s, i) => `${i + 1}. ${s.title} (${s.publisher ?? 'unknown'})`)
.join('\n');
const userMessage = `Today's market news headlines:\n\n${headlines}\n\nAlready identified catalyst tickers: ${existingTickers.join(', ') || 'none'}`;
try {
const response = await this.client.messages.create({
model: 'claude-haiku-4-5',
max_tokens: 1024,
system: SYSTEM_PROMPT,
messages: [{ role: 'user', content: userMessage }],
});
const raw = response.content[0]?.text ?? '';
const cleaned = raw
.replace(/^```(?:json)?\s*/i, '')
.replace(/```\s*$/i, '')
.trim();
return JSON.parse(cleaned);
} catch (err) {
this.logger.warn('LLMAnalyst: analysis failed —', err.message);
return null;
}
}
}