/** * QM Symbol Actions - Symbol search and spider operations */ import type { BaseHandler, ExecutionContext } from '@stock-bot/handlers'; import { QM_CONFIG, QM_SESSION_IDS } from '../shared/config'; import { QMSessionManager } from '../shared/session-manager'; import { QMOperationTracker } from '../shared/operation-tracker'; import { initializeQMOperations } from '../shared/operation-registry'; import type { Exchange, SymbolSpiderJob } from '../shared/types'; /** * Spider search for symbols - recursively searches QM API * Root job (no prefix) creates A-Z jobs * Each job searches its prefix and creates child jobs if needed */ export async function spiderSymbol( this: BaseHandler, input: SymbolSpiderJob, _context: ExecutionContext ): Promise<{ message: string; symbolsFound?: number; jobsCreated?: number; }> { const { prefix, depth = 0, maxDepth = 4 } = input || {}; this.logger.info(`Spider symbol search ${prefix}`, { prefix, depth, maxDepth }); if (!prefix) { // Root job - create A-Z jobs let jobsCreated = 0; for (let i = 0; i < 26; i++) { const letter = String.fromCharCode(65 + i); // A-Z await this.scheduleOperation('spider-symbols', { prefix: letter, depth: 1, source: 'qm', maxDepth }, { priority: 5 }); jobsCreated++; } this.logger.info('Created root spider jobs', { jobsCreated }); return { message: `Queued ${jobsCreated} root jobs (A-Z)`, jobsCreated }; } try { // Search current prefix const symbols = await searchSymbols.call(this, { query: prefix }); if (!symbols || symbols.length === 0) { this.logger.debug('No symbols found for prefix', { prefix }); return { message: `No symbols found for prefix: ${prefix}`, symbolsFound: 0 }; } await this.mongodb.batchUpsert('qmSymbols', symbols, ['qmSearchCode']); this.logger.info(`Stored symbols from spider search ${prefix} - ${symbols.length}`, { prefix, count: symbols.length }); // Extract and store unique exchanges const exchanges: Exchange[] = []; for (const symbol of symbols) { if (symbol.exchange && !exchanges.some(ex => ex.exchange === symbol.exchange)) { exchanges.push({ exchange: symbol.exchange, exchangeCode: symbol.exchangeCode || '', exchangeShortName: symbol.exchangeShortName || '', countryCode: symbol.countryCode || '', source: 'qm', }); } } if (exchanges.length > 0) { await this.mongodb.batchUpsert('qmExchanges', exchanges, ['exchange']); this.logger.debug('Stored exchanges from spider search', { count: exchanges.length }); } // If not at max depth and we found symbols, create child jobs if (depth < maxDepth && symbols.length > 0) { let jobsCreated = 0; // Only create child jobs if we found a significant number of symbols // This prevents excessive branching on sparse results if (symbols.length >= 50) { for (let i = 0; i < 26; i++) { const nextPrefix = prefix + String.fromCharCode(65 + i); await this.scheduleOperation('spider-symbols', { prefix: nextPrefix, depth: depth + 1, source: 'qm', maxDepth }, { priority: Math.max(1, 5 - depth) }); jobsCreated++; } } return { message: `Found ${symbols.length} symbols for ${prefix}, queued ${jobsCreated} child jobs`, symbolsFound: symbols.length, jobsCreated }; } return { message: `Found ${symbols.length} symbols for ${prefix} (at max depth or too few results)`, symbolsFound: symbols.length }; } catch (error) { this.logger.error(`Spider search failed ${prefix}`, { prefix, error }); return { message: `Spider search failed for prefix: ${prefix}`, symbolsFound: 0 }; } } /** * Search QM symbols API directly */ export async function searchSymbols( this: BaseHandler, input: { query: string }, _context?: ExecutionContext ): Promise { const { query } = input; this.logger.debug('Searching QM symbols', { query }); const sessionManager = QMSessionManager.getInstance(); sessionManager.initialize(this.cache, this.logger); // Get a session const sessionId = QM_SESSION_IDS.LOOKUP; const session = await sessionManager.getSession(sessionId); if (!session || !session.uuid) { throw new Error(`No active session found for QM LOOKUP`); } try { // Build API request const searchParams = new URLSearchParams({ marketType: 'equity', pathName: '/demo/portal/company-summary.php', q: query, qmodTool: 'SmartSymbolLookup', searchType: 'symbol', showFree: 'false', showHisa: 'false', webmasterId: '500' }); const apiUrl = `${QM_CONFIG.LOOKUP_URL}?${searchParams.toString()}`; const response = await fetch(apiUrl, { method: 'GET', headers: session.headers, proxy: session.proxy, }); if (!response.ok) { throw new Error(`QM API request failed: ${response.status} ${response.statusText}`); } const symbols = await response.json(); // Update session success stats await sessionManager.incrementSuccessfulCalls(sessionId, session.uuid); // Process symbol data const processedSymbols = Array.isArray(symbols) ? symbols.map((symbol: any) => ({ ...symbol, qmSearchCode: symbol.symbol || '', symbol: (symbol.symbol as string)?.split(':')[0] || '', })) : []; this.logger.debug('QM API returned symbols ${query} - ${processedSymbols.length}', { query, count: processedSymbols.length }); return processedSymbols; } catch (error) { // Update session failure stats if (session.uuid) { await sessionManager.incrementFailedCalls(sessionId, session.uuid); } this.logger.error('Error searching QM symbols', { query, error: error instanceof Error ? error.message : 'Unknown error' }); throw error; } }