From 0930e1149532983a48f3e991973508a752157846 Mon Sep 17 00:00:00 2001 From: Henry Winkel Date: Thu, 14 May 2026 08:04:14 +0200 Subject: [PATCH] feat: add trading graph orchestrator --- app/agents/__tests__/tradingGraph.test.ts | 32 ++++++++++ app/agents/tradingGraph.ts | 76 +++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 app/agents/__tests__/tradingGraph.test.ts create mode 100644 app/agents/tradingGraph.ts diff --git a/app/agents/__tests__/tradingGraph.test.ts b/app/agents/__tests__/tradingGraph.test.ts new file mode 100644 index 0000000..3f294fb --- /dev/null +++ b/app/agents/__tests__/tradingGraph.test.ts @@ -0,0 +1,32 @@ +import { describe, it, expect, vi } from "vitest"; +import { TradingGraph } from "../tradingGraph"; + +describe("TradingGraph", () => { + const mockClient = { + createChatCompletion: vi.fn().mockResolvedValue({ + choices: [{ message: { content: '{"signal":"bullish","confidence":0.8,"reasoning":"Test"}' } }], + }), + }; + + const mockInput = { + financialData: "Revenue: 1B, Growth: 10%, Debt: low", + technicalData: { + prices: [100, 101, 102, 103, 104, 105, 106, 107, 108, 109], + sma: 105, + ema: 106, + rsi: 65, + macd: 2.5, + }, + sentimentData: { + headlines: ["Company beats earnings expectations"], + source: "news" as const, + }, + }; + + it("should run full analysis", async () => { + const graph = new TradingGraph(mockClient as any); + const decision = await graph.propagate("AAPL", mockInput); + expect(decision).toHaveProperty("action"); + expect(decision).toHaveProperty("confidence"); + }); +}); \ No newline at end of file diff --git a/app/agents/tradingGraph.ts b/app/agents/tradingGraph.ts new file mode 100644 index 0000000..771cc19 --- /dev/null +++ b/app/agents/tradingGraph.ts @@ -0,0 +1,76 @@ +import { OpenRouterClient } from "../lib/openrouter"; +import { FundamentalsAnalyst } from "./fundamentals"; +import { TechnicalAnalyst } from "./technical"; +import { SentimentAnalyst } from "./sentiment"; +import { BullishResearcher, BearishResearcher } from "./researchers"; +import { Trader } from "./trader"; +import type { AnalystReport, DebateRound, TradingDecision, AgentSignal } from "../types/agents"; + +export class TradingGraph { + private client: OpenRouterClient; + private model: string; + private fundamentalsAnalyst: FundamentalsAnalyst; + private technicalAnalyst: TechnicalAnalyst; + private sentimentAnalyst: SentimentAnalyst; + private bullishResearcher: BullishResearcher; + private bearishResearcher: BearishResearcher; + private trader: Trader; + + constructor(client: OpenRouterClient, model?: string) { + this.client = client; + this.model = model ?? "google/gemini-2.0-flash-exp:free"; + + this.fundamentalsAnalyst = new FundamentalsAnalyst(client, { model: this.model }); + this.technicalAnalyst = new TechnicalAnalyst(client, { model: this.model }); + this.sentimentAnalyst = new SentimentAnalyst(client, { model: this.model }); + this.bullishResearcher = new BullishResearcher(client, this.model); + this.bearishResearcher = new BearishResearcher(client, this.model); + this.trader = new Trader(client, this.model); + } + + async propagate( + ticker: string, + input: { + financialData: string; + technicalData: { prices: number[]; sma: number; ema: number; rsi: number; macd: number }; + sentimentData: { headlines: string[]; source?: "news" | "social" | "stocktwits" }; + } + ): Promise { + const reports = await this.runAnalysts(ticker, input); + const debates = await this.runDebate(ticker, reports); + const decision = await this.trader.decide(ticker, reports, debates); + return decision; + } + + private async runAnalysts( + ticker: string, + input: { + financialData: string; + technicalData: { prices: number[]; sma: number; ema: number; rsi: number; macd: number }; + sentimentData: { headlines: string[]; source?: "news" | "social" | "stocktwits" }; + } + ): Promise { + const [fundamentals, technical, sentiment] = await Promise.all([ + this.fundamentalsAnalyst.analyze(ticker, input.financialData), + this.technicalAnalyst.analyze(ticker, input.technicalData), + this.sentimentAnalyst.analyze(ticker, input.sentimentData), + ]); + + return [fundamentals, technical, sentiment]; + } + + private async runDebate(ticker: string, reports: AnalystReport[]): Promise { + const [bullish, bearish] = await Promise.all([ + this.bullishResearcher.research(ticker, reports), + this.bearishResearcher.research(ticker, reports), + ]); + + return [ + { + bullishView: bullish.bullishView, + bearishView: bearish.bearishView, + researcher: "bullish", + }, + ]; + } +} \ No newline at end of file