Skip to main content
Glama

Valuation Snapshot

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;
Behavior4/5

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

With no annotations provided, the description carries the full burden and successfully discloses key behaviors: it pulls specific financial multiples, performs synthesis (not just raw data), and returns a structured verdict with a specific buy zone price level. Missing minor operational details like data freshness or calculation methodology prevents a 5.

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?

Two sentences with zero waste. First sentence establishes the valuation spectrum assessment; second details the specific metrics pulled and the synthesis methodology. Information density is high with no filler words or redundant restatements of the tool name.

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?

Given the absence of an output schema, the description effectively compensates by detailing the return structure (specific metrics, verdict, buy zone price). It adequately covers the tool's functionality for a single-parameter analysis tool, though mentioning data sources or caching behavior would provide full completeness.

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% with 'ticker' fully documented including examples. The description focuses entirely on behavior/output rather than parameters, which is acceptable given the schema's completeness. Baseline score of 3 is appropriate as the description neither adds to nor detracts from parameter understanding.

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?

Description uses specific verb 'Assess' with clear resource (stock valuation) and explicitly defines the three possible outcomes (cheap, fair, expensive). It distinguishes from siblings like 'stock_thesis' and 'earnings_analysis' by specifying valuation multiples (P/E, EV/EBITDA) and quantitative synthesis rather than qualitative thesis or earnings-specific analysis.

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

Usage Guidelines3/5

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

The description implies usage through specificity (valuation screening, buy zone identification) but lacks explicit guidance on when to prefer this over 'stock_thesis' for comprehensive analysis or 'bear_vs_bull' for sentiment. No 'when-not-to-use' or prerequisite conditions are mentioned.

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