Skip to main content
Glama

Stock Investment Thesis

stock_thesis

Create long-term investment theses for stocks by analyzing financials, valuation metrics, insider trades, and analyst ratings to generate comprehensive research notes with verdicts, strengths, risks, and valuation assessments.

Instructions

Generate a long-term investment thesis for any stock. Pulls live financials, valuation metrics, insider trades, and analyst ratings, then synthesizes them into a Motley Fool-style research note. Returns a bullish/neutral/bearish verdict, thesis paragraphs, key strengths, risks, and valuation read. Use when you want fundamental analysis of a stock for long-term investing.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
tickerYesStock ticker symbol (e.g. NVDA, AAPL, MSFT)
timeHorizonNoInvestment time horizon3-5 years

Implementation Reference

  • The handler function for the stock_thesis tool. It fetches financial data from various APIs (Polygon, Finnhub, FMP), constructs a prompt for Claude, and returns an LLM-generated investment thesis.
    async function handler(input: Input) {
      const { ticker, timeHorizon } = input;
    
      if (!config.anthropicApiKey) throw new Error("ANTHROPIC_API_KEY is not configured");
      if (!config.polygonApiKey) throw new Error("POLYGON_API_KEY is not configured");
      if (!config.finnhubApiKey) throw new Error("FINNHUB_API_KEY is not configured");
      if (!config.fmpApiKey) throw new Error("FMP_API_KEY is not configured");
    
      // Fetch all data sources in parallel
      const [overview, prevClose, metrics, recommendations, insiders, incomeStatements, keyMetrics] =
        await Promise.all([
          fetchPolygonOverview(ticker),
          fetchPolygonPrevClose(ticker),
          fetchFinnhubMetrics(ticker),
          fetchFinnhubRecommendations(ticker),
          fetchFinnhubInsiders(ticker),
          fetchFMPIncomeStatement(ticker),
          fetchFMPKeyMetrics(ticker),
        ]);
    
      const hasData =
        Object.keys(overview).length > 0 ||
        (incomeStatements as any[]).length > 0 ||
        Object.keys(metrics).length > 0;
    
      if (!hasData) {
        throw new Error(`No data found for ticker "${ticker}". Please verify the symbol is correct.`);
      }
    
      // ----- Build data context for the prompt -----
      const companyName = (overview as any).name || ticker;
      const description = ((overview as any).description || "").substring(0, 600);
      const sector = (overview as any).sic_description || "";
      const employees = (overview as any).total_employees as number | undefined;
      const marketCap = (overview as any).market_cap as number | undefined;
      const currentPrice = (prevClose as any).c as number | undefined;
    
      const incomeRows = (incomeStatements as any[]).map((s: any) => {
        const rev = s.revenue ? `$${(s.revenue / 1e9).toFixed(2)}B` : "N/A";
        const margin = s.netIncomeRatio != null ? `${(s.netIncomeRatio * 100).toFixed(1)}%` : "N/A";
        const eps = s.eps != null ? `$${s.eps.toFixed(2)}` : "N/A";
        const year = s.calendarYear || (s.date || "").substring(0, 4);
        return `  ${year}: Revenue ${rev} | Net Margin ${margin} | EPS ${eps}`;
      });
    
      const latestRec = (recommendations as any[])[0];
      const analystLine = latestRec
        ? `${latestRec.buy} buy / ${latestRec.hold} hold / ${latestRec.sell} sell (${latestRec.period})`
        : "Not available";
    
      const insiderLines = (insiders as any[]).slice(0, 5).map((t: any) => {
        const direction = t.transactionCode === "P" ? "purchase" : t.transactionCode === "S" ? "sale" : t.transactionCode;
        return `  ${t.transactionDate}: ${t.name} — ${direction} (${t.change > 0 ? "+" : ""}${t.change?.toLocaleString()} shares)`;
      });
    
      const km = keyMetrics as any;
      const fh = metrics as any;
      // Finnhub returns quality/growth metrics as percentages (e.g. 33.6 = 33.6%) — divide to get decimal
      const fhPct = (v: unknown) => (v != null && isFinite(Number(v)) ? Number(v) / 100 : undefined);
      // Reject implausible values
      const sane = (v: unknown, min: number, max: number): number | null => {
        const n = Number(v); return v != null && isFinite(n) && n >= min && n <= max ? n : null;
      };
    
      const pe = sane(km.peRatioTTM ?? fh.peNormalizedAnnual, 0, 2000);
      const ps = sane(km.priceToSalesRatioTTM ?? fh.psTTM, 0, 1000);
      const pb = sane(km.pbRatioTTM ?? fh.pbAnnual, 0, 500);
      const roe = sane(km.roeTTM ?? fhPct(fh.roeTTM), -5, 10);
      const debtToEquity = sane(km.debtToEquityTTM ?? fh["totalDebt/totalEquityAnnual"], 0, 100);
      const pfcfTTM = Number(fh.pfcfShareTTM);
      const fcfYield = sane(km.freeCashFlowYieldTTM ?? (pfcfTTM > 0 && isFinite(pfcfTTM) ? 1 / pfcfTTM : undefined), -1, 1);
      // Finnhub revenueGrowth3Y is already a percentage (e.g. 12.5 = 12.5%) — use directly
      const revenueGrowth3Y = sane(fh.revenueGrowth3Y, -100, 1000);
    
      const lines: string[] = [
        `Company: ${companyName} (${ticker})`,
        sector ? `Sector: ${sector}` : "",
        employees ? `Employees: ${employees.toLocaleString()}` : "",
        marketCap ? `Market Cap: $${(marketCap / 1e9).toFixed(1)}B` : "",
        currentPrice ? `Current Price: $${currentPrice}` : "",
        description ? `\nBusiness:\n${description}` : "",
        incomeRows.length > 0 ? `\nFinancial Performance (Annual):\n${incomeRows.join("\n")}` : "",
        revenueGrowth3Y != null ? `3-Year Revenue CAGR: ${revenueGrowth3Y.toFixed(1)}%` : "",
        `\nValuation:`,
        pe != null ? `  P/E (TTM): ${Number(pe).toFixed(1)}` : "",
        ps != null ? `  P/S (TTM): ${Number(ps).toFixed(1)}` : "",
        pb != null ? `  P/B: ${Number(pb).toFixed(1)}` : "",
        fcfYield != null ? `  FCF Yield: ${(Number(fcfYield) * 100).toFixed(1)}%` : "",
        roe != null ? `  ROE: ${(Number(roe) * 100).toFixed(1)}%` : "",
        debtToEquity != null ? `  Debt/Equity: ${Number(debtToEquity).toFixed(2)}` : "",
        `\nAnalyst Consensus: ${analystLine}`,
        insiderLines.length > 0
          ? `\nRecent Insider Activity:\n${insiderLines.join("\n")}`
          : "\nRecent Insider Activity: None reported",
      ];
    
      const dataContext = lines.filter(Boolean).join("\n");
    
      // ----- Claude analysis -----
      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, " +
        "grounded in fundamentals, focused on long-term investing. Avoid jargon. Write for a smart retail investor. " +
        "Always respond with valid JSON matching the exact schema requested. " +
        "Base your analysis strictly on the data provided. Do not fabricate numbers. " +
        "If data is limited, work with what's available and be transparent about it.";
    
      const userPrompt =
        `Analyze the following data for ${ticker} and write a ${timeHorizon} investment thesis.\n\n` +
        dataContext +
        `\n\nReturn a JSON object with this exact structure:
    {
      "companyName": "full company name",
      "sector": "sector or industry",
      "verdict": "bullish" | "neutral" | "bearish",
      "oneLiner": "one compelling sentence that captures the core investment case",
      "thesis": "2-3 paragraphs. Write like a human analyst — reference specific numbers, explain what they mean for long-term investors. Be direct about the opportunity or lack thereof.",
      "keyStrengths": ["strength with specific data point", "strength 2", "strength 3"],
      "keyRisks": ["risk 1", "risk 2"],
      "valuation": "1-2 sentences — does the stock look cheap, fair, or expensive at the current price? Reference the P/E, P/S, or other relevant metric.",
      "insiderRead": "1 sentence interpreting the insider activity — meaningful buying/selling or routine noise?",
      "analystRead": "1 sentence interpreting the analyst consensus — is the street bullish, divided, or cautious?",
      "watchFor": "the single most important metric or event to watch in the next earnings report",
      "timeHorizon": "${timeHorizon}"
    }`;
    
      const message = await client.messages.create({
        model: "claude-haiku-4-5-20251001",
        max_tokens: 2048,
        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,
        dataSnapshot: {
          marketCapBillions: marketCap != null ? parseFloat((marketCap / 1e9).toFixed(1)) : null,
          currentPrice: currentPrice ?? null,
          peRatio: pe != null ? parseFloat(Number(pe).toFixed(1)) : null,
          analystConsensus: latestRec
            ? { buy: latestRec.buy, hold: latestRec.hold, sell: latestRec.sell }
            : null,
        },
        generatedAt: new Date().toISOString(),
      };
    }
  • Input validation schema for the stock_thesis tool using Zod.
    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)"),
      timeHorizon: z
        .enum(["1-2 years", "3-5 years", "5+ years"])
        .default("3-5 years")
        .describe("Investment time horizon for the thesis"),
    });
  • Tool definition and registration of the stock_thesis (named 'stock-thesis') tool.
    const stockThesisTool: ToolDefinition<Input> = {
      name: "stock-thesis",
      description:
        "Generate a long-term investment thesis for any stock. Pulls live financials, valuation metrics, " +
        "insider trades, and analyst ratings, then synthesizes them into a Motley Fool-style research note. " +
        "Returns verdict (bullish/neutral/bearish), thesis, key strengths, risks, and valuation read. Powered by Claude.",
      version: "1.0.0",
      inputSchema,
      handler,
      metadata: {
        tags: ["stocks", "investing", "finance", "analysis", "llm"],
        pricing: "$0.05 per call",
        pricingMicros: 50_000,
        exampleInput: {
          ticker: "NVDA",
          timeHorizon: "3-5 years",
        },
      },
    };
    
    registerTool(stockThesisTool);
Behavior4/5

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

No annotations provided, so description carries full burden. Successfully discloses data sources ('live financials, valuation metrics, insider trades, analyst ratings'), processing style ('Motley Fool-style research note'), and output structure ('bullish/neutral/bearish verdict, thesis paragraphs...'). Missing only side-effect warnings or rate limit notes.

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?

Four sentences with zero waste: (1) Purpose, (2) Data sources & processing, (3) Output structure, (4) Usage guidance. Front-loaded and efficiently structured; every sentence earns its place.

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?

Despite no output schema, description explicitly documents return values (verdict categories, thesis components). Covers input semantics, behavioral traits, and output structure adequately for a 2-parameter synthesis tool. Minor gap: no mention of data freshness or caching behavior.

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?

Schema coverage is 100%, establishing baseline 3. Description references 'long-term' and 'any stock' which loosely map to parameters, but adds no syntax, format details, or semantic constraints beyond the schema's 'e.g. NVDA, AAPL' example.

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

Purpose5/5

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

Specific verb ('Generate') + resource ('long-term investment thesis') + scope ('any stock'). Distinguishes from siblings like 'earnings_analysis' (narrow focus), 'valuation_snapshot' (metrics only), and 'bear_vs_bull' (comparison format) by emphasizing comprehensive fundamental synthesis.

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

Usage Guidelines4/5

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

Provides explicit positive guidance ('Use when you want fundamental analysis of a stock for long-term investing') with clear context. Lacks explicit negative constraints or named alternatives (e.g., when to use 'earnings_analysis' instead), preventing a 5.

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