258 lines
No EOL
8.1 KiB
TypeScript
258 lines
No EOL
8.1 KiB
TypeScript
/**
|
|
* 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;
|
|
}
|
|
} |