feat: add technical analyst agent
This commit is contained in:
@@ -0,0 +1,41 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { TechnicalAnalyst } from "../technical";
|
||||||
|
import type { OpenRouterClient } from "../../lib/openrouter";
|
||||||
|
|
||||||
|
describe("TechnicalAnalyst", () => {
|
||||||
|
it("should analyze technical indicators", async () => {
|
||||||
|
const mockClient = {
|
||||||
|
createChatCompletion: async () => ({
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
message: {
|
||||||
|
content:
|
||||||
|
'{"signal":"bullish","confidence":0.85,"reasoning":"Strong technical setup"}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
} as unknown as OpenRouterClient;
|
||||||
|
|
||||||
|
const analyst = new TechnicalAnalyst(mockClient);
|
||||||
|
const result = await analyst.analyze("AAPL", {
|
||||||
|
prices: [100, 101, 102, 103, 104, 105, 106, 107, 108, 109],
|
||||||
|
sma: 105,
|
||||||
|
ema: 106,
|
||||||
|
rsi: 65,
|
||||||
|
macd: 2.5,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.analyst).toBe("technical");
|
||||||
|
expect(result.signal.signal).toBe("bullish");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use specified model", () => {
|
||||||
|
const mockClient = {} as unknown as OpenRouterClient;
|
||||||
|
const analyst = new TechnicalAnalyst(mockClient, {
|
||||||
|
model: "custom/model",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(analyst.getModel()).toBe("custom/model");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { OpenRouterClient } from "../lib/openrouter";
|
||||||
|
import type { AnalystReport, AgentSignal, SignalType } from "../types/agents";
|
||||||
|
|
||||||
|
export interface TechnicalData {
|
||||||
|
prices: number[];
|
||||||
|
sma: number;
|
||||||
|
ema: number;
|
||||||
|
rsi: number;
|
||||||
|
macd: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TechnicalAnalystConfig {
|
||||||
|
model?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TechnicalAnalyst {
|
||||||
|
private client: OpenRouterClient;
|
||||||
|
private model: string;
|
||||||
|
|
||||||
|
constructor(client: OpenRouterClient, config?: TechnicalAnalystConfig) {
|
||||||
|
this.client = client;
|
||||||
|
this.model = config?.model ?? "google/gemini-2.0-flash-exp:free";
|
||||||
|
}
|
||||||
|
|
||||||
|
getModel(): string {
|
||||||
|
return this.model;
|
||||||
|
}
|
||||||
|
|
||||||
|
async analyze(ticker: string, data: TechnicalData): Promise<AnalystReport> {
|
||||||
|
const messages = [
|
||||||
|
{
|
||||||
|
role: "system" as const,
|
||||||
|
content:
|
||||||
|
"You are a technical analyst. Analyze the technical indicators (SMA, EMA, RSI, MACD) for the given ticker and provide a bullish, bearish, or neutral signal with reasoning. Respond in JSON format with 'signal', 'confidence', and 'reasoning' fields.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "user" as const,
|
||||||
|
content: `Analyze ${ticker} technical data:
|
||||||
|
SMA: ${data.sma}
|
||||||
|
EMA: ${data.ema}
|
||||||
|
RSI: ${data.rsi}
|
||||||
|
MACD: ${data.macd}
|
||||||
|
Prices: ${data.prices.slice(-10).join(", ")}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const response = await this.client.createChatCompletion(
|
||||||
|
messages,
|
||||||
|
this.model
|
||||||
|
);
|
||||||
|
|
||||||
|
const parsedResponse = response as {
|
||||||
|
choices?: Array<{ message?: { content?: string } }>;
|
||||||
|
};
|
||||||
|
const content = parsedResponse.choices?.[0]?.message?.content ?? "";
|
||||||
|
|
||||||
|
let signal: SignalType = "neutral";
|
||||||
|
let confidence = 0.5;
|
||||||
|
let reasoning = content;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(content);
|
||||||
|
if (
|
||||||
|
parsed.signal === "bullish" ||
|
||||||
|
parsed.signal === "bearish" ||
|
||||||
|
parsed.signal === "neutral"
|
||||||
|
) {
|
||||||
|
signal = parsed.signal;
|
||||||
|
confidence = parsed.confidence ?? 0.5;
|
||||||
|
reasoning = parsed.reasoning ?? content;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
const lowerContent = content.toLowerCase();
|
||||||
|
if (lowerContent.includes("bullish")) signal = "bullish";
|
||||||
|
else if (lowerContent.includes("bearish")) signal = "bearish";
|
||||||
|
}
|
||||||
|
|
||||||
|
const agentSignal: AgentSignal = {
|
||||||
|
agent: "technical",
|
||||||
|
signal,
|
||||||
|
confidence,
|
||||||
|
reasoning,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
analyst: "technical",
|
||||||
|
report: content,
|
||||||
|
signal: agentSignal,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user