From 86fe670ca09b29d9e503ced7f1633db5859a9ec8 Mon Sep 17 00:00:00 2001 From: Henry Winkel Date: Thu, 14 May 2026 08:01:59 +0200 Subject: [PATCH] feat: add trader agent --- app/agents/__tests__/trader.test.ts | 39 ++++++++++++++ app/agents/trader.ts | 84 +++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 app/agents/__tests__/trader.test.ts create mode 100644 app/agents/trader.ts diff --git a/app/agents/__tests__/trader.test.ts b/app/agents/__tests__/trader.test.ts new file mode 100644 index 0000000..487fe7a --- /dev/null +++ b/app/agents/__tests__/trader.test.ts @@ -0,0 +1,39 @@ +import { describe, it, expect, vi } from "vitest"; +import { Trader } from "../trader"; +import type { AnalystReport, DebateRound } from "../../types/agents"; + +describe("Trader", () => { + const mockClient = { + createChatCompletion: vi.fn().mockResolvedValue({ + choices: [{ message: { content: '{"action": "buy", "confidence": 0.75, "reasoning": "Strong bullish signals"}' } }], + }), + }; + + 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", + }, + }, + ]; + + const mockDebates: DebateRound[] = [ + { + bullishView: "Strong fundamentals", + bearishView: "Market volatility", + researcher: "bullish", + }, + ]; + + it("should make trading decision", async () => { + const trader = new Trader(mockClient as any); + const decision = await trader.decide("AAPL", mockReports, mockDebates); + expect(decision.action).toBe("buy"); + }); +}); \ No newline at end of file diff --git a/app/agents/trader.ts b/app/agents/trader.ts new file mode 100644 index 0000000..9fdd51f --- /dev/null +++ b/app/agents/trader.ts @@ -0,0 +1,84 @@ +import { OpenRouterClient } from "../lib/openrouter"; +import type { AnalystReport, DebateRound, TradingDecision } from "../types/agents"; + +type ChatResponse = { + choices?: Array<{ message?: { content?: string } }>; +}; + +export class Trader { + 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 decide( + ticker: string, + reports: AnalystReport[], + debates: DebateRound[] + ): Promise { + const signalSummaries = reports + .map((r) => `${r.analyst}: ${r.signal.signal} (confidence: ${r.signal.confidence}) - ${r.report}`) + .join("\n"); + + const debateSummaries = debates + .map((d) => `Bullish: ${d.bullishView}\nBearish: ${d.bearishView}`) + .join("\n"); + + const allSignals = reports.map((r) => r.signal); + + const prompt = `Analyze all signals and debate rounds for ${ticker}: + +Signals: +${signalSummaries} + +Debate Rounds: +${debateSummaries} + +Based on all the information above, make a trading decision. Respond with: +- action: "buy", "sell", or "hold" +- confidence: a number between 0 and 1 +- reasoning: brief explanation + +Format your response as JSON with these fields.`; + + const response = await this.client.createChatCompletion( + [ + { role: "system", content: "You are a trading agent that makes buy/sell/hold decisions based on all available signals." }, + { role: "user", content: prompt }, + ], + this.model + ); + + const content = ((response as ChatResponse).choices?.[0]?.message?.content) ?? ""; + + let action: 'buy' | 'sell' | 'hold' = 'hold'; + let confidence = 0.5; + let reasoning = content; + + const actionMatch = content.match(/"action"\s*:\s*"(buy|sell|hold)"/); + if (actionMatch) { + action = actionMatch[1] as 'buy' | 'sell' | 'hold'; + } + + const confidenceMatch = content.match(/"confidence"\s*:\s*([0-9.]+)/); + if (confidenceMatch) { + confidence = parseFloat(confidenceMatch[1]); + } + + const reasoningMatch = content.match(/"reasoning"\s*:\s*"([^"]+)"/); + if (reasoningMatch) { + reasoning = reasoningMatch[1]; + } + + return { + action, + confidence, + reasoning, + agentSignals: allSignals, + debateRounds: debates, + }; + } +} \ No newline at end of file