phase-6: typescript introduction
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user