/** * QM Symbol Info Actions - Fetch and update symbol metadata */ import type { ExecutionContext } from '@stock-bot/handlers'; import type { QMHandler } from '../qm.handler'; import { QM_CONFIG, QM_SESSION_IDS } from '../shared/config'; import { QMOperationTracker } from '../shared/operation-tracker'; import { QMSessionManager } from '../shared/session-manager'; // Cache tracker instance let operationTracker: QMOperationTracker | null = null; /** * Get or initialize the operation tracker */ async function getOperationTracker(handler: QMHandler): Promise { if (!operationTracker) { const { initializeQMOperations } = await import('../shared/operation-registry'); operationTracker = await initializeQMOperations(handler.mongodb, handler.logger); } return operationTracker; } /** * Update symbol info for a single symbol * This is a simple API fetch operation - no tracking logic here */ export async function updateSymbolInfo( this: QMHandler, input: { qmSearchCode: string; }, _context?: ExecutionContext ): Promise<{ success: boolean; qmSearchCode: string; message: string; data?: any; }> { const { qmSearchCode } = input; this.logger.info(`Fetching symbol info ${qmSearchCode}`, { qmSearchCode }); const sessionManager = QMSessionManager.getInstance(); await sessionManager.initialize(this.cache, this.logger); // Get a session const sessionId = QM_SESSION_IDS.SYMBOL; const session = await sessionManager.getSession(sessionId); if (!session || !session.uuid) { throw new Error(`No active session found for QM LOOKUP`); } try { // Build API request for symbol info const searchParams = new URLSearchParams({ fullDescription: 'true', qmodTool: 'CompanyProfile', symbols: qmSearchCode, lang: 'en', pathName: '/demo/portal/company-summary.php', webmasterId: '500', } as Record); const apiUrl = `${QM_CONFIG.SYMBOL_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 symbolData = await response.json(); // Update session success stats await sessionManager.incrementSuccessfulCalls(sessionId, session.uuid); // Process and store symbol info if (symbolData && Array.isArray(symbolData?.results?.company) && symbolData?.results?.company.length > 0) { // Update symbol in database with new metadata const updateData = { qmSearchCode: qmSearchCode, profile: symbolData?.results?.company[0].profile, symbolInfo: symbolData?.results?.company[0].symbolinfo, } await this.mongodb.updateOne( 'qmSymbols', { qmSearchCode, }, { $set: updateData }, { upsert: true } ); // Update operation tracking const tracker = await getOperationTracker(this); await tracker.updateSymbolOperation(qmSearchCode, 'symbol_info', { status: 'success', lastRecordDate: new Date() }); this.logger.info(`Symbol info updated successfully ${qmSearchCode}`, { qmSearchCode, }); return { success: true, qmSearchCode, message: `Symbol info updated for ${qmSearchCode}`, }; } else { this.logger.warn('No symbol data returned from API', { qmSearchCode }); return { success: false, qmSearchCode, message: `No data found for symbol ${qmSearchCode}` }; } } catch (error) { // Update session failure stats if (session.uuid) { await sessionManager.incrementFailedCalls(sessionId, session.uuid); } this.logger.error('Error fetching symbol info', { qmSearchCode, error: error instanceof Error ? error.message : 'Unknown error' }); // Update operation tracking for failure const tracker = await getOperationTracker(this); await tracker.updateSymbolOperation(qmSearchCode, 'symbol_info', { status: 'failure' }); return { success: false, qmSearchCode, message: `Failed to fetch symbol info: ${error instanceof Error ? error.message : 'Unknown error'}` }; } } /** * Schedule symbol info updates for symbols that need refreshing * This is the scheduled job that finds stale symbols and queues individual updates */ export async function scheduleSymbolInfoUpdates( this: QMHandler, input: { limit?: number; forceUpdate?: boolean; } = {}, _context?: ExecutionContext ): Promise<{ message: string; symbolsQueued: number; errors: number; }> { const { limit = 100000, forceUpdate = false } = input; const tracker = await getOperationTracker(this); this.logger.info('Scheduling symbol info updates', { limit, forceUpdate }); try { // Get symbols that need updating const staleSymbols = await tracker.getStaleSymbols('symbol_info', { minHoursSinceRun: forceUpdate ? 0 : 24 * 7, // Weekly by default limit }); if (staleSymbols.length === 0) { this.logger.info('No symbols need info updates'); return { message: 'No symbols need info updates', symbolsQueued: 0, errors: 0 }; } this.logger.info(`Found ${staleSymbols.length} symbols needing info updates`); // Get full symbol data to include qmSearchCode const symbolDocs = await this.mongodb.find('qmSymbols', { qmSearchCode: { $in: staleSymbols } }, { projection: { qmSearchCode: 1 } }); let queued = 0; let errors = 0; // Schedule individual update jobs for each symbol for (const doc of symbolDocs) { try { await this.scheduleOperation('update-symbol-info', { qmSearchCode: doc.qmSearchCode }, { // priority: 3, // Add some delay to avoid overwhelming the API // delay: queued * 1000 // 1 second between jobs }); queued++; } catch (error) { this.logger.error(`Failed to schedule update for ${doc.qmSearchCode}`, { error }); errors++; } } this.logger.info('Symbol info update scheduling completed', { symbolsQueued: queued, errors, total: staleSymbols.length }); return { message: `Scheduled info updates for ${queued} symbols`, symbolsQueued: queued, errors }; } catch (error) { this.logger.error('Symbol info scheduling failed', { error }); throw error; } }