Skip to main content
Glama

bear_vs_bull

Analyze stock investments by generating structured bull and bear cases with specific data, delivering a net verdict and key investor questions for informed decisions.

Instructions

Generate a structured bull vs. bear case for any stock. Steelmans both sides with specific data, then delivers a net verdict and the key question investors need to answer before buying.

Input Schema

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

Implementation Reference

  • The handler function for the bear-vs-bull tool, which fetches market data and uses an LLM to generate a bull/bear analysis.
    async function handler(input: Input) {
      const { ticker } = 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");
    
      const [overview, prevClose, incomeStatements, keyMetrics, finnhubMetrics, recommendations, insiders] =
        await Promise.all([
          fetchPolygonOverview(ticker),
          fetchPolygonPrevClose(ticker),
          fetchFMPIncomeStatement(ticker),
          fetchFMPKeyMetrics(ticker),
          fetchFinnhubMetrics(ticker),
          fetchFinnhubRecommendations(ticker),
          fetchFinnhubInsiders(ticker),
        ]);
    
      const hasData = Object.keys(overview).length > 0 || (incomeStatements as any[]).length > 0;
      if (!hasData) {
        throw new Error(`No data found for "${ticker}". Please verify the symbol.`);
      }
    
      const ov = overview as any;
      const pc = prevClose as any;
      const km = keyMetrics as any;
      const fh = finnhubMetrics as any;
      const rec = (recommendations as any[])[0];
    
      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)}% net margin` : "N/A";
        const eps = s.eps != null ? `EPS $${s.eps.toFixed(2)}` : "";
        return `  ${s.calendarYear ?? s.date?.substring(0, 4)}: Revenue ${rev} | ${margin}${eps ? ` | ${eps}` : ""}`;
      });
    
      const insiderPurchases = (insiders as any[]).filter((t: any) => t.transactionCode === "P").length;
      const insiderSales = (insiders as any[]).filter((t: any) => t.transactionCode === "S").length;
    
      const dataContext = [
        `Company: ${ov.name || ticker} (${ticker})`,
        ov.sic_description ? `Sector: ${ov.sic_description}` : "",
        ov.market_cap ? `Market Cap: $${(ov.market_cap / 1e9).toFixed(1)}B` : "",
        pc.c ? `Current Price: $${pc.c}` : "",
        ov.description ? `\nBusiness: ${ov.description.substring(0, 400)}` : "",
        incomeRows.length > 0 ? `\nFinancials:\n${incomeRows.join("\n")}` : "",
        fh.revenueGrowth3Y != null ? `3Y Revenue CAGR: ${Number(fh.revenueGrowth3Y).toFixed(1)}%` : "",
        `\nValuation:`,
        (km.peRatioTTM ?? fh.peNormalizedAnnual) != null ? `  P/E (TTM): ${Number(km.peRatioTTM ?? fh.peNormalizedAnnual).toFixed(1)}x` : "",
        (km.priceToSalesRatioTTM ?? fh.psTTM) != null ? `  P/S: ${Number(km.priceToSalesRatioTTM ?? fh.psTTM).toFixed(1)}x` : "",
        km.freeCashFlowYieldTTM != null ? `  FCF Yield: ${(Number(km.freeCashFlowYieldTTM) * 100).toFixed(1)}%` : (fh.pfcfShareTTM > 0 ? `  FCF Yield: ${(100 / Number(fh.pfcfShareTTM)).toFixed(1)}%` : ""),
        (km.debtToEquityTTM ?? fh["totalDebt/totalEquityAnnual"]) != null ? `  Debt/Equity: ${Number(km.debtToEquityTTM ?? fh["totalDebt/totalEquityAnnual"]).toFixed(2)}` : "",
        rec ? `\nAnalysts: ${rec.buy} buy / ${rec.hold} hold / ${rec.sell} sell` : "",
        `\nInsider activity (recent): ${insiderPurchases} open-market purchases, ${insiderSales} open-market sales`,
      ].filter(Boolean).join("\n");
    
      const client = new Anthropic({ apiKey: config.anthropicApiKey });
    
      const systemPrompt =
        "You are a balanced, rigorous stock analyst in the style of The Motley Fool. " +
        "Your job is to steelman BOTH the bull and bear cases with equal intellectual honesty. " +
        "The bull case should be genuinely optimistic with specific data. " +
        "The bear case should be genuinely challenging — not strawman risks. " +
        "The net verdict should be your honest synthesis of both sides. " +
        "Write clearly for a retail investor. Always respond with valid JSON.";
    
      const userPrompt =
        `Build a rigorous bull vs. bear case for ${ticker}.\n\n` +
        dataContext +
        `\n\nReturn a JSON object with this exact structure:
    {
      "companyName": "full company name",
      "bullCase": [
        { "argument": "title of the bull point", "detail": "2-3 sentences explaining it with specific data" },
        { "argument": "title", "detail": "..." },
        { "argument": "title", "detail": "..." }
      ],
      "bearCase": [
        { "argument": "title of the bear point", "detail": "2-3 sentences explaining the genuine risk" },
        { "argument": "title", "detail": "..." },
        { "argument": "title", "detail": "..." }
      ],
      "verdict": "bull_wins" | "slight_bull" | "too_close" | "slight_bear" | "bear_wins",
      "verdictRationale": "2-3 sentences explaining which side is more compelling and why. Be direct.",
      "keyDebate": "the single most important question investors need to answer about this stock",
      "forInvestorsWho": "1 sentence describing what type of investor this stock suits (or doesn't suit)"
    }`;
    
      const message = await client.messages.create({
        model: "claude-haiku-4-5-20251001",
        max_tokens: 1500,
        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,
        generatedAt: new Date().toISOString(),
      };
    }
  • Schema definition for the input to the bear-vs-bull tool.
    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)"),
    });
  • Definition and registration of the bear-vs-bull tool.
    const bearVsBullTool: ToolDefinition<Input> = {
      name: "bear-vs-bull",
      description:
        "Generate a structured bull vs. bear case for any stock. Steelmans both sides equally — 3 bull arguments " +
        "and 3 bear arguments with specific data, then delivers a net verdict and the key question investors need " +
        "to answer. Great for stress-testing a thesis or getting a balanced view before investing. 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" },
      },
    };
    
    registerTool(bearVsBullTool);
    export default bearVsBullTool;

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