130 lines
4.7 KiB
TypeScript
130 lines
4.7 KiB
TypeScript
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<TradingDecision> {
|
|
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 "buy" or "sell", also include an "executionPlan" object with:
|
|
- amount: number (shares to trade)
|
|
- riskManagement: object (e.g., { maxLossPercent: 2 })
|
|
- takeProfit: number (target take-profit price)
|
|
- stopLoss: number (stop-loss price or absolute value)
|
|
|
|
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 for buy and sell actions." },
|
|
{ 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"|\bamount\b)\s*:\s*([0-9.]+)/);
|
|
const takeProfitMatch = content.match(/(?:"takeProfit"|\btakeProfit\b)\s*:\s*([0-9.]+)/);
|
|
const stopLossMatch = content.match(/(?:"stopLoss"|\bstopLoss\b)\s*:\s*([0-9.]+)/);
|
|
const maxLossMatch = content.match(/(?:"maxLossPercent"|\bmaxLossPercent\b)\s*:\s*([0-9.]+)/);
|
|
const methodMatch = content.match(/(?:"method"|\bmethod\b)\s*:\s*"([^"]+)"/);
|
|
executionPlan = {} as any;
|
|
if (amountMatch) executionPlan.amount = parseFloat(amountMatch[1]);
|
|
if (takeProfitMatch) executionPlan.takeProfit = parseFloat(takeProfitMatch[1]);
|
|
if (stopLossMatch) executionPlan.stopLoss = parseFloat(stopLossMatch[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"|\bmaxLossPercent\b)\s*:\s*([0-9.]+)/);
|
|
const methodMatch2 = content.match(/(?:"method"|\bmethod\b)\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' || action === 'buy') && executionPlan) {
|
|
decision.executionPlan = executionPlan;
|
|
}
|
|
|
|
return decision;
|
|
}
|
|
} |