SPB Git

spb/vquant Public MIT

VibeQuant — AI-powered institutional-grade financial intelligence platform.

TypeScript 84.3% Python 11.7% JavaScript 1.6% CSS 1.5% HTML 0.7%
103.6 KB · 2,109 lines typescript
Raw Blame History
1/*2 * =============================================================================3 *  VibeQuant (vquant) — AI-Powered Financial Intelligence Platform4 * -----------------------------------------------------------------------------5 *  File:      server/routes.ts6 *7 *  Author:    Simon-Pierre Boucher8 *  Contact:   contact@spboucher.ai9 *  Website:   https://www.spboucher.ai10 *  Demo:      https://www.vquant.ai11 *  License:   MIT (see LICENSE)12 *13 *  Copyright © 2026 Simon-Pierre Boucher. All rights reserved.14 * =============================================================================15 */1617import type { Express, Request, Response } from "express";18import { createServer, type Server } from "http";19import path from "path";20import { storage } from "./storage";21import { callClaudeStreaming } from "./services/claudeService";22import { callOpenAIStreaming } from "./services/openaiService";23import { randomBytes } from "crypto";24import { registerAuthRoutes } from "./routes/auth";25import { registerAdminRoutes } from "./routes/admin";26import { registerAnalyticsRoutes } from "./routes/analytics";27import { registerFmpRoutes } from "./routes/fmp";28import { registerReportRoutes } from "./routes/reports";29import { chatBodySchema } from "./routes/validation";30import { searchImages, getMapDirections, searchGoogleShopping, searchFinance, searchJobs, searchFlights, searchHotels, searchVideos, searchGoogleTrends, searchGoogleScholar, searchAppStore } from "./services/serpapiService";31import { searchExa, getContentsExa } from "./services/exaService";32import { searchFirecrawl, scrapeFirecrawl, crawlFirecrawl, extractFirecrawl, mapFirecrawl, agentFirecrawl, batchScrapeFirecrawl } from "./services/firecrawlService";33import { searchTavily, extractTavily, crawlTavily, mapTavily, researchTavily, getUsageTavily } from "./services/tavilyService";34import {35  getCompanyProfile,36  getIncomeStatement,37  getBalanceSheet,38  getCashFlowStatement,39  getKeyMetrics,40  getFinancialRatios,41  getStockQuote,42  getHistoricalPrice,43  searchCompanies,44  getStockPeers,45  getFinancialNews,46  getRSI,47  getMACD,48  getEMA,49  getSMA,50  getADX,51  getWilliamsR,52  getCCI,53  getStochasticOscillator,54  getEconomicCalendar,55  getTreasuryRates,56  getEconomicIndicator,57  getInsiderTrading,58  getInsiderTradeStatistics,59  getEarningsCalendar,60  getEarningsSurprises,61  getAnalystEstimates,62  getForexQuote,63  getForexList,64  getCommodityQuotes,65  getCOTReport,66  getCOTAnalysis,67  getPressReleases,68  getDividendHistory,69  getStockSplitHistory,70  getIPOCalendar,71  getIntradayPrice,72  getPriceTarget,73  getPriceTargetSummary,74  getUpgradesDowngrades,75  getInstitutionalHolders,76  getESGScore,77  getSocialSentiment,78  getCongressionalTrading,79  getSenateTrading,80  searchByCIK,81  searchByCUSIP,82  searchByISIN,83  getStockScreener,84  getMarketHours,85  getGainers,86  getLosers,87  getActives,88  getETFHoldings,89  getETFSectorWeightings,90  getETFCountryWeightings,91  getFinancialGrowth,92  getCompanyOutlook,93  getStockNewsSentiment,94  getCryptoQuote,95  getCryptoList,96  getForexHistorical,97  getSECFilings,98  getCompanyNotes,99  getEarningsCallTranscript,100  // NEW IMPORTS FOR EXPANDED FMP API101  searchSymbol,102  searchName,103  searchExchangeVariants,104  getStockList,105  getFinancialStatementSymbolList,106  getCIKList,107  getSymbolChange,108  getETFList,109  getActivelyTradingList,110  getEarningsTranscriptList,111  getAvailableExchanges,112  getAvailableSectors,113  getAvailableIndustries,114  getAvailableCountries,115  getProfileByCIK,116  getDelistedCompanies,117  getEmployeeCount,118  getHistoricalEmployeeCount,119  getMarketCapitalization,120  getBatchMarketCapitalization,121  getHistoricalMarketCapitalization,122  getSharesFloat,123  getAllSharesFloat,124  getLatestMergersAcquisitions,125  searchMergersAcquisitions,126  getKeyExecutives,127  getExecutiveCompensation,128  getExecutiveCompensationBenchmark,129  getQuoteShort,130  getAftermarketTrade,131  getAftermarketQuote,132  getStockPriceChange,133  getBatchQuote,134  getBatchQuoteShort,135  getBatchExchangeQuote,136  getBatchMutualfundQuotes,137  getBatchETFQuotes,138  getBatchCommodityQuotes,139  getBatchCryptoQuotes,140  getBatchForexQuotes,141  getBatchIndexQuotes,142  getLatestFinancialStatements,143  getIncomeStatementTTM,144  getBalanceSheetTTM,145  getCashflowStatementTTM,146  getKeyMetricsTTM,147  getRatiosTTM,148  getFinancialScores,149  getOwnerEarnings,150  getEnterpriseValues,151  getRevenueProductSegmentation,152  getRevenueGeographicSegmentation,153  getMarketRiskPremium,154  getDividendsCompany,155  getDividendsCalendar,156  getEarningsReport,157  getIPODisclosures,158  getIPOProspectus,159  getSplits,160  getSplitsCalendar,161  getLatestEarningTranscripts,162  getEarningCallTranscriptDates,163  getFMPArticles,164  getGeneralNews,165  getPressReleasesLatest,166  getStockNewsLatest,167  getCryptoNews,168  getForexNews,169  searchPressReleasesNew,170  searchStockNews,171  searchCryptoNews,172  searchForexNews,173  getInstitutionalOwnershipFilings,174  extractSECFilings,175  getForm13FFilingsDates,176  getFilingsExtractWithAnalytics,177  getHolderPerformanceSummary,178  getHoldersIndustryBreakdown,179  getPositionsSummary,180  getIndustryPerformanceSummary,181  getRatingsSnapshot,182  getHistoricalRatings,183  getPriceTargetConsensus,184  getGrades,185  getHistoricalGrades,186  getGradesSummary,187  getMarketSectorPerformanceSnapshot,188  getIndustryPerformanceSnapshot,189  getHistoricalSectorPerformance,190  getHistoricalIndustryPerformance,191  getSectorPESnapshot,192  getIndustryPESnapshot,193  getHistoricalSectorPE,194  getHistoricalIndustryPE,195  getWMA,196  getDEMA,197  getTEMA,198  getStandardDeviation,199  getETFAssetExposure,200  getMutualFundDisclosureLatest,201  getMutualFundDisclosure,202  getMutualFundDisclosureSearch,203  getMutualFundDisclosureDates,204  getLatest8KSECFilings,205  getLatestSECFilings,206  getSECFilingsByFormType,207  getSECFilingsBySymbol,208  getSECFilingsByCIK,209  getSECFilingsByName,210  getSECFilingsCompanySearchBySymbol,211  getSECFilingsCompanySearchByCIK,212  getSECCompanyFullProfile,213  getIndustryClassificationList,214  searchIndustryClassification,215  getAllIndustryClassification,216  getLatestInsiderTrading,217  searchInsiderTrades,218  searchInsiderTradesByName,219  getAllInsiderTransactionTypes,220  getAcquisitionOfBeneficialOwnership,221  getIndexList,222  getSP500Constituent,223  getNasdaqConstituent,224  getDowJonesConstituent,225  getHistoricalSP500Constituent,226  getHistoricalNasdaqConstituent,227  getHistoricalDowJonesConstituent,228  getExchangeMarketHours,229  getHolidaysByExchange,230  getAllExchangeMarketHours,231  getCommoditiesList,232  getDCFValuation,233  getLeveredDCF,234  getCustomDCF,235  getCustomLeveredDCF,236  getForexCurrencyPairs,237  getLatestSenateDisclosures,238  getLatestHouseDisclosures,239  getSenateTradesSymbol,240  getSenateTradesByName,241  getHouseTradesSymbol,242  getHouseTradesByName,243  getESGDisclosures,244  getESGRatings,245  getESGBenchmark,246  getCOTList,247  getLatestCrowdfundingCampaigns,248  searchCrowdfundingCampaigns,249  getCrowdfundingByCIK,250  getEquityOfferingUpdates,251  searchEquityOfferings,252  getCompanyEquityOfferingsByCIK253} from "./services/fmpService";254import {255  getEodhdHistorical,256  getEodhdRealTimeQuote,257  getEodhdIntraday,258  getEodhdFundamentals,259  getEodhdDividends,260  getEodhdSplits,261  searchEodhd,262  getEodhdNews,263  getEodhdOptionsChain,264  getEodhdOptionContract265} from "./services/eodhdService";266import { executeMonteCarloSimulation, executeOptionsPricing, executeGarchModel, executeVarCalculation, executePortfolioOptimization, executeRiskMetricsAnalysis, executePlot, executeDataDownload, executeCustomPython } from "./services/pythonExecutor";267import { randomUUID } from "crypto";268import type { SearchResult } from "@shared/types";269import { logger } from "./utils/logger";270import multer from "multer";271import FormData from "form-data";272import axios from "axios";273274export async function registerRoutes(app: Express): Promise<Server> {275  276  // Multer setup for audio file uploads (speech-to-text)277  const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 25 * 1024 * 1024 } });278279  // Speech-to-Text endpoint using ElevenLabs Scribe v2280  app.post("/api/speech-to-text", upload.single("audio"), async (req: Request, res: Response) => {281    try {282      const ELEVENLABS_API_KEY = process.env.ELEVENLABS_API_KEY;283      if (!ELEVENLABS_API_KEY) {284        logger.error('ELEVENLABS_API_KEY not set in environment');285        return res.status(500).json({ error: "ElevenLabs API key not configured" });286      }287288      const file = (req as any).file;289      if (!file) {290        logger.error('No audio file in request');291        return res.status(400).json({ error: "Audio file is required" });292      }293294      logger.section('SPEECH-TO-TEXT');295      logger.kv('File size', `${(file.size / 1024).toFixed(1)} KB`);296      logger.kv('MIME type', file.mimetype);297      logger.kv('Original name', file.originalname);298299      // Determine file extension from mimetype300      let ext = "webm";301      if (file.mimetype.includes("ogg")) ext = "ogg";302      else if (file.mimetype.includes("mp4")) ext = "mp4";303      else if (file.mimetype.includes("mpeg")) ext = "mp3";304305      const formData = new FormData();306      formData.append("file", file.buffer, {307        filename: `audio.${ext}`,308        contentType: file.mimetype || "audio/webm",309      });310      formData.append("model_id", "scribe_v2");311312      logger.info('Sending audio to ElevenLabs STT API...');313314      const response = await axios.post(315        "https://api.elevenlabs.io/v1/speech-to-text",316        formData,317        {318          headers: {319            "xi-api-key": ELEVENLABS_API_KEY,320            ...formData.getHeaders(),321          },322          timeout: 60000,323        }324      );325326      const text = response.data?.text || "";327      logger.success(`Transcription (${text.length} chars): "${text.substring(0, 150)}"`);328329      return res.json({ text, language_code: response.data?.language_code });330    } catch (error: any) {331      const detail = error.response?.data ? JSON.stringify(error.response.data) : error.message;332      logger.error(`Speech-to-text error: ${detail}`);333      return res.status(error.response?.status || 500).json({334        error: error.response?.data?.detail || error.message || "Speech-to-text failed",335      });336    }337  });338339  // Chat endpoint - AI chat with streaming using Claude Opus 4.6 via HTTP340  app.post("/api/chat", async (req: Request, res: Response) => {341    let sseStarted = false;342343    try {344      const parsed = chatBodySchema.safeParse(req.body);345      if (!parsed.success) {346        return res.status(400).json({ error: parsed.error.issues[0].message });347      }348      const { query, history, sessionId: reqSessionId, imageData, imageMimeType, model } = parsed.data;349      const sessionId = reqSessionId || randomUUID();350351      logger.section('CHAT REQUEST');352      logger.request('POST', '/api/chat');353      logger.kv('Session ID', sessionId);354      logger.kv('Query', query.substring(0, 100) + (query.length > 100 ? '...' : ''));355      logger.kv('History length', history.length);356357      // Set up SSE (Server-Sent Events) for streaming358      res.setHeader('Content-Type', 'text/event-stream');359      res.setHeader('Cache-Control', 'no-cache, no-transform');360      res.setHeader('Connection', 'keep-alive');361      res.setHeader('X-Accel-Buffering', 'no'); // Disable nginx buffering362      res.setHeader('Content-Encoding', 'none'); // Disable compression363      res.flushHeaders(); // Send headers immediately364      sseStarted = true;365      logger.success('SSE streaming initialized');366      367      let fullAnswer = '';368      let sources: SearchResult[] = [];369      let messages: any[] = [];370      let allToolResults: Array<{ tool: string; result: any }> = [];371      let allSearchQueries: Array<{ query: string; results?: SearchResult[] }> = [];372      let pythonCodeUsed: string = '';373      let monteCarloData: any = null;374      let customPythonFiguresData: Array<{ id: string; figures: string[]; output?: string; description?: string }> = [];375      let totalInputTokens = 0;376      let totalOutputTokens = 0;377      let totalCost = 0;378      379      // Save user message380      logger.database('Saving user message to database...');381      await storage.createMessage({382        sessionId,383        role: 'user',384        content: query,385        sources: null,386      });387      logger.success('User message saved');388389      // Send status message390      logger.info('Sending status update to client');391      res.write(`data: ${JSON.stringify({ type: 'status', message: '📊 Financial analysis in progress...' })}\n\n`);392      393      // Build messages array with conversation history394      // Add previous conversation messages (only question and answer text)395      for (const msg of history) {396        messages.push({397          role: 'user',398          content: msg.question,399        });400        messages.push({401          role: 'assistant',402          content: msg.answer,403        });404      }405      406      // Add current user message (multimodal if image attached)407      if (imageData && imageMimeType) {408        logger.kv('Image attached', imageMimeType);409        messages.push({410          role: 'user',411          content: [412            {413              type: 'image',414              source: {415                type: 'base64',416                media_type: imageMimeType,417                data: imageData,418              },419            },420            {421              type: 'text',422              text: query,423            },424          ],425        });426      } else {427        messages.push({428          role: 'user',429          content: query,430        });431      }432      433      // Function to handle Claude streaming and tool execution434      let isFirstChunkAfterTools = false;435      const isOpenAICompat = model.startsWith('gpt-') || model.startsWith('gemini-');436      const callModelStreaming = isOpenAICompat ? callOpenAIStreaming : callClaudeStreaming;437      const processClaudeResponse = async () => {438        logger.section(model.startsWith('gemini-') ? 'Gemini API Call' : isOpenAICompat ? 'OpenAI API Call' : 'Claude API Call');439        logger.kv('Messages in context', messages.length);440        const { answer, toolCalls, usage } = await callModelStreaming(441          query,442          (chunk) => {443            // Add line break before first chunk if coming after tool execution444            if (isFirstChunkAfterTools) {445              const lineBreak = '\n\n';446              fullAnswer += lineBreak + chunk;447              res.write(`data: ${JSON.stringify({ type: 'text', content: lineBreak + chunk })}\n\n`);448              // @ts-ignore - flush may not be in types but exists at runtime449              if (res.flush) res.flush();450              isFirstChunkAfterTools = false;451            } else {452              fullAnswer += chunk;453              res.write(`data: ${JSON.stringify({ type: 'text', content: chunk })}\n\n`);454              // @ts-ignore - flush may not be in types but exists at runtime455              if (res.flush) res.flush();456            }457          },458          messages,459          model,460          (thinking) => {461            res.write(`data: ${JSON.stringify({ type: 'thinking', content: thinking })}462463`);464            // @ts-ignore - flush may not be in types but exists at runtime465            if (res.flush) res.flush();466          }467        );468        logger.success('Claude API response received');469470        // Accumulate token usage471        totalInputTokens += usage.inputTokens;472        totalOutputTokens += usage.outputTokens;473        totalCost += usage.cost;474        logger.kv('Tokens (input/output)', `${usage.inputTokens}/${usage.outputTokens}`);475        logger.kv('Total accumulated (in/out)', `${totalInputTokens}/${totalOutputTokens}`);476        logger.kv('Cost for this call', `$${usage.cost.toFixed(6)}`);477478        // If Claude wants to use tools479        if (toolCalls && toolCalls.length > 0) {480          logger.section('Tools Requested by Claude');481          logger.kv('Number of tools', toolCalls.length);482483          // ==================== SEQUENTIAL BATCH EXECUTION ====================484          // Execute ALL tools in batches, accumulate results, then send all to Claude at once485          const MAX_TOOLS_PER_BATCH = 5; // Process 5 tools at a time to avoid overwhelming APIs486          const allToolResultsForClaude: any[] = [];487488          // Split tools into batches489          const batches: any[][] = [];490          for (let i = 0; i < toolCalls.length; i += MAX_TOOLS_PER_BATCH) {491            batches.push(toolCalls.slice(i, i + MAX_TOOLS_PER_BATCH));492          }493494          if (batches.length > 1) {495            logger.warn(`Large request: executing ${toolCalls.length} tools in ${batches.length} batches`);496          }497498          res.write(`data: ${JSON.stringify({ type: 'tools', tools: toolCalls })}\n\n`);499500          // Add ALL assistant's tool use to messages at once501          messages.push({502            role: 'assistant',503            content: toolCalls.map((tc: any) => ({504              type: 'tool_use',505              id: tc.id,506              name: tc.name,507              input: tc.input,508            })),509          });510511          // Execute batches sequentially512          for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) {513            const batch = batches[batchIndex];514515            if (batches.length > 1) {516              logger.info(`Processing batch ${batchIndex + 1}/${batches.length} (${batch.length} tools)`);517              res.write(`data: ${JSON.stringify({518                type: 'status',519                message: `📊 Batch ${batchIndex + 1}/${batches.length}: ${batch.map((t: any) => t.name).join(', ')}...`520              })}\n\n`);521            }522523            // Execute tools in current batch524            for (const toolCall of batch) {525              logger.startTimer(toolCall.id);526              logger.tool(toolCall.name, `Executing... (ID: ${toolCall.id})`);527              const toolStartTime = Date.now();528529              // Notify client that tool execution is starting530              res.write(`data: ${JSON.stringify({531                type: 'tool_start',532                tool: toolCall.name,533                toolId: toolCall.id,534                input: toolCall.input535              })}\n\n`);536537              try {538                let result: any;539540                if (toolCall.name === 'get_company_profile') {541                logger.info(`Getting company profile for: ${toolCall.input.symbol}`);542                result = await getCompanyProfile(toolCall.input.symbol);543                logger.success('Company profile retrieved');544              } else if (toolCall.name === 'get_income_statement') {545                logger.debug(`   → Getting income statement: ${toolCall.input.symbol} (${toolCall.input.period || 'annual'})`);546                result = await getIncomeStatement(547                  toolCall.input.symbol,548                  toolCall.input.period || 'annual',549                  toolCall.input.limit || 5550                );551                logger.debug('   ✓ Income statement retrieved');552              } else if (toolCall.name === 'get_balance_sheet') {553                result = await getBalanceSheet(554                  toolCall.input.symbol,555                  toolCall.input.period || 'annual',556                  toolCall.input.limit || 5557                );558              } else if (toolCall.name === 'get_cash_flow') {559                result = await getCashFlowStatement(560                  toolCall.input.symbol,561                  toolCall.input.period || 'annual',562                  toolCall.input.limit || 5563                );564              } else if (toolCall.name === 'get_key_metrics') {565                result = await getKeyMetrics(566                  toolCall.input.symbol,567                  toolCall.input.period || 'annual',568                  toolCall.input.limit || 5569                );570              } else if (toolCall.name === 'get_financial_ratios') {571                result = await getFinancialRatios(572                  toolCall.input.symbol,573                  toolCall.input.period || 'annual',574                  toolCall.input.limit || 5575                );576              } else if (toolCall.name === 'get_stock_quote') {577                result = await getStockQuote(toolCall.input.symbol);578              } else if (toolCall.name === 'get_historical_price') {579                result = await getHistoricalPrice(580                  toolCall.input.symbol,581                  toolCall.input.from,582                  toolCall.input.to583                );584              } else if (toolCall.name === 'search_companies') {585                result = await searchCompanies(586                  toolCall.input.query,587                  toolCall.input.limit || 10588                );589              } else if (toolCall.name === 'get_stock_peers') {590                result = await getStockPeers(toolCall.input.symbol);591              } else if (toolCall.name === 'get_financial_news') {592                result = await getFinancialNews(593                  toolCall.input.symbol,594                  toolCall.input.limit || 5595                );596              } else if (toolCall.name === 'get_rsi') {597                result = await getRSI(598                  toolCall.input.symbol,599                  toolCall.input.period || 14,600                  toolCall.input.timePeriod || '1day'601                );602              } else if (toolCall.name === 'get_macd') {603                result = await getMACD(604                  toolCall.input.symbol,605                  toolCall.input.timePeriod || '1day'606                );607              } else if (toolCall.name === 'get_ema') {608                result = await getEMA(609                  toolCall.input.symbol,610                  toolCall.input.period || 50,611                  toolCall.input.timePeriod || '1day'612                );613              } else if (toolCall.name === 'get_sma') {614                result = await getSMA(615                  toolCall.input.symbol,616                  toolCall.input.period || 50,617                  toolCall.input.timePeriod || '1day'618                );619              } else if (toolCall.name === 'get_adx') {620                result = await getADX(621                  toolCall.input.symbol,622                  toolCall.input.period || 14,623                  toolCall.input.timePeriod || '1day'624                );625              } else if (toolCall.name === 'get_williams_r') {626                result = await getWilliamsR(627                  toolCall.input.symbol,628                  toolCall.input.period || 14,629                  toolCall.input.timePeriod || '1day'630                );631              } else if (toolCall.name === 'get_cci') {632                result = await getCCI(633                  toolCall.input.symbol,634                  toolCall.input.period || 20,635                  toolCall.input.timePeriod || '1day'636                );637              } else if (toolCall.name === 'get_stochastic') {638                result = await getStochasticOscillator(639                  toolCall.input.symbol,640                  toolCall.input.period || 14,641                  toolCall.input.timePeriod || '1day'642                );643              } else if (toolCall.name === 'get_economic_calendar') {644                result = await getEconomicCalendar(645                  toolCall.input.from,646                  toolCall.input.to647                );648              } else if (toolCall.name === 'get_treasury_rates') {649                result = await getTreasuryRates(650                  toolCall.input.from,651                  toolCall.input.to652                );653              } else if (toolCall.name === 'get_economic_indicator') {654                result = await getEconomicIndicator(655                  toolCall.input.indicator,656                  toolCall.input.from,657                  toolCall.input.to658                );659              } else if (toolCall.name === 'get_insider_trading') {660                result = await getInsiderTrading(661                  toolCall.input.symbol,662                  toolCall.input.limit || 20663                );664              } else if (toolCall.name === 'get_insider_trade_statistics') {665                result = await getInsiderTradeStatistics(toolCall.input.symbol);666              } else if (toolCall.name === 'get_earnings_calendar') {667                result = await getEarningsCalendar(668                  toolCall.input.from,669                  toolCall.input.to670                );671              } else if (toolCall.name === 'get_earnings_surprises') {672                result = await getEarningsSurprises(toolCall.input.symbol);673              } else if (toolCall.name === 'get_analyst_estimates') {674                result = await getAnalystEstimates(675                  toolCall.input.symbol,676                  toolCall.input.period || 'annual',677                  toolCall.input.limit || 5678                );679              } else if (toolCall.name === 'get_forex_quote') {680                result = await getForexQuote(toolCall.input.pair);681              } else if (toolCall.name === 'get_forex_list') {682                result = await getForexList();683              } else if (toolCall.name === 'get_commodity_quotes') {684                result = await getCommodityQuotes();685              } else if (toolCall.name === 'get_cot_report') {686                result = await getCOTReport(toolCall.input.symbol);687              } else if (toolCall.name === 'get_cot_analysis') {688                result = await getCOTAnalysis(toolCall.input.symbol);689              } else if (toolCall.name === 'get_press_releases') {690                result = await getPressReleases(691                  toolCall.input.symbol,692                  toolCall.input.limit || 20693                );694              } else if (toolCall.name === 'get_dividend_history') {695                result = await getDividendHistory(toolCall.input.symbol);696              } else if (toolCall.name === 'get_stock_split_history') {697                result = await getStockSplitHistory(toolCall.input.symbol);698              } else if (toolCall.name === 'get_ipo_calendar') {699                result = await getIPOCalendar(700                  toolCall.input.from,701                  toolCall.input.to702                );703              } else if (toolCall.name === 'get_intraday_price') {704                result = await getIntradayPrice(705                  toolCall.input.symbol,706                  toolCall.input.interval || '15min'707                );708              } else if (toolCall.name === 'get_price_target') {709                result = await getPriceTarget(toolCall.input.symbol);710              } else if (toolCall.name === 'get_price_target_summary') {711                result = await getPriceTargetSummary(toolCall.input.symbol);712              } else if (toolCall.name === 'get_upgrades_downgrades') {713                result = await getUpgradesDowngrades(toolCall.input.symbol);714              } else if (toolCall.name === 'get_institutional_holders') {715                result = await getInstitutionalHolders(toolCall.input.symbol);716              } else if (toolCall.name === 'get_esg_score') {717                result = await getESGScore(toolCall.input.symbol);718              } else if (toolCall.name === 'get_social_sentiment') {719                result = await getSocialSentiment(720                  toolCall.input.symbol,721                  toolCall.input.limit || 10722                );723              } else if (toolCall.name === 'get_congressional_trading') {724                result = await getCongressionalTrading(toolCall.input.symbol);725              } else if (toolCall.name === 'get_senate_trading') {726                result = await getSenateTrading(toolCall.input.symbol);727              } else if (toolCall.name === 'search_by_cik') {728                result = await searchByCIK(toolCall.input.cik);729              } else if (toolCall.name === 'search_by_cusip') {730                result = await searchByCUSIP(toolCall.input.cusip);731              } else if (toolCall.name === 'search_by_isin') {732                result = await searchByISIN(toolCall.input.isin);733              } else if (toolCall.name === 'get_stock_screener') {734                result = await getStockScreener(toolCall.input);735              } else if (toolCall.name === 'get_market_hours') {736                result = await getMarketHours();737              } else if (toolCall.name === 'get_etf_holdings') {738                result = await getETFHoldings(toolCall.input.symbol);739              } else if (toolCall.name === 'get_etf_sector_weightings') {740                result = await getETFSectorWeightings(toolCall.input.symbol);741              } else if (toolCall.name === 'get_etf_country_weightings') {742                result = await getETFCountryWeightings(toolCall.input.symbol);743              } else if (toolCall.name === 'get_financial_growth') {744                result = await getFinancialGrowth(745                  toolCall.input.symbol,746                  toolCall.input.period || 'annual',747                  toolCall.input.limit || 5748                );749              } else if (toolCall.name === 'get_company_outlook') {750                result = await getCompanyOutlook(toolCall.input.symbol);751              } else if (toolCall.name === 'get_stock_news_sentiment') {752                result = await getStockNewsSentiment(753                  toolCall.input.symbol,754                  toolCall.input.limit || 5755                );756              } else if (toolCall.name === 'get_crypto_quote') {757                result = await getCryptoQuote(toolCall.input.symbol);758              } else if (toolCall.name === 'get_crypto_list') {759                result = await getCryptoList();760              } else if (toolCall.name === 'get_forex_historical') {761                result = await getForexHistorical(762                  toolCall.input.pair,763                  toolCall.input.from,764                  toolCall.input.to765                );766              } else if (toolCall.name === 'get_sec_filings') {767                result = await getSECFilings(768                  toolCall.input.symbol,769                  toolCall.input.type,770                  toolCall.input.limit || 20771                );772              } else if (toolCall.name === 'get_company_notes') {773                result = await getCompanyNotes(toolCall.input.symbol);774              } else if (toolCall.name === 'get_earnings_call_transcript') {775                result = await getEarningsCallTranscript(776                  toolCall.input.symbol,777                  toolCall.input.year,778                  toolCall.input.quarter779                );780              } else if (toolCall.name === 'run_monte_carlo_simulation') {781                logger.debug('   → Running Monte Carlo simulation...');782                logger.debug('      Simulations:', toolCall.input.num_simulations || 10000);783                logger.debug('      Time horizon:', toolCall.input.time_horizon || 252);784                logger.debug('      Initial investment:', toolCall.input.initial_investment || 10000);785786                // NEW: Support symbol-based mode (recommended) or manual historical_prices mode787                let simulationInput: any = {788                  num_simulations: toolCall.input.num_simulations || 10000,789                  time_horizon: toolCall.input.time_horizon || 252,790                  initial_investment: toolCall.input.initial_investment || 10000791                };792793                if (toolCall.input.symbol) {794                  logger.debug('      📊 Mode: API-based (fetching data for symbol:', toolCall.input.symbol + ')');795                  simulationInput.symbol = toolCall.input.symbol;796                } else if (toolCall.input.historical_prices && Array.isArray(toolCall.input.historical_prices)) {797                  logger.debug('      📊 Mode: Manual (using provided historical prices)');798                  simulationInput.historical_prices = toolCall.input.historical_prices;799                } else {800                  throw new Error('Either symbol or historical_prices parameter is required');801                }802803                const simulationResult = await executeMonteCarloSimulation(simulationInput);804805                logger.debug('   ✓ Monte Carlo simulation complete:', {806                  success: simulationResult.success,807                  hasResult: !!simulationResult.result,808                  error: simulationResult.error809                });810811                // Check if simulation was successful812                if (!simulationResult.success || !simulationResult.result) {813                  throw new Error(simulationResult.error || 'Monte Carlo simulation failed');814                }815816                result = simulationResult.result;817818                // Store for session saving819                pythonCodeUsed = simulationResult.code || '';820                monteCarloData = result;821822                // Send Python code separately823                logger.debug('   📊 Sending Python code to client (length:', simulationResult.code?.length || 0, 'chars)');824                res.write(`data: ${JSON.stringify({825                  type: 'python_code',826                  code: simulationResult.code827                })}\n\n`);828              } else if (toolCall.name === 'calculate_options_price') {829                logger.debug('   → Calculating options prices...');830                logger.debug('      Strike price:', toolCall.input.strike_price);831                logger.debug('      Time to maturity:', toolCall.input.time_to_maturity);832833                // NEW: Support symbol-based mode (recommended) or manual mode834                let optionsInput: any = {835                  strike_price: toolCall.input.strike_price,836                  time_to_maturity: toolCall.input.time_to_maturity,837                  risk_free_rate: toolCall.input.risk_free_rate || 0.05,838                  option_type: toolCall.input.option_type || 'call'839                };840841                if (toolCall.input.symbol) {842                  logger.debug('      📊 Mode: API-based (fetching data for symbol:', toolCall.input.symbol + ')');843                  optionsInput.symbol = toolCall.input.symbol;844                  if (toolCall.input.volatility) {845                    logger.debug('      Using provided volatility:', toolCall.input.volatility);846                    optionsInput.volatility = toolCall.input.volatility;847                  } else {848                    logger.debug('      Will calculate historical volatility automatically');849                  }850                } else if (toolCall.input.stock_price) {851                  logger.debug('      📊 Mode: Manual');852                  logger.debug('      Stock price:', toolCall.input.stock_price);853                  optionsInput.stock_price = toolCall.input.stock_price;854                  optionsInput.volatility = toolCall.input.volatility;855                } else {856                  throw new Error('Either symbol or stock_price parameter is required');857                }858859                const optionsResult = await executeOptionsPricing(optionsInput);860861                logger.debug('   ✓ Options pricing complete:', {862                  success: optionsResult.success,863                  hasResult: !!optionsResult.result,864                  error: optionsResult.error865                });866867                // Check if pricing was successful868                if (!optionsResult.success || !optionsResult.result) {869                  throw new Error(optionsResult.error || 'Options pricing failed');870                }871872                result = optionsResult.result;873874                // Send Python code separately875                logger.debug('   📊 Sending Python code to client (length:', optionsResult.code?.length || 0, 'chars)');876                res.write(`data: ${JSON.stringify({877                  type: 'python_code',878                  code: optionsResult.code879                })}\n\n`);880              } else if (toolCall.name === 'estimate_garch_volatility') {881                logger.debug('   → Estimating GARCH volatility model...');882                logger.debug('      Symbol:', toolCall.input.symbol);883                logger.debug('      GARCH order: p=' + (toolCall.input.p || 1) + ', q=' + (toolCall.input.q || 1));884885                const garchInput: any = {886                  symbol: toolCall.input.symbol,887                  p: toolCall.input.p || 1,888                  q: toolCall.input.q || 1,889                  forecast_horizon: toolCall.input.forecast_horizon || 30,890                  data_period: toolCall.input.data_period || 504891                };892893                logger.debug('      Forecast horizon:', garchInput.forecast_horizon, 'days');894                logger.debug('      Data period:', garchInput.data_period, 'days');895896                const garchResult = await executeGarchModel(garchInput);897898                logger.debug('   ✓ GARCH model estimation complete:', {899                  success: garchResult.success,900                  hasResult: !!garchResult.result,901                  error: garchResult.error902                });903904                // Check if estimation was successful905                if (!garchResult.success || !garchResult.result) {906                  throw new Error(garchResult.error || 'GARCH model estimation failed');907                }908909                result = garchResult.result;910911                // Send Python code separately912                logger.debug('   📊 Sending Python code to client (length:', garchResult.code?.length || 0, 'chars)');913                res.write(`data: ${JSON.stringify({914                  type: 'python_code',915                  code: garchResult.code916                })}\n\n`);917              } else if (toolCall.name === 'calculate_var') {918                logger.debug('   → Calculating Value at Risk (VaR)...');919                logger.debug('      Symbol:', toolCall.input.symbol);920                logger.debug('      Portfolio value:', toolCall.input.portfolio_value || 100000);921922                const varInput: any = {923                  symbol: toolCall.input.symbol,924                  portfolio_value: toolCall.input.portfolio_value || 100000,925                  confidence_levels: toolCall.input.confidence_levels || [0.90, 0.95, 0.99],926                  time_horizon: toolCall.input.time_horizon || 1,927                  num_simulations: toolCall.input.num_simulations || 10000,928                  data_period: toolCall.input.data_period || 504929                };930931                logger.debug('      Confidence levels:', varInput.confidence_levels);932                logger.debug('      Time horizon:', varInput.time_horizon, 'days');933                logger.debug('      Monte Carlo simulations:', varInput.num_simulations);934935                const varResult = await executeVarCalculation(varInput);936937                logger.debug('   ✓ VaR calculation complete:', {938                  success: varResult.success,939                  hasResult: !!varResult.result,940                  error: varResult.error941                });942943                // Check if calculation was successful944                if (!varResult.success || !varResult.result) {945                  throw new Error(varResult.error || 'VaR calculation failed');946                }947948                result = varResult.result;949950                // Send Python code separately951                logger.debug('   📊 Sending Python code to client (length:', varResult.code?.length || 0, 'chars)');952                res.write(`data: ${JSON.stringify({953                  type: 'python_code',954                  code: varResult.code955                })}\n\n`);956              } else if (toolCall.name === 'optimize_portfolio') {957                logger.debug('   → Optimizing portfolio allocation...');958                logger.debug('      Symbols:', toolCall.input.symbols?.join(', '));959960                if (!toolCall.input.symbols || !Array.isArray(toolCall.input.symbols) || toolCall.input.symbols.length < 2) {961                  throw new Error('At least 2 symbols are required for portfolio optimization');962                }963964                const portfolioInput: any = {965                  symbols: toolCall.input.symbols,966                  risk_free_rate: toolCall.input.risk_free_rate || 0.02,967                  min_weight: toolCall.input.min_weight || 0.0,968                  max_weight: toolCall.input.max_weight || 1.0,969                  data_period: toolCall.input.data_period || 504,970                  generate_frontier: toolCall.input.generate_frontier !== false,971                  frontier_points: toolCall.input.frontier_points || 50972                };973974                logger.debug('      Risk-free rate:', portfolioInput.risk_free_rate);975                logger.debug('      Weight constraints: [' + portfolioInput.min_weight + ', ' + portfolioInput.max_weight + ']');976                logger.debug('      Generate efficient frontier:', portfolioInput.generate_frontier);977978                const portfolioResult = await executePortfolioOptimization(portfolioInput);979980                logger.debug('   ✓ Portfolio optimization complete:', {981                  success: portfolioResult.success,982                  hasResult: !!portfolioResult.result,983                  error: portfolioResult.error984                });985986                // Check if optimization was successful987                if (!portfolioResult.success || !portfolioResult.result) {988                  throw new Error(portfolioResult.error || 'Portfolio optimization failed');989                }990991                result = portfolioResult.result;992993                // Send Python code separately994                logger.debug('   📊 Sending Python code to client (length:', portfolioResult.code?.length || 0, 'chars)');995                res.write(`data: ${JSON.stringify({996                  type: 'python_code',997                  code: portfolioResult.code998                })}\n\n`);999              } else if (toolCall.name === 'analyze_risk_metrics') {1000                logger.debug('   → Analyzing risk metrics...');1001                logger.debug('      Symbol:', toolCall.input.symbol);1002                logger.debug('      Benchmark:', toolCall.input.benchmark_symbol || 'SPY');10031004                const riskMetricsInput: any = {1005                  symbol: toolCall.input.symbol,1006                  benchmark_symbol: toolCall.input.benchmark_symbol || 'SPY',1007                  risk_free_rate: toolCall.input.risk_free_rate || 0.02,1008                  data_period: toolCall.input.data_period || 5041009                };10101011                logger.debug('      Risk-free rate:', riskMetricsInput.risk_free_rate);1012                logger.debug('      Data period:', riskMetricsInput.data_period, 'days');10131014                const riskMetricsResult = await executeRiskMetricsAnalysis(riskMetricsInput);10151016                logger.debug('   ✓ Risk metrics analysis complete:', {1017                  success: riskMetricsResult.success,1018                  hasResult: !!riskMetricsResult.result,1019                  error: riskMetricsResult.error1020                });10211022                // Check if analysis was successful1023                if (!riskMetricsResult.success || !riskMetricsResult.result) {1024                  throw new Error(riskMetricsResult.error || 'Risk metrics analysis failed');1025                }10261027                result = riskMetricsResult.result;10281029                // Send Python code separately1030                logger.debug('   📊 Sending Python code to client (length:', riskMetricsResult.code?.length || 0, 'chars)');1031                res.write(`data: ${JSON.stringify({1032                  type: 'python_code',1033                  code: riskMetricsResult.code1034                })}\n\n`);1035              } else if (toolCall.name === 'create_plot') {1036                logger.debug('   → Creating custom plot...');1037                logger.debug('      Plot type:', toolCall.input.plot_type);1038                logger.debug('      Title:', toolCall.input.title || 'Financial Chart');10391040                const plotInput: any = {1041                  plot_type: toolCall.input.plot_type || 'line',1042                  data: toolCall.input.data,1043                  title: toolCall.input.title,1044                  xlabel: toolCall.input.xlabel,1045                  ylabel: toolCall.input.ylabel,1046                  color: toolCall.input.color,1047                  figsize: toolCall.input.figsize,1048                  grid: toolCall.input.grid,1049                  legend: toolCall.input.legend,1050                  style: toolCall.input.style,1051                  marker: toolCall.input.marker,1052                  alpha: toolCall.input.alpha,1053                  theme: toolCall.input.theme1054                };10551056                // Log if data is a symbol (string) or custom data1057                if (typeof plotInput.data === 'string') {1058                  logger.debug('      Data source: Symbol', plotInput.data);1059                } else {1060                  logger.debug('      Data source: Custom data');1061                }10621063                const plotResult = await executePlot(plotInput);10641065                logger.debug('   ✓ Plot creation complete:', {1066                  success: plotResult.success,1067                  hasResult: !!plotResult.result,1068                  error: plotResult.error1069                });10701071                // Check if plot was successful1072                if (!plotResult.success || !plotResult.result) {1073                  throw new Error(plotResult.error || 'Plot creation failed');1074                }10751076                result = plotResult.result;10771078                // Send Python code separately1079                logger.debug('   📊 Sending Python code to client (length:', plotResult.code?.length || 0, 'chars)');1080                res.write(`data: ${JSON.stringify({1081                  type: 'python_code',1082                  code: plotResult.code1083                })}\n\n`);10841085                // Send plot URL to client so frontend can correct figure references1086                if (result.image_url) {1087                  const plotUrl = result.image_url;1088                  logger.debug('   🖼️  Sending plot URL to client:', plotUrl);1089                  res.write(`data: ${JSON.stringify({1090                    type: 'custom_python_figures',1091                    figureId: `plot-${Date.now()}`,1092                    figures: [plotUrl],1093                    figureUrls: [plotUrl],1094                    output: '',1095                    description: toolCall.input.title || 'Financial Chart'1096                  })}\n\n`);1097                }1098              } else if (toolCall.name === 'execute_custom_python_analysis') {1099                logger.debug('   → Executing custom Python code...');1100                logger.debug('      Description:', toolCall.input.description || 'Custom analysis');1101                logger.debug('      Code length:', toolCall.input.code?.length || 0, 'chars');11021103                const customPythonResult = await executeCustomPython({1104                  code: toolCall.input.code,1105                  context: toolCall.input.context,1106                  description: toolCall.input.description1107                });11081109                logger.debug('   ✓ Custom Python execution complete:', {1110                  success: customPythonResult.success,1111                  hasOutput: !!customPythonResult.result?.output,1112                  figuresCount: customPythonResult.result?.figures?.length || 0,1113                  error: customPythonResult.error1114                });11151116                // Check if execution was successful1117                if (!customPythonResult.success || !customPythonResult.result) {1118                  throw new Error(customPythonResult.error || 'Custom Python execution failed');1119                }11201121                result = customPythonResult.result;11221123                // Don't send Python code to client (too slow to stream)1124                logger.debug('   📊 Python code generated (length:', customPythonResult.code?.length || 0, 'chars) - not streaming to client for speed');11251126                // Send figures if any were generated1127                if (result.figures && result.figures.length > 0) {1128                  // Generate unique ID for this figure batch1129                  const figureId = `fig-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;11301131                  logger.debug('   🖼️  Saving', result.figures.length, 'figure(s) with ID:', figureId);11321133                  // Store the original figures array1134                  const originalFigures = result.figures;1135                  const figureUrls: string[] = [];11361137                  // Save each figure as a PNG file and get URL1138                  const fs = await import('fs');1139                  const figuresDir = path.join(process.cwd(), 'dist', 'public', 'figures');11401141                  // Ensure directory exists1142                  if (!fs.existsSync(figuresDir)) {1143                    fs.mkdirSync(figuresDir, { recursive: true });1144                  }11451146                  for (let idx = 0; idx < originalFigures.length; idx++) {1147                    const figureFilename = `${figureId}-${idx}.png`;1148                    const figurePath = path.join(figuresDir, figureFilename);11491150                    // Write base64 image to file1151                    const base64Data = originalFigures[idx];1152                    fs.writeFileSync(figurePath, Buffer.from(base64Data, 'base64'));11531154                    // Create URL (works for both local and production)1155                    const figureUrl = `/figures/${figureFilename}`;1156                    figureUrls.push(figureUrl);11571158                    logger.debug(`   💾 Saved figure: ${figureFilename}`);1159                  }11601161                  // Send base64 figures + file URLs to client1162                  // base64 in `figures` ensures persistence when shared to DB1163                  // file URLs in `figureUrls` used for fast display during current session1164                  res.write(`data: ${JSON.stringify({1165                    type: 'custom_python_figures',1166                    figureId: figureId,1167                    figures: originalFigures,   // base64 data — persisted to DB via share1168                    figureUrls: figureUrls,     // file URLs — fast display during session1169                    output: result.output,1170                    description: toolCall.input.description1171                  })}\n\n`);11721173                  // Create figure references with actual URLs1174                  const figureReferences = figureUrls.map((url, idx) =>1175                    `![Figure ${idx + 1}](${url})`1176                  );11771178                  // Format output so Claude naturally includes figures in response1179                  // Structure: results first, then each figure with a prompt for commentary1180                  let formattedOutput = result.output || '';1181                  formattedOutput += '\n\n---\n📊 **ANALYSE DES FIGURES GÉNÉRÉES:**\n';1182                  figureUrls.forEach((url, idx) => {1183                    formattedOutput += `\n**Figure ${idx + 1}:**\n![Figure ${idx + 1}](${url})\n_(Décris cette figure ci-dessus dans ta réponse)_\n`;1184                  });1185                  formattedOutput += '\n---\n⚠️ IMPORTANT: Tu DOIS inclure chaque image ![Figure X](url) dans ta réponse markdown avec ton analyse de chaque figure.';11861187                  result.output = formattedOutput;11881189                  logger.debug('   ✅ Figure URLs:', figureUrls);11901191                  // Remove base64 data from result sent to Claude1192                  delete result.figures;11931194                  // Store figures for session saving1195                  customPythonFiguresData.push({1196                    id: figureId,1197                    figures: originalFigures,1198                    figureUrls: figureUrls,1199                    output: result.output,1200                    description: toolCall.input.description1201                  });1202                }1203                // Handle generated files (Excel, etc.)1204                if (result.files && result.files.length > 0) {1205                  const fsFiles = await import('fs');1206                  const downloadsDir = path.resolve('downloads');1207                  if (!fsFiles.existsSync(downloadsDir)) {1208                    fsFiles.mkdirSync(downloadsDir, { recursive: true });1209                  }12101211                  const fileLinks: Array<{ url: string; name: string }> = [];1212                  for (const file of result.files) {1213                    const uniqueName = `${Date.now()}_${file.filename}`;1214                    const destPath = path.join(downloadsDir, uniqueName);1215                    fsFiles.copyFileSync(file.filepath, destPath);1216                    // Cleanup temp file1217                    try { fsFiles.unlinkSync(file.filepath); } catch {}12181219                    const downloadUrl = `/api/download/${uniqueName}`;1220                    fileLinks.push({ url: downloadUrl, name: file.filename });1221                  }12221223                  // Add download links to output for Claude to include1224                  let fileOutput = '\n\n📥 **FICHIERS GÉNÉRÉS:**\n';1225                  fileLinks.forEach(f => {1226                    fileOutput += `\n- [Télécharger ${f.name}](${f.url})\n`;1227                  });1228                  fileOutput += '\n⚠️ IMPORTANT: Tu DOIS inclure chaque lien de téléchargement dans ta réponse.';1229                  result.output = (result.output || '') + fileOutput;12301231                  logger.debug('   📥 Files processed:', fileLinks.map(f => f.name));12321233                  delete result.files;1234                }12351236              } else if (toolCall.name === 'web_search_exa') {1237                logger.debug('   → Web search (Exa):', toolCall.input.query);1238                const searchResponse = await searchExa(1239                  toolCall.input.query,1240                  {1241                    numResults: toolCall.input.numResults,1242                    type: toolCall.input.type,1243                    category: toolCall.input.category,1244                    includeDomains: toolCall.input.includeDomains,1245                    excludeDomains: toolCall.input.excludeDomains,1246                    startPublishedDate: toolCall.input.startPublishedDate,1247                    endPublishedDate: toolCall.input.endPublishedDate,1248                    includeText: toolCall.input.includeText !== false,1249                    maxCharacters: 2000,1250                  }1251                );1252                result = searchResponse.results;1253                logger.debug('   ✓ Search completed:', result.length, 'results');1254              } else if (toolCall.name === 'get_contents_exa') {1255                result = await getContentsExa(1256                  toolCall.input.urls,1257                  {1258                    includeText: toolCall.input.includeText !== false,1259                    maxCharacters: toolCall.input.maxCharacters || 3000,1260                    includeHighlights: toolCall.input.includeHighlights,1261                    highlightsQuery: toolCall.input.highlightsQuery,1262                    numSentences: toolCall.input.numSentences,1263                    includeSummary: toolCall.input.includeSummary,1264                    summaryQuery: toolCall.input.summaryQuery,1265                  }1266                );1267              } else if (toolCall.name === 'get_option_chain') {1268                logger.info(`Getting option chain for: ${toolCall.input.symbol}`);1269                result = await getEodhdOptionsChain(toolCall.input.symbol, {1270                  expDateFrom: toolCall.input.exp_date_from,1271                  expDateTo: toolCall.input.exp_date_to,1272                  strikeFrom: toolCall.input.strike_from,1273                  strikeTo: toolCall.input.strike_to,1274                  type: toolCall.input.option_type,1275                  limit: toolCall.input.limit,1276                });1277                logger.success(`Option chain retrieved: ${result.returned_contracts}/${result.total_contracts} contracts`);1278              } else if (toolCall.name === 'get_option_prices') {1279                logger.info(`Getting option prices for: ${toolCall.input.identifier}`);1280                result = await getEodhdOptionContract(toolCall.input.identifier);1281                logger.success('Option prices retrieved');1282              } else if (toolCall.name === 'get_option_greeks') {1283                logger.info(`Getting option Greeks for: ${toolCall.input.identifier}`);1284                result = await getEodhdOptionContract(toolCall.input.identifier);1285                logger.success('Option Greeks retrieved');1286              } else if (toolCall.name === 'get_eodhd_historical') {1287                logger.info(`Getting EODHD historical prices for: ${toolCall.input.symbol}`);1288                result = await getEodhdHistorical(1289                  toolCall.input.symbol,1290                  toolCall.input.from,1291                  toolCall.input.to,1292                  toolCall.input.period1293                );1294                logger.success(`EODHD historical prices retrieved: ${result.length} bars`);1295              } else if (toolCall.name === 'get_eodhd_quote') {1296                logger.info(`Getting EODHD quote for: ${toolCall.input.symbol}`);1297                result = await getEodhdRealTimeQuote(1298                  toolCall.input.symbol,1299                  toolCall.input.additional_symbols1300                );1301                logger.success('EODHD quote retrieved');1302              } else if (toolCall.name === 'get_eodhd_intraday') {1303                logger.info(`Getting EODHD intraday bars for: ${toolCall.input.symbol}`);1304                result = await getEodhdIntraday(1305                  toolCall.input.symbol,1306                  toolCall.input.interval,1307                  toolCall.input.from,1308                  toolCall.input.to1309                );1310                logger.success(`EODHD intraday bars retrieved: ${result.length} bars`);1311              } else if (toolCall.name === 'get_eodhd_fundamentals') {1312                logger.info(`Getting EODHD fundamentals for: ${toolCall.input.symbol}`);1313                result = await getEodhdFundamentals(1314                  toolCall.input.symbol,1315                  toolCall.input.filter1316                );1317                logger.success('EODHD fundamentals retrieved');1318              } else if (toolCall.name === 'get_eodhd_dividends') {1319                logger.info(`Getting EODHD dividends for: ${toolCall.input.symbol}`);1320                result = await getEodhdDividends(1321                  toolCall.input.symbol,1322                  toolCall.input.from,1323                  toolCall.input.to1324                );1325                logger.success(`EODHD dividends retrieved: ${result.length} payments`);1326              } else if (toolCall.name === 'get_eodhd_splits') {1327                logger.info(`Getting EODHD splits for: ${toolCall.input.symbol}`);1328                result = await getEodhdSplits(1329                  toolCall.input.symbol,1330                  toolCall.input.from,1331                  toolCall.input.to1332                );1333                logger.success(`EODHD splits retrieved: ${result.length} splits`);1334              } else if (toolCall.name === 'search_eodhd') {1335                logger.info(`Searching EODHD instruments: ${toolCall.input.query}`);1336                result = await searchEodhd(toolCall.input.query, toolCall.input.limit);1337                logger.success(`EODHD search completed: ${result.length} results`);1338              } else if (toolCall.name === 'get_eodhd_news') {1339                logger.info(`Getting EODHD news (symbol: ${toolCall.input.symbol || '-'}, tag: ${toolCall.input.tag || '-'})`);1340                result = await getEodhdNews(1341                  toolCall.input.symbol,1342                  toolCall.input.tag,1343                  toolCall.input.limit,1344                  toolCall.input.from,1345                  toolCall.input.to1346                );1347                logger.success(`EODHD news retrieved: ${result.length} articles`);1348              } else if (toolCall.name === 'download_fmp_data') {1349                logger.info(`Downloading FMP data: ${toolCall.input.data_type} (format: ${toolCall.input.format})`);1350                const downloadResult = await executeDataDownload({1351                  data_type: toolCall.input.data_type,1352                  symbol: toolCall.input.symbol,1353                  format: toolCall.input.format,1354                  period: toolCall.input.period,1355                  limit: toolCall.input.limit,1356                  from_date: toolCall.input.from_date,1357                  to_date: toolCall.input.to_date,1358                  indicator_period: toolCall.input.indicator_period,1359                  time_period: toolCall.input.time_period,1360                  interval: toolCall.input.interval,1361                  news_limit: toolCall.input.news_limit,1362                  pair: toolCall.input.pair1363                });13641365                if (downloadResult.success && downloadResult.result) {1366                  // Generate download URL1367                  const downloadUrl = `/api/download/${downloadResult.result.filename}`;1368                  result = {1369                    success: true,1370                    filename: downloadResult.result.filename,1371                    download_url: downloadUrl,1372                    format: downloadResult.result.format,1373                    file_size: downloadResult.result.file_size,1374                    file_size_formatted: `${(downloadResult.result.file_size / 1024).toFixed(2)} KB`,1375                    metadata: downloadResult.result.metadata1376                  };1377                  logger.success(`Data download ready: ${downloadResult.result.filename}`);1378                } else {1379                  throw new Error(downloadResult.error || 'Data download failed');1380                }1381              } else if (toolCall.name === 'firecrawl_scrape') {1382                logger.info(`Firecrawl scrape: ${toolCall.input.url}`);1383                result = await scrapeFirecrawl({1384                  url: toolCall.input.url,1385                  formats: toolCall.input.formats,1386                  onlyMainContent: toolCall.input.onlyMainContent,1387                  includeTags: toolCall.input.includeTags,1388                  excludeTags: toolCall.input.excludeTags,1389                  maxAge: toolCall.input.maxAge,1390                  waitFor: toolCall.input.waitFor,1391                  mobile: toolCall.input.mobile,1392                  blockAds: toolCall.input.blockAds,1393                  timeout: toolCall.input.timeout,1394                  parsers: toolCall.input.parsers,1395                });1396              } else if (toolCall.name === 'firecrawl_search') {1397                logger.info(`Firecrawl search: "${toolCall.input.query}"`);1398                result = await searchFirecrawl({1399                  query: toolCall.input.query,1400                  limit: toolCall.input.limit,1401                  sources: toolCall.input.sources,1402                  categories: toolCall.input.categories,1403                  country: toolCall.input.country,1404                  tbs: toolCall.input.tbs,1405                  onlyMainContent: toolCall.input.onlyMainContent,1406                });1407              } else if (toolCall.name === 'firecrawl_crawl') {1408                logger.info(`Firecrawl crawl: ${toolCall.input.url} (limit: ${toolCall.input.limit || 10})`);1409                result = await crawlFirecrawl({1410                  url: toolCall.input.url,1411                  prompt: toolCall.input.prompt,1412                  limit: toolCall.input.limit,1413                  includePaths: toolCall.input.includePaths,1414                  excludePaths: toolCall.input.excludePaths,1415                  maxDiscoveryDepth: toolCall.input.maxDiscoveryDepth,1416                  sitemap: toolCall.input.sitemap,1417                  crawlEntireDomain: toolCall.input.crawlEntireDomain,1418                  allowSubdomains: toolCall.input.allowSubdomains,1419                  ignoreQueryParameters: toolCall.input.ignoreQueryParameters,1420                  onlyMainContent: toolCall.input.onlyMainContent,1421                });1422              } else if (toolCall.name === 'firecrawl_extract') {1423                logger.info(`Firecrawl extract: ${toolCall.input.urls.length} URL(s)`);1424                result = await extractFirecrawl({1425                  urls: toolCall.input.urls,1426                  prompt: toolCall.input.prompt,1427                  schema: toolCall.input.schema,1428                  enableWebSearch: toolCall.input.enableWebSearch,1429                  includeSubdomains: toolCall.input.includeSubdomains,1430                  showSources: toolCall.input.showSources,1431                });1432              } else if (toolCall.name === 'firecrawl_map') {1433                logger.info(`Firecrawl map: ${toolCall.input.url}`);1434                result = await mapFirecrawl({1435                  url: toolCall.input.url,1436                  search: toolCall.input.search,1437                  sitemap: toolCall.input.sitemap,1438                  includeSubdomains: toolCall.input.includeSubdomains,1439                  ignoreQueryParameters: toolCall.input.ignoreQueryParameters,1440                  limit: toolCall.input.limit,1441                });1442              } else if (toolCall.name === 'firecrawl_agent') {1443                logger.info(`Firecrawl agent: "${toolCall.input.prompt.substring(0, 50)}..."`);1444                result = await agentFirecrawl({1445                  prompt: toolCall.input.prompt,1446                  urls: toolCall.input.urls,1447                  schema: toolCall.input.schema,1448                  maxCredits: toolCall.input.maxCredits,1449                  strictConstrainToURLs: toolCall.input.strictConstrainToURLs,1450                  model: toolCall.input.model,1451                });1452              } else if (toolCall.name === 'firecrawl_batch_scrape') {1453                logger.info(`Firecrawl batch scrape: ${toolCall.input.urls.length} URL(s)`);1454                result = await batchScrapeFirecrawl({1455                  urls: toolCall.input.urls,1456                  maxConcurrency: toolCall.input.maxConcurrency,1457                  onlyMainContent: toolCall.input.onlyMainContent,1458                  parsers: toolCall.input.parsers,1459                  formats: toolCall.input.formats,1460                  timeout: toolCall.input.timeout,1461                  blockAds: toolCall.input.blockAds,1462                });1463              }1464              // ==================== TAVILY HANDLERS ====================1465              else if (toolCall.name === 'tavily_search') {1466                logger.info(`Tavily search: "${toolCall.input.query}" (depth: ${toolCall.input.search_depth || 'basic'})`);1467                result = await searchTavily({1468                  query: toolCall.input.query,1469                  search_depth: toolCall.input.search_depth,1470                  max_results: toolCall.input.max_results,1471                  topic: toolCall.input.topic,1472                  time_range: toolCall.input.time_range,1473                  start_date: toolCall.input.start_date,1474                  end_date: toolCall.input.end_date,1475                  include_answer: toolCall.input.include_answer,1476                  include_raw_content: toolCall.input.include_raw_content,1477                  include_images: toolCall.input.include_images,1478                  include_domains: toolCall.input.include_domains,1479                  exclude_domains: toolCall.input.exclude_domains,1480                  country: toolCall.input.country,1481                });1482              } else if (toolCall.name === 'tavily_extract') {1483                const urlCount = Array.isArray(toolCall.input.urls) ? toolCall.input.urls.length : 1;1484                logger.info(`Tavily extract: ${urlCount} URL(s)`);1485                result = await extractTavily({1486                  urls: toolCall.input.urls,1487                  query: toolCall.input.query,1488                  chunks_per_source: toolCall.input.chunks_per_source,1489                  extract_depth: toolCall.input.extract_depth,1490                  include_images: toolCall.input.include_images,1491                  format: toolCall.input.format,1492                  timeout: toolCall.input.timeout,1493                });1494              } else if (toolCall.name === 'tavily_crawl') {1495                logger.info(`Tavily crawl: ${toolCall.input.url} (limit: ${toolCall.input.limit || 50})`);1496                result = await crawlTavily({1497                  url: toolCall.input.url,1498                  instructions: toolCall.input.instructions,1499                  max_depth: toolCall.input.max_depth,1500                  max_breadth: toolCall.input.max_breadth,1501                  limit: toolCall.input.limit,1502                  select_paths: toolCall.input.select_paths,1503                  exclude_paths: toolCall.input.exclude_paths,1504                  select_domains: toolCall.input.select_domains,1505                  exclude_domains: toolCall.input.exclude_domains,1506                  allow_external: toolCall.input.allow_external,1507                  extract_depth: toolCall.input.extract_depth,1508                  format: toolCall.input.format,1509                  timeout: toolCall.input.timeout,1510                });1511              } else if (toolCall.name === 'tavily_map') {1512                logger.info(`Tavily map: ${toolCall.input.url}`);1513                result = await mapTavily({1514                  url: toolCall.input.url,1515                  instructions: toolCall.input.instructions,1516                  max_depth: toolCall.input.max_depth,1517                  max_breadth: toolCall.input.max_breadth,1518                  limit: toolCall.input.limit,1519                  select_paths: toolCall.input.select_paths,1520                  exclude_paths: toolCall.input.exclude_paths,1521                  allow_external: toolCall.input.allow_external,1522                  timeout: toolCall.input.timeout,1523                });1524              } else if (toolCall.name === 'tavily_research') {1525                logger.info(`Tavily research: "${toolCall.input.input.substring(0, 50)}..." (model: ${toolCall.input.model || 'auto'})`);1526                result = await researchTavily({1527                  input: toolCall.input.input,1528                  model: toolCall.input.model,1529                  output_schema: toolCall.input.output_schema,1530                  citation_format: toolCall.input.citation_format,1531                });1532              }1533              // ==================== SEARCH & DISCOVERY HANDLERS ====================1534              else if (toolCall.name === 'search_symbol') {1535                result = await searchSymbol(toolCall.input.query);1536              } else if (toolCall.name === 'search_name') {1537                result = await searchName(toolCall.input.query);1538              } else if (toolCall.name === 'search_exchange_variants') {1539                result = await searchExchangeVariants(toolCall.input.symbol);1540              } else if (toolCall.name === 'get_stock_list') {1541                result = await getStockList();1542              } else if (toolCall.name === 'get_financial_statement_symbol_list') {1543                result = await getFinancialStatementSymbolList();1544              } else if (toolCall.name === 'get_cik_list') {1545                result = await getCIKList(toolCall.input.page, toolCall.input.limit);1546              } else if (toolCall.name === 'get_symbol_change') {1547                result = await getSymbolChange();1548              } else if (toolCall.name === 'get_etf_list') {1549                result = await getETFList();1550              } else if (toolCall.name === 'get_actively_trading_list') {1551                result = await getActivelyTradingList();1552              } else if (toolCall.name === 'get_earnings_transcript_list') {1553                result = await getEarningsTranscriptList();1554              } else if (toolCall.name === 'get_available_exchanges') {1555                result = await getAvailableExchanges();1556              } else if (toolCall.name === 'get_available_sectors') {1557                result = await getAvailableSectors();1558              } else if (toolCall.name === 'get_available_industries') {1559                result = await getAvailableIndustries();1560              } else if (toolCall.name === 'get_available_countries') {1561                result = await getAvailableCountries();1562              }1563              // ==================== ADVANCED COMPANY DATA HANDLERS ====================1564              else if (toolCall.name === 'get_profile_by_cik') {1565                result = await getProfileByCIK(toolCall.input.cik);1566              } else if (toolCall.name === 'get_delisted_companies') {1567                result = await getDelistedCompanies(toolCall.input.page, toolCall.input.limit);1568              } else if (toolCall.name === 'get_employee_count') {1569                result = await getEmployeeCount(toolCall.input.symbol);1570              } else if (toolCall.name === 'get_historical_employee_count') {1571                result = await getHistoricalEmployeeCount(toolCall.input.symbol);1572              } else if (toolCall.name === 'get_market_capitalization') {1573                result = await getMarketCapitalization(toolCall.input.symbol);1574              } else if (toolCall.name === 'get_batch_market_capitalization') {1575                result = await getBatchMarketCapitalization(toolCall.input.symbols);1576              } else if (toolCall.name === 'get_historical_market_capitalization') {1577                result = await getHistoricalMarketCapitalization(toolCall.input.symbol, toolCall.input.limit);1578              } else if (toolCall.name === 'get_shares_float') {1579                result = await getSharesFloat(toolCall.input.symbol);1580              } else if (toolCall.name === 'get_all_shares_float') {1581                result = await getAllSharesFloat(toolCall.input.page, toolCall.input.limit);1582              } else if (toolCall.name === 'get_latest_mergers_acquisitions') {1583                result = await getLatestMergersAcquisitions(toolCall.input.page, toolCall.input.limit);1584              } else if (toolCall.name === 'search_mergers_acquisitions') {1585                result = await searchMergersAcquisitions(toolCall.input.name);1586              } else if (toolCall.name === 'get_key_executives') {1587                result = await getKeyExecutives(toolCall.input.symbol);1588              } else if (toolCall.name === 'get_executive_compensation') {1589                result = await getExecutiveCompensation(toolCall.input.symbol);1590              } else if (toolCall.name === 'get_executive_compensation_benchmark') {1591                result = await getExecutiveCompensationBenchmark(toolCall.input.year);1592              }1593              // ==================== QUOTES & MARKET DATA HANDLERS ====================1594              else if (toolCall.name === 'get_quote_short') {1595                result = await getQuoteShort(toolCall.input.symbol);1596              } else if (toolCall.name === 'get_aftermarket_trade') {1597                result = await getAftermarketTrade(toolCall.input.symbol);1598              } else if (toolCall.name === 'get_aftermarket_quote') {1599                result = await getAftermarketQuote(toolCall.input.symbol);1600              } else if (toolCall.name === 'get_stock_price_change') {1601                result = await getStockPriceChange(toolCall.input.symbol);1602              } else if (toolCall.name === 'get_batch_quote') {1603                result = await getBatchQuote(toolCall.input.symbols);1604              } else if (toolCall.name === 'get_batch_quote_short') {1605                result = await getBatchQuoteShort(toolCall.input.symbols);1606              } else if (toolCall.name === 'get_batch_exchange_quote') {1607                result = await getBatchExchangeQuote(toolCall.input.exchange);1608              } else if (toolCall.name === 'get_batch_mutualfund_quotes') {1609                result = await getBatchMutualfundQuotes();1610              } else if (toolCall.name === 'get_batch_etf_quotes') {1611                result = await getBatchETFQuotes();1612              } else if (toolCall.name === 'get_batch_commodity_quotes') {1613                result = await getBatchCommodityQuotes();1614              } else if (toolCall.name === 'get_batch_crypto_quotes') {1615                result = await getBatchCryptoQuotes();1616              } else if (toolCall.name === 'get_batch_forex_quotes') {1617                result = await getBatchForexQuotes();1618              } else if (toolCall.name === 'get_batch_index_quotes') {1619                result = await getBatchIndexQuotes();1620              }1621              // ==================== FINANCIAL STATEMENTS TTM HANDLERS ====================1622              else if (toolCall.name === 'get_latest_financial_statements') {1623                result = await getLatestFinancialStatements(toolCall.input.page, toolCall.input.limit);1624              } else if (toolCall.name === 'get_income_statement_ttm') {1625                result = await getIncomeStatementTTM(toolCall.input.symbol);1626              } else if (toolCall.name === 'get_balance_sheet_ttm') {1627                result = await getBalanceSheetTTM(toolCall.input.symbol);1628              } else if (toolCall.name === 'get_cashflow_statement_ttm') {1629                result = await getCashflowStatementTTM(toolCall.input.symbol);1630              } else if (toolCall.name === 'get_key_metrics_ttm') {1631                result = await getKeyMetricsTTM(toolCall.input.symbol);1632              } else if (toolCall.name === 'get_ratios_ttm') {1633                result = await getRatiosTTM(toolCall.input.symbol);1634              } else if (toolCall.name === 'get_financial_scores') {1635                result = await getFinancialScores(toolCall.input.symbol);1636              } else if (toolCall.name === 'get_owner_earnings') {1637                result = await getOwnerEarnings(toolCall.input.symbol);1638              } else if (toolCall.name === 'get_enterprise_values') {1639                result = await getEnterpriseValues(toolCall.input.symbol, toolCall.input.period, toolCall.input.limit);1640              } else if (toolCall.name === 'get_revenue_product_segmentation') {1641                result = await getRevenueProductSegmentation(toolCall.input.symbol, toolCall.input.period);1642              } else if (toolCall.name === 'get_revenue_geographic_segmentation') {1643                result = await getRevenueGeographicSegmentation(toolCall.input.symbol, toolCall.input.period);1644              }1645              // ==================== ECONOMIC & DIVIDEND HANDLERS ====================1646              else if (toolCall.name === 'get_market_risk_premium') {1647                result = await getMarketRiskPremium(toolCall.input.country);1648              } else if (toolCall.name === 'get_dividends_company') {1649                result = await getDividendsCompany(toolCall.input.symbol);1650              } else if (toolCall.name === 'get_dividends_calendar') {1651                result = await getDividendsCalendar(toolCall.input.from, toolCall.input.to);1652              } else if (toolCall.name === 'get_earnings_report') {1653                result = await getEarningsReport(toolCall.input.symbol);1654              } else if (toolCall.name === 'get_ipo_disclosures') {1655                result = await getIPODisclosures(toolCall.input.from, toolCall.input.to);1656              } else if (toolCall.name === 'get_ipo_prospectus') {1657                result = await getIPOProspectus(toolCall.input.from, toolCall.input.to);1658              } else if (toolCall.name === 'get_splits') {1659                result = await getSplits(toolCall.input.symbol);1660              } else if (toolCall.name === 'get_splits_calendar') {1661                result = await getSplitsCalendar(toolCall.input.from, toolCall.input.to);1662              }1663              // ==================== NEWS & TRANSCRIPTS HANDLERS ====================1664              else if (toolCall.name === 'get_latest_earning_transcripts') {1665                result = await getLatestEarningTranscripts();1666              } else if (toolCall.name === 'get_earning_call_transcript_dates') {1667                result = await getEarningCallTranscriptDates(toolCall.input.symbol);1668              } else if (toolCall.name === 'get_fmp_articles') {1669                result = await getFMPArticles(toolCall.input.page, toolCall.input.limit);1670              } else if (toolCall.name === 'get_general_news') {1671                result = await getGeneralNews(toolCall.input.page, toolCall.input.limit);1672              } else if (toolCall.name === 'get_press_releases_latest') {1673                result = await getPressReleasesLatest(toolCall.input.page, toolCall.input.limit);1674              } else if (toolCall.name === 'get_stock_news_latest') {1675                result = await getStockNewsLatest(toolCall.input.page, toolCall.input.limit);1676              } else if (toolCall.name === 'get_crypto_news') {1677                result = await getCryptoNews(toolCall.input.page, toolCall.input.limit);1678              } else if (toolCall.name === 'get_forex_news') {1679                result = await getForexNews(toolCall.input.page, toolCall.input.limit);1680              } else if (toolCall.name === 'search_press_releases_new') {1681                result = await searchPressReleasesNew(toolCall.input.symbols);1682              } else if (toolCall.name === 'search_stock_news') {1683                result = await searchStockNews(toolCall.input.symbols);1684              } else if (toolCall.name === 'search_crypto_news') {1685                result = await searchCryptoNews(toolCall.input.symbols);1686              } else if (toolCall.name === 'search_forex_news') {1687                result = await searchForexNews(toolCall.input.symbols);1688              }1689              // ==================== FORM 13F & INSTITUTIONAL OWNERSHIP HANDLERS ====================1690              else if (toolCall.name === 'get_institutional_ownership_filings') {1691                result = await getInstitutionalOwnershipFilings(toolCall.input.page, toolCall.input.limit);1692              } else if (toolCall.name === 'extract_sec_filings') {1693                result = await extractSECFilings(toolCall.input.cik, toolCall.input.year, toolCall.input.quarter);1694              } else if (toolCall.name === 'get_form_13f_filings_dates') {1695                result = await getForm13FFilingsDates(toolCall.input.cik);1696              } else if (toolCall.name === 'get_filings_extract_with_analytics') {1697                result = await getFilingsExtractWithAnalytics(toolCall.input.symbol, toolCall.input.year, toolCall.input.quarter, toolCall.input.page, toolCall.input.limit);1698              } else if (toolCall.name === 'get_holder_performance_summary') {1699                result = await getHolderPerformanceSummary(toolCall.input.cik, toolCall.input.page);1700              } else if (toolCall.name === 'get_holders_industry_breakdown') {1701                result = await getHoldersIndustryBreakdown(toolCall.input.cik, toolCall.input.year, toolCall.input.quarter);1702              } else if (toolCall.name === 'get_positions_summary') {1703                result = await getPositionsSummary(toolCall.input.symbol, toolCall.input.year, toolCall.input.quarter);1704              } else if (toolCall.name === 'get_industry_performance_summary') {1705                result = await getIndustryPerformanceSummary(toolCall.input.year, toolCall.input.quarter);1706              }1707              // ==================== ANALYST RATINGS & ESTIMATES HANDLERS ====================1708              else if (toolCall.name === 'get_ratings_snapshot') {1709                result = await getRatingsSnapshot(toolCall.input.symbol);1710              } else if (toolCall.name === 'get_historical_ratings') {1711                result = await getHistoricalRatings(toolCall.input.symbol, toolCall.input.limit);1712              } else if (toolCall.name === 'get_price_target_consensus') {1713                result = await getPriceTargetConsensus(toolCall.input.symbol);1714              } else if (toolCall.name === 'get_grades') {1715                result = await getGrades(toolCall.input.symbol, toolCall.input.limit);1716              } else if (toolCall.name === 'get_historical_grades') {1717                result = await getHistoricalGrades(toolCall.input.symbol, toolCall.input.limit);1718              } else if (toolCall.name === 'get_grades_summary') {1719                result = await getGradesSummary(toolCall.input.symbol);1720              }1721              // ==================== MARKET PERFORMANCE & SECTORS HANDLERS ====================1722              else if (toolCall.name === 'get_market_sector_performance_snapshot') {1723                result = await getMarketSectorPerformanceSnapshot(toolCall.input.date);1724              } else if (toolCall.name === 'get_industry_performance_snapshot') {1725                result = await getIndustryPerformanceSnapshot(toolCall.input.date);1726              } else if (toolCall.name === 'get_historical_sector_performance') {1727                result = await getHistoricalSectorPerformance(toolCall.input.sector, toolCall.input.limit);1728              } else if (toolCall.name === 'get_historical_industry_performance') {1729                result = await getHistoricalIndustryPerformance(toolCall.input.industry, toolCall.input.limit);1730              } else if (toolCall.name === 'get_sector_pe_snapshot') {1731                result = await getSectorPESnapshot(toolCall.input.date);1732              } else if (toolCall.name === 'get_industry_pe_snapshot') {1733                result = await getIndustryPESnapshot(toolCall.input.date);1734              } else if (toolCall.name === 'get_historical_sector_pe') {1735                result = await getHistoricalSectorPE(toolCall.input.sector, toolCall.input.limit);1736              } else if (toolCall.name === 'get_historical_industry_pe') {1737                result = await getHistoricalIndustryPE(toolCall.input.industry, toolCall.input.limit);1738              }1739              // ==================== ADDITIONAL TECHNICAL INDICATORS HANDLERS ====================1740              else if (toolCall.name === 'get_wma') {1741                result = await getWMA(toolCall.input.symbol, toolCall.input.periodLength, toolCall.input.timeframe);1742              } else if (toolCall.name === 'get_dema') {1743                result = await getDEMA(toolCall.input.symbol, toolCall.input.periodLength, toolCall.input.timeframe);1744              } else if (toolCall.name === 'get_tema') {1745                result = await getTEMA(toolCall.input.symbol, toolCall.input.periodLength, toolCall.input.timeframe);1746              } else if (toolCall.name === 'get_standard_deviation') {1747                result = await getStandardDeviation(toolCall.input.symbol, toolCall.input.periodLength, toolCall.input.timeframe);1748              }1749              // ==================== ETF & MUTUAL FUNDS HANDLERS ====================1750              else if (toolCall.name === 'get_etf_asset_exposure') {1751                result = await getETFAssetExposure(toolCall.input.symbol);1752              } else if (toolCall.name === 'get_mutual_fund_disclosure_latest') {1753                result = await getMutualFundDisclosureLatest(toolCall.input.symbol);1754              } else if (toolCall.name === 'get_mutual_fund_disclosure') {1755                result = await getMutualFundDisclosure(toolCall.input.symbol, toolCall.input.year, toolCall.input.quarter);1756              } else if (toolCall.name === 'get_mutual_fund_disclosure_search') {1757                result = await getMutualFundDisclosureSearch(toolCall.input.name);1758              } else if (toolCall.name === 'get_mutual_fund_disclosure_dates') {1759                result = await getMutualFundDisclosureDates(toolCall.input.symbol);1760              }1761              // ==================== SEC FILINGS ADVANCED HANDLERS ====================1762              else if (toolCall.name === 'get_latest_8k_sec_filings') {1763                result = await getLatest8KSECFilings(toolCall.input.from, toolCall.input.to, toolCall.input.page, toolCall.input.limit);1764              } else if (toolCall.name === 'get_latest_sec_filings') {1765                result = await getLatestSECFilings(toolCall.input.from, toolCall.input.to, toolCall.input.page, toolCall.input.limit);1766              } else if (toolCall.name === 'get_sec_filings_by_form_type') {1767                result = await getSECFilingsByFormType(toolCall.input.formType, toolCall.input.from, toolCall.input.to, toolCall.input.page, toolCall.input.limit);1768              } else if (toolCall.name === 'get_sec_filings_by_symbol') {1769                result = await getSECFilingsBySymbol(toolCall.input.symbol, toolCall.input.from, toolCall.input.to, toolCall.input.page, toolCall.input.limit);1770              } else if (toolCall.name === 'get_sec_filings_by_cik') {1771                result = await getSECFilingsByCIK(toolCall.input.cik, toolCall.input.from, toolCall.input.to, toolCall.input.page, toolCall.input.limit);1772              } else if (toolCall.name === 'get_sec_filings_by_name') {1773                result = await getSECFilingsByName(toolCall.input.company);1774              } else if (toolCall.name === 'get_sec_filings_company_search_by_symbol') {1775                result = await getSECFilingsCompanySearchBySymbol(toolCall.input.symbol);1776              } else if (toolCall.name === 'get_sec_filings_company_search_by_cik') {1777                result = await getSECFilingsCompanySearchByCIK(toolCall.input.cik);1778              } else if (toolCall.name === 'get_sec_company_full_profile') {1779                result = await getSECCompanyFullProfile(toolCall.input.symbol);1780              } else if (toolCall.name === 'get_industry_classification_list') {1781                result = await getIndustryClassificationList();1782              } else if (toolCall.name === 'search_industry_classification') {1783                result = await searchIndustryClassification(toolCall.input.industryTitle, toolCall.input.sicCode);1784              } else if (toolCall.name === 'get_all_industry_classification') {1785                result = await getAllIndustryClassification();1786              }1787              // ==================== INSIDER TRADES ADVANCED HANDLERS ====================1788              else if (toolCall.name === 'get_latest_insider_trading') {1789                result = await getLatestInsiderTrading(toolCall.input.page, toolCall.input.limit);1790              } else if (toolCall.name === 'search_insider_trades') {1791                result = await searchInsiderTrades(toolCall.input.symbol, toolCall.input.companyName, toolCall.input.reportingName, toolCall.input.page, toolCall.input.limit);1792              } else if (toolCall.name === 'search_insider_trades_by_name') {1793                result = await searchInsiderTradesByName(toolCall.input.name);1794              } else if (toolCall.name === 'get_all_insider_transaction_types') {1795                result = await getAllInsiderTransactionTypes();1796              } else if (toolCall.name === 'get_acquisition_of_beneficial_ownership') {1797                result = await getAcquisitionOfBeneficialOwnership(toolCall.input.symbol);1798              }1799              // ==================== INDEXES HANDLERS ====================1800              else if (toolCall.name === 'get_index_list') {1801                result = await getIndexList();1802              } else if (toolCall.name === 'get_sp500_constituent') {1803                result = await getSP500Constituent();1804              } else if (toolCall.name === 'get_nasdaq_constituent') {1805                result = await getNasdaqConstituent();1806              } else if (toolCall.name === 'get_dow_jones_constituent') {1807                result = await getDowJonesConstituent();1808              } else if (toolCall.name === 'get_historical_sp500_constituent') {1809                result = await getHistoricalSP500Constituent();1810              } else if (toolCall.name === 'get_historical_nasdaq_constituent') {1811                result = await getHistoricalNasdaqConstituent();1812              } else if (toolCall.name === 'get_historical_dow_jones_constituent') {1813                result = await getHistoricalDowJonesConstituent();1814              } else if (toolCall.name === 'get_exchange_market_hours') {1815                result = await getExchangeMarketHours(toolCall.input.exchange);1816              } else if (toolCall.name === 'get_holidays_by_exchange') {1817                result = await getHolidaysByExchange(toolCall.input.exchange);1818              } else if (toolCall.name === 'get_all_exchange_market_hours') {1819                result = await getAllExchangeMarketHours();1820              }1821              // ==================== COMMODITIES, DCF, FOREX HANDLERS ====================1822              else if (toolCall.name === 'get_commodities_list') {1823                result = await getCommoditiesList();1824              } else if (toolCall.name === 'get_dcf_valuation') {1825                result = await getDCFValuation(toolCall.input.symbol);1826              } else if (toolCall.name === 'get_levered_dcf') {1827                result = await getLeveredDCF(toolCall.input.symbol);1828              } else if (toolCall.name === 'get_custom_dcf') {1829                result = await getCustomDCF(toolCall.input.symbol, toolCall.input.years, toolCall.input.growthRate, toolCall.input.discountRate);1830              } else if (toolCall.name === 'get_custom_levered_dcf') {1831                result = await getCustomLeveredDCF(toolCall.input.symbol, toolCall.input.years, toolCall.input.growthRate, toolCall.input.discountRate);1832              } else if (toolCall.name === 'get_forex_currency_pairs') {1833                result = await getForexCurrencyPairs();1834              }1835              // ==================== SENATE/HOUSE, ESG, COT, CROWDFUNDING HANDLERS ====================1836              else if (toolCall.name === 'get_latest_senate_disclosures') {1837                result = await getLatestSenateDisclosures(toolCall.input.page, toolCall.input.limit);1838              } else if (toolCall.name === 'get_latest_house_disclosures') {1839                result = await getLatestHouseDisclosures(toolCall.input.page, toolCall.input.limit);1840              } else if (toolCall.name === 'get_senate_trades_symbol') {1841                result = await getSenateTradesSymbol(toolCall.input.symbol);1842              } else if (toolCall.name === 'get_senate_trades_by_name') {1843                result = await getSenateTradesByName(toolCall.input.name);1844              } else if (toolCall.name === 'get_house_trades_symbol') {1845                result = await getHouseTradesSymbol(toolCall.input.symbol);1846              } else if (toolCall.name === 'get_house_trades_by_name') {1847                result = await getHouseTradesByName(toolCall.input.name);1848              } else if (toolCall.name === 'get_esg_disclosures') {1849                result = await getESGDisclosures(toolCall.input.symbol);1850              } else if (toolCall.name === 'get_esg_ratings') {1851                result = await getESGRatings(toolCall.input.symbol);1852              } else if (toolCall.name === 'get_esg_benchmark') {1853                result = await getESGBenchmark(toolCall.input.year);1854              } else if (toolCall.name === 'get_cot_list') {1855                result = await getCOTList();1856              } else if (toolCall.name === 'get_latest_crowdfunding_campaigns') {1857                result = await getLatestCrowdfundingCampaigns(toolCall.input.page, toolCall.input.limit);1858              } else if (toolCall.name === 'search_crowdfunding_campaigns') {1859                result = await searchCrowdfundingCampaigns(toolCall.input.name);1860              } else if (toolCall.name === 'get_crowdfunding_by_cik') {1861                result = await getCrowdfundingByCIK(toolCall.input.cik);1862              } else if (toolCall.name === 'get_equity_offering_updates') {1863                result = await getEquityOfferingUpdates(toolCall.input.page, toolCall.input.limit);1864              } else if (toolCall.name === 'search_equity_offerings') {1865                result = await searchEquityOfferings(toolCall.input.name);1866              } else if (toolCall.name === 'get_company_equity_offerings_by_cik') {1867                result = await getCompanyEquityOfferingsByCIK(toolCall.input.cik);1868              }18691870              const toolDuration = Date.now() - toolStartTime;1871              logger.endTimer(toolCall.id, `Tool ${toolCall.name} completed`);18721873              // Stringify and truncate result to prevent context overflow1874              const resultString = JSON.stringify(result);18751876              // Higher limit for content extraction tools (Firecrawl, Exa) since they're meant to extract full content1877              // ALSO higher limit for custom Python analysis which includes figure references in output1878              const isContentExtractionTool = [1879                'firecrawl_search',1880                'firecrawl_scrape',1881                'firecrawl_crawl',1882                'firecrawl_extract',1883                'firecrawl_map',1884                'firecrawl_agent',1885                'firecrawl_batch_scrape',1886                'tavily_search',1887                'tavily_extract',1888                'tavily_crawl',1889                'tavily_map',1890                'tavily_research',1891                'get_contents_exa',1892                'execute_custom_python_analysis',  // CRITICAL: Must not truncate figure references!1893              ].includes(toolCall.name);1894              const MAX_TOOL_RESULT_LENGTH = isContentExtractionTool ? 15000 : 3000; // 15k for content extraction & Python analysis & HF data, 3k for others18951896              const truncatedResult = resultString.length > MAX_TOOL_RESULT_LENGTH1897                ? resultString.substring(0, MAX_TOOL_RESULT_LENGTH) + `\n\n[... truncated, original length: ${resultString.length} chars]`1898                : resultString;18991900              // Add to results for Claude1901              allToolResultsForClaude.push({1902                type: 'tool_result',1903                tool_use_id: toolCall.id,1904                content: truncatedResult,1905              });19061907              // Also store for session saving1908              allToolResults.push({ tool: toolCall.name, result });19091910              const resultPreview = JSON.stringify(result).substring(0, 100);1911              logger.debug(`Result preview: ${resultPreview}...`);19121913              // Notify client that tool completed1914              res.write(`data: ${JSON.stringify({1915                type: 'tool_complete',1916                tool: toolCall.name,1917                toolId: toolCall.id,1918                duration: toolDuration1919              })}\n\n`);19201921              res.write(`data: ${JSON.stringify({1922                type: 'tool_result',1923                tool: toolCall.name,1924                result: result1925              })}\n\n`);1926            } catch (toolError) {1927              logger.error(`Error executing tool ${toolCall.name}: ${(toolError as Error).message}`);19281929              // Notify client of tool error1930              res.write(`data: ${JSON.stringify({1931                type: 'tool_error',1932                tool: toolCall.name,1933                toolId: toolCall.id,1934                error: (toolError as Error).message1935              })}\n\n`);19361937              allToolResultsForClaude.push({1938                type: 'tool_result',1939                tool_use_id: toolCall.id,1940                content: `Error: ${(toolError as Error).message}`,1941                is_error: true,1942              });1943            }1944          }19451946          // Wait a bit between batches to avoid API rate limits1947          if (batchIndex < batches.length - 1) {1948            logger.info('Waiting 500ms before next batch to avoid rate limits...');1949            await new Promise(resolve => setTimeout(resolve, 500));1950          }1951        }19521953        logger.success(`All ${toolCalls.length} tools executed successfully across ${batches.length} batch(es)`);19541955        // Now send ALL tool results to Claude at once1956        logger.info(`Sending all ${allToolResultsForClaude.length} tool results to Claude for final analysis`);19571958        if (batches.length > 1) {1959          res.write(`data: ${JSON.stringify({1960            type: 'status',1961            message: `🤖 All ${allToolResultsForClaude.length} tools completed. Generating comprehensive analysis...`1962          })}\n\n`);1963        }19641965        messages.push({1966          role: 'user',1967          content: allToolResultsForClaude,1968        });19691970        // Set flag to add line break before next response1971        isFirstChunkAfterTools = true;19721973        // Recursively process Claude's response with ALL tool results1974        logger.info('Recursively processing Claude response with all tool results...');1975        await processClaudeResponse();1976        } else {1977          // No more tools needed, finalize1978          logger.section('Finalizing Response');1979          logger.kv('Answer length', `${fullAnswer.length} characters`);1980          logger.kv('Tools used', allToolResults.length);19811982          // Save assistant message1983          logger.database('Saving assistant message to database...');1984          await storage.createMessage({1985            sessionId,1986            role: 'assistant',1987            content: fullAnswer,1988            sources: JSON.stringify(sources.map(s => s.url)),1989          });1990          logger.success('Assistant message saved');19911992          // Save complete conversation session1993          try {1994            logger.database('Saving conversation session...');1995            logger.kv('FINAL totalInputTokens', totalInputTokens);1996            logger.kv('FINAL totalOutputTokens', totalOutputTokens);1997            logger.kv('FINAL totalCost', `$${totalCost.toFixed(6)}`);1998            const existingSession = await storage.getConversationSession(sessionId);1999            const sessionMessages = existingSession ? JSON.parse(existingSession.messages) : [];20002001            // Add current message to session2002            sessionMessages.push({2003              question: query,2004              answer: fullAnswer,2005              toolResults: allToolResults,2006              sources: sources,2007              searchQueries: allSearchQueries,2008              pythonCode: pythonCodeUsed || null,2009              monteCarloResults: monteCarloData || null,2010              customPythonFigures: customPythonFiguresData.length > 0 ? customPythonFiguresData : null,2011            });20122013            if (existingSession) {2014              // Update existing session2015              logger.info('Updating existing session');2016              await storage.updateConversationSession(sessionId, {2017                messages: JSON.stringify(sessionMessages),2018                inputTokens: (existingSession.inputTokens || 0) + totalInputTokens,2019                outputTokens: (existingSession.outputTokens || 0) + totalOutputTokens,2020                totalCost: (existingSession.totalCost || 0) + totalCost,2021              });2022            } else {2023              // Create new session with first question as title2024              const userId = (req.session as any).userId;2025              logger.info(`Creating new session for user: ${userId || 'anonymous'}`);2026              await storage.createConversationSession({2027                sessionId,2028                userId: userId || null,2029                title: query.length > 60 ? query.substring(0, 60) + '...' : query,2030                messages: JSON.stringify(sessionMessages),2031                inputTokens: totalInputTokens,2032                outputTokens: totalOutputTokens,2033                totalCost: totalCost,2034              });2035            }2036            logger.success('Conversation session saved');2037            logger.kv('Total tokens (session)', `${totalInputTokens + totalOutputTokens}`);2038            logger.kv('Total cost (this request)', `$${totalCost.toFixed(6)}`);2039          } catch (sessionError) {2040            logger.error(`Error saving conversation session: ${(sessionError as Error).message}`);2041          }20422043          // Automatically create a shared report ONLY for anonymous users2044          // Logged-in users get private history instead2045          const currentUserId = (req.session as any).userId;2046          if (!currentUserId) {2047            try {2048              logger.database('Creating shared report (anonymous user)...');2049              const shareId = randomUUID().replace(/-/g, '').substring(0, 8);2050              await storage.createSharedReport({2051                shareId,2052                question: query,2053                answer: fullAnswer,2054                toolResults: allToolResults.length > 0 ? JSON.stringify(allToolResults) : null,2055                sources: sources.length > 0 ? JSON.stringify(sources) : null,2056                customPythonFigures: customPythonFiguresData.length > 0 ? JSON.stringify(customPythonFiguresData) : null,2057              });2058              logger.success(`Shared report created with ID: ${shareId}`);2059            } catch (shareError) {2060              logger.error(`Error creating shared report: ${(shareError as Error).message}`);2061            }2062          } else {2063            logger.info('Skipping shared report creation (logged-in user - private history)');2064          }20652066          // Send sources and completion2067          logger.info('Sending final response to client');2068          res.write(`data: ${JSON.stringify({ type: 'sources', sources })}\n\n`);2069          res.write(`data: ${JSON.stringify({ type: 'done', sessionId })}\n\n`);2070          res.end();2071          logger.success('CHAT REQUEST COMPLETED');2072        }2073      };20742075      // Start the process2076      logger.server('Starting Claude response processing...');2077      await processClaudeResponse();20782079    } catch (error) {2080      logger.error(`CHAT ERROR: ${(error as Error).message}`);2081      logger.debug(`Stack: ${(error as Error).stack}`);2082      if (sseStarted) {2083        res.write(`data: ${JSON.stringify({ type: 'error', error: 'Failed to process chat' })}\n\n`);2084        res.write(`data: ${JSON.stringify({ type: 'done' })}\n\n`);2085        res.end();2086      } else {2087        res.status(500).json({ error: 'Failed to process chat' });2088      }2089    }2090  });20912092  // ─── Register sub-route modules ─────────────────────────2093  registerAuthRoutes(app);2094  registerAdminRoutes(app);2095  registerAnalyticsRoutes(app);2096  registerFmpRoutes(app);2097  registerReportRoutes(app);20982099  const httpServer = createServer(app);2100  return httpServer;2101}21022103// ── REMOVED: All routes below have been extracted to server/routes/ ──2104// auth.ts     → /api/auth/*2105// admin.ts    → /api/admin/*2106// analytics.ts → /api/analytics/*2107// fmp.ts      → /api/fmp/*2108// reports.ts  → /api/share/*, /api/sessions/*, /api/generate-*, /api/download/*2109