212 lines
5.6 KiB
TypeScript
212 lines
5.6 KiB
TypeScript
import type { IServiceContainer } from '@stock-bot/handlers';
|
|
import { getLogger } from '@stock-bot/logger';
|
|
import type { MasterExchange } from '@stock-bot/mongodb';
|
|
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,
|
|
container: IServiceContainer
|
|
): Promise<{ syncedCount: number; totalExchanges: number }> {
|
|
logger.info('Syncing IB exchanges from database...');
|
|
|
|
try {
|
|
const mongoClient = container.mongodb;
|
|
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, container);
|
|
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,
|
|
container: IServiceContainer
|
|
): Promise<void> {
|
|
const mongoClient = container.mongodb;
|
|
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
|
|
}
|