203 lines
No EOL
6 KiB
TypeScript
203 lines
No EOL
6 KiB
TypeScript
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;
|
|
} |