/** * QM Intraday Actions - Fetch and update intraday price bars */ 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 intraday bars for a single symbol * This handles both initial crawl and incremental updates */ export async function updateIntradayBars( this: QMHandler, input: { symbol: string; symbolId: number; qmSearchCode: string; crawlDate?: string; // ISO date string for specific date crawl }, _context?: ExecutionContext ): Promise<{ success: boolean; symbol: string; message: string; data?: any; }> { const { symbol, symbolId, qmSearchCode, crawlDate } = input; this.logger.info('Fetching intraday bars', { symbol, symbolId, crawlDate }); const sessionManager = QMSessionManager.getInstance(); sessionManager.initialize(this.cache, this.logger); // Get a session - you'll need to add the appropriate session ID for intraday const sessionId = QM_SESSION_IDS.LOOKUP; // 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 intraday`); } try { // Determine the date to fetch const targetDate = crawlDate ? new Date(crawlDate) : new Date(); // Build API request for intraday bars const searchParams = new URLSearchParams({ symbol: symbol, symbolId: symbolId.toString(), qmodTool: 'IntradayBars', webmasterId: '500', date: targetDate.toISOString().split('T')[0], interval: '1' // 1-minute bars } as Record); // TODO: Update with correct intraday endpoint const apiUrl = `${QM_CONFIG.BASE_URL}/datatool/intraday.json?${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 barsData = await response.json(); // Update session success stats await sessionManager.incrementSuccessfulCalls(sessionId, session.uuid); // Process and store intraday data if (barsData && barsData.length > 0) { // Store bars in a separate collection const processedBars = barsData.map((bar: any) => ({ ...bar, symbol, symbolId, timestamp: new Date(bar.timestamp), date: targetDate, updated_at: new Date() })); await this.mongodb.batchUpsert( 'qmIntradayBars', processedBars, ['symbol', 'timestamp'] // Unique keys ); this.logger.info('Intraday bars updated successfully', { symbol, date: targetDate, barCount: barsData.length }); return { success: true, symbol, message: `Intraday bars updated for ${symbol} on ${targetDate.toISOString().split('T')[0]}`, data: { count: barsData.length, date: targetDate } }; } else { // No data for this date (weekend, holiday, or no trading) this.logger.info('No intraday data for date', { symbol, date: targetDate }); return { success: true, symbol, message: `No intraday data for ${symbol} on ${targetDate.toISOString().split('T')[0]}`, data: { count: 0, date: targetDate } }; } } catch (error) { // Update session failure stats if (session.uuid) { await sessionManager.incrementFailedCalls(sessionId, session.uuid); } this.logger.error('Error fetching intraday bars', { symbol, error: error instanceof Error ? error.message : 'Unknown error' }); return { success: false, symbol, message: `Failed to fetch intraday bars: ${error instanceof Error ? error.message : 'Unknown error'}` }; } } /** * Schedule intraday updates for symbols * This handles both initial crawls and regular updates */ export async function scheduleIntradayUpdates( this: QMHandler, input: { limit?: number; mode?: 'crawl' | 'update'; // crawl for historical, update for recent forceUpdate?: boolean; } = {}, _context?: ExecutionContext ): Promise<{ message: string; symbolsQueued: number; jobsQueued: number; errors: number; }> { const { limit = 50, mode = 'update', forceUpdate = false } = input; const tracker = await getOperationTracker(this); this.logger.info('Scheduling intraday updates', { limit, mode, forceUpdate }); try { let symbolsToProcess: any[] = []; if (mode === 'crawl') { // Get symbols that need historical crawl symbolsToProcess = await tracker.getSymbolsForIntradayCrawl('intraday_bars', { limit }); } else { // Get symbols that need regular updates const staleSymbols = await tracker.getStaleSymbols('intraday_bars', { minHoursSinceRun: forceUpdate ? 0 : 1, // Hourly updates limit }); if (staleSymbols.length === 0) { this.logger.info('No symbols need intraday updates'); return { message: 'No symbols need intraday updates', symbolsQueued: 0, jobsQueued: 0, errors: 0 }; } // Get full symbol data symbolsToProcess = await this.mongodb.find('qmSymbols', { qmSearchCode: { $in: staleSymbols } }, { projection: { symbol: 1, symbolId: 1, qmSearchCode: 1 } }); } if (symbolsToProcess.length === 0) { this.logger.info('No symbols to process for intraday'); return { message: 'No symbols to process', symbolsQueued: 0, jobsQueued: 0, errors: 0 }; } this.logger.info(`Found ${symbolsToProcess.length} symbols for intraday ${mode}`); let symbolsQueued = 0; let jobsQueued = 0; let errors = 0; // Process each symbol for (const doc of symbolsToProcess) { try { if (!doc.symbolId) { this.logger.warn(`Symbol ${doc.symbol} missing symbolId, skipping`); continue; } if (mode === 'crawl' && doc.crawlState) { // For crawl mode, schedule multiple days going backwards const startDate = doc.crawlState.oldestDateReached || new Date(); const daysToFetch = 30; // Fetch 30 days at a time for (let i = 0; i < daysToFetch; i++) { const crawlDate = new Date(startDate); crawlDate.setDate(crawlDate.getDate() - i); await this.scheduleOperation('update-intraday-bars', { symbol: doc.symbol, symbolId: doc.symbolId, qmSearchCode: doc.qmSearchCode, crawlDate: crawlDate.toISOString() }, { priority: 6, delay: jobsQueued * 1000 // 1 second between jobs }); jobsQueued++; } // Update crawl state await tracker.updateSymbolOperation(doc.qmSearchCode, 'intraday_bars', { status: 'partial', crawlState: { finished: false, oldestDateReached: new Date(startDate.getTime() - daysToFetch * 24 * 60 * 60 * 1000), } }); } else { // For update mode, just fetch today's data await this.scheduleOperation('update-intraday-bars', { symbol: doc.symbol, symbolId: doc.symbolId, qmSearchCode: doc.qmSearchCode }, { priority: 8, // High priority for current data delay: jobsQueued * 500 // 0.5 seconds between jobs }); jobsQueued++; } symbolsQueued++; } catch (error) { this.logger.error(`Failed to schedule intraday update for ${doc.symbol}`, { error }); errors++; } } this.logger.info('Intraday update scheduling completed', { symbolsQueued, jobsQueued, errors, mode }); return { message: `Scheduled intraday ${mode} for ${symbolsQueued} symbols (${jobsQueued} jobs)`, symbolsQueued, jobsQueued, errors }; } catch (error) { this.logger.error('Intraday scheduling failed', { error }); throw error; } }