diff --git a/app/agents/__tests__/technical.test.ts b/app/agents/__tests__/technical.test.ts new file mode 100644 index 0000000..b5114d0 --- /dev/null +++ b/app/agents/__tests__/technical.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect } from "vitest"; +import { TechnicalAnalyst } from "../technical"; +import type { OpenRouterClient } from "../../lib/openrouter"; + +describe("TechnicalAnalyst", () => { + it("should analyze technical indicators", async () => { + const mockClient = { + createChatCompletion: async () => ({ + choices: [ + { + message: { + content: + '{"signal":"bullish","confidence":0.85,"reasoning":"Strong technical setup"}', + }, + }, + ], + }), + } as unknown as OpenRouterClient; + + const analyst = new TechnicalAnalyst(mockClient); + const result = await analyst.analyze("AAPL", { + prices: [100, 101, 102, 103, 104, 105, 106, 107, 108, 109], + sma: 105, + ema: 106, + rsi: 65, + macd: 2.5, + }); + + expect(result.analyst).toBe("technical"); + expect(result.signal.signal).toBe("bullish"); + }); + + it("should use specified model", () => { + const mockClient = {} as unknown as OpenRouterClient; + const analyst = new TechnicalAnalyst(mockClient, { + model: "custom/model", + }); + + expect(analyst.getModel()).toBe("custom/model"); + }); +}); \ No newline at end of file diff --git a/app/agents/technical.ts b/app/agents/technical.ts new file mode 100644 index 0000000..80bf726 --- /dev/null +++ b/app/agents/technical.ts @@ -0,0 +1,92 @@ +import { OpenRouterClient } from "../lib/openrouter"; +import type { AnalystReport, AgentSignal, SignalType } from "../types/agents"; + +export interface TechnicalData { + prices: number[]; + sma: number; + ema: number; + rsi: number; + macd: number; +} + +export interface TechnicalAnalystConfig { + model?: string; +} + +export class TechnicalAnalyst { + private client: OpenRouterClient; + private model: string; + + constructor(client: OpenRouterClient, config?: TechnicalAnalystConfig) { + this.client = client; + this.model = config?.model ?? "google/gemini-2.0-flash-exp:free"; + } + + getModel(): string { + return this.model; + } + + async analyze(ticker: string, data: TechnicalData): Promise { + const messages = [ + { + role: "system" as const, + content: + "You are a technical analyst. Analyze the technical indicators (SMA, EMA, RSI, MACD) 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} technical data: +SMA: ${data.sma} +EMA: ${data.ema} +RSI: ${data.rsi} +MACD: ${data.macd} +Prices: ${data.prices.slice(-10).join(", ")}`, + }, + ]; + + 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: "technical", + signal, + confidence, + reasoning, + timestamp: new Date().toISOString(), + }; + + return { + analyst: "technical", + report: content, + signal: agentSignal, + }; + } +} \ No newline at end of file