diff --git a/app/agents/__tests__/sentiment.test.ts b/app/agents/__tests__/sentiment.test.ts new file mode 100644 index 0000000..5fa5c0c --- /dev/null +++ b/app/agents/__tests__/sentiment.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect } from "vitest"; +import { SentimentAnalyst } from "../sentiment"; +import type { OpenRouterClient } from "../../lib/openrouter"; + +describe("SentimentAnalyst", () => { + it("should analyze sentiment from headlines", async () => { + const mockClient = { + createChatCompletion: async () => ({ + choices: [ + { + message: { + content: + '{"signal":"bullish","confidence":0.85,"reasoning":"Positive sentiment from news"}', + }, + }, + ], + }), + } as unknown as OpenRouterClient; + + const analyst = new SentimentAnalyst(mockClient); + const result = await analyst.analyze("AAPL", { + headlines: ["Apple beats earnings", "New iPhone launch"], + source: "news", + }); + + expect(result.analyst).toBe("sentiment"); + expect(result.signal.signal).toBe("bullish"); + }); + + it("should use specified model", () => { + const mockClient = {} as unknown as OpenRouterClient; + const analyst = new SentimentAnalyst(mockClient, { + model: "custom/model", + }); + + expect(analyst.getModel()).toBe("custom/model"); + }); +}); \ No newline at end of file diff --git a/app/agents/sentiment.ts b/app/agents/sentiment.ts new file mode 100644 index 0000000..c4264cf --- /dev/null +++ b/app/agents/sentiment.ts @@ -0,0 +1,84 @@ +import { OpenRouterClient } from "../lib/openrouter"; +import type { AnalystReport, AgentSignal, SignalType } from "../types/agents"; + +export interface SentimentConfig { + model?: string; +} + +export interface SentimentData { + headlines: string[]; + source?: "news" | "social" | "stocktwits"; +} + +export class SentimentAnalyst { + private client: OpenRouterClient; + private model: string; + + constructor(client: OpenRouterClient, config?: SentimentConfig) { + this.client = client; + this.model = config?.model ?? "google/gemini-2.0-flash-exp:free"; + } + + getModel(): string { + return this.model; + } + + async analyze(ticker: string, data: SentimentData): Promise { + const messages = [ + { + role: "system" as const, + content: + "You are a sentiment analyst. Analyze the headlines for the given ticker and provide a bullish, bearish, or neutral signal with reasoning. Respond in JSON format with 'signal', 'confidence', and 'reasoning' fields.", + }, + { + role: "user" as const, + content: `Analyze ${ticker} sentiment from ${data.source ?? "news"}:\n${data.headlines.join("\n")}`, + }, + ]; + + const response = await this.client.createChatCompletion( + messages, + this.model + ); + + const parsedResponse = response as { + choices?: Array<{ message?: { content?: string } }>; + }; + const content = parsedResponse.choices?.[0]?.message?.content ?? ""; + + let signal: SignalType = "neutral"; + let confidence = 0.5; + let reasoning = content; + + try { + const parsed = JSON.parse(content); + if ( + parsed.signal === "bullish" || + parsed.signal === "bearish" || + parsed.signal === "neutral" + ) { + signal = parsed.signal; + confidence = parsed.confidence ?? 0.5; + reasoning = parsed.reasoning ?? content; + } + } catch { + const lowerContent = content.toLowerCase(); + if (lowerContent.includes("bullish")) signal = "bullish"; + else if (lowerContent.includes("bearish")) signal = "bearish"; + } + + const agentSignal: AgentSignal = { + agent: "sentiment", + signal, + confidence, + reasoning, + timestamp: new Date().toISOString(), + }; + + return { + analyst: "sentiment", + report: content, + signal: agentSignal, + }; + } +} \ No newline at end of file