305 lines
No EOL
8.9 KiB
TypeScript
305 lines
No EOL
8.9 KiB
TypeScript
/**
|
|
* 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<QMOperationTracker> {
|
|
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<string, string>);
|
|
|
|
// 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;
|
|
}
|
|
} |