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%
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 ``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\n_(Décris cette figure ci-dessus dans ta réponse)_\n`;1184 });1185 formattedOutput += '\n---\n⚠️ IMPORTANT: Tu DOIS inclure chaque image  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