feat: add fundamentals analyst agent

This commit is contained in:
2026-05-14 07:54:00 +02:00
parent 7b81adb6a2
commit 55d6ba4fee
2 changed files with 115 additions and 0 deletions
+35
View File
@@ -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");
});
});
+80
View File
@@ -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,
};
}
}