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
| Name | Required | Description | Default |
|---|---|---|---|
| ticker | Yes | Stock ticker symbol (e.g. NVDA, AAPL, MSFT) |
Implementation Reference
- src/tools/valuation-snapshot.ts:61-218 (handler)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(), }; } - src/tools/valuation-snapshot.ts:6-13 (schema)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)"), }); - src/tools/valuation-snapshot.ts:220-238 (registration)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;