Skip to main content
Glama

valuation_snapshot

Analyze stock valuation by calculating key financial ratios and determining if a stock is cheap, fair, or expensive with specific buy zone pricing.

Instructions

Assess whether a stock is cheap, fair, or expensive. Pulls P/E, P/S, EV/EBITDA, FCF yield, ROE, and margins, then synthesizes them into a verdict with a specific buy zone price level.

Input Schema

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

Implementation Reference

  • The `handler` function fetches various financial metrics for a given ticker from multiple APIs (FMP, Finnhub, Polygon), aggregates them, and uses Anthropic's Claude API to generate a valuation analysis in a structured JSON format.
    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");
      if (!config.polygonApiKey) throw new Error("POLYGON_API_KEY is not configured");
    
      const [keyMetrics, ratiosTTM, finnhubMetrics, overview] = await Promise.all([
        fetchKeyMetrics(ticker),
        fetchRatiosTTM(ticker),
        fetchFinnhubMetrics(ticker),
        fetchPolygonOverview(ticker),
      ]);
    
      const hasData = Object.keys(keyMetrics).length > 0 || Object.keys(finnhubMetrics).length > 0;
      if (!hasData) {
        throw new Error(`No valuation data found for "${ticker}". Please verify the symbol.`);
      }
    
      const km = keyMetrics as any;
      const rt = ratiosTTM as any;
      const fh = finnhubMetrics as any;
      const ov = overview as any;
    
      // Helper: Finnhub returns quality metrics as percentages (e.g. 35.5 = 35.5%), convert to decimal
      const fhPct = (v: unknown) => (v != null && isFinite(Number(v)) ? Number(v) / 100 : undefined);
      // Sanity-check a ratio value — returns null if it looks like bad data
      const sanity = (v: unknown, min: number, max: number): number | null => {
        const n = Number(v);
        return v != null && isFinite(n) && n >= min && n <= max ? n : null;
      };
    
      // Collect metrics with fallbacks across FMP key-metrics, FMP ratios, and Finnhub
      const pe = sanity(km.peRatioTTM ?? fh.peNormalizedAnnual, 0, 2000);
      const ps = sanity(km.priceToSalesRatioTTM ?? rt.priceToSalesRatioTTM ?? fh.psTTM, 0, 1000);
      const pb = sanity(km.pbRatioTTM ?? rt.pbRatioTTM ?? fh.pbAnnual, 0, 500);
      // evEbitdaTTM from Finnhub is a raw multiple (not a percentage)
      const evEbitda = sanity(km.evToEbitdaTTM ?? rt.enterpriseValueMultipleTTM ?? fh.evEbitdaTTM, 0, 500);
      // FCF yield: prefer FMP direct; derive from Finnhub's price/FCF ratio if missing
      const pfcfTTM = Number(fh.pfcfShareTTM);
      const fcfYieldFromFh = pfcfTTM > 0 && isFinite(pfcfTTM) ? 1 / pfcfTTM : undefined;
      const fcfYield = sanity(km.freeCashFlowYieldTTM ?? fcfYieldFromFh, -1, 1);
    
      // Dividend yield: FMP ratios-ttm returns as decimal (0.007), Finnhub as decimal — cap at 30%
      const rawDividendYield = rt.dividendYieldTTM ?? fh.dividendYieldIndicatedAnnual;
      const dividendYield = sanity(rawDividendYield, 0, 0.30);
    
      // Quality metrics — Finnhub returns as percentage, convert to decimal for consistency
      const roe = sanity(km.roeTTM ?? rt.returnOnEquityTTM ?? fhPct(fh.roeTTM), -5, 10);
      const roic = sanity(km.roicTTM ?? rt.returnOnCapitalEmployedTTM ?? fhPct(fh.roicTTM), -5, 10);
      const debtToEquity = sanity(km.debtToEquityTTM ?? rt.debtEquityRatioTTM ?? fh["totalDebt/totalEquityAnnual"], 0, 100);
      const currentRatio = sanity(km.currentRatioTTM ?? rt.currentRatioTTM ?? fh.currentRatioAnnual, 0, 50);
      const grossMargin = sanity(rt.grossProfitMarginTTM ?? fhPct(fh.grossMarginTTM), -1, 1);
      const netMargin = sanity(rt.netProfitMarginTTM ?? fhPct(fh.netProfitMarginTTM), -1, 1);
    
      // Growth — Finnhub returns as percentage, convert to decimal
      const revenueGrowth3Y = sanity(fhPct(fh.revenueGrowth3Y), -1, 10);
      const epsGrowth3Y = sanity(fhPct(fh.epsGrowth3Y), -5, 50);
    
      // Historical P/E range from Finnhub
      const peHigh5Y = fh["pe5Y"];
      const peLow5Y = fh["peLow5Y"] ?? fh["peTTM"];
      const peHistoricalAvg = fh["peNormalizedAnnual"];
    
      const companyName = ov.name || ticker;
      const sector = ov.sic_description || "";
      const marketCap = ov.market_cap;
    
      const fmt = (v: number | null | undefined, suffix = "", decimals = 1) =>
        v != null ? `${Number(v).toFixed(decimals)}${suffix}` : "N/A";
      const fmtPct = (v: number | null | undefined) =>
        v != null ? `${(Number(v) * 100).toFixed(1)}%` : "N/A";
    
      const dataContext = [
        `Company: ${companyName} (${ticker})`,
        sector ? `Sector: ${sector}` : "",
        marketCap ? `Market Cap: $${(marketCap / 1e9).toFixed(1)}B` : "",
        "",
        "Valuation Multiples (TTM):",
        `  P/E Ratio: ${fmt(pe, "x")}`,
        `  P/S Ratio: ${fmt(ps, "x")}`,
        `  P/B Ratio: ${fmt(pb, "x")}`,
        `  EV/EBITDA: ${fmt(evEbitda, "x")}`,
        `  FCF Yield: ${fmtPct(fcfYield)}`,
        dividendYield != null ? `  Dividend Yield: ${fmtPct(dividendYield)}` : "",
        "",
        "Quality Metrics:",
        `  ROE: ${fmtPct(roe)}`,
        `  ROIC: ${fmtPct(roic)}`,
        `  Gross Margin: ${fmtPct(grossMargin)}`,
        `  Net Margin: ${fmtPct(netMargin)}`,
        `  Debt/Equity: ${fmt(debtToEquity)}`,
        `  Current Ratio: ${fmt(currentRatio)}`,
        "",
        "Growth:",
        `  3-Year Revenue CAGR: ${fmtPct(revenueGrowth3Y)}`,
        `  3-Year EPS CAGR: ${fmtPct(epsGrowth3Y)}`,
        peHigh5Y != null ? `\nHistorical P/E context:\n  5-Year high P/E: ${fmt(peHigh5Y, "x")}\n  Current P/E: ${fmt(pe, "x")}` : "",
      ].filter(Boolean).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, direct, " +
        "focused on what the valuation means for a long-term investor. Avoid jargon. " +
        "Interpret multiples in context: a high P/E can be justified by high growth; a low P/E can signal problems. " +
        "Always respond with valid JSON matching the exact schema. Base analysis strictly on provided data.";
    
      const userPrompt =
        `Assess the valuation of ${ticker} for a long-term investor.\n\n` +
        dataContext +
        `\n\nReturn a JSON object with this exact structure:
    {
      "verdict": "very_cheap" | "cheap" | "fair" | "expensive" | "very_expensive",
      "oneLiner": "one sentence capturing the valuation story",
      "peRead": "1-2 sentences interpreting the P/E ratio in context of the company's growth and sector",
      "multiplesSummary": "2-3 sentences synthesizing the valuation multiples together — is the premium (or discount) justified?",
      "qualityRead": "1-2 sentences on ROE/ROIC/margins — is this a high-quality business at this price?",
      "growthContext": "1-2 sentences on whether growth rates justify the current multiple (PEG-style thinking)",
      "buyZone": "at what P/E or price level would this become clearly attractive? Be specific.",
      "bottomLine": "2 sentences — net verdict for a long-term investor: is this a good entry point, a hold, or a wait?"
    }`;
    
      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,
        companyName,
        ...parsed,
        metrics: {
          peRatio: pe != null ? parseFloat(Number(pe).toFixed(1)) : null,
          psRatio: ps != null ? parseFloat(Number(ps).toFixed(1)) : null,
          pbRatio: pb != null ? parseFloat(Number(pb).toFixed(1)) : null,
          evEbitda: evEbitda != null ? parseFloat(Number(evEbitda).toFixed(1)) : null,
          fcfYield: fcfYield != null ? parseFloat((Number(fcfYield) * 100).toFixed(1)) : null,
          roe: roe != null ? parseFloat((Number(roe) * 100).toFixed(1)) : null,
          netMargin: netMargin != null ? parseFloat((Number(netMargin) * 100).toFixed(1)) : null,
          debtToEquity: debtToEquity != null ? parseFloat(Number(debtToEquity).toFixed(2)) : null,
        },
        generatedAt: new Date().toISOString(),
      };
    }
  • The `inputSchema` defines the expected input for the valuation snapshot tool, requiring a stock 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)"),
    });
  • The tool definition object `valuationSnapshotTool` ties together the name, description, input schema, and handler, and registers the tool for use.
    const valuationSnapshotTool: ToolDefinition<Input> = {
      name: "valuation-snapshot",
      description:
        "Assess whether a stock is cheap, fair, or expensive. Returns P/E, P/S, EV/EBITDA, FCF yield, ROE, and margins, " +
        "then synthesizes them into a Motley Fool-style verdict on whether the price is justified by the business quality " +
        "and growth. Includes a specific buy zone price level. Powered by Claude.",
      version: "1.0.0",
      inputSchema,
      handler,
      metadata: {
        tags: ["stocks", "investing", "finance", "valuation", "llm"],
        pricing: "$0.05 per call",
        pricingMicros: 50_000,
        exampleInput: { ticker: "NVDA" },
      },
    };
    
    registerTool(valuationSnapshotTool);
    export default valuationSnapshotTool;

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