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 ?? "openai/gpt-oss-120b: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 JSON containing these fields: - action: "buy", "sell", or "hold" - confidence: a number between 0 and 1 - reasoning: brief explanation If the action is "sell", also include an "executionPlan" object with: - amount: number (shares to sell) - riskManagement: object (e.g., { maxLossPercent: 2 }) - takeProfit: number (target take-profit price) 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 and provides execution guidance when selling." }, { 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; let executionPlan: any | undefined; 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]; } // Try to parse executionPlan if provided in JSON const execMatch = content.match(/"executionPlan"\s*:\s*(\{[\s\S]*\})/); if (execMatch) { try { executionPlan = JSON.parse(execMatch[1]); } catch (err) { // fallback: try to extract primitive fields const amountMatch = content.match(/"amount"\s*:\s*([0-9.]+)/); const takeProfitMatch = content.match(/"takeProfit"\s*:\s*([0-9.]+)/); const maxLossMatch = content.match(/"maxLossPercent"\s*:\s*([0-9.]+)/); const methodMatch = content.match(/"method"\s*:\s*"([^"]+)"/); executionPlan = {} as any; if (amountMatch) executionPlan.amount = parseFloat(amountMatch[1]); if (takeProfitMatch) executionPlan.takeProfit = parseFloat(takeProfitMatch[1]); executionPlan.riskManagement = {}; if (maxLossMatch) executionPlan.riskManagement.maxLossPercent = parseFloat(maxLossMatch[1]); if (methodMatch) executionPlan.riskManagement.method = methodMatch[1]; } } // Additional fallback: if executionPlan parsed but missing nested riskManagement fields, try to extract them if (executionPlan && executionPlan.riskManagement == null) { const maxLossMatch2 = content.match(/"maxLossPercent"\s*:\s*([0-9.]+)/); const methodMatch2 = content.match(/"method"\s*:\s*"([^"]+)"/); if (maxLossMatch2 || methodMatch2) { executionPlan.riskManagement = executionPlan.riskManagement || {}; if (maxLossMatch2) executionPlan.riskManagement.maxLossPercent = parseFloat(maxLossMatch2[1]); if (methodMatch2) executionPlan.riskManagement.method = methodMatch2[1]; } } const decision: TradingDecision = { action, confidence, reasoning, agentSignals: allSignals, debateRounds: debates, }; if (action === 'sell' && executionPlan) { decision.executionPlan = executionPlan; } return decision; } }