From e913b32f34a9ef4a9300f3830ba4e98ee212d15a Mon Sep 17 00:00:00 2001 From: Henry Winkel Date: Thu, 14 May 2026 07:59:44 +0200 Subject: [PATCH] feat: add bullish and bearish researcher agents --- app/agents/__tests__/researchers.test.ts | 37 +++++++++++ app/agents/researchers.ts | 80 ++++++++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 app/agents/__tests__/researchers.test.ts create mode 100644 app/agents/researchers.ts diff --git a/app/agents/__tests__/researchers.test.ts b/app/agents/__tests__/researchers.test.ts new file mode 100644 index 0000000..1656c23 --- /dev/null +++ b/app/agents/__tests__/researchers.test.ts @@ -0,0 +1,37 @@ +import { describe, it, expect, vi } from "vitest"; +import { BullishResearcher, BearishResearcher } from "../researchers"; +import type { AnalystReport } from "../../types/agents"; + +describe("Researchers", () => { + const mockClient = { + createChatCompletion: vi.fn().mockResolvedValue({ + choices: [{ message: { content: "Bullish thesis content" } }], + }), + }; + + const mockReports: AnalystReport[] = [ + { + analyst: "fundamentals", + report: "Strong earnings growth", + signal: { + agent: "fundamentals", + signal: "bullish", + confidence: 0.8, + reasoning: "Revenue up 20%", + timestamp: "2024-01-01", + }, + }, + ]; + + it("should create bullish researcher", async () => { + const researcher = new BullishResearcher(mockClient as any); + const result = await researcher.research("AAPL", mockReports); + expect(result.researcher).toBe("bullish"); + }); + + it("should create bearish researcher", async () => { + const researcher = new BearishResearcher(mockClient as any); + const result = await researcher.research("AAPL", mockReports); + expect(result.researcher).toBe("bearish"); + }); +}); \ No newline at end of file diff --git a/app/agents/researchers.ts b/app/agents/researchers.ts new file mode 100644 index 0000000..b8eb4da --- /dev/null +++ b/app/agents/researchers.ts @@ -0,0 +1,80 @@ +import { OpenRouterClient } from "../lib/openrouter"; +import type { AnalystReport, DebateRound } from "../types/agents"; + +type ChatResponse = { + choices?: Array<{ message?: { content?: string } }>; +}; + +export class BullishResearcher { + private client: OpenRouterClient; + private model: string; + + constructor(client: OpenRouterClient, model?: string) { + this.client = client; + this.model = model ?? "google/gemini-2.0-flash-exp:free"; + } + + async research(ticker: string, reports: AnalystReport[]): Promise { + const reportSummaries = reports + .map((r) => `${r.analyst}: ${r.signal.signal} - ${r.report}`) + .join("\n"); + + const prompt = `Analyze these analyst reports for ${ticker} and synthesize a bullish thesis: +${reportSummaries} + +Provide a bullish view based on the positive signals and reasoning.`; + + const response = await this.client.createChatCompletion( + [ + { role: "system", content: "You are a bullish equity researcher who finds the positive investment case." }, + { role: "user", content: prompt }, + ], + this.model + ); + + const content = ((response as ChatResponse).choices?.[0]?.message?.content) ?? ""; + + return { + bullishView: content, + bearishView: "", + researcher: "bullish", + }; + } +} + +export class BearishResearcher { + private client: OpenRouterClient; + private model: string; + + constructor(client: OpenRouterClient, model?: string) { + this.client = client; + this.model = model ?? "google/gemini-2.0-flash-exp:free"; + } + + async research(ticker: string, reports: AnalystReport[]): Promise { + const reportSummaries = reports + .map((r) => `${r.analyst}: ${r.signal.signal} - ${r.report}`) + .join("\n"); + + const prompt = `Analyze these analyst reports for ${ticker} and synthesize a bearish thesis: +${reportSummaries} + +Provide a bearish view based on the risks and negative signals.`; + + const response = await this.client.createChatCompletion( + [ + { role: "system", content: "You are a bearish equity researcher who identifies investment risks." }, + { role: "user", content: prompt }, + ], + this.model + ); + + const content = ((response as ChatResponse).choices?.[0]?.message?.content) ?? ""; + + return { + bullishView: "", + bearishView: content, + researcher: "bearish", + }; + } +} \ No newline at end of file