format
This commit is contained in:
parent
d858222af7
commit
7d9044ab29
202 changed files with 10755 additions and 10972 deletions
|
|
@ -1,27 +1,27 @@
|
|||
import { PostgreSQLClient } from '@stock-bot/postgres';
|
||||
import { MongoDBClient } from '@stock-bot/mongodb';
|
||||
|
||||
let postgresClient: PostgreSQLClient | null = null;
|
||||
let mongodbClient: MongoDBClient | null = null;
|
||||
|
||||
export function setPostgreSQLClient(client: PostgreSQLClient): void {
|
||||
postgresClient = client;
|
||||
}
|
||||
|
||||
export function getPostgreSQLClient(): PostgreSQLClient {
|
||||
if (!postgresClient) {
|
||||
throw new Error('PostgreSQL client not initialized. Call setPostgreSQLClient first.');
|
||||
}
|
||||
return postgresClient;
|
||||
}
|
||||
|
||||
export function setMongoDBClient(client: MongoDBClient): void {
|
||||
mongodbClient = client;
|
||||
}
|
||||
|
||||
export function getMongoDBClient(): MongoDBClient {
|
||||
if (!mongodbClient) {
|
||||
throw new Error('MongoDB client not initialized. Call setMongoDBClient first.');
|
||||
}
|
||||
return mongodbClient;
|
||||
}
|
||||
import { MongoDBClient } from '@stock-bot/mongodb';
|
||||
import { PostgreSQLClient } from '@stock-bot/postgres';
|
||||
|
||||
let postgresClient: PostgreSQLClient | null = null;
|
||||
let mongodbClient: MongoDBClient | null = null;
|
||||
|
||||
export function setPostgreSQLClient(client: PostgreSQLClient): void {
|
||||
postgresClient = client;
|
||||
}
|
||||
|
||||
export function getPostgreSQLClient(): PostgreSQLClient {
|
||||
if (!postgresClient) {
|
||||
throw new Error('PostgreSQL client not initialized. Call setPostgreSQLClient first.');
|
||||
}
|
||||
return postgresClient;
|
||||
}
|
||||
|
||||
export function setMongoDBClient(client: MongoDBClient): void {
|
||||
mongodbClient = client;
|
||||
}
|
||||
|
||||
export function getMongoDBClient(): MongoDBClient {
|
||||
if (!mongodbClient) {
|
||||
throw new Error('MongoDB client not initialized. Call setMongoDBClient first.');
|
||||
}
|
||||
return mongodbClient;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,58 +1,58 @@
|
|||
import { getLogger } from '@stock-bot/logger';
|
||||
import { handlerRegistry, type HandlerConfig, type ScheduledJobConfig } from '@stock-bot/queue';
|
||||
import { exchangeOperations } from './operations';
|
||||
|
||||
const logger = getLogger('exchanges-handler');
|
||||
|
||||
const HANDLER_NAME = 'exchanges';
|
||||
|
||||
const exchangesHandlerConfig: HandlerConfig = {
|
||||
concurrency: 1,
|
||||
maxAttempts: 3,
|
||||
scheduledJobs: [
|
||||
{
|
||||
operation: 'sync-all-exchanges',
|
||||
cronPattern: '0 0 * * 0', // Weekly on Sunday at midnight
|
||||
payload: { clearFirst: true },
|
||||
priority: 10,
|
||||
immediately: false,
|
||||
} as ScheduledJobConfig,
|
||||
{
|
||||
operation: 'sync-qm-exchanges',
|
||||
cronPattern: '0 1 * * *', // Daily at 1 AM
|
||||
payload: {},
|
||||
priority: 5,
|
||||
immediately: false,
|
||||
} as ScheduledJobConfig,
|
||||
{
|
||||
operation: 'sync-ib-exchanges',
|
||||
cronPattern: '0 3 * * *', // Daily at 3 AM
|
||||
payload: {},
|
||||
priority: 3,
|
||||
immediately: false,
|
||||
} as ScheduledJobConfig,
|
||||
{
|
||||
operation: 'sync-qm-provider-mappings',
|
||||
cronPattern: '0 3 * * *', // Daily at 3 AM
|
||||
payload: {},
|
||||
priority: 7,
|
||||
immediately: false,
|
||||
} as ScheduledJobConfig,
|
||||
],
|
||||
operations: {
|
||||
'sync-all-exchanges': exchangeOperations.syncAllExchanges,
|
||||
'sync-qm-exchanges': exchangeOperations.syncQMExchanges,
|
||||
'sync-ib-exchanges': exchangeOperations.syncIBExchanges,
|
||||
'sync-qm-provider-mappings': exchangeOperations.syncQMProviderMappings,
|
||||
'clear-postgresql-data': exchangeOperations.clearPostgreSQLData,
|
||||
'get-exchange-stats': exchangeOperations.getExchangeStats,
|
||||
'get-provider-mapping-stats': exchangeOperations.getProviderMappingStats,
|
||||
'enhanced-sync-status': exchangeOperations['enhanced-sync-status'],
|
||||
},
|
||||
};
|
||||
|
||||
export function initializeExchangesHandler(): void {
|
||||
logger.info('Registering exchanges handler...');
|
||||
handlerRegistry.registerHandler(HANDLER_NAME, exchangesHandlerConfig);
|
||||
logger.info('Exchanges handler registered successfully');
|
||||
}
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import { handlerRegistry, type HandlerConfig, type ScheduledJobConfig } from '@stock-bot/queue';
|
||||
import { exchangeOperations } from './operations';
|
||||
|
||||
const logger = getLogger('exchanges-handler');
|
||||
|
||||
const HANDLER_NAME = 'exchanges';
|
||||
|
||||
const exchangesHandlerConfig: HandlerConfig = {
|
||||
concurrency: 1,
|
||||
maxAttempts: 3,
|
||||
scheduledJobs: [
|
||||
{
|
||||
operation: 'sync-all-exchanges',
|
||||
cronPattern: '0 0 * * 0', // Weekly on Sunday at midnight
|
||||
payload: { clearFirst: true },
|
||||
priority: 10,
|
||||
immediately: false,
|
||||
} as ScheduledJobConfig,
|
||||
{
|
||||
operation: 'sync-qm-exchanges',
|
||||
cronPattern: '0 1 * * *', // Daily at 1 AM
|
||||
payload: {},
|
||||
priority: 5,
|
||||
immediately: false,
|
||||
} as ScheduledJobConfig,
|
||||
{
|
||||
operation: 'sync-ib-exchanges',
|
||||
cronPattern: '0 3 * * *', // Daily at 3 AM
|
||||
payload: {},
|
||||
priority: 3,
|
||||
immediately: false,
|
||||
} as ScheduledJobConfig,
|
||||
{
|
||||
operation: 'sync-qm-provider-mappings',
|
||||
cronPattern: '0 3 * * *', // Daily at 3 AM
|
||||
payload: {},
|
||||
priority: 7,
|
||||
immediately: false,
|
||||
} as ScheduledJobConfig,
|
||||
],
|
||||
operations: {
|
||||
'sync-all-exchanges': exchangeOperations.syncAllExchanges,
|
||||
'sync-qm-exchanges': exchangeOperations.syncQMExchanges,
|
||||
'sync-ib-exchanges': exchangeOperations.syncIBExchanges,
|
||||
'sync-qm-provider-mappings': exchangeOperations.syncQMProviderMappings,
|
||||
'clear-postgresql-data': exchangeOperations.clearPostgreSQLData,
|
||||
'get-exchange-stats': exchangeOperations.getExchangeStats,
|
||||
'get-provider-mapping-stats': exchangeOperations.getProviderMappingStats,
|
||||
'enhanced-sync-status': exchangeOperations['enhanced-sync-status'],
|
||||
},
|
||||
};
|
||||
|
||||
export function initializeExchangesHandler(): void {
|
||||
logger.info('Registering exchanges handler...');
|
||||
handlerRegistry.registerHandler(HANDLER_NAME, exchangesHandlerConfig);
|
||||
logger.info('Exchanges handler registered successfully');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export async function clearPostgreSQLData(payload: JobPayload): Promise<{
|
|||
|
||||
try {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
|
||||
|
||||
// Start transaction for atomic operations
|
||||
await postgresClient.query('BEGIN');
|
||||
|
||||
|
|
@ -21,9 +21,7 @@ export async function clearPostgreSQLData(payload: JobPayload): Promise<{
|
|||
const exchangeCountResult = await postgresClient.query(
|
||||
'SELECT COUNT(*) as count FROM exchanges'
|
||||
);
|
||||
const symbolCountResult = await postgresClient.query(
|
||||
'SELECT COUNT(*) as count FROM symbols'
|
||||
);
|
||||
const symbolCountResult = await postgresClient.query('SELECT COUNT(*) as count FROM symbols');
|
||||
const mappingCountResult = await postgresClient.query(
|
||||
'SELECT COUNT(*) as count FROM provider_mappings'
|
||||
);
|
||||
|
|
@ -57,4 +55,4 @@ export async function clearPostgreSQLData(payload: JobPayload): Promise<{
|
|||
logger.error('Failed to clear PostgreSQL data', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,11 +16,11 @@ export async function getSyncStatus(payload: JobPayload): Promise<SyncStatus[]>
|
|||
ORDER BY provider, data_type
|
||||
`;
|
||||
const result = await postgresClient.query(query);
|
||||
|
||||
|
||||
logger.info(`Retrieved sync status for ${result.rows.length} entries`);
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
logger.error('Failed to get sync status', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,11 +18,11 @@ export async function getExchangeStats(payload: JobPayload): Promise<any> {
|
|||
FROM exchanges
|
||||
`;
|
||||
const result = await postgresClient.query(query);
|
||||
|
||||
|
||||
logger.info('Retrieved exchange statistics');
|
||||
return result.rows[0];
|
||||
} catch (error) {
|
||||
logger.error('Failed to get exchange statistics', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
import { syncAllExchanges } from './sync-all-exchanges.operations';
|
||||
import { syncQMExchanges } from './qm-exchanges.operations';
|
||||
import { syncIBExchanges } from './sync-ib-exchanges.operations';
|
||||
import { syncQMProviderMappings } from './sync-qm-provider-mappings.operations';
|
||||
import { clearPostgreSQLData } from './clear-postgresql-data.operations';
|
||||
import { getExchangeStats } from './exchange-stats.operations';
|
||||
import { getProviderMappingStats } from './provider-mapping-stats.operations';
|
||||
import { getSyncStatus } from './enhanced-sync-status.operations';
|
||||
|
||||
export const exchangeOperations = {
|
||||
syncAllExchanges,
|
||||
syncQMExchanges,
|
||||
syncIBExchanges,
|
||||
syncQMProviderMappings,
|
||||
clearPostgreSQLData,
|
||||
getExchangeStats,
|
||||
getProviderMappingStats,
|
||||
'enhanced-sync-status': getSyncStatus,
|
||||
};
|
||||
import { clearPostgreSQLData } from './clear-postgresql-data.operations';
|
||||
import { getSyncStatus } from './enhanced-sync-status.operations';
|
||||
import { getExchangeStats } from './exchange-stats.operations';
|
||||
import { getProviderMappingStats } from './provider-mapping-stats.operations';
|
||||
import { syncQMExchanges } from './qm-exchanges.operations';
|
||||
import { syncAllExchanges } from './sync-all-exchanges.operations';
|
||||
import { syncIBExchanges } from './sync-ib-exchanges.operations';
|
||||
import { syncQMProviderMappings } from './sync-qm-provider-mappings.operations';
|
||||
|
||||
export const exchangeOperations = {
|
||||
syncAllExchanges,
|
||||
syncQMExchanges,
|
||||
syncIBExchanges,
|
||||
syncQMProviderMappings,
|
||||
clearPostgreSQLData,
|
||||
getExchangeStats,
|
||||
getProviderMappingStats,
|
||||
'enhanced-sync-status': getSyncStatus,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -22,11 +22,11 @@ export async function getProviderMappingStats(payload: JobPayload): Promise<any>
|
|||
ORDER BY provider
|
||||
`;
|
||||
const result = await postgresClient.query(query);
|
||||
|
||||
|
||||
logger.info('Retrieved provider mapping statistics');
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
logger.error('Failed to get provider mapping statistics', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,102 +1,113 @@
|
|||
import { getLogger } from '@stock-bot/logger';
|
||||
import { getMongoDBClient, getPostgreSQLClient } from '../../../clients';
|
||||
import type { JobPayload } from '../../../types/job-payloads';
|
||||
|
||||
const logger = getLogger('sync-qm-exchanges');
|
||||
|
||||
export async function syncQMExchanges(payload: JobPayload): Promise<{ processed: number; created: number; updated: number }> {
|
||||
logger.info('Starting QM exchanges sync...');
|
||||
|
||||
try {
|
||||
const mongoClient = getMongoDBClient();
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
|
||||
// 1. Get all QM exchanges from MongoDB
|
||||
const qmExchanges = await mongoClient.find('qmExchanges', {});
|
||||
logger.info(`Found ${qmExchanges.length} QM exchanges to process`);
|
||||
|
||||
let created = 0;
|
||||
let updated = 0;
|
||||
|
||||
for (const exchange of qmExchanges) {
|
||||
try {
|
||||
// 2. Check if exchange exists
|
||||
const existingExchange = await findExchange(exchange.exchangeCode, postgresClient);
|
||||
|
||||
if (existingExchange) {
|
||||
// Update existing
|
||||
await updateExchange(existingExchange.id, exchange, postgresClient);
|
||||
updated++;
|
||||
} else {
|
||||
// Create new
|
||||
await createExchange(exchange, postgresClient);
|
||||
created++;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to process exchange', { error, exchange: exchange.exchangeCode });
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Update sync status
|
||||
await updateSyncStatus('qm', 'exchanges', qmExchanges.length, postgresClient);
|
||||
|
||||
const result = { processed: qmExchanges.length, created, updated };
|
||||
logger.info('QM exchanges sync completed', result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('QM exchanges sync failed', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
async function findExchange(exchangeCode: string, postgresClient: any): Promise<any> {
|
||||
const query = 'SELECT * FROM exchanges WHERE code = $1';
|
||||
const result = await postgresClient.query(query, [exchangeCode]);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
async function createExchange(qmExchange: any, postgresClient: any): Promise<void> {
|
||||
const query = `
|
||||
INSERT INTO exchanges (code, name, country, currency, visible)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (code) DO NOTHING
|
||||
`;
|
||||
|
||||
await postgresClient.query(query, [
|
||||
qmExchange.exchangeCode || qmExchange.exchange,
|
||||
qmExchange.exchangeShortName || qmExchange.name,
|
||||
qmExchange.countryCode || 'US',
|
||||
'USD', // Default currency, can be improved
|
||||
true, // New exchanges are visible by default
|
||||
]);
|
||||
}
|
||||
|
||||
async function updateExchange(exchangeId: string, qmExchange: any, postgresClient: any): Promise<void> {
|
||||
const query = `
|
||||
UPDATE exchanges
|
||||
SET name = COALESCE($2, name),
|
||||
country = COALESCE($3, country),
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
`;
|
||||
|
||||
await postgresClient.query(query, [
|
||||
exchangeId,
|
||||
qmExchange.exchangeShortName || qmExchange.name,
|
||||
qmExchange.countryCode,
|
||||
]);
|
||||
}
|
||||
|
||||
async function updateSyncStatus(provider: string, dataType: string, count: number, postgresClient: any): Promise<void> {
|
||||
const query = `
|
||||
UPDATE sync_status
|
||||
SET last_sync_at = NOW(),
|
||||
last_sync_count = $3,
|
||||
sync_errors = NULL,
|
||||
updated_at = NOW()
|
||||
WHERE provider = $1 AND data_type = $2
|
||||
`;
|
||||
|
||||
await postgresClient.query(query, [provider, dataType, count]);
|
||||
}
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import { getMongoDBClient, getPostgreSQLClient } from '../../../clients';
|
||||
import type { JobPayload } from '../../../types/job-payloads';
|
||||
|
||||
const logger = getLogger('sync-qm-exchanges');
|
||||
|
||||
export async function syncQMExchanges(
|
||||
payload: JobPayload
|
||||
): Promise<{ processed: number; created: number; updated: number }> {
|
||||
logger.info('Starting QM exchanges sync...');
|
||||
|
||||
try {
|
||||
const mongoClient = getMongoDBClient();
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
|
||||
// 1. Get all QM exchanges from MongoDB
|
||||
const qmExchanges = await mongoClient.find('qmExchanges', {});
|
||||
logger.info(`Found ${qmExchanges.length} QM exchanges to process`);
|
||||
|
||||
let created = 0;
|
||||
let updated = 0;
|
||||
|
||||
for (const exchange of qmExchanges) {
|
||||
try {
|
||||
// 2. Check if exchange exists
|
||||
const existingExchange = await findExchange(exchange.exchangeCode, postgresClient);
|
||||
|
||||
if (existingExchange) {
|
||||
// Update existing
|
||||
await updateExchange(existingExchange.id, exchange, postgresClient);
|
||||
updated++;
|
||||
} else {
|
||||
// Create new
|
||||
await createExchange(exchange, postgresClient);
|
||||
created++;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to process exchange', { error, exchange: exchange.exchangeCode });
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Update sync status
|
||||
await updateSyncStatus('qm', 'exchanges', qmExchanges.length, postgresClient);
|
||||
|
||||
const result = { processed: qmExchanges.length, created, updated };
|
||||
logger.info('QM exchanges sync completed', result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('QM exchanges sync failed', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
async function findExchange(exchangeCode: string, postgresClient: any): Promise<any> {
|
||||
const query = 'SELECT * FROM exchanges WHERE code = $1';
|
||||
const result = await postgresClient.query(query, [exchangeCode]);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
async function createExchange(qmExchange: any, postgresClient: any): Promise<void> {
|
||||
const query = `
|
||||
INSERT INTO exchanges (code, name, country, currency, visible)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (code) DO NOTHING
|
||||
`;
|
||||
|
||||
await postgresClient.query(query, [
|
||||
qmExchange.exchangeCode || qmExchange.exchange,
|
||||
qmExchange.exchangeShortName || qmExchange.name,
|
||||
qmExchange.countryCode || 'US',
|
||||
'USD', // Default currency, can be improved
|
||||
true, // New exchanges are visible by default
|
||||
]);
|
||||
}
|
||||
|
||||
async function updateExchange(
|
||||
exchangeId: string,
|
||||
qmExchange: any,
|
||||
postgresClient: any
|
||||
): Promise<void> {
|
||||
const query = `
|
||||
UPDATE exchanges
|
||||
SET name = COALESCE($2, name),
|
||||
country = COALESCE($3, country),
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
`;
|
||||
|
||||
await postgresClient.query(query, [
|
||||
exchangeId,
|
||||
qmExchange.exchangeShortName || qmExchange.name,
|
||||
qmExchange.countryCode,
|
||||
]);
|
||||
}
|
||||
|
||||
async function updateSyncStatus(
|
||||
provider: string,
|
||||
dataType: string,
|
||||
count: number,
|
||||
postgresClient: any
|
||||
): Promise<void> {
|
||||
const query = `
|
||||
UPDATE sync_status
|
||||
SET last_sync_at = NOW(),
|
||||
last_sync_count = $3,
|
||||
sync_errors = NULL,
|
||||
updated_at = NOW()
|
||||
WHERE provider = $1 AND data_type = $2
|
||||
`;
|
||||
|
||||
await postgresClient.query(query, [provider, dataType, count]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,266 +1,275 @@
|
|||
import { getLogger } from '@stock-bot/logger';
|
||||
import { getMongoDBClient, getPostgreSQLClient } from '../../../clients';
|
||||
import type { JobPayload, SyncResult } from '../../../types/job-payloads';
|
||||
|
||||
const logger = getLogger('enhanced-sync-all-exchanges');
|
||||
|
||||
export async function syncAllExchanges(payload: JobPayload): Promise<SyncResult> {
|
||||
const clearFirst = payload.clearFirst || true;
|
||||
logger.info('Starting comprehensive exchange sync...', { clearFirst });
|
||||
|
||||
const result: SyncResult = {
|
||||
processed: 0,
|
||||
created: 0,
|
||||
updated: 0,
|
||||
skipped: 0,
|
||||
errors: 0,
|
||||
};
|
||||
|
||||
try {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
|
||||
// Clear existing data if requested
|
||||
if (clearFirst) {
|
||||
await clearPostgreSQLData(postgresClient);
|
||||
}
|
||||
|
||||
// Start transaction for atomic operations
|
||||
await postgresClient.query('BEGIN');
|
||||
|
||||
// 1. Sync from EOD exchanges (comprehensive global data)
|
||||
const eodResult = await syncEODExchanges();
|
||||
mergeResults(result, eodResult);
|
||||
|
||||
// 2. Sync from IB exchanges (detailed asset information)
|
||||
const ibResult = await syncIBExchanges();
|
||||
mergeResults(result, ibResult);
|
||||
|
||||
// 3. Update sync status
|
||||
await updateSyncStatus('all', 'exchanges', result.processed, postgresClient);
|
||||
|
||||
await postgresClient.query('COMMIT');
|
||||
|
||||
logger.info('Comprehensive exchange sync completed', result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
await postgresClient.query('ROLLBACK');
|
||||
logger.error('Comprehensive exchange sync failed', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function clearPostgreSQLData(postgresClient: any): Promise<void> {
|
||||
logger.info('Clearing existing PostgreSQL data...');
|
||||
|
||||
// Clear data in correct order (respect foreign keys)
|
||||
await postgresClient.query('DELETE FROM provider_mappings');
|
||||
await postgresClient.query('DELETE FROM symbols');
|
||||
await postgresClient.query('DELETE FROM exchanges');
|
||||
|
||||
// Reset sync status
|
||||
await postgresClient.query(
|
||||
'UPDATE sync_status SET last_sync_at = NULL, last_sync_count = 0, sync_errors = NULL'
|
||||
);
|
||||
|
||||
logger.info('PostgreSQL data cleared successfully');
|
||||
}
|
||||
|
||||
async function syncEODExchanges(): Promise<SyncResult> {
|
||||
const mongoClient = getMongoDBClient();
|
||||
const exchanges = await mongoClient.find('eodExchanges', { active: true });
|
||||
const result: SyncResult = { processed: 0, created: 0, updated: 0, skipped: 0, errors: 0 };
|
||||
|
||||
for (const exchange of exchanges) {
|
||||
try {
|
||||
// Create provider exchange mapping for EOD
|
||||
await createProviderExchangeMapping(
|
||||
'eod', // provider
|
||||
exchange.Code,
|
||||
exchange.Name,
|
||||
exchange.CountryISO2,
|
||||
exchange.Currency,
|
||||
0.95 // very high confidence for EOD data
|
||||
);
|
||||
|
||||
result.processed++;
|
||||
result.created++; // Count as created mapping
|
||||
} catch (error) {
|
||||
logger.error('Failed to process EOD exchange', { error, exchange });
|
||||
result.errors++;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function syncIBExchanges(): Promise<SyncResult> {
|
||||
const mongoClient = getMongoDBClient();
|
||||
const exchanges = await mongoClient.find('ibExchanges', {});
|
||||
const result: SyncResult = { processed: 0, created: 0, updated: 0, skipped: 0, errors: 0 };
|
||||
|
||||
for (const exchange of exchanges) {
|
||||
try {
|
||||
// Create provider exchange mapping for IB
|
||||
await createProviderExchangeMapping(
|
||||
'ib', // provider
|
||||
exchange.exchange_id,
|
||||
exchange.name,
|
||||
exchange.country_code,
|
||||
'USD', // IB doesn't specify currency, default to USD
|
||||
0.85 // good confidence for IB data
|
||||
);
|
||||
|
||||
result.processed++;
|
||||
result.created++; // Count as created mapping
|
||||
} catch (error) {
|
||||
logger.error('Failed to process IB exchange', { error, exchange });
|
||||
result.errors++;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function createProviderExchangeMapping(
|
||||
provider: string,
|
||||
providerExchangeCode: string,
|
||||
providerExchangeName: string,
|
||||
countryCode: string | null,
|
||||
currency: string | null,
|
||||
confidence: number
|
||||
): Promise<void> {
|
||||
if (!providerExchangeCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
|
||||
// Check if mapping already exists
|
||||
const existingMapping = await findProviderExchangeMapping(provider, providerExchangeCode);
|
||||
if (existingMapping) {
|
||||
// Don't override existing mappings to preserve manual work
|
||||
return;
|
||||
}
|
||||
|
||||
// Find or create master exchange
|
||||
const masterExchange = await findOrCreateMasterExchange(
|
||||
providerExchangeCode,
|
||||
providerExchangeName,
|
||||
countryCode,
|
||||
currency
|
||||
);
|
||||
|
||||
// Create the provider exchange mapping
|
||||
const query = `
|
||||
INSERT INTO provider_exchange_mappings
|
||||
(provider, provider_exchange_code, provider_exchange_name, master_exchange_id,
|
||||
country_code, currency, confidence, active, auto_mapped)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, false, true)
|
||||
ON CONFLICT (provider, provider_exchange_code) DO NOTHING
|
||||
`;
|
||||
|
||||
await postgresClient.query(query, [
|
||||
provider,
|
||||
providerExchangeCode,
|
||||
providerExchangeName,
|
||||
masterExchange.id,
|
||||
countryCode,
|
||||
currency,
|
||||
confidence,
|
||||
]);
|
||||
}
|
||||
|
||||
async function findOrCreateMasterExchange(
|
||||
providerCode: string,
|
||||
providerName: string,
|
||||
countryCode: string | null,
|
||||
currency: string | null
|
||||
): Promise<any> {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
|
||||
// First, try to find exact match
|
||||
let masterExchange = await findExchangeByCode(providerCode);
|
||||
|
||||
if (masterExchange) {
|
||||
return masterExchange;
|
||||
}
|
||||
|
||||
// Try to find by similar codes (basic mapping)
|
||||
const basicMapping = getBasicExchangeMapping(providerCode);
|
||||
if (basicMapping) {
|
||||
masterExchange = await findExchangeByCode(basicMapping);
|
||||
if (masterExchange) {
|
||||
return masterExchange;
|
||||
}
|
||||
}
|
||||
|
||||
// Create new master exchange (inactive by default)
|
||||
const query = `
|
||||
INSERT INTO exchanges (code, name, country, currency, active)
|
||||
VALUES ($1, $2, $3, $4, false)
|
||||
ON CONFLICT (code) DO UPDATE SET
|
||||
name = COALESCE(EXCLUDED.name, exchanges.name),
|
||||
country = COALESCE(EXCLUDED.country, exchanges.country),
|
||||
currency = COALESCE(EXCLUDED.currency, exchanges.currency)
|
||||
RETURNING id, code, name, country, currency
|
||||
`;
|
||||
|
||||
const result = await postgresClient.query(query, [
|
||||
providerCode,
|
||||
providerName || providerCode,
|
||||
countryCode || 'US',
|
||||
currency || 'USD',
|
||||
]);
|
||||
|
||||
return result.rows[0];
|
||||
}
|
||||
|
||||
function getBasicExchangeMapping(providerCode: string): string | null {
|
||||
const mappings: Record<string, string> = {
|
||||
NYE: 'NYSE',
|
||||
NAS: 'NASDAQ',
|
||||
TO: 'TSX',
|
||||
LN: 'LSE',
|
||||
LON: 'LSE',
|
||||
};
|
||||
|
||||
return mappings[providerCode.toUpperCase()] || null;
|
||||
}
|
||||
|
||||
async function findProviderExchangeMapping(provider: string, providerExchangeCode: string): Promise<any> {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
const query = 'SELECT * FROM provider_exchange_mappings WHERE provider = $1 AND provider_exchange_code = $2';
|
||||
const result = await postgresClient.query(query, [provider, providerExchangeCode]);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
async function findExchangeByCode(code: string): Promise<any> {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
const query = 'SELECT * FROM exchanges WHERE code = $1';
|
||||
const result = await postgresClient.query(query, [code]);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
async function updateSyncStatus(provider: string, dataType: string, count: number, postgresClient: any): Promise<void> {
|
||||
const query = `
|
||||
INSERT INTO sync_status (provider, data_type, last_sync_at, last_sync_count, sync_errors)
|
||||
VALUES ($1, $2, NOW(), $3, NULL)
|
||||
ON CONFLICT (provider, data_type)
|
||||
DO UPDATE SET
|
||||
last_sync_at = NOW(),
|
||||
last_sync_count = EXCLUDED.last_sync_count,
|
||||
sync_errors = NULL,
|
||||
updated_at = NOW()
|
||||
`;
|
||||
|
||||
await postgresClient.query(query, [provider, dataType, count]);
|
||||
}
|
||||
|
||||
function mergeResults(target: SyncResult, source: SyncResult): void {
|
||||
target.processed += source.processed;
|
||||
target.created += source.created;
|
||||
target.updated += source.updated;
|
||||
target.skipped += source.skipped;
|
||||
target.errors += source.errors;
|
||||
}
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import { getMongoDBClient, getPostgreSQLClient } from '../../../clients';
|
||||
import type { JobPayload, SyncResult } from '../../../types/job-payloads';
|
||||
|
||||
const logger = getLogger('enhanced-sync-all-exchanges');
|
||||
|
||||
export async function syncAllExchanges(payload: JobPayload): Promise<SyncResult> {
|
||||
const clearFirst = payload.clearFirst || true;
|
||||
logger.info('Starting comprehensive exchange sync...', { clearFirst });
|
||||
|
||||
const result: SyncResult = {
|
||||
processed: 0,
|
||||
created: 0,
|
||||
updated: 0,
|
||||
skipped: 0,
|
||||
errors: 0,
|
||||
};
|
||||
|
||||
try {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
|
||||
// Clear existing data if requested
|
||||
if (clearFirst) {
|
||||
await clearPostgreSQLData(postgresClient);
|
||||
}
|
||||
|
||||
// Start transaction for atomic operations
|
||||
await postgresClient.query('BEGIN');
|
||||
|
||||
// 1. Sync from EOD exchanges (comprehensive global data)
|
||||
const eodResult = await syncEODExchanges();
|
||||
mergeResults(result, eodResult);
|
||||
|
||||
// 2. Sync from IB exchanges (detailed asset information)
|
||||
const ibResult = await syncIBExchanges();
|
||||
mergeResults(result, ibResult);
|
||||
|
||||
// 3. Update sync status
|
||||
await updateSyncStatus('all', 'exchanges', result.processed, postgresClient);
|
||||
|
||||
await postgresClient.query('COMMIT');
|
||||
|
||||
logger.info('Comprehensive exchange sync completed', result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
await postgresClient.query('ROLLBACK');
|
||||
logger.error('Comprehensive exchange sync failed', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function clearPostgreSQLData(postgresClient: any): Promise<void> {
|
||||
logger.info('Clearing existing PostgreSQL data...');
|
||||
|
||||
// Clear data in correct order (respect foreign keys)
|
||||
await postgresClient.query('DELETE FROM provider_mappings');
|
||||
await postgresClient.query('DELETE FROM symbols');
|
||||
await postgresClient.query('DELETE FROM exchanges');
|
||||
|
||||
// Reset sync status
|
||||
await postgresClient.query(
|
||||
'UPDATE sync_status SET last_sync_at = NULL, last_sync_count = 0, sync_errors = NULL'
|
||||
);
|
||||
|
||||
logger.info('PostgreSQL data cleared successfully');
|
||||
}
|
||||
|
||||
async function syncEODExchanges(): Promise<SyncResult> {
|
||||
const mongoClient = getMongoDBClient();
|
||||
const exchanges = await mongoClient.find('eodExchanges', { active: true });
|
||||
const result: SyncResult = { processed: 0, created: 0, updated: 0, skipped: 0, errors: 0 };
|
||||
|
||||
for (const exchange of exchanges) {
|
||||
try {
|
||||
// Create provider exchange mapping for EOD
|
||||
await createProviderExchangeMapping(
|
||||
'eod', // provider
|
||||
exchange.Code,
|
||||
exchange.Name,
|
||||
exchange.CountryISO2,
|
||||
exchange.Currency,
|
||||
0.95 // very high confidence for EOD data
|
||||
);
|
||||
|
||||
result.processed++;
|
||||
result.created++; // Count as created mapping
|
||||
} catch (error) {
|
||||
logger.error('Failed to process EOD exchange', { error, exchange });
|
||||
result.errors++;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function syncIBExchanges(): Promise<SyncResult> {
|
||||
const mongoClient = getMongoDBClient();
|
||||
const exchanges = await mongoClient.find('ibExchanges', {});
|
||||
const result: SyncResult = { processed: 0, created: 0, updated: 0, skipped: 0, errors: 0 };
|
||||
|
||||
for (const exchange of exchanges) {
|
||||
try {
|
||||
// Create provider exchange mapping for IB
|
||||
await createProviderExchangeMapping(
|
||||
'ib', // provider
|
||||
exchange.exchange_id,
|
||||
exchange.name,
|
||||
exchange.country_code,
|
||||
'USD', // IB doesn't specify currency, default to USD
|
||||
0.85 // good confidence for IB data
|
||||
);
|
||||
|
||||
result.processed++;
|
||||
result.created++; // Count as created mapping
|
||||
} catch (error) {
|
||||
logger.error('Failed to process IB exchange', { error, exchange });
|
||||
result.errors++;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function createProviderExchangeMapping(
|
||||
provider: string,
|
||||
providerExchangeCode: string,
|
||||
providerExchangeName: string,
|
||||
countryCode: string | null,
|
||||
currency: string | null,
|
||||
confidence: number
|
||||
): Promise<void> {
|
||||
if (!providerExchangeCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
|
||||
// Check if mapping already exists
|
||||
const existingMapping = await findProviderExchangeMapping(provider, providerExchangeCode);
|
||||
if (existingMapping) {
|
||||
// Don't override existing mappings to preserve manual work
|
||||
return;
|
||||
}
|
||||
|
||||
// Find or create master exchange
|
||||
const masterExchange = await findOrCreateMasterExchange(
|
||||
providerExchangeCode,
|
||||
providerExchangeName,
|
||||
countryCode,
|
||||
currency
|
||||
);
|
||||
|
||||
// Create the provider exchange mapping
|
||||
const query = `
|
||||
INSERT INTO provider_exchange_mappings
|
||||
(provider, provider_exchange_code, provider_exchange_name, master_exchange_id,
|
||||
country_code, currency, confidence, active, auto_mapped)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, false, true)
|
||||
ON CONFLICT (provider, provider_exchange_code) DO NOTHING
|
||||
`;
|
||||
|
||||
await postgresClient.query(query, [
|
||||
provider,
|
||||
providerExchangeCode,
|
||||
providerExchangeName,
|
||||
masterExchange.id,
|
||||
countryCode,
|
||||
currency,
|
||||
confidence,
|
||||
]);
|
||||
}
|
||||
|
||||
async function findOrCreateMasterExchange(
|
||||
providerCode: string,
|
||||
providerName: string,
|
||||
countryCode: string | null,
|
||||
currency: string | null
|
||||
): Promise<any> {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
|
||||
// First, try to find exact match
|
||||
let masterExchange = await findExchangeByCode(providerCode);
|
||||
|
||||
if (masterExchange) {
|
||||
return masterExchange;
|
||||
}
|
||||
|
||||
// Try to find by similar codes (basic mapping)
|
||||
const basicMapping = getBasicExchangeMapping(providerCode);
|
||||
if (basicMapping) {
|
||||
masterExchange = await findExchangeByCode(basicMapping);
|
||||
if (masterExchange) {
|
||||
return masterExchange;
|
||||
}
|
||||
}
|
||||
|
||||
// Create new master exchange (inactive by default)
|
||||
const query = `
|
||||
INSERT INTO exchanges (code, name, country, currency, active)
|
||||
VALUES ($1, $2, $3, $4, false)
|
||||
ON CONFLICT (code) DO UPDATE SET
|
||||
name = COALESCE(EXCLUDED.name, exchanges.name),
|
||||
country = COALESCE(EXCLUDED.country, exchanges.country),
|
||||
currency = COALESCE(EXCLUDED.currency, exchanges.currency)
|
||||
RETURNING id, code, name, country, currency
|
||||
`;
|
||||
|
||||
const result = await postgresClient.query(query, [
|
||||
providerCode,
|
||||
providerName || providerCode,
|
||||
countryCode || 'US',
|
||||
currency || 'USD',
|
||||
]);
|
||||
|
||||
return result.rows[0];
|
||||
}
|
||||
|
||||
function getBasicExchangeMapping(providerCode: string): string | null {
|
||||
const mappings: Record<string, string> = {
|
||||
NYE: 'NYSE',
|
||||
NAS: 'NASDAQ',
|
||||
TO: 'TSX',
|
||||
LN: 'LSE',
|
||||
LON: 'LSE',
|
||||
};
|
||||
|
||||
return mappings[providerCode.toUpperCase()] || null;
|
||||
}
|
||||
|
||||
async function findProviderExchangeMapping(
|
||||
provider: string,
|
||||
providerExchangeCode: string
|
||||
): Promise<any> {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
const query =
|
||||
'SELECT * FROM provider_exchange_mappings WHERE provider = $1 AND provider_exchange_code = $2';
|
||||
const result = await postgresClient.query(query, [provider, providerExchangeCode]);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
async function findExchangeByCode(code: string): Promise<any> {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
const query = 'SELECT * FROM exchanges WHERE code = $1';
|
||||
const result = await postgresClient.query(query, [code]);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
async function updateSyncStatus(
|
||||
provider: string,
|
||||
dataType: string,
|
||||
count: number,
|
||||
postgresClient: any
|
||||
): Promise<void> {
|
||||
const query = `
|
||||
INSERT INTO sync_status (provider, data_type, last_sync_at, last_sync_count, sync_errors)
|
||||
VALUES ($1, $2, NOW(), $3, NULL)
|
||||
ON CONFLICT (provider, data_type)
|
||||
DO UPDATE SET
|
||||
last_sync_at = NOW(),
|
||||
last_sync_count = EXCLUDED.last_sync_count,
|
||||
sync_errors = NULL,
|
||||
updated_at = NOW()
|
||||
`;
|
||||
|
||||
await postgresClient.query(query, [provider, dataType, count]);
|
||||
}
|
||||
|
||||
function mergeResults(target: SyncResult, source: SyncResult): void {
|
||||
target.processed += source.processed;
|
||||
target.created += source.created;
|
||||
target.updated += source.updated;
|
||||
target.skipped += source.skipped;
|
||||
target.errors += source.errors;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,206 +1,208 @@
|
|||
import { getLogger } from '@stock-bot/logger';
|
||||
import { getMongoDBClient } from '../../../clients';
|
||||
import type { JobPayload } from '../../../types/job-payloads';
|
||||
import type { MasterExchange } from '@stock-bot/mongodb';
|
||||
|
||||
const logger = getLogger('sync-ib-exchanges');
|
||||
|
||||
interface IBExchange {
|
||||
id?: string;
|
||||
_id?: any;
|
||||
name?: string;
|
||||
code?: string;
|
||||
country_code?: string;
|
||||
currency?: string;
|
||||
}
|
||||
|
||||
export async function syncIBExchanges(payload: JobPayload): Promise<{ syncedCount: number; totalExchanges: number }> {
|
||||
logger.info('Syncing IB exchanges from database...');
|
||||
|
||||
try {
|
||||
const mongoClient = getMongoDBClient();
|
||||
const db = mongoClient.getDatabase();
|
||||
|
||||
// Filter by country code US and CA
|
||||
const ibExchanges = await db
|
||||
.collection<IBExchange>('ibExchanges')
|
||||
.find({
|
||||
country_code: { $in: ['US', 'CA'] },
|
||||
})
|
||||
.toArray();
|
||||
|
||||
logger.info('Found IB exchanges in database', { count: ibExchanges.length });
|
||||
|
||||
let syncedCount = 0;
|
||||
|
||||
for (const exchange of ibExchanges) {
|
||||
try {
|
||||
await createOrUpdateMasterExchange(exchange);
|
||||
syncedCount++;
|
||||
|
||||
logger.debug('Synced IB exchange', {
|
||||
ibId: exchange.id,
|
||||
country: exchange.country_code,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to sync IB exchange', { exchange: exchange.id, error });
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('IB exchange sync completed', {
|
||||
syncedCount,
|
||||
totalExchanges: ibExchanges.length,
|
||||
});
|
||||
|
||||
return { syncedCount, totalExchanges: ibExchanges.length };
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch IB exchanges from database', { error });
|
||||
return { syncedCount: 0, totalExchanges: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update master exchange record 1:1 from IB exchange
|
||||
*/
|
||||
async function createOrUpdateMasterExchange(ibExchange: IBExchange): Promise<void> {
|
||||
const mongoClient = getMongoDBClient();
|
||||
const db = mongoClient.getDatabase();
|
||||
const collection = db.collection<MasterExchange>('masterExchanges');
|
||||
|
||||
const masterExchangeId = generateMasterExchangeId(ibExchange);
|
||||
const now = new Date();
|
||||
|
||||
// Check if master exchange already exists
|
||||
const existing = await collection.findOne({ masterExchangeId });
|
||||
|
||||
if (existing) {
|
||||
// Update existing record
|
||||
await collection.updateOne(
|
||||
{ masterExchangeId },
|
||||
{
|
||||
$set: {
|
||||
officialName: ibExchange.name || `Exchange ${ibExchange.id}`,
|
||||
country: ibExchange.country_code || 'UNKNOWN',
|
||||
currency: ibExchange.currency || 'USD',
|
||||
timezone: inferTimezone(ibExchange),
|
||||
updated_at: now,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
logger.debug('Updated existing master exchange', { masterExchangeId });
|
||||
} else {
|
||||
// Create new master exchange
|
||||
const masterExchange: MasterExchange = {
|
||||
masterExchangeId,
|
||||
shortName: masterExchangeId, // Set shortName to masterExchangeId on creation
|
||||
officialName: ibExchange.name || `Exchange ${ibExchange.id}`,
|
||||
country: ibExchange.country_code || 'UNKNOWN',
|
||||
currency: ibExchange.currency || 'USD',
|
||||
timezone: inferTimezone(ibExchange),
|
||||
active: false, // Set active to false only on creation
|
||||
|
||||
sourceMappings: {
|
||||
ib: {
|
||||
id: ibExchange.id || ibExchange._id?.toString() || 'unknown',
|
||||
name: ibExchange.name || `Exchange ${ibExchange.id}`,
|
||||
code: ibExchange.code || ibExchange.id || '',
|
||||
aliases: generateAliases(ibExchange),
|
||||
lastUpdated: now,
|
||||
},
|
||||
},
|
||||
|
||||
confidence: 1.0, // High confidence for direct IB mapping
|
||||
verified: true, // Mark as verified since it's direct from IB
|
||||
|
||||
// DocumentBase fields
|
||||
source: 'ib-exchange-sync',
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
await collection.insertOne(masterExchange);
|
||||
logger.debug('Created new master exchange', { masterExchangeId });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate master exchange ID from IB exchange
|
||||
*/
|
||||
function generateMasterExchangeId(ibExchange: IBExchange): string {
|
||||
// Use code if available, otherwise use ID, otherwise generate from name
|
||||
if (ibExchange.code) {
|
||||
return ibExchange.code.toUpperCase().replace(/[^A-Z0-9]/g, '');
|
||||
}
|
||||
|
||||
if (ibExchange.id) {
|
||||
return ibExchange.id.toUpperCase().replace(/[^A-Z0-9]/g, '');
|
||||
}
|
||||
|
||||
if (ibExchange.name) {
|
||||
return ibExchange.name
|
||||
.toUpperCase()
|
||||
.split(' ')
|
||||
.slice(0, 2)
|
||||
.join('_')
|
||||
.replace(/[^A-Z0-9_]/g, '');
|
||||
}
|
||||
|
||||
return 'UNKNOWN_EXCHANGE';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate aliases for the exchange
|
||||
*/
|
||||
function generateAliases(ibExchange: IBExchange): string[] {
|
||||
const aliases: string[] = [];
|
||||
|
||||
if (ibExchange.name && ibExchange.name.includes(' ')) {
|
||||
// Add abbreviated version
|
||||
aliases.push(
|
||||
ibExchange.name
|
||||
.split(' ')
|
||||
.map(w => w[0])
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
);
|
||||
}
|
||||
|
||||
if (ibExchange.code) {
|
||||
aliases.push(ibExchange.code.toUpperCase());
|
||||
}
|
||||
|
||||
return aliases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer timezone from exchange name/location
|
||||
*/
|
||||
function inferTimezone(ibExchange: IBExchange): string {
|
||||
if (!ibExchange.name) {
|
||||
return 'UTC';
|
||||
}
|
||||
|
||||
const name = ibExchange.name.toUpperCase();
|
||||
|
||||
if (name.includes('NEW YORK') || name.includes('NYSE') || name.includes('NASDAQ')) {
|
||||
return 'America/New_York';
|
||||
}
|
||||
if (name.includes('LONDON')) {
|
||||
return 'Europe/London';
|
||||
}
|
||||
if (name.includes('TOKYO')) {
|
||||
return 'Asia/Tokyo';
|
||||
}
|
||||
if (name.includes('SHANGHAI')) {
|
||||
return 'Asia/Shanghai';
|
||||
}
|
||||
if (name.includes('TORONTO')) {
|
||||
return 'America/Toronto';
|
||||
}
|
||||
if (name.includes('FRANKFURT')) {
|
||||
return 'Europe/Berlin';
|
||||
}
|
||||
|
||||
return 'UTC'; // Default
|
||||
}
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import type { MasterExchange } from '@stock-bot/mongodb';
|
||||
import { getMongoDBClient } from '../../../clients';
|
||||
import type { JobPayload } from '../../../types/job-payloads';
|
||||
|
||||
const logger = getLogger('sync-ib-exchanges');
|
||||
|
||||
interface IBExchange {
|
||||
id?: string;
|
||||
_id?: any;
|
||||
name?: string;
|
||||
code?: string;
|
||||
country_code?: string;
|
||||
currency?: string;
|
||||
}
|
||||
|
||||
export async function syncIBExchanges(
|
||||
payload: JobPayload
|
||||
): Promise<{ syncedCount: number; totalExchanges: number }> {
|
||||
logger.info('Syncing IB exchanges from database...');
|
||||
|
||||
try {
|
||||
const mongoClient = getMongoDBClient();
|
||||
const db = mongoClient.getDatabase();
|
||||
|
||||
// Filter by country code US and CA
|
||||
const ibExchanges = await db
|
||||
.collection<IBExchange>('ibExchanges')
|
||||
.find({
|
||||
country_code: { $in: ['US', 'CA'] },
|
||||
})
|
||||
.toArray();
|
||||
|
||||
logger.info('Found IB exchanges in database', { count: ibExchanges.length });
|
||||
|
||||
let syncedCount = 0;
|
||||
|
||||
for (const exchange of ibExchanges) {
|
||||
try {
|
||||
await createOrUpdateMasterExchange(exchange);
|
||||
syncedCount++;
|
||||
|
||||
logger.debug('Synced IB exchange', {
|
||||
ibId: exchange.id,
|
||||
country: exchange.country_code,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to sync IB exchange', { exchange: exchange.id, error });
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('IB exchange sync completed', {
|
||||
syncedCount,
|
||||
totalExchanges: ibExchanges.length,
|
||||
});
|
||||
|
||||
return { syncedCount, totalExchanges: ibExchanges.length };
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch IB exchanges from database', { error });
|
||||
return { syncedCount: 0, totalExchanges: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update master exchange record 1:1 from IB exchange
|
||||
*/
|
||||
async function createOrUpdateMasterExchange(ibExchange: IBExchange): Promise<void> {
|
||||
const mongoClient = getMongoDBClient();
|
||||
const db = mongoClient.getDatabase();
|
||||
const collection = db.collection<MasterExchange>('masterExchanges');
|
||||
|
||||
const masterExchangeId = generateMasterExchangeId(ibExchange);
|
||||
const now = new Date();
|
||||
|
||||
// Check if master exchange already exists
|
||||
const existing = await collection.findOne({ masterExchangeId });
|
||||
|
||||
if (existing) {
|
||||
// Update existing record
|
||||
await collection.updateOne(
|
||||
{ masterExchangeId },
|
||||
{
|
||||
$set: {
|
||||
officialName: ibExchange.name || `Exchange ${ibExchange.id}`,
|
||||
country: ibExchange.country_code || 'UNKNOWN',
|
||||
currency: ibExchange.currency || 'USD',
|
||||
timezone: inferTimezone(ibExchange),
|
||||
updated_at: now,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
logger.debug('Updated existing master exchange', { masterExchangeId });
|
||||
} else {
|
||||
// Create new master exchange
|
||||
const masterExchange: MasterExchange = {
|
||||
masterExchangeId,
|
||||
shortName: masterExchangeId, // Set shortName to masterExchangeId on creation
|
||||
officialName: ibExchange.name || `Exchange ${ibExchange.id}`,
|
||||
country: ibExchange.country_code || 'UNKNOWN',
|
||||
currency: ibExchange.currency || 'USD',
|
||||
timezone: inferTimezone(ibExchange),
|
||||
active: false, // Set active to false only on creation
|
||||
|
||||
sourceMappings: {
|
||||
ib: {
|
||||
id: ibExchange.id || ibExchange._id?.toString() || 'unknown',
|
||||
name: ibExchange.name || `Exchange ${ibExchange.id}`,
|
||||
code: ibExchange.code || ibExchange.id || '',
|
||||
aliases: generateAliases(ibExchange),
|
||||
lastUpdated: now,
|
||||
},
|
||||
},
|
||||
|
||||
confidence: 1.0, // High confidence for direct IB mapping
|
||||
verified: true, // Mark as verified since it's direct from IB
|
||||
|
||||
// DocumentBase fields
|
||||
source: 'ib-exchange-sync',
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
await collection.insertOne(masterExchange);
|
||||
logger.debug('Created new master exchange', { masterExchangeId });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate master exchange ID from IB exchange
|
||||
*/
|
||||
function generateMasterExchangeId(ibExchange: IBExchange): string {
|
||||
// Use code if available, otherwise use ID, otherwise generate from name
|
||||
if (ibExchange.code) {
|
||||
return ibExchange.code.toUpperCase().replace(/[^A-Z0-9]/g, '');
|
||||
}
|
||||
|
||||
if (ibExchange.id) {
|
||||
return ibExchange.id.toUpperCase().replace(/[^A-Z0-9]/g, '');
|
||||
}
|
||||
|
||||
if (ibExchange.name) {
|
||||
return ibExchange.name
|
||||
.toUpperCase()
|
||||
.split(' ')
|
||||
.slice(0, 2)
|
||||
.join('_')
|
||||
.replace(/[^A-Z0-9_]/g, '');
|
||||
}
|
||||
|
||||
return 'UNKNOWN_EXCHANGE';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate aliases for the exchange
|
||||
*/
|
||||
function generateAliases(ibExchange: IBExchange): string[] {
|
||||
const aliases: string[] = [];
|
||||
|
||||
if (ibExchange.name && ibExchange.name.includes(' ')) {
|
||||
// Add abbreviated version
|
||||
aliases.push(
|
||||
ibExchange.name
|
||||
.split(' ')
|
||||
.map(w => w[0])
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
);
|
||||
}
|
||||
|
||||
if (ibExchange.code) {
|
||||
aliases.push(ibExchange.code.toUpperCase());
|
||||
}
|
||||
|
||||
return aliases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer timezone from exchange name/location
|
||||
*/
|
||||
function inferTimezone(ibExchange: IBExchange): string {
|
||||
if (!ibExchange.name) {
|
||||
return 'UTC';
|
||||
}
|
||||
|
||||
const name = ibExchange.name.toUpperCase();
|
||||
|
||||
if (name.includes('NEW YORK') || name.includes('NYSE') || name.includes('NASDAQ')) {
|
||||
return 'America/New_York';
|
||||
}
|
||||
if (name.includes('LONDON')) {
|
||||
return 'Europe/London';
|
||||
}
|
||||
if (name.includes('TOKYO')) {
|
||||
return 'Asia/Tokyo';
|
||||
}
|
||||
if (name.includes('SHANGHAI')) {
|
||||
return 'Asia/Shanghai';
|
||||
}
|
||||
if (name.includes('TORONTO')) {
|
||||
return 'America/Toronto';
|
||||
}
|
||||
if (name.includes('FRANKFURT')) {
|
||||
return 'Europe/Berlin';
|
||||
}
|
||||
|
||||
return 'UTC'; // Default
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,203 +1,207 @@
|
|||
import { getLogger } from '@stock-bot/logger';
|
||||
import { getMongoDBClient, getPostgreSQLClient } from '../../../clients';
|
||||
import type { JobPayload, SyncResult } from '../../../types/job-payloads';
|
||||
|
||||
const logger = getLogger('enhanced-sync-qm-provider-mappings');
|
||||
|
||||
export async function syncQMProviderMappings(payload: JobPayload): Promise<SyncResult> {
|
||||
logger.info('Starting QM provider exchange mappings sync...');
|
||||
|
||||
const result: SyncResult = {
|
||||
processed: 0,
|
||||
created: 0,
|
||||
updated: 0,
|
||||
skipped: 0,
|
||||
errors: 0,
|
||||
};
|
||||
|
||||
try {
|
||||
const mongoClient = getMongoDBClient();
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
|
||||
// Start transaction
|
||||
await postgresClient.query('BEGIN');
|
||||
|
||||
// Get unique exchange combinations from QM symbols
|
||||
const db = mongoClient.getDatabase();
|
||||
const pipeline = [
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
exchangeCode: '$exchangeCode',
|
||||
exchange: '$exchange',
|
||||
countryCode: '$countryCode',
|
||||
},
|
||||
count: { $sum: 1 },
|
||||
sampleExchange: { $first: '$exchange' },
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
exchangeCode: '$_id.exchangeCode',
|
||||
exchange: '$_id.exchange',
|
||||
countryCode: '$_id.countryCode',
|
||||
count: 1,
|
||||
sampleExchange: 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const qmExchanges = await db.collection('qmSymbols').aggregate(pipeline).toArray();
|
||||
logger.info(`Found ${qmExchanges.length} unique QM exchange combinations`);
|
||||
|
||||
for (const exchange of qmExchanges) {
|
||||
try {
|
||||
// Create provider exchange mapping for QM
|
||||
await createProviderExchangeMapping(
|
||||
'qm', // provider
|
||||
exchange.exchangeCode,
|
||||
exchange.sampleExchange || exchange.exchangeCode,
|
||||
exchange.countryCode,
|
||||
exchange.countryCode === 'CA' ? 'CAD' : 'USD', // Simple currency mapping
|
||||
0.8 // good confidence for QM data
|
||||
);
|
||||
|
||||
result.processed++;
|
||||
result.created++;
|
||||
} catch (error) {
|
||||
logger.error('Failed to process QM exchange mapping', { error, exchange });
|
||||
result.errors++;
|
||||
}
|
||||
}
|
||||
|
||||
await postgresClient.query('COMMIT');
|
||||
|
||||
logger.info('QM provider exchange mappings sync completed', result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
await postgresClient.query('ROLLBACK');
|
||||
logger.error('QM provider exchange mappings sync failed', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function createProviderExchangeMapping(
|
||||
provider: string,
|
||||
providerExchangeCode: string,
|
||||
providerExchangeName: string,
|
||||
countryCode: string | null,
|
||||
currency: string | null,
|
||||
confidence: number
|
||||
): Promise<void> {
|
||||
if (!providerExchangeCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
|
||||
// Check if mapping already exists
|
||||
const existingMapping = await findProviderExchangeMapping(provider, providerExchangeCode);
|
||||
if (existingMapping) {
|
||||
// Don't override existing mappings to preserve manual work
|
||||
return;
|
||||
}
|
||||
|
||||
// Find or create master exchange
|
||||
const masterExchange = await findOrCreateMasterExchange(
|
||||
providerExchangeCode,
|
||||
providerExchangeName,
|
||||
countryCode,
|
||||
currency
|
||||
);
|
||||
|
||||
// Create the provider exchange mapping
|
||||
const query = `
|
||||
INSERT INTO provider_exchange_mappings
|
||||
(provider, provider_exchange_code, provider_exchange_name, master_exchange_id,
|
||||
country_code, currency, confidence, active, auto_mapped)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, false, true)
|
||||
ON CONFLICT (provider, provider_exchange_code) DO NOTHING
|
||||
`;
|
||||
|
||||
await postgresClient.query(query, [
|
||||
provider,
|
||||
providerExchangeCode,
|
||||
providerExchangeName,
|
||||
masterExchange.id,
|
||||
countryCode,
|
||||
currency,
|
||||
confidence,
|
||||
]);
|
||||
}
|
||||
|
||||
async function findProviderExchangeMapping(provider: string, providerExchangeCode: string): Promise<any> {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
const query = 'SELECT * FROM provider_exchange_mappings WHERE provider = $1 AND provider_exchange_code = $2';
|
||||
const result = await postgresClient.query(query, [provider, providerExchangeCode]);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
async function findOrCreateMasterExchange(
|
||||
providerCode: string,
|
||||
providerName: string,
|
||||
countryCode: string | null,
|
||||
currency: string | null
|
||||
): Promise<any> {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
|
||||
// First, try to find exact match
|
||||
let masterExchange = await findExchangeByCode(providerCode);
|
||||
|
||||
if (masterExchange) {
|
||||
return masterExchange;
|
||||
}
|
||||
|
||||
// Try to find by similar codes (basic mapping)
|
||||
const basicMapping = getBasicExchangeMapping(providerCode);
|
||||
if (basicMapping) {
|
||||
masterExchange = await findExchangeByCode(basicMapping);
|
||||
if (masterExchange) {
|
||||
return masterExchange;
|
||||
}
|
||||
}
|
||||
|
||||
// Create new master exchange (inactive by default)
|
||||
const query = `
|
||||
INSERT INTO exchanges (code, name, country, currency, active)
|
||||
VALUES ($1, $2, $3, $4, false)
|
||||
ON CONFLICT (code) DO UPDATE SET
|
||||
name = COALESCE(EXCLUDED.name, exchanges.name),
|
||||
country = COALESCE(EXCLUDED.country, exchanges.country),
|
||||
currency = COALESCE(EXCLUDED.currency, exchanges.currency)
|
||||
RETURNING id, code, name, country, currency
|
||||
`;
|
||||
|
||||
const result = await postgresClient.query(query, [
|
||||
providerCode,
|
||||
providerName || providerCode,
|
||||
countryCode || 'US',
|
||||
currency || 'USD',
|
||||
]);
|
||||
|
||||
return result.rows[0];
|
||||
}
|
||||
|
||||
function getBasicExchangeMapping(providerCode: string): string | null {
|
||||
const mappings: Record<string, string> = {
|
||||
NYE: 'NYSE',
|
||||
NAS: 'NASDAQ',
|
||||
TO: 'TSX',
|
||||
LN: 'LSE',
|
||||
LON: 'LSE',
|
||||
};
|
||||
|
||||
return mappings[providerCode.toUpperCase()] || null;
|
||||
}
|
||||
|
||||
async function findExchangeByCode(code: string): Promise<any> {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
const query = 'SELECT * FROM exchanges WHERE code = $1';
|
||||
const result = await postgresClient.query(query, [code]);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import { getMongoDBClient, getPostgreSQLClient } from '../../../clients';
|
||||
import type { JobPayload, SyncResult } from '../../../types/job-payloads';
|
||||
|
||||
const logger = getLogger('enhanced-sync-qm-provider-mappings');
|
||||
|
||||
export async function syncQMProviderMappings(payload: JobPayload): Promise<SyncResult> {
|
||||
logger.info('Starting QM provider exchange mappings sync...');
|
||||
|
||||
const result: SyncResult = {
|
||||
processed: 0,
|
||||
created: 0,
|
||||
updated: 0,
|
||||
skipped: 0,
|
||||
errors: 0,
|
||||
};
|
||||
|
||||
try {
|
||||
const mongoClient = getMongoDBClient();
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
|
||||
// Start transaction
|
||||
await postgresClient.query('BEGIN');
|
||||
|
||||
// Get unique exchange combinations from QM symbols
|
||||
const db = mongoClient.getDatabase();
|
||||
const pipeline = [
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
exchangeCode: '$exchangeCode',
|
||||
exchange: '$exchange',
|
||||
countryCode: '$countryCode',
|
||||
},
|
||||
count: { $sum: 1 },
|
||||
sampleExchange: { $first: '$exchange' },
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
exchangeCode: '$_id.exchangeCode',
|
||||
exchange: '$_id.exchange',
|
||||
countryCode: '$_id.countryCode',
|
||||
count: 1,
|
||||
sampleExchange: 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const qmExchanges = await db.collection('qmSymbols').aggregate(pipeline).toArray();
|
||||
logger.info(`Found ${qmExchanges.length} unique QM exchange combinations`);
|
||||
|
||||
for (const exchange of qmExchanges) {
|
||||
try {
|
||||
// Create provider exchange mapping for QM
|
||||
await createProviderExchangeMapping(
|
||||
'qm', // provider
|
||||
exchange.exchangeCode,
|
||||
exchange.sampleExchange || exchange.exchangeCode,
|
||||
exchange.countryCode,
|
||||
exchange.countryCode === 'CA' ? 'CAD' : 'USD', // Simple currency mapping
|
||||
0.8 // good confidence for QM data
|
||||
);
|
||||
|
||||
result.processed++;
|
||||
result.created++;
|
||||
} catch (error) {
|
||||
logger.error('Failed to process QM exchange mapping', { error, exchange });
|
||||
result.errors++;
|
||||
}
|
||||
}
|
||||
|
||||
await postgresClient.query('COMMIT');
|
||||
|
||||
logger.info('QM provider exchange mappings sync completed', result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
await postgresClient.query('ROLLBACK');
|
||||
logger.error('QM provider exchange mappings sync failed', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function createProviderExchangeMapping(
|
||||
provider: string,
|
||||
providerExchangeCode: string,
|
||||
providerExchangeName: string,
|
||||
countryCode: string | null,
|
||||
currency: string | null,
|
||||
confidence: number
|
||||
): Promise<void> {
|
||||
if (!providerExchangeCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
|
||||
// Check if mapping already exists
|
||||
const existingMapping = await findProviderExchangeMapping(provider, providerExchangeCode);
|
||||
if (existingMapping) {
|
||||
// Don't override existing mappings to preserve manual work
|
||||
return;
|
||||
}
|
||||
|
||||
// Find or create master exchange
|
||||
const masterExchange = await findOrCreateMasterExchange(
|
||||
providerExchangeCode,
|
||||
providerExchangeName,
|
||||
countryCode,
|
||||
currency
|
||||
);
|
||||
|
||||
// Create the provider exchange mapping
|
||||
const query = `
|
||||
INSERT INTO provider_exchange_mappings
|
||||
(provider, provider_exchange_code, provider_exchange_name, master_exchange_id,
|
||||
country_code, currency, confidence, active, auto_mapped)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, false, true)
|
||||
ON CONFLICT (provider, provider_exchange_code) DO NOTHING
|
||||
`;
|
||||
|
||||
await postgresClient.query(query, [
|
||||
provider,
|
||||
providerExchangeCode,
|
||||
providerExchangeName,
|
||||
masterExchange.id,
|
||||
countryCode,
|
||||
currency,
|
||||
confidence,
|
||||
]);
|
||||
}
|
||||
|
||||
async function findProviderExchangeMapping(
|
||||
provider: string,
|
||||
providerExchangeCode: string
|
||||
): Promise<any> {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
const query =
|
||||
'SELECT * FROM provider_exchange_mappings WHERE provider = $1 AND provider_exchange_code = $2';
|
||||
const result = await postgresClient.query(query, [provider, providerExchangeCode]);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
async function findOrCreateMasterExchange(
|
||||
providerCode: string,
|
||||
providerName: string,
|
||||
countryCode: string | null,
|
||||
currency: string | null
|
||||
): Promise<any> {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
|
||||
// First, try to find exact match
|
||||
let masterExchange = await findExchangeByCode(providerCode);
|
||||
|
||||
if (masterExchange) {
|
||||
return masterExchange;
|
||||
}
|
||||
|
||||
// Try to find by similar codes (basic mapping)
|
||||
const basicMapping = getBasicExchangeMapping(providerCode);
|
||||
if (basicMapping) {
|
||||
masterExchange = await findExchangeByCode(basicMapping);
|
||||
if (masterExchange) {
|
||||
return masterExchange;
|
||||
}
|
||||
}
|
||||
|
||||
// Create new master exchange (inactive by default)
|
||||
const query = `
|
||||
INSERT INTO exchanges (code, name, country, currency, active)
|
||||
VALUES ($1, $2, $3, $4, false)
|
||||
ON CONFLICT (code) DO UPDATE SET
|
||||
name = COALESCE(EXCLUDED.name, exchanges.name),
|
||||
country = COALESCE(EXCLUDED.country, exchanges.country),
|
||||
currency = COALESCE(EXCLUDED.currency, exchanges.currency)
|
||||
RETURNING id, code, name, country, currency
|
||||
`;
|
||||
|
||||
const result = await postgresClient.query(query, [
|
||||
providerCode,
|
||||
providerName || providerCode,
|
||||
countryCode || 'US',
|
||||
currency || 'USD',
|
||||
]);
|
||||
|
||||
return result.rows[0];
|
||||
}
|
||||
|
||||
function getBasicExchangeMapping(providerCode: string): string | null {
|
||||
const mappings: Record<string, string> = {
|
||||
NYE: 'NYSE',
|
||||
NAS: 'NASDAQ',
|
||||
TO: 'TSX',
|
||||
LN: 'LSE',
|
||||
LON: 'LSE',
|
||||
};
|
||||
|
||||
return mappings[providerCode.toUpperCase()] || null;
|
||||
}
|
||||
|
||||
async function findExchangeByCode(code: string): Promise<any> {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
const query = 'SELECT * FROM exchanges WHERE code = $1';
|
||||
const result = await postgresClient.query(query, [code]);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { syncQMSymbols } from './qm-symbols.operations';
|
||||
import { syncSymbolsFromProvider } from './sync-symbols-from-provider.operations';
|
||||
import { getSyncStatus } from './sync-status.operations';
|
||||
|
||||
export const symbolOperations = {
|
||||
syncQMSymbols,
|
||||
syncSymbolsFromProvider,
|
||||
getSyncStatus,
|
||||
};
|
||||
import { syncQMSymbols } from './qm-symbols.operations';
|
||||
import { getSyncStatus } from './sync-status.operations';
|
||||
import { syncSymbolsFromProvider } from './sync-symbols-from-provider.operations';
|
||||
|
||||
export const symbolOperations = {
|
||||
syncQMSymbols,
|
||||
syncSymbolsFromProvider,
|
||||
getSyncStatus,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,167 +1,183 @@
|
|||
import { getLogger } from '@stock-bot/logger';
|
||||
import { getMongoDBClient, getPostgreSQLClient } from '../../../clients';
|
||||
import type { JobPayload } from '../../../types/job-payloads';
|
||||
|
||||
const logger = getLogger('sync-qm-symbols');
|
||||
|
||||
export async function syncQMSymbols(payload: JobPayload): Promise<{ processed: number; created: number; updated: number }> {
|
||||
logger.info('Starting QM symbols sync...');
|
||||
|
||||
try {
|
||||
const mongoClient = getMongoDBClient();
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
|
||||
// 1. Get all QM symbols from MongoDB
|
||||
const qmSymbols = await mongoClient.find('qmSymbols', {});
|
||||
logger.info(`Found ${qmSymbols.length} QM symbols to process`);
|
||||
|
||||
let created = 0;
|
||||
let updated = 0;
|
||||
|
||||
for (const symbol of qmSymbols) {
|
||||
try {
|
||||
// 2. Resolve exchange
|
||||
const exchangeId = await resolveExchange(symbol.exchangeCode || symbol.exchange, postgresClient);
|
||||
|
||||
if (!exchangeId) {
|
||||
logger.warn('Unknown exchange, skipping symbol', {
|
||||
symbol: symbol.symbol,
|
||||
exchange: symbol.exchangeCode || symbol.exchange,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. Check if symbol exists
|
||||
const existingSymbol = await findSymbol(symbol.symbol, exchangeId, postgresClient);
|
||||
|
||||
if (existingSymbol) {
|
||||
// Update existing
|
||||
await updateSymbol(existingSymbol.id, symbol, postgresClient);
|
||||
await upsertProviderMapping(existingSymbol.id, 'qm', symbol, postgresClient);
|
||||
updated++;
|
||||
} else {
|
||||
// Create new
|
||||
const newSymbolId = await createSymbol(symbol, exchangeId, postgresClient);
|
||||
await upsertProviderMapping(newSymbolId, 'qm', symbol, postgresClient);
|
||||
created++;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to process symbol', { error, symbol: symbol.symbol });
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Update sync status
|
||||
await updateSyncStatus('qm', 'symbols', qmSymbols.length, postgresClient);
|
||||
|
||||
const result = { processed: qmSymbols.length, created, updated };
|
||||
logger.info('QM symbols sync completed', result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('QM symbols sync failed', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
async function resolveExchange(exchangeCode: string, postgresClient: any): Promise<string | null> {
|
||||
if (!exchangeCode) return null;
|
||||
|
||||
// Simple mapping - expand this as needed
|
||||
const exchangeMap: Record<string, string> = {
|
||||
NASDAQ: 'NASDAQ',
|
||||
NYSE: 'NYSE',
|
||||
TSX: 'TSX',
|
||||
TSE: 'TSX', // TSE maps to TSX
|
||||
LSE: 'LSE',
|
||||
CME: 'CME',
|
||||
};
|
||||
|
||||
const normalizedCode = exchangeMap[exchangeCode.toUpperCase()];
|
||||
if (!normalizedCode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const query = 'SELECT id FROM exchanges WHERE code = $1';
|
||||
const result = await postgresClient.query(query, [normalizedCode]);
|
||||
return result.rows[0]?.id || null;
|
||||
}
|
||||
|
||||
async function findSymbol(symbol: string, exchangeId: string, postgresClient: any): Promise<any> {
|
||||
const query = 'SELECT * FROM symbols WHERE symbol = $1 AND exchange_id = $2';
|
||||
const result = await postgresClient.query(query, [symbol, exchangeId]);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
async function createSymbol(qmSymbol: any, exchangeId: string, postgresClient: any): Promise<string> {
|
||||
const query = `
|
||||
INSERT INTO symbols (symbol, exchange_id, company_name, country, currency)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id
|
||||
`;
|
||||
|
||||
const result = await postgresClient.query(query, [
|
||||
qmSymbol.symbol,
|
||||
exchangeId,
|
||||
qmSymbol.companyName || qmSymbol.name,
|
||||
qmSymbol.countryCode || 'US',
|
||||
qmSymbol.currency || 'USD',
|
||||
]);
|
||||
|
||||
return result.rows[0].id;
|
||||
}
|
||||
|
||||
async function updateSymbol(symbolId: string, qmSymbol: any, postgresClient: any): Promise<void> {
|
||||
const query = `
|
||||
UPDATE symbols
|
||||
SET company_name = COALESCE($2, company_name),
|
||||
country = COALESCE($3, country),
|
||||
currency = COALESCE($4, currency),
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
`;
|
||||
|
||||
await postgresClient.query(query, [
|
||||
symbolId,
|
||||
qmSymbol.companyName || qmSymbol.name,
|
||||
qmSymbol.countryCode,
|
||||
qmSymbol.currency,
|
||||
]);
|
||||
}
|
||||
|
||||
async function upsertProviderMapping(
|
||||
symbolId: string,
|
||||
provider: string,
|
||||
qmSymbol: any,
|
||||
postgresClient: any
|
||||
): Promise<void> {
|
||||
const query = `
|
||||
INSERT INTO provider_mappings
|
||||
(symbol_id, provider, provider_symbol, provider_exchange, last_seen)
|
||||
VALUES ($1, $2, $3, $4, NOW())
|
||||
ON CONFLICT (provider, provider_symbol)
|
||||
DO UPDATE SET
|
||||
symbol_id = EXCLUDED.symbol_id,
|
||||
provider_exchange = EXCLUDED.provider_exchange,
|
||||
last_seen = NOW()
|
||||
`;
|
||||
|
||||
await postgresClient.query(query, [
|
||||
symbolId,
|
||||
provider,
|
||||
qmSymbol.qmSearchCode || qmSymbol.symbol,
|
||||
qmSymbol.exchangeCode || qmSymbol.exchange,
|
||||
]);
|
||||
}
|
||||
|
||||
async function updateSyncStatus(provider: string, dataType: string, count: number, postgresClient: any): Promise<void> {
|
||||
const query = `
|
||||
UPDATE sync_status
|
||||
SET last_sync_at = NOW(),
|
||||
last_sync_count = $3,
|
||||
sync_errors = NULL,
|
||||
updated_at = NOW()
|
||||
WHERE provider = $1 AND data_type = $2
|
||||
`;
|
||||
|
||||
await postgresClient.query(query, [provider, dataType, count]);
|
||||
}
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import { getMongoDBClient, getPostgreSQLClient } from '../../../clients';
|
||||
import type { JobPayload } from '../../../types/job-payloads';
|
||||
|
||||
const logger = getLogger('sync-qm-symbols');
|
||||
|
||||
export async function syncQMSymbols(
|
||||
payload: JobPayload
|
||||
): Promise<{ processed: number; created: number; updated: number }> {
|
||||
logger.info('Starting QM symbols sync...');
|
||||
|
||||
try {
|
||||
const mongoClient = getMongoDBClient();
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
|
||||
// 1. Get all QM symbols from MongoDB
|
||||
const qmSymbols = await mongoClient.find('qmSymbols', {});
|
||||
logger.info(`Found ${qmSymbols.length} QM symbols to process`);
|
||||
|
||||
let created = 0;
|
||||
let updated = 0;
|
||||
|
||||
for (const symbol of qmSymbols) {
|
||||
try {
|
||||
// 2. Resolve exchange
|
||||
const exchangeId = await resolveExchange(
|
||||
symbol.exchangeCode || symbol.exchange,
|
||||
postgresClient
|
||||
);
|
||||
|
||||
if (!exchangeId) {
|
||||
logger.warn('Unknown exchange, skipping symbol', {
|
||||
symbol: symbol.symbol,
|
||||
exchange: symbol.exchangeCode || symbol.exchange,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. Check if symbol exists
|
||||
const existingSymbol = await findSymbol(symbol.symbol, exchangeId, postgresClient);
|
||||
|
||||
if (existingSymbol) {
|
||||
// Update existing
|
||||
await updateSymbol(existingSymbol.id, symbol, postgresClient);
|
||||
await upsertProviderMapping(existingSymbol.id, 'qm', symbol, postgresClient);
|
||||
updated++;
|
||||
} else {
|
||||
// Create new
|
||||
const newSymbolId = await createSymbol(symbol, exchangeId, postgresClient);
|
||||
await upsertProviderMapping(newSymbolId, 'qm', symbol, postgresClient);
|
||||
created++;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to process symbol', { error, symbol: symbol.symbol });
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Update sync status
|
||||
await updateSyncStatus('qm', 'symbols', qmSymbols.length, postgresClient);
|
||||
|
||||
const result = { processed: qmSymbols.length, created, updated };
|
||||
logger.info('QM symbols sync completed', result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('QM symbols sync failed', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
async function resolveExchange(exchangeCode: string, postgresClient: any): Promise<string | null> {
|
||||
if (!exchangeCode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Simple mapping - expand this as needed
|
||||
const exchangeMap: Record<string, string> = {
|
||||
NASDAQ: 'NASDAQ',
|
||||
NYSE: 'NYSE',
|
||||
TSX: 'TSX',
|
||||
TSE: 'TSX', // TSE maps to TSX
|
||||
LSE: 'LSE',
|
||||
CME: 'CME',
|
||||
};
|
||||
|
||||
const normalizedCode = exchangeMap[exchangeCode.toUpperCase()];
|
||||
if (!normalizedCode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const query = 'SELECT id FROM exchanges WHERE code = $1';
|
||||
const result = await postgresClient.query(query, [normalizedCode]);
|
||||
return result.rows[0]?.id || null;
|
||||
}
|
||||
|
||||
async function findSymbol(symbol: string, exchangeId: string, postgresClient: any): Promise<any> {
|
||||
const query = 'SELECT * FROM symbols WHERE symbol = $1 AND exchange_id = $2';
|
||||
const result = await postgresClient.query(query, [symbol, exchangeId]);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
async function createSymbol(
|
||||
qmSymbol: any,
|
||||
exchangeId: string,
|
||||
postgresClient: any
|
||||
): Promise<string> {
|
||||
const query = `
|
||||
INSERT INTO symbols (symbol, exchange_id, company_name, country, currency)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id
|
||||
`;
|
||||
|
||||
const result = await postgresClient.query(query, [
|
||||
qmSymbol.symbol,
|
||||
exchangeId,
|
||||
qmSymbol.companyName || qmSymbol.name,
|
||||
qmSymbol.countryCode || 'US',
|
||||
qmSymbol.currency || 'USD',
|
||||
]);
|
||||
|
||||
return result.rows[0].id;
|
||||
}
|
||||
|
||||
async function updateSymbol(symbolId: string, qmSymbol: any, postgresClient: any): Promise<void> {
|
||||
const query = `
|
||||
UPDATE symbols
|
||||
SET company_name = COALESCE($2, company_name),
|
||||
country = COALESCE($3, country),
|
||||
currency = COALESCE($4, currency),
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
`;
|
||||
|
||||
await postgresClient.query(query, [
|
||||
symbolId,
|
||||
qmSymbol.companyName || qmSymbol.name,
|
||||
qmSymbol.countryCode,
|
||||
qmSymbol.currency,
|
||||
]);
|
||||
}
|
||||
|
||||
async function upsertProviderMapping(
|
||||
symbolId: string,
|
||||
provider: string,
|
||||
qmSymbol: any,
|
||||
postgresClient: any
|
||||
): Promise<void> {
|
||||
const query = `
|
||||
INSERT INTO provider_mappings
|
||||
(symbol_id, provider, provider_symbol, provider_exchange, last_seen)
|
||||
VALUES ($1, $2, $3, $4, NOW())
|
||||
ON CONFLICT (provider, provider_symbol)
|
||||
DO UPDATE SET
|
||||
symbol_id = EXCLUDED.symbol_id,
|
||||
provider_exchange = EXCLUDED.provider_exchange,
|
||||
last_seen = NOW()
|
||||
`;
|
||||
|
||||
await postgresClient.query(query, [
|
||||
symbolId,
|
||||
provider,
|
||||
qmSymbol.qmSearchCode || qmSymbol.symbol,
|
||||
qmSymbol.exchangeCode || qmSymbol.exchange,
|
||||
]);
|
||||
}
|
||||
|
||||
async function updateSyncStatus(
|
||||
provider: string,
|
||||
dataType: string,
|
||||
count: number,
|
||||
postgresClient: any
|
||||
): Promise<void> {
|
||||
const query = `
|
||||
UPDATE sync_status
|
||||
SET last_sync_at = NOW(),
|
||||
last_sync_count = $3,
|
||||
sync_errors = NULL,
|
||||
updated_at = NOW()
|
||||
WHERE provider = $1 AND data_type = $2
|
||||
`;
|
||||
|
||||
await postgresClient.query(query, [provider, dataType, count]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
import { getLogger } from '@stock-bot/logger';
|
||||
import { getPostgreSQLClient } from '../../../clients';
|
||||
import type { JobPayload } from '../../../types/job-payloads';
|
||||
|
||||
const logger = getLogger('sync-status');
|
||||
|
||||
export async function getSyncStatus(payload: JobPayload): Promise<Record<string, unknown>[]> {
|
||||
logger.info('Getting sync status...');
|
||||
|
||||
try {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
const query = 'SELECT * FROM sync_status ORDER BY provider, data_type';
|
||||
const result = await postgresClient.query(query);
|
||||
|
||||
logger.info(`Retrieved sync status for ${result.rows.length} entries`);
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
logger.error('Failed to get sync status', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import { getPostgreSQLClient } from '../../../clients';
|
||||
import type { JobPayload } from '../../../types/job-payloads';
|
||||
|
||||
const logger = getLogger('sync-status');
|
||||
|
||||
export async function getSyncStatus(payload: JobPayload): Promise<Record<string, unknown>[]> {
|
||||
logger.info('Getting sync status...');
|
||||
|
||||
try {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
const query = 'SELECT * FROM sync_status ORDER BY provider, data_type';
|
||||
const result = await postgresClient.query(query);
|
||||
|
||||
logger.info(`Retrieved sync status for ${result.rows.length} entries`);
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
logger.error('Failed to get sync status', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,215 +1,231 @@
|
|||
import { getLogger } from '@stock-bot/logger';
|
||||
import { getMongoDBClient, getPostgreSQLClient } from '../../../clients';
|
||||
import type { JobPayload, SyncResult } from '../../../types/job-payloads';
|
||||
|
||||
const logger = getLogger('enhanced-sync-symbols-from-provider');
|
||||
|
||||
export async function syncSymbolsFromProvider(payload: JobPayload): Promise<SyncResult> {
|
||||
const provider = payload.provider;
|
||||
const clearFirst = payload.clearFirst || false;
|
||||
|
||||
if (!provider) {
|
||||
throw new Error('Provider is required in payload');
|
||||
}
|
||||
|
||||
logger.info(`Starting ${provider} symbols sync...`, { clearFirst });
|
||||
|
||||
const result: SyncResult = {
|
||||
processed: 0,
|
||||
created: 0,
|
||||
updated: 0,
|
||||
skipped: 0,
|
||||
errors: 0,
|
||||
};
|
||||
|
||||
try {
|
||||
const mongoClient = getMongoDBClient();
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
|
||||
// Clear existing data if requested (only symbols and mappings, keep exchanges)
|
||||
if (clearFirst) {
|
||||
await postgresClient.query('BEGIN');
|
||||
await postgresClient.query('DELETE FROM provider_mappings');
|
||||
await postgresClient.query('DELETE FROM symbols');
|
||||
await postgresClient.query('COMMIT');
|
||||
logger.info('Cleared existing symbols and mappings before sync');
|
||||
}
|
||||
|
||||
// Start transaction
|
||||
await postgresClient.query('BEGIN');
|
||||
|
||||
let symbols: Record<string, unknown>[] = [];
|
||||
|
||||
// Get symbols based on provider
|
||||
const db = mongoClient.getDatabase();
|
||||
switch (provider.toLowerCase()) {
|
||||
case 'qm':
|
||||
symbols = await db.collection('qmSymbols').find({}).toArray();
|
||||
break;
|
||||
case 'eod':
|
||||
symbols = await db.collection('eodSymbols').find({}).toArray();
|
||||
break;
|
||||
case 'ib':
|
||||
symbols = await db.collection('ibSymbols').find({}).toArray();
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported provider: ${provider}`);
|
||||
}
|
||||
|
||||
logger.info(`Found ${symbols.length} ${provider} symbols to process`);
|
||||
result.processed = symbols.length;
|
||||
|
||||
for (const symbol of symbols) {
|
||||
try {
|
||||
await processSingleSymbol(symbol, provider, result);
|
||||
} catch (error) {
|
||||
logger.error('Failed to process symbol', {
|
||||
error,
|
||||
symbol: symbol.symbol || symbol.code,
|
||||
provider,
|
||||
});
|
||||
result.errors++;
|
||||
}
|
||||
}
|
||||
|
||||
// Update sync status
|
||||
await updateSyncStatus(provider, 'symbols', result.processed, postgresClient);
|
||||
|
||||
await postgresClient.query('COMMIT');
|
||||
|
||||
logger.info(`${provider} symbols sync completed`, result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
await postgresClient.query('ROLLBACK');
|
||||
logger.error(`${provider} symbols sync failed`, { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function processSingleSymbol(symbol: any, provider: string, result: SyncResult): Promise<void> {
|
||||
const symbolCode = symbol.symbol || symbol.code;
|
||||
const exchangeCode = symbol.exchangeCode || symbol.exchange || symbol.exchange_id;
|
||||
|
||||
if (!symbolCode || !exchangeCode) {
|
||||
result.skipped++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Find active provider exchange mapping
|
||||
const providerMapping = await findActiveProviderExchangeMapping(provider, exchangeCode);
|
||||
|
||||
if (!providerMapping) {
|
||||
result.skipped++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if symbol exists
|
||||
const existingSymbol = await findSymbolByCodeAndExchange(
|
||||
symbolCode,
|
||||
providerMapping.master_exchange_id
|
||||
);
|
||||
|
||||
if (existingSymbol) {
|
||||
await updateSymbol(existingSymbol.id, symbol);
|
||||
await upsertProviderMapping(existingSymbol.id, provider, symbol);
|
||||
result.updated++;
|
||||
} else {
|
||||
const newSymbolId = await createSymbol(symbol, providerMapping.master_exchange_id);
|
||||
await upsertProviderMapping(newSymbolId, provider, symbol);
|
||||
result.created++;
|
||||
}
|
||||
}
|
||||
|
||||
async function findActiveProviderExchangeMapping(provider: string, providerExchangeCode: string): Promise<any> {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
const query = `
|
||||
SELECT pem.*, e.code as master_exchange_code
|
||||
FROM provider_exchange_mappings pem
|
||||
JOIN exchanges e ON pem.master_exchange_id = e.id
|
||||
WHERE pem.provider = $1 AND pem.provider_exchange_code = $2 AND pem.active = true
|
||||
`;
|
||||
const result = await postgresClient.query(query, [provider, providerExchangeCode]);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
async function findSymbolByCodeAndExchange(symbol: string, exchangeId: string): Promise<any> {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
const query = 'SELECT * FROM symbols WHERE symbol = $1 AND exchange_id = $2';
|
||||
const result = await postgresClient.query(query, [symbol, exchangeId]);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
async function createSymbol(symbol: any, exchangeId: string): Promise<string> {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
const query = `
|
||||
INSERT INTO symbols (symbol, exchange_id, company_name, country, currency)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id
|
||||
`;
|
||||
|
||||
const result = await postgresClient.query(query, [
|
||||
symbol.symbol || symbol.code,
|
||||
exchangeId,
|
||||
symbol.companyName || symbol.name || symbol.company_name,
|
||||
symbol.countryCode || symbol.country_code || 'US',
|
||||
symbol.currency || 'USD',
|
||||
]);
|
||||
|
||||
return result.rows[0].id;
|
||||
}
|
||||
|
||||
async function updateSymbol(symbolId: string, symbol: any): Promise<void> {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
const query = `
|
||||
UPDATE symbols
|
||||
SET company_name = COALESCE($2, company_name),
|
||||
country = COALESCE($3, country),
|
||||
currency = COALESCE($4, currency),
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
`;
|
||||
|
||||
await postgresClient.query(query, [
|
||||
symbolId,
|
||||
symbol.companyName || symbol.name || symbol.company_name,
|
||||
symbol.countryCode || symbol.country_code,
|
||||
symbol.currency,
|
||||
]);
|
||||
}
|
||||
|
||||
async function upsertProviderMapping(symbolId: string, provider: string, symbol: any): Promise<void> {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
const query = `
|
||||
INSERT INTO provider_mappings
|
||||
(symbol_id, provider, provider_symbol, provider_exchange, last_seen)
|
||||
VALUES ($1, $2, $3, $4, NOW())
|
||||
ON CONFLICT (provider, provider_symbol)
|
||||
DO UPDATE SET
|
||||
symbol_id = EXCLUDED.symbol_id,
|
||||
provider_exchange = EXCLUDED.provider_exchange,
|
||||
last_seen = NOW()
|
||||
`;
|
||||
|
||||
await postgresClient.query(query, [
|
||||
symbolId,
|
||||
provider,
|
||||
symbol.qmSearchCode || symbol.symbol || symbol.code,
|
||||
symbol.exchangeCode || symbol.exchange || symbol.exchange_id,
|
||||
]);
|
||||
}
|
||||
|
||||
async function updateSyncStatus(provider: string, dataType: string, count: number, postgresClient: any): Promise<void> {
|
||||
const query = `
|
||||
INSERT INTO sync_status (provider, data_type, last_sync_at, last_sync_count, sync_errors)
|
||||
VALUES ($1, $2, NOW(), $3, NULL)
|
||||
ON CONFLICT (provider, data_type)
|
||||
DO UPDATE SET
|
||||
last_sync_at = NOW(),
|
||||
last_sync_count = EXCLUDED.last_sync_count,
|
||||
sync_errors = NULL,
|
||||
updated_at = NOW()
|
||||
`;
|
||||
|
||||
await postgresClient.query(query, [provider, dataType, count]);
|
||||
}
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import { getMongoDBClient, getPostgreSQLClient } from '../../../clients';
|
||||
import type { JobPayload, SyncResult } from '../../../types/job-payloads';
|
||||
|
||||
const logger = getLogger('enhanced-sync-symbols-from-provider');
|
||||
|
||||
export async function syncSymbolsFromProvider(payload: JobPayload): Promise<SyncResult> {
|
||||
const provider = payload.provider;
|
||||
const clearFirst = payload.clearFirst || false;
|
||||
|
||||
if (!provider) {
|
||||
throw new Error('Provider is required in payload');
|
||||
}
|
||||
|
||||
logger.info(`Starting ${provider} symbols sync...`, { clearFirst });
|
||||
|
||||
const result: SyncResult = {
|
||||
processed: 0,
|
||||
created: 0,
|
||||
updated: 0,
|
||||
skipped: 0,
|
||||
errors: 0,
|
||||
};
|
||||
|
||||
try {
|
||||
const mongoClient = getMongoDBClient();
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
|
||||
// Clear existing data if requested (only symbols and mappings, keep exchanges)
|
||||
if (clearFirst) {
|
||||
await postgresClient.query('BEGIN');
|
||||
await postgresClient.query('DELETE FROM provider_mappings');
|
||||
await postgresClient.query('DELETE FROM symbols');
|
||||
await postgresClient.query('COMMIT');
|
||||
logger.info('Cleared existing symbols and mappings before sync');
|
||||
}
|
||||
|
||||
// Start transaction
|
||||
await postgresClient.query('BEGIN');
|
||||
|
||||
let symbols: Record<string, unknown>[] = [];
|
||||
|
||||
// Get symbols based on provider
|
||||
const db = mongoClient.getDatabase();
|
||||
switch (provider.toLowerCase()) {
|
||||
case 'qm':
|
||||
symbols = await db.collection('qmSymbols').find({}).toArray();
|
||||
break;
|
||||
case 'eod':
|
||||
symbols = await db.collection('eodSymbols').find({}).toArray();
|
||||
break;
|
||||
case 'ib':
|
||||
symbols = await db.collection('ibSymbols').find({}).toArray();
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported provider: ${provider}`);
|
||||
}
|
||||
|
||||
logger.info(`Found ${symbols.length} ${provider} symbols to process`);
|
||||
result.processed = symbols.length;
|
||||
|
||||
for (const symbol of symbols) {
|
||||
try {
|
||||
await processSingleSymbol(symbol, provider, result);
|
||||
} catch (error) {
|
||||
logger.error('Failed to process symbol', {
|
||||
error,
|
||||
symbol: symbol.symbol || symbol.code,
|
||||
provider,
|
||||
});
|
||||
result.errors++;
|
||||
}
|
||||
}
|
||||
|
||||
// Update sync status
|
||||
await updateSyncStatus(provider, 'symbols', result.processed, postgresClient);
|
||||
|
||||
await postgresClient.query('COMMIT');
|
||||
|
||||
logger.info(`${provider} symbols sync completed`, result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
await postgresClient.query('ROLLBACK');
|
||||
logger.error(`${provider} symbols sync failed`, { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function processSingleSymbol(
|
||||
symbol: any,
|
||||
provider: string,
|
||||
result: SyncResult
|
||||
): Promise<void> {
|
||||
const symbolCode = symbol.symbol || symbol.code;
|
||||
const exchangeCode = symbol.exchangeCode || symbol.exchange || symbol.exchange_id;
|
||||
|
||||
if (!symbolCode || !exchangeCode) {
|
||||
result.skipped++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Find active provider exchange mapping
|
||||
const providerMapping = await findActiveProviderExchangeMapping(provider, exchangeCode);
|
||||
|
||||
if (!providerMapping) {
|
||||
result.skipped++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if symbol exists
|
||||
const existingSymbol = await findSymbolByCodeAndExchange(
|
||||
symbolCode,
|
||||
providerMapping.master_exchange_id
|
||||
);
|
||||
|
||||
if (existingSymbol) {
|
||||
await updateSymbol(existingSymbol.id, symbol);
|
||||
await upsertProviderMapping(existingSymbol.id, provider, symbol);
|
||||
result.updated++;
|
||||
} else {
|
||||
const newSymbolId = await createSymbol(symbol, providerMapping.master_exchange_id);
|
||||
await upsertProviderMapping(newSymbolId, provider, symbol);
|
||||
result.created++;
|
||||
}
|
||||
}
|
||||
|
||||
async function findActiveProviderExchangeMapping(
|
||||
provider: string,
|
||||
providerExchangeCode: string
|
||||
): Promise<any> {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
const query = `
|
||||
SELECT pem.*, e.code as master_exchange_code
|
||||
FROM provider_exchange_mappings pem
|
||||
JOIN exchanges e ON pem.master_exchange_id = e.id
|
||||
WHERE pem.provider = $1 AND pem.provider_exchange_code = $2 AND pem.active = true
|
||||
`;
|
||||
const result = await postgresClient.query(query, [provider, providerExchangeCode]);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
async function findSymbolByCodeAndExchange(symbol: string, exchangeId: string): Promise<any> {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
const query = 'SELECT * FROM symbols WHERE symbol = $1 AND exchange_id = $2';
|
||||
const result = await postgresClient.query(query, [symbol, exchangeId]);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
async function createSymbol(symbol: any, exchangeId: string): Promise<string> {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
const query = `
|
||||
INSERT INTO symbols (symbol, exchange_id, company_name, country, currency)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id
|
||||
`;
|
||||
|
||||
const result = await postgresClient.query(query, [
|
||||
symbol.symbol || symbol.code,
|
||||
exchangeId,
|
||||
symbol.companyName || symbol.name || symbol.company_name,
|
||||
symbol.countryCode || symbol.country_code || 'US',
|
||||
symbol.currency || 'USD',
|
||||
]);
|
||||
|
||||
return result.rows[0].id;
|
||||
}
|
||||
|
||||
async function updateSymbol(symbolId: string, symbol: any): Promise<void> {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
const query = `
|
||||
UPDATE symbols
|
||||
SET company_name = COALESCE($2, company_name),
|
||||
country = COALESCE($3, country),
|
||||
currency = COALESCE($4, currency),
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
`;
|
||||
|
||||
await postgresClient.query(query, [
|
||||
symbolId,
|
||||
symbol.companyName || symbol.name || symbol.company_name,
|
||||
symbol.countryCode || symbol.country_code,
|
||||
symbol.currency,
|
||||
]);
|
||||
}
|
||||
|
||||
async function upsertProviderMapping(
|
||||
symbolId: string,
|
||||
provider: string,
|
||||
symbol: any
|
||||
): Promise<void> {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
const query = `
|
||||
INSERT INTO provider_mappings
|
||||
(symbol_id, provider, provider_symbol, provider_exchange, last_seen)
|
||||
VALUES ($1, $2, $3, $4, NOW())
|
||||
ON CONFLICT (provider, provider_symbol)
|
||||
DO UPDATE SET
|
||||
symbol_id = EXCLUDED.symbol_id,
|
||||
provider_exchange = EXCLUDED.provider_exchange,
|
||||
last_seen = NOW()
|
||||
`;
|
||||
|
||||
await postgresClient.query(query, [
|
||||
symbolId,
|
||||
provider,
|
||||
symbol.qmSearchCode || symbol.symbol || symbol.code,
|
||||
symbol.exchangeCode || symbol.exchange || symbol.exchange_id,
|
||||
]);
|
||||
}
|
||||
|
||||
async function updateSyncStatus(
|
||||
provider: string,
|
||||
dataType: string,
|
||||
count: number,
|
||||
postgresClient: any
|
||||
): Promise<void> {
|
||||
const query = `
|
||||
INSERT INTO sync_status (provider, data_type, last_sync_at, last_sync_count, sync_errors)
|
||||
VALUES ($1, $2, NOW(), $3, NULL)
|
||||
ON CONFLICT (provider, data_type)
|
||||
DO UPDATE SET
|
||||
last_sync_at = NOW(),
|
||||
last_sync_count = EXCLUDED.last_sync_count,
|
||||
sync_errors = NULL,
|
||||
updated_at = NOW()
|
||||
`;
|
||||
|
||||
await postgresClient.query(query, [provider, dataType, count]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,41 +1,41 @@
|
|||
import { getLogger } from '@stock-bot/logger';
|
||||
import { handlerRegistry, type HandlerConfig, type ScheduledJobConfig } from '@stock-bot/queue';
|
||||
import { symbolOperations } from './operations';
|
||||
|
||||
const logger = getLogger('symbols-handler');
|
||||
|
||||
const HANDLER_NAME = 'symbols';
|
||||
|
||||
const symbolsHandlerConfig: HandlerConfig = {
|
||||
concurrency: 1,
|
||||
maxAttempts: 3,
|
||||
scheduledJobs: [
|
||||
{
|
||||
operation: 'sync-qm-symbols',
|
||||
cronPattern: '0 2 * * *', // Daily at 2 AM
|
||||
payload: {},
|
||||
priority: 5,
|
||||
immediately: false,
|
||||
} as ScheduledJobConfig,
|
||||
{
|
||||
operation: 'sync-symbols-qm',
|
||||
cronPattern: '0 4 * * *', // Daily at 4 AM
|
||||
payload: { provider: 'qm', clearFirst: false },
|
||||
priority: 5,
|
||||
immediately: false,
|
||||
} as ScheduledJobConfig,
|
||||
],
|
||||
operations: {
|
||||
'sync-qm-symbols': symbolOperations.syncQMSymbols,
|
||||
'sync-symbols-qm': symbolOperations.syncSymbolsFromProvider,
|
||||
'sync-symbols-eod': symbolOperations.syncSymbolsFromProvider,
|
||||
'sync-symbols-ib': symbolOperations.syncSymbolsFromProvider,
|
||||
'sync-status': symbolOperations.getSyncStatus,
|
||||
},
|
||||
};
|
||||
|
||||
export function initializeSymbolsHandler(): void {
|
||||
logger.info('Registering symbols handler...');
|
||||
handlerRegistry.registerHandler(HANDLER_NAME, symbolsHandlerConfig);
|
||||
logger.info('Symbols handler registered successfully');
|
||||
}
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import { handlerRegistry, type HandlerConfig, type ScheduledJobConfig } from '@stock-bot/queue';
|
||||
import { symbolOperations } from './operations';
|
||||
|
||||
const logger = getLogger('symbols-handler');
|
||||
|
||||
const HANDLER_NAME = 'symbols';
|
||||
|
||||
const symbolsHandlerConfig: HandlerConfig = {
|
||||
concurrency: 1,
|
||||
maxAttempts: 3,
|
||||
scheduledJobs: [
|
||||
{
|
||||
operation: 'sync-qm-symbols',
|
||||
cronPattern: '0 2 * * *', // Daily at 2 AM
|
||||
payload: {},
|
||||
priority: 5,
|
||||
immediately: false,
|
||||
} as ScheduledJobConfig,
|
||||
{
|
||||
operation: 'sync-symbols-qm',
|
||||
cronPattern: '0 4 * * *', // Daily at 4 AM
|
||||
payload: { provider: 'qm', clearFirst: false },
|
||||
priority: 5,
|
||||
immediately: false,
|
||||
} as ScheduledJobConfig,
|
||||
],
|
||||
operations: {
|
||||
'sync-qm-symbols': symbolOperations.syncQMSymbols,
|
||||
'sync-symbols-qm': symbolOperations.syncSymbolsFromProvider,
|
||||
'sync-symbols-eod': symbolOperations.syncSymbolsFromProvider,
|
||||
'sync-symbols-ib': symbolOperations.syncSymbolsFromProvider,
|
||||
'sync-status': symbolOperations.getSyncStatus,
|
||||
},
|
||||
};
|
||||
|
||||
export function initializeSymbolsHandler(): void {
|
||||
logger.info('Registering symbols handler...');
|
||||
handlerRegistry.registerHandler(HANDLER_NAME, symbolsHandlerConfig);
|
||||
logger.info('Symbols handler registered successfully');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
// Framework imports
|
||||
import { initializeServiceConfig } from '@stock-bot/config';
|
||||
import { Hono } from 'hono';
|
||||
import { cors } from 'hono/cors';
|
||||
import { initializeServiceConfig } from '@stock-bot/config';
|
||||
// Library imports
|
||||
import { getLogger, setLoggerConfig, shutdownLoggers } from '@stock-bot/logger';
|
||||
import { MongoDBClient } from '@stock-bot/mongodb';
|
||||
import { PostgreSQLClient } from '@stock-bot/postgres';
|
||||
import { QueueManager, type QueueManagerConfig } from '@stock-bot/queue';
|
||||
import { Shutdown } from '@stock-bot/shutdown';
|
||||
import { setMongoDBClient, setPostgreSQLClient } from './clients';
|
||||
// Local imports
|
||||
import { enhancedSyncRoutes, healthRoutes, statsRoutes, syncRoutes } from './routes';
|
||||
import { setMongoDBClient, setPostgreSQLClient } from './clients';
|
||||
|
||||
const config = initializeServiceConfig();
|
||||
console.log('Data Sync Service Configuration:', JSON.stringify(config, null, 2));
|
||||
|
|
@ -66,17 +66,20 @@ async function initializeServices() {
|
|||
// Initialize MongoDB client
|
||||
logger.debug('Connecting to MongoDB...');
|
||||
const mongoConfig = databaseConfig.mongodb;
|
||||
mongoClient = new MongoDBClient({
|
||||
uri: mongoConfig.uri,
|
||||
database: mongoConfig.database,
|
||||
host: mongoConfig.host || 'localhost',
|
||||
port: mongoConfig.port || 27017,
|
||||
timeouts: {
|
||||
connectTimeout: 30000,
|
||||
socketTimeout: 30000,
|
||||
serverSelectionTimeout: 5000,
|
||||
mongoClient = new MongoDBClient(
|
||||
{
|
||||
uri: mongoConfig.uri,
|
||||
database: mongoConfig.database,
|
||||
host: mongoConfig.host || 'localhost',
|
||||
port: mongoConfig.port || 27017,
|
||||
timeouts: {
|
||||
connectTimeout: 30000,
|
||||
socketTimeout: 30000,
|
||||
serverSelectionTimeout: 5000,
|
||||
},
|
||||
},
|
||||
}, logger);
|
||||
logger
|
||||
);
|
||||
await mongoClient.connect();
|
||||
setMongoDBClient(mongoClient);
|
||||
logger.info('MongoDB connected');
|
||||
|
|
@ -84,18 +87,21 @@ async function initializeServices() {
|
|||
// Initialize PostgreSQL client
|
||||
logger.debug('Connecting to PostgreSQL...');
|
||||
const pgConfig = databaseConfig.postgres;
|
||||
postgresClient = new PostgreSQLClient({
|
||||
host: pgConfig.host,
|
||||
port: pgConfig.port,
|
||||
database: pgConfig.database,
|
||||
username: pgConfig.user,
|
||||
password: pgConfig.password,
|
||||
poolSettings: {
|
||||
min: 2,
|
||||
max: pgConfig.poolSize || 10,
|
||||
idleTimeoutMillis: pgConfig.idleTimeout || 30000,
|
||||
postgresClient = new PostgreSQLClient(
|
||||
{
|
||||
host: pgConfig.host,
|
||||
port: pgConfig.port,
|
||||
database: pgConfig.database,
|
||||
username: pgConfig.user,
|
||||
password: pgConfig.password,
|
||||
poolSettings: {
|
||||
min: 2,
|
||||
max: pgConfig.poolSize || 10,
|
||||
idleTimeoutMillis: pgConfig.idleTimeout || 30000,
|
||||
},
|
||||
},
|
||||
}, logger);
|
||||
logger
|
||||
);
|
||||
await postgresClient.connect();
|
||||
setPostgreSQLClient(postgresClient);
|
||||
logger.info('PostgreSQL connected');
|
||||
|
|
@ -124,7 +130,7 @@ async function initializeServices() {
|
|||
enableDLQ: true,
|
||||
},
|
||||
enableScheduledJobs: true,
|
||||
delayWorkerStart: true, // Prevent workers from starting until all singletons are ready
|
||||
delayWorkerStart: true, // Prevent workers from starting until all singletons are ready
|
||||
};
|
||||
|
||||
queueManager = QueueManager.getOrInitialize(queueManagerConfig);
|
||||
|
|
@ -134,10 +140,10 @@ async function initializeServices() {
|
|||
logger.debug('Initializing sync handlers...');
|
||||
const { initializeExchangesHandler } = await import('./handlers/exchanges/exchanges.handler');
|
||||
const { initializeSymbolsHandler } = await import('./handlers/symbols/symbols.handler');
|
||||
|
||||
|
||||
initializeExchangesHandler();
|
||||
initializeSymbolsHandler();
|
||||
|
||||
|
||||
logger.info('Sync handlers initialized');
|
||||
|
||||
// Create scheduled jobs from registered handlers
|
||||
|
|
@ -271,4 +277,4 @@ startServer().catch(error => {
|
|||
process.exit(1);
|
||||
});
|
||||
|
||||
logger.info('Data sync service startup initiated');
|
||||
logger.info('Data sync service startup initiated');
|
||||
|
|
|
|||
|
|
@ -11,13 +11,13 @@ enhancedSync.post('/exchanges/all', async c => {
|
|||
const clearFirst = c.req.query('clear') === 'true';
|
||||
const queueManager = QueueManager.getInstance();
|
||||
const exchangesQueue = queueManager.getQueue('exchanges');
|
||||
|
||||
|
||||
const job = await exchangesQueue.addJob('sync-all-exchanges', {
|
||||
handler: 'exchanges',
|
||||
operation: 'sync-all-exchanges',
|
||||
payload: { clearFirst },
|
||||
});
|
||||
|
||||
|
||||
return c.json({ success: true, jobId: job.id, message: 'Enhanced exchange sync job queued' });
|
||||
} catch (error) {
|
||||
logger.error('Failed to queue enhanced exchange sync job', { error });
|
||||
|
|
@ -32,14 +32,18 @@ enhancedSync.post('/provider-mappings/qm', async c => {
|
|||
try {
|
||||
const queueManager = QueueManager.getInstance();
|
||||
const exchangesQueue = queueManager.getQueue('exchanges');
|
||||
|
||||
|
||||
const job = await exchangesQueue.addJob('sync-qm-provider-mappings', {
|
||||
handler: 'exchanges',
|
||||
operation: 'sync-qm-provider-mappings',
|
||||
payload: {},
|
||||
});
|
||||
|
||||
return c.json({ success: true, jobId: job.id, message: 'QM provider mappings sync job queued' });
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
jobId: job.id,
|
||||
message: 'QM provider mappings sync job queued',
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to queue QM provider mappings sync job', { error });
|
||||
return c.json(
|
||||
|
|
@ -55,13 +59,13 @@ enhancedSync.post('/symbols/:provider', async c => {
|
|||
const clearFirst = c.req.query('clear') === 'true';
|
||||
const queueManager = QueueManager.getInstance();
|
||||
const symbolsQueue = queueManager.getQueue('symbols');
|
||||
|
||||
|
||||
const job = await symbolsQueue.addJob(`sync-symbols-${provider}`, {
|
||||
handler: 'symbols',
|
||||
operation: `sync-symbols-${provider}`,
|
||||
payload: { provider, clearFirst },
|
||||
});
|
||||
|
||||
|
||||
return c.json({ success: true, jobId: job.id, message: `${provider} symbols sync job queued` });
|
||||
} catch (error) {
|
||||
logger.error('Failed to queue enhanced symbol sync job', { error });
|
||||
|
|
@ -77,13 +81,13 @@ enhancedSync.get('/status/enhanced', async c => {
|
|||
try {
|
||||
const queueManager = QueueManager.getInstance();
|
||||
const exchangesQueue = queueManager.getQueue('exchanges');
|
||||
|
||||
|
||||
const job = await exchangesQueue.addJob('enhanced-sync-status', {
|
||||
handler: 'exchanges',
|
||||
operation: 'enhanced-sync-status',
|
||||
payload: {},
|
||||
});
|
||||
|
||||
|
||||
// Wait for job to complete and return result
|
||||
const result = await job.waitUntilFinished();
|
||||
return c.json(result);
|
||||
|
|
@ -93,4 +97,4 @@ enhancedSync.get('/status/enhanced', async c => {
|
|||
}
|
||||
});
|
||||
|
||||
export { enhancedSync as enhancedSyncRoutes };
|
||||
export { enhancedSync as enhancedSyncRoutes };
|
||||
|
|
|
|||
|
|
@ -2,4 +2,4 @@
|
|||
export { healthRoutes } from './health.routes';
|
||||
export { syncRoutes } from './sync.routes';
|
||||
export { enhancedSyncRoutes } from './enhanced-sync.routes';
|
||||
export { statsRoutes } from './stats.routes';
|
||||
export { statsRoutes } from './stats.routes';
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ stats.get('/exchanges', async c => {
|
|||
try {
|
||||
const queueManager = QueueManager.getInstance();
|
||||
const exchangesQueue = queueManager.getQueue('exchanges');
|
||||
|
||||
|
||||
const job = await exchangesQueue.addJob('get-exchange-stats', {
|
||||
handler: 'exchanges',
|
||||
operation: 'get-exchange-stats',
|
||||
payload: {},
|
||||
});
|
||||
|
||||
|
||||
// Wait for job to complete and return result
|
||||
const result = await job.waitUntilFinished();
|
||||
return c.json(result);
|
||||
|
|
@ -30,13 +30,13 @@ stats.get('/provider-mappings', async c => {
|
|||
try {
|
||||
const queueManager = QueueManager.getInstance();
|
||||
const exchangesQueue = queueManager.getQueue('exchanges');
|
||||
|
||||
|
||||
const job = await exchangesQueue.addJob('get-provider-mapping-stats', {
|
||||
handler: 'exchanges',
|
||||
operation: 'get-provider-mapping-stats',
|
||||
payload: {},
|
||||
});
|
||||
|
||||
|
||||
// Wait for job to complete and return result
|
||||
const result = await job.waitUntilFinished();
|
||||
return c.json(result);
|
||||
|
|
@ -46,4 +46,4 @@ stats.get('/provider-mappings', async c => {
|
|||
}
|
||||
});
|
||||
|
||||
export { stats as statsRoutes };
|
||||
export { stats as statsRoutes };
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ sync.post('/symbols', async c => {
|
|||
try {
|
||||
const queueManager = QueueManager.getInstance();
|
||||
const symbolsQueue = queueManager.getQueue('symbols');
|
||||
|
||||
|
||||
const job = await symbolsQueue.addJob('sync-qm-symbols', {
|
||||
handler: 'symbols',
|
||||
operation: 'sync-qm-symbols',
|
||||
payload: {},
|
||||
});
|
||||
|
||||
|
||||
return c.json({ success: true, jobId: job.id, message: 'QM symbols sync job queued' });
|
||||
} catch (error) {
|
||||
logger.error('Failed to queue symbol sync job', { error });
|
||||
|
|
@ -31,13 +31,13 @@ sync.post('/exchanges', async c => {
|
|||
try {
|
||||
const queueManager = QueueManager.getInstance();
|
||||
const exchangesQueue = queueManager.getQueue('exchanges');
|
||||
|
||||
|
||||
const job = await exchangesQueue.addJob('sync-qm-exchanges', {
|
||||
handler: 'exchanges',
|
||||
operation: 'sync-qm-exchanges',
|
||||
payload: {},
|
||||
});
|
||||
|
||||
|
||||
return c.json({ success: true, jobId: job.id, message: 'QM exchanges sync job queued' });
|
||||
} catch (error) {
|
||||
logger.error('Failed to queue exchange sync job', { error });
|
||||
|
|
@ -53,13 +53,13 @@ sync.get('/status', async c => {
|
|||
try {
|
||||
const queueManager = QueueManager.getInstance();
|
||||
const symbolsQueue = queueManager.getQueue('symbols');
|
||||
|
||||
|
||||
const job = await symbolsQueue.addJob('sync-status', {
|
||||
handler: 'symbols',
|
||||
operation: 'sync-status',
|
||||
payload: {},
|
||||
});
|
||||
|
||||
|
||||
// Wait for job to complete and return result
|
||||
const result = await job.waitUntilFinished();
|
||||
return c.json(result);
|
||||
|
|
@ -74,13 +74,13 @@ sync.post('/clear', async c => {
|
|||
try {
|
||||
const queueManager = QueueManager.getInstance();
|
||||
const exchangesQueue = queueManager.getQueue('exchanges');
|
||||
|
||||
|
||||
const job = await exchangesQueue.addJob('clear-postgresql-data', {
|
||||
handler: 'exchanges',
|
||||
operation: 'clear-postgresql-data',
|
||||
payload: {},
|
||||
});
|
||||
|
||||
|
||||
// Wait for job to complete and return result
|
||||
const result = await job.waitUntilFinished();
|
||||
return c.json({ success: true, result });
|
||||
|
|
@ -93,4 +93,4 @@ sync.post('/clear', async c => {
|
|||
}
|
||||
});
|
||||
|
||||
export { sync as syncRoutes };
|
||||
export { sync as syncRoutes };
|
||||
|
|
|
|||
|
|
@ -1,27 +1,27 @@
|
|||
export interface JobPayload {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface SyncResult {
|
||||
processed: number;
|
||||
created: number;
|
||||
updated: number;
|
||||
skipped: number;
|
||||
errors: number;
|
||||
}
|
||||
|
||||
export interface SyncStatus {
|
||||
provider: string;
|
||||
dataType: string;
|
||||
lastSyncAt?: Date;
|
||||
lastSyncCount: number;
|
||||
syncErrors?: string;
|
||||
}
|
||||
|
||||
export interface ExchangeMapping {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
country: string;
|
||||
currency: string;
|
||||
}
|
||||
export interface JobPayload {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface SyncResult {
|
||||
processed: number;
|
||||
created: number;
|
||||
updated: number;
|
||||
skipped: number;
|
||||
errors: number;
|
||||
}
|
||||
|
||||
export interface SyncStatus {
|
||||
provider: string;
|
||||
dataType: string;
|
||||
lastSyncAt?: Date;
|
||||
lastSyncCount: number;
|
||||
syncErrors?: string;
|
||||
}
|
||||
|
||||
export interface ExchangeMapping {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
country: string;
|
||||
currency: string;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue