/** * QM Financials Actions - Fetch and update financial statements */ import type { ExecutionContext } from '@stock-bot/handlers'; import type { QMHandler } from '../qm.handler'; import { QM_CONFIG, QM_SESSION_IDS } from '../shared/config'; import { QMSessionManager } from '../shared/session-manager'; /** * Update financials for a single symbol */ export async function updateFinancials( this: QMHandler, input: { symbol: string; exchange: number; qmSearchCode: string; reportType: 'Q' | 'A'; // Quarterly or Annual lastRecordDate?: Date; // Optional, used for tracking }, _context?: ExecutionContext ): Promise<{ success: boolean; qmSearchCode: string; message: string; data?: any; }> { const { symbol, exchange, qmSearchCode, reportType, lastRecordDate } = input; this.logger.info('Fetching financials', { symbol, exchange, qmSearchCode }); const sessionManager = QMSessionManager.getInstance(); await sessionManager.initialize(this.cache, this.logger); // Get a session - you'll need to add the appropriate session ID for financials const sessionId = QM_SESSION_IDS.FINANCIALS; // TODO: Update with correct session ID const session = await sessionManager.getSession(sessionId); if (!session || !session.uuid) { throw new Error(`No active session found for QM financials`); } try { // Build API request for financials const searchParams = new URLSearchParams({ currency: 'true', lang: 'en', latestfiscaldate: 'true', numberOfReports: lastRecordDate ? '5' : '250', pathName: '/demo/portal/company-research.php', qmodTool: 'Financials', reportType: reportType, symbol: 'AAPL', webmasterId: '500', }); // TODO: Update with correct financials endpoint const apiUrl = `${QM_CONFIG.FINANCIALS_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 financialData = await response.json(); let reports = []; if(Array.isArray(financialData.results.Company)){ //WEIRD BUG ON THEIR END FOR SOME REASON FOR ONE COMP ITS A ARRAY INSTEAD OF OBJECT reports = financialData?.results?.Company[0].Report || []; }else{ reports = financialData?.results?.Company.Report || []; } // Process and store financial data if (reports && reports.length > 0) { // Store financial statements in a separate collection await this.mongodb.batchUpsert( 'qmFinancials-new', reports.map((statement: any) => ({ ...statement, symbol, exchange, qmSearchCode })), ['qmSearchCode', 'reportPeriod', 'reportDate'] // Unique keys ); // Update symbol to track last financials update await this.operationRegistry.updateOperation('qm', qmSearchCode, `financials_update_${reportType === 'Q'? 'quarterly' : 'annual'}`, { status: 'success', lastRecordDate: new Date(), recordCount: reports.length }); this.logger.info(`Financials updated successfully ${reportType} - ${qmSearchCode} (${reports.length})`, { qmSearchCode, statementCount: reports.length }); return { success: true, qmSearchCode, message: `Financials updated for ${qmSearchCode} - ${reportType} - ${reports.length}`, data: { count: reports.length } }; } else { this.logger.warn('No financial data returned from API', { qmSearchCode }); return { success: false, qmSearchCode, message: `No financial 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 financials', { symbol, error: error instanceof Error ? error.message : 'Unknown error' }); // Track failure await this.operationRegistry.updateOperation('qm', qmSearchCode, `financials_update_${reportType === 'Q'? 'quarterly' : 'annual'}`, { status: 'failure', }); return { success: false, qmSearchCode, message: `Failed to fetch financials: ${error instanceof Error ? error.message : 'Unknown error'}` }; } } /** * Schedule financial updates for symbols that need refreshing */ export async function scheduleFinancialsUpdates( this: QMHandler, input: { limit?: number; forceUpdate?: boolean; } = {}, _context?: ExecutionContext ): Promise<{ message: string; symbolsQueued: number; errors: number; }> { const { limit = 100000, forceUpdate = false } = input; this.logger.info('Scheduling financials updates', { limit, forceUpdate }); try { // Get symbols that need updating for both quarterly and annual const staleSymbolsQ = await this.operationRegistry.getStaleSymbols('qm', 'financials_update_quarterly', { minHoursSinceRun: forceUpdate ? 0 : 24 * 7, // Weekly by default limit }); const staleSymbolsA = await this.operationRegistry.getStaleSymbols('qm', 'financials_update_annual', { minHoursSinceRun: forceUpdate ? 0 : 24 * 7, // Weekly by default limit }); if (staleSymbolsQ.length === 0 && staleSymbolsA.length === 0) { this.logger.info('No symbols need financials updates'); return { message: 'No symbols need financials updates', symbolsQueued: 0, errors: 0 }; } this.logger.info(`Found ${staleSymbolsQ.length} symbols needing quarterly updates and ${staleSymbolsA.length} symbols needing annual updates`); // Combine unique symbols from both lists const allStaleSymbols = [...new Set([...staleSymbolsQ, ...staleSymbolsA])]; // Get full symbol data const symbolDocs = await this.mongodb.find('qmSymbols', { qmSearchCode: { $in: allStaleSymbols } }, { projection: { symbol: 1, exchange: 1, qmSearchCode: 1 } }); let queued = 0; let errors = 0; // Schedule individual update jobs for each symbol and report type for (const doc of symbolDocs) { // Check if this symbol needs quarterly updates if (staleSymbolsQ.includes(doc.qmSearchCode)) { try { await this.scheduleOperation('update-financials', { symbol: doc.symbol, exchange: doc.exchange, qmSearchCode: doc.qmSearchCode, reportType: 'Q', lastRecordDate: doc.operations?.price_update?.lastRecordDate, }, { priority: 4, delay: queued // 1 second between jobs }); queued++; } catch (error) { this.logger.error(`Failed to schedule quarterly financials update for ${doc.qmSearchCode}`, { error }); errors++; } } // Check if this symbol needs annual updates if (staleSymbolsA.includes(doc.qmSearchCode)) { try { await this.scheduleOperation('update-financials', { symbol: doc.symbol, exchange: doc.exchange, qmSearchCode: doc.qmSearchCode, reportType: 'A', lastRecordDate: doc.operations?.price_update?.lastRecordDate, }, { priority: 4, delay: queued // 1 second between jobs }); queued++; } catch (error) { this.logger.error(`Failed to schedule annual financials update for ${doc.qmSearchCode}`, { error }); errors++; } } } this.logger.info('Financials update scheduling completed', { symbolsQueued: queued, errors, totalQuarterly: staleSymbolsQ.length, totalAnnual: staleSymbolsA.length }); return { message: `Scheduled ${queued} financials updates (${staleSymbolsQ.length} quarterly, ${staleSymbolsA.length} annual)`, symbolsQueued: queued, errors }; } catch (error) { this.logger.error('Financials scheduling failed', { error }); throw error; } }