Skip to main content
Glama

Earnings Analysis

earnings_analysis

Analyze stock earnings performance by evaluating EPS beat/miss history, revenue trends, and long-term investment implications. Provides verdict, beat rate, trajectory summary, and future indicators.

Instructions

Analyze a stock's earnings track record — EPS beat/miss history, revenue trend, and what it means for long-term investors. Returns verdict, beat rate, revenue trajectory, last quarter summary, and what to watch next.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
tickerYesStock ticker symbol (e.g. NVDA, AAPL, MSFT)

Implementation Reference

  • The 'handler' function manages the data fetching from financial APIs and processes the data through Anthropic's Claude API to generate a Motley Fool-style stock earnings analysis.
    async function handler(input: Input) {
      const { ticker } = input;
    
      if (!config.anthropicApiKey) throw new Error("ANTHROPIC_API_KEY is not configured");
      if (!config.fmpApiKey) throw new Error("FMP_API_KEY is not configured");
      if (!config.finnhubApiKey) throw new Error("FINNHUB_API_KEY is not configured");
    
      const [surprises, quarterlyIncome, upcomingEarnings] = await Promise.all([
        fetchEarningsSurprises(ticker),
        fetchQuarterlyIncome(ticker),
        fetchUpcomingEarnings(ticker),
      ]);
    
      if ((surprises as any[]).length === 0 && (quarterlyIncome as any[]).length === 0) {
        throw new Error(`No earnings data found for "${ticker}". Please verify the symbol.`);
      }
    
      // Build EPS beat/miss history
      const epsRows = (surprises as any[]).map((s: any) => {
        const actual = s.actualEarningResult;
        const estimate = s.estimatedEarning;
        const beat = actual != null && estimate != null ? actual >= estimate : null;
        const pct = actual != null && estimate != null && estimate !== 0
          ? (((actual - estimate) / Math.abs(estimate)) * 100).toFixed(1)
          : null;
        return `  ${s.date}: EPS actual $${actual?.toFixed(2) ?? "N/A"} vs estimate $${estimate?.toFixed(2) ?? "N/A"}${pct != null ? ` (${pct > "0" ? "+" : ""}${pct}% ${beat ? "BEAT" : "MISS"})` : ""}`;
      });
    
      const beatsTotal = (surprises as any[]).filter((s: any) =>
        s.actualEarningResult != null && s.estimatedEarning != null && s.actualEarningResult >= s.estimatedEarning
      ).length;
      const beatRate = (surprises as any[]).length > 0
        ? `${beatsTotal}/${(surprises as any[]).length} quarters beat (${Math.round((beatsTotal / (surprises as any[]).length) * 100)}%)`
        : "N/A";
    
      // Revenue trend from quarterly income
      const revenueRows = (quarterlyIncome as any[]).slice(0, 8).map((q: any) => {
        const rev = q.revenue ? `$${(q.revenue / 1e9).toFixed(2)}B` : "N/A";
        const margin = q.netIncomeRatio != null ? `${(q.netIncomeRatio * 100).toFixed(1)}% net margin` : "";
        return `  ${q.period ?? ""} ${q.calendarYear ?? q.date?.substring(0, 7) ?? ""}: Revenue ${rev}${margin ? ` | ${margin}` : ""}`;
      });
    
      // Upcoming earnings
      const upcomingLine = (upcomingEarnings as any).date
        ? `Next earnings: ${(upcomingEarnings as any).date}${(upcomingEarnings as any).epsEstimate ? ` | EPS estimate: $${(upcomingEarnings as any).epsEstimate}` : ""}`
        : "No upcoming earnings date found in next 90 days";
    
      const dataContext = [
        `Ticker: ${ticker}`,
        `Beat Rate (last ${(surprises as any[]).length} quarters): ${beatRate}`,
        "",
        "EPS Surprises (most recent first):",
        ...epsRows,
        "",
        "Quarterly Revenue Trend (most recent first):",
        ...revenueRows,
        "",
        upcomingLine,
      ].join("\n");
    
      const client = new Anthropic({ apiKey: config.anthropicApiKey });
    
      const systemPrompt =
        "You are a professional stock analyst writing in the style of The Motley Fool — clear, conversational, " +
        "focused on what earnings results mean for long-term investors. Avoid jargon. " +
        "Always respond with valid JSON matching the exact schema requested. " +
        "Base your analysis strictly on the data provided. Do not fabricate numbers.";
    
      const userPrompt =
        `Analyze the earnings track record for ${ticker} and explain what it means for a long-term investor.\n\n` +
        dataContext +
        `\n\nReturn a JSON object with this exact structure:
    {
      "verdict": "strong_compounder" | "consistent" | "mixed" | "volatile" | "deteriorating",
      "oneLiner": "one sentence capturing the key takeaway from this earnings history",
      "beatRate": "X/Y quarters beat (Z%)",
      "revenueTrend": "accelerating" | "stable" | "decelerating" | "declining",
      "revenueRead": "1-2 sentences on the revenue trajectory and what it signals",
      "epsRead": "1-2 sentences on EPS consistency and what it signals for reliability",
      "lastQuarterSummary": "2-3 sentences on the most recent quarter — what happened, was it meaningful?",
      "longTermRead": "2 sentences on what this earnings history means for a 3-5 year investor",
      "watchForNext": "the single most important thing to watch in the next earnings report",
      "upcomingDate": "next earnings date if known, or null"
    }`;
    
      const message = await client.messages.create({
        model: "claude-haiku-4-5-20251001",
        max_tokens: 1024,
        messages: [{ role: "user", content: userPrompt }],
        system: systemPrompt,
      });
    
      const rawText = message.content[0].type === "text" ? message.content[0].text : "";
      const jsonText = rawText.replace(/^```(?:json)?\n?/m, "").replace(/\n?```$/m, "").trim();
    
      let parsed: Record<string, unknown>;
      try {
        parsed = JSON.parse(jsonText);
      } catch {
        throw new Error("Failed to parse structured response from LLM");
      }
    
      return {
        ticker,
        ...parsed,
        rawData: {
          quartersAnalyzed: (surprises as any[]).length,
          beatsTotal,
          upcomingEarnings: Object.keys(upcomingEarnings).length > 0 ? upcomingEarnings : null,
        },
        generatedAt: new Date().toISOString(),
      };
    }
  • Zod schema defining the input requirements for the earnings analysis tool, specifically the ticker symbol.
    const inputSchema = z.object({
      ticker: z
        .string()
        .min(1)
        .max(10)
        .transform((v) => v.toUpperCase().trim())
        .describe("Stock ticker symbol (e.g. NVDA, AAPL, MSFT)"),
    });
  • Tool registration for 'earnings-analysis', mapping the name, schema, and handler.
    const earningsAnalysisTool: ToolDefinition<Input> = {
      name: "earnings-analysis",
      description:
        "Analyze a stock's earnings track record — EPS beat/miss history, revenue trend, and what it means " +
        "for long-term investors. Returns a Motley Fool-style read on earnings consistency, the last quarter, " +
        "and what to watch next. Powered by Claude.",
      version: "1.0.0",
      inputSchema,
      handler,
      metadata: {
        tags: ["stocks", "investing", "finance", "earnings", "llm"],
        pricing: "$0.05 per call",
        pricingMicros: 50_000,
        exampleInput: { ticker: "NVDA" },
      },
    };
    
    registerTool(earningsAnalysisTool);
    export default earningsAnalysisTool;
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It compensates well for the missing output schema by listing return values (verdict, beat rate, trajectory, summary), but fails to disclose operational traits like safety profile, idempotency, data freshness, or rate limits.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description consists of two highly efficient sentences. The first front-loads the action and scope (earnings analysis), while the second discloses output format. No words are wasted.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple single-parameter tool without output schema, the description adequately covers the tool's purpose and return structure. It could be improved by mentioning data sources or freshness, but it meets the minimum requirements for agent selection.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema has 100% description coverage for the single 'ticker' parameter. The description does not explicitly discuss the parameter, but since the schema fully documents it, the baseline score of 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool analyzes a stock's earnings track record, specifying EPS beat/miss history and revenue trends. While it effectively distinguishes itself from general stock analysis tools like 'stock_thesis' or 'valuation_snapshot' through specific focus areas, it does not explicitly name siblings for differentiation.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description mentions the analysis is for 'long-term investors,' implying a use case, but provides no explicit guidance on when to use this tool versus alternatives like 'stock_thesis' or 'valuation_snapshot,' nor does it state prerequisites or exclusions.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/marras0914/agent-toolbelt'

If you have feedback or need assistance with the MCP directory API, please join our Discord server