feat: add fundamentals analyst agent
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { FundamentalsAnalyst } from "../fundamentals";
|
||||
import type { OpenRouterClient } from "../../lib/openrouter";
|
||||
|
||||
describe("FundamentalsAnalyst", () => {
|
||||
it("should analyze company fundamentals", async () => {
|
||||
const mockClient = {
|
||||
createChatCompletion: async () => ({
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
content:
|
||||
'{"signal":"bullish","confidence":0.85,"reasoning":"Strong revenue growth"}',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
} as unknown as OpenRouterClient;
|
||||
|
||||
const analyst = new FundamentalsAnalyst(mockClient);
|
||||
const result = await analyst.analyze("AAPL", "Revenue: 100B, Profit: 20B");
|
||||
|
||||
expect(result.analyst).toBe("fundamentals");
|
||||
expect(result.signal.signal).toBe("bullish");
|
||||
});
|
||||
|
||||
it("should use specified model", () => {
|
||||
const mockClient = {} as unknown as OpenRouterClient;
|
||||
const analyst = new FundamentalsAnalyst(mockClient, {
|
||||
model: "custom/model",
|
||||
});
|
||||
|
||||
expect(analyst.getModel()).toBe("custom/model");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
import { OpenRouterClient } from "../lib/openrouter";
|
||||
import type { AnalystReport, AgentSignal, SignalType } from "../types/agents";
|
||||
|
||||
export interface FundamentalsConfig {
|
||||
model?: string;
|
||||
}
|
||||
|
||||
export class FundamentalsAnalyst {
|
||||
private client: OpenRouterClient;
|
||||
private model: string;
|
||||
|
||||
constructor(client: OpenRouterClient, config?: FundamentalsConfig) {
|
||||
this.client = client;
|
||||
this.model = config?.model ?? "google/gemini-2.0-flash-exp:free";
|
||||
}
|
||||
|
||||
getModel(): string {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
async analyze(ticker: string, financialData: string): Promise<AnalystReport> {
|
||||
const messages = [
|
||||
{
|
||||
role: "system" as const,
|
||||
content:
|
||||
"You are a fundamental analyst. Analyze the financial data 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} fundamentals:\n${financialData}`,
|
||||
},
|
||||
];
|
||||
|
||||
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 {
|
||||
// If not valid JSON, check for keywords in the response
|
||||
const lowerContent = content.toLowerCase();
|
||||
if (lowerContent.includes("bullish")) signal = "bullish";
|
||||
else if (lowerContent.includes("bearish")) signal = "bearish";
|
||||
}
|
||||
|
||||
const agentSignal: AgentSignal = {
|
||||
agent: "fundamentals",
|
||||
signal,
|
||||
confidence,
|
||||
reasoning,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
return {
|
||||
analyst: "fundamentals",
|
||||
report: content,
|
||||
signal: agentSignal,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user