qm scaffolding done

This commit is contained in:
Boki 2025-06-28 20:48:17 -04:00
parent 736b86e66a
commit c799962f05
11 changed files with 1693 additions and 336 deletions

View file

@ -0,0 +1,236 @@
/**
* QM Financials Actions - Fetch and update financial statements
*/
import type { BaseHandler, ExecutionContext } from '@stock-bot/handlers';
import { QM_CONFIG, QM_SESSION_IDS } from '../shared/config';
import { QMSessionManager } from '../shared/session-manager';
import { QMOperationTracker } from '../shared/operation-tracker';
// Cache tracker instance
let operationTracker: QMOperationTracker | null = null;
/**
* Get or initialize the operation tracker
*/
async function getOperationTracker(handler: BaseHandler): Promise<QMOperationTracker> {
if (!operationTracker) {
const { initializeQMOperations } = await import('../shared/operation-registry');
operationTracker = await initializeQMOperations(handler.mongodb, handler.logger);
}
return operationTracker;
}
/**
* Update financials for a single symbol
*/
export async function updateFinancials(
this: BaseHandler,
input: {
symbol: string;
symbolId: number;
},
_context?: ExecutionContext
): Promise<{
success: boolean;
symbol: string;
message: string;
data?: any;
}> {
const { symbol, symbolId } = input;
this.logger.info('Fetching financials', { symbol, symbolId });
const sessionManager = QMSessionManager.getInstance();
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.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 financials`);
}
try {
// Build API request for financials
const searchParams = new URLSearchParams({
symbol: symbol,
symbolId: symbolId.toString(),
qmodTool: 'Financials',
webmasterId: '500'
});
// TODO: Update with correct financials endpoint
const apiUrl = `${QM_CONFIG.BASE_URL}/datatool/financials.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 financialData = await response.json();
// Update session success stats
await sessionManager.incrementSuccessfulCalls(sessionId, session.uuid);
// Process and store financial data
if (financialData && financialData.length > 0) {
// Store financial statements in a separate collection
await this.mongodb.batchUpsert(
'qmFinancials',
financialData.map((statement: any) => ({
...statement,
symbol,
symbolId,
updated_at: new Date()
})),
['symbol', 'period', 'statementType'] // Unique keys
);
// Update symbol to track last financials update
const tracker = await getOperationTracker(this);
await tracker.updateSymbolOperation(symbol, 'financials_update', {
status: 'success',
lastRecordDate: new Date(),
recordCount: financialData.length
});
this.logger.info('Financials updated successfully', {
symbol,
statementCount: financialData.length
});
return {
success: true,
symbol,
message: `Financials updated for ${symbol}`,
data: { count: financialData.length }
};
} else {
this.logger.warn('No financial data returned from API', { symbol });
return {
success: false,
symbol,
message: `No financial data found for symbol ${symbol}`
};
}
} 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
const tracker = await getOperationTracker(this);
await tracker.updateSymbolOperation(symbol, 'financials_update', {
status: 'failure',
lastRunAt: new Date()
});
return {
success: false,
symbol,
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: BaseHandler,
input: {
limit?: number;
forceUpdate?: boolean;
} = {},
_context?: ExecutionContext
): Promise<{
message: string;
symbolsQueued: number;
errors: number;
}> {
const { limit = 100, forceUpdate = false } = input;
const tracker = await getOperationTracker(this);
this.logger.info('Scheduling financials updates', { limit, forceUpdate });
try {
// Get symbols that need updating
const staleSymbols = await tracker.getStaleSymbols('financials_update', {
minHoursSinceRun: forceUpdate ? 0 : 24 * 7, // Weekly by default
limit
});
if (staleSymbols.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 ${staleSymbols.length} symbols needing financials updates`);
// Get full symbol data to include symbolId
const symbolDocs = await this.mongodb.find('qmSymbols', {
symbol: { $in: staleSymbols }
}, {
projection: { symbol: 1, symbolId: 1 }
});
let queued = 0;
let errors = 0;
// Schedule individual update jobs for each symbol
for (const doc of symbolDocs) {
try {
if (!doc.symbolId) {
this.logger.warn(`Symbol ${doc.symbol} missing symbolId, skipping`);
continue;
}
await this.scheduleOperation('update-financials', {
symbol: doc.symbol,
symbolId: doc.symbolId
}, {
priority: 4,
delay: queued * 2000 // 2 seconds between jobs
});
queued++;
} catch (error) {
this.logger.error(`Failed to schedule financials update for ${doc.symbol}`, { error });
errors++;
}
}
this.logger.info('Financials update scheduling completed', {
symbolsQueued: queued,
errors,
total: staleSymbols.length
});
return {
message: `Scheduled financials updates for ${queued} symbols`,
symbolsQueued: queued,
errors
};
} catch (error) {
this.logger.error('Financials scheduling failed', { error });
throw error;
}
}