372 lines
No EOL
12 KiB
TypeScript
372 lines
No EOL
12 KiB
TypeScript
import { getLogger } from '@stock-bot/logger';
|
|
import { getPostgreSQLClient, getMongoDBClient } from '../clients';
|
|
import {
|
|
Exchange,
|
|
ExchangeWithMappings,
|
|
ProviderMapping,
|
|
CreateExchangeRequest,
|
|
UpdateExchangeRequest,
|
|
CreateProviderMappingRequest,
|
|
UpdateProviderMappingRequest,
|
|
ProviderExchange,
|
|
ExchangeStats,
|
|
} from '../types/exchange.types';
|
|
|
|
const logger = getLogger('exchange-service');
|
|
|
|
export class ExchangeService {
|
|
private get postgresClient() {
|
|
return getPostgreSQLClient();
|
|
}
|
|
|
|
private get mongoClient() {
|
|
return getMongoDBClient();
|
|
}
|
|
|
|
// Exchanges
|
|
async getAllExchanges(): Promise<ExchangeWithMappings[]> {
|
|
const exchangesQuery = `
|
|
SELECT
|
|
e.id,
|
|
e.code,
|
|
e.name,
|
|
e.country,
|
|
e.currency,
|
|
e.active,
|
|
e.visible,
|
|
e.created_at,
|
|
e.updated_at,
|
|
COUNT(pem.id) as provider_mapping_count,
|
|
COUNT(CASE WHEN pem.active = true THEN 1 END) as active_mapping_count,
|
|
COUNT(CASE WHEN pem.verified = true THEN 1 END) as verified_mapping_count,
|
|
STRING_AGG(DISTINCT pem.provider, ', ') as providers
|
|
FROM exchanges e
|
|
LEFT JOIN provider_exchange_mappings pem ON e.id = pem.master_exchange_id
|
|
WHERE e.visible = true
|
|
GROUP BY e.id, e.code, e.name, e.country, e.currency, e.active, e.visible, e.created_at, e.updated_at
|
|
ORDER BY e.code
|
|
`;
|
|
|
|
const exchangesResult = await this.postgresClient.query(exchangesQuery);
|
|
|
|
// Get all provider mappings
|
|
const mappingsQuery = `
|
|
SELECT
|
|
pem.*,
|
|
e.code as master_exchange_code,
|
|
e.name as master_exchange_name
|
|
FROM provider_exchange_mappings pem
|
|
JOIN exchanges e ON pem.master_exchange_id = e.id
|
|
WHERE e.visible = true
|
|
ORDER BY pem.master_exchange_id, pem.provider, pem.provider_exchange_code
|
|
`;
|
|
const mappingsResult = await this.postgresClient.query(mappingsQuery);
|
|
|
|
// Group mappings by exchange ID
|
|
const mappingsByExchange = mappingsResult.rows.reduce((acc, mapping) => {
|
|
const exchangeId = mapping.master_exchange_id;
|
|
if (!acc[exchangeId]) {
|
|
acc[exchangeId] = [];
|
|
}
|
|
acc[exchangeId].push(mapping);
|
|
return acc;
|
|
}, {} as Record<string, ProviderMapping[]>);
|
|
|
|
// Attach mappings to exchanges
|
|
return exchangesResult.rows.map(exchange => ({
|
|
...exchange,
|
|
provider_mappings: mappingsByExchange[exchange.id] || [],
|
|
}));
|
|
}
|
|
|
|
async getExchangeById(id: string): Promise<{ exchange: Exchange; provider_mappings: ProviderMapping[] } | null> {
|
|
const exchangeQuery = 'SELECT * FROM exchanges WHERE id = $1 AND visible = true';
|
|
const exchangeResult = await this.postgresClient.query(exchangeQuery, [id]);
|
|
|
|
if (exchangeResult.rows.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const mappingsQuery = `
|
|
SELECT
|
|
pem.*,
|
|
e.code as master_exchange_code,
|
|
e.name as master_exchange_name
|
|
FROM provider_exchange_mappings pem
|
|
JOIN exchanges e ON pem.master_exchange_id = e.id
|
|
WHERE pem.master_exchange_id = $1
|
|
ORDER BY pem.provider, pem.provider_exchange_code
|
|
`;
|
|
const mappingsResult = await this.postgresClient.query(mappingsQuery, [id]);
|
|
|
|
return {
|
|
exchange: exchangeResult.rows[0],
|
|
provider_mappings: mappingsResult.rows,
|
|
};
|
|
}
|
|
|
|
async createExchange(data: CreateExchangeRequest): Promise<Exchange> {
|
|
const query = `
|
|
INSERT INTO exchanges (code, name, country, currency, active, visible)
|
|
VALUES ($1, $2, $3, $4, $5, true)
|
|
RETURNING *
|
|
`;
|
|
|
|
const result = await this.postgresClient.query(query, [
|
|
data.code,
|
|
data.name,
|
|
data.country,
|
|
data.currency,
|
|
data.active,
|
|
]);
|
|
|
|
logger.info('Exchange created', {
|
|
exchangeId: result.rows[0].id,
|
|
code: data.code,
|
|
name: data.name,
|
|
});
|
|
|
|
return result.rows[0];
|
|
}
|
|
|
|
async updateExchange(id: string, updates: UpdateExchangeRequest): Promise<Exchange | null> {
|
|
const updateFields = [];
|
|
const values = [];
|
|
let paramIndex = 1;
|
|
|
|
Object.entries(updates).forEach(([key, value]) => {
|
|
updateFields.push(`${key} = $${paramIndex++}`);
|
|
values.push(value);
|
|
});
|
|
|
|
updateFields.push(`updated_at = NOW()`);
|
|
values.push(id);
|
|
|
|
const query = `
|
|
UPDATE exchanges
|
|
SET ${updateFields.join(', ')}
|
|
WHERE id = $${paramIndex}
|
|
RETURNING *
|
|
`;
|
|
|
|
const result = await this.postgresClient.query(query, values);
|
|
|
|
if (result.rows.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
// If hiding an exchange, delete its provider mappings
|
|
if (updates.visible === false) {
|
|
await this.deleteProviderMappingsByExchangeId(id);
|
|
}
|
|
|
|
logger.info('Exchange updated', { exchangeId: id, updates });
|
|
|
|
return result.rows[0];
|
|
}
|
|
|
|
// Provider Mappings
|
|
async getAllProviderMappings(): Promise<ProviderMapping[]> {
|
|
const query = `
|
|
SELECT
|
|
pem.*,
|
|
e.code as master_exchange_code,
|
|
e.name as master_exchange_name,
|
|
e.active as master_exchange_active
|
|
FROM provider_exchange_mappings pem
|
|
JOIN exchanges e ON pem.master_exchange_id = e.id
|
|
WHERE e.visible = true
|
|
ORDER BY pem.provider, pem.provider_exchange_code
|
|
`;
|
|
|
|
const result = await this.postgresClient.query(query);
|
|
return result.rows;
|
|
}
|
|
|
|
async getProviderMappingsByProvider(provider: string): Promise<ProviderMapping[]> {
|
|
const query = `
|
|
SELECT
|
|
pem.*,
|
|
e.code as master_exchange_code,
|
|
e.name as master_exchange_name,
|
|
e.active as master_exchange_active
|
|
FROM provider_exchange_mappings pem
|
|
JOIN exchanges e ON pem.master_exchange_id = e.id
|
|
WHERE pem.provider = $1 AND e.visible = true
|
|
ORDER BY pem.provider_exchange_code
|
|
`;
|
|
|
|
const result = await this.postgresClient.query(query, [provider]);
|
|
return result.rows;
|
|
}
|
|
|
|
async createProviderMapping(data: CreateProviderMappingRequest): Promise<ProviderMapping> {
|
|
const query = `
|
|
INSERT INTO provider_exchange_mappings
|
|
(provider, provider_exchange_code, provider_exchange_name, master_exchange_id,
|
|
country_code, currency, confidence, active, verified, auto_mapped)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, false)
|
|
RETURNING *
|
|
`;
|
|
|
|
const result = await this.postgresClient.query(query, [
|
|
data.provider,
|
|
data.provider_exchange_code,
|
|
data.provider_exchange_name,
|
|
data.master_exchange_id,
|
|
data.country_code,
|
|
data.currency,
|
|
data.confidence,
|
|
data.active,
|
|
data.verified,
|
|
]);
|
|
|
|
logger.info('Provider mapping created', {
|
|
provider: data.provider,
|
|
provider_exchange_code: data.provider_exchange_code,
|
|
master_exchange_id: data.master_exchange_id,
|
|
});
|
|
|
|
return result.rows[0];
|
|
}
|
|
|
|
async updateProviderMapping(id: string, updates: UpdateProviderMappingRequest): Promise<ProviderMapping | null> {
|
|
const updateFields = [];
|
|
const values = [];
|
|
let paramIndex = 1;
|
|
|
|
Object.entries(updates).forEach(([key, value]) => {
|
|
updateFields.push(`${key} = $${paramIndex++}`);
|
|
values.push(value);
|
|
});
|
|
|
|
updateFields.push(`updated_at = NOW()`);
|
|
updateFields.push(`auto_mapped = false`); // Mark as manually managed
|
|
values.push(id);
|
|
|
|
const query = `
|
|
UPDATE provider_exchange_mappings
|
|
SET ${updateFields.join(', ')}
|
|
WHERE id = $${paramIndex}
|
|
RETURNING *
|
|
`;
|
|
|
|
const result = await this.postgresClient.query(query, values);
|
|
|
|
if (result.rows.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
logger.info('Provider mapping updated', { mappingId: id, updates });
|
|
|
|
return result.rows[0];
|
|
}
|
|
|
|
async deleteProviderMappingsByExchangeId(exchangeId: string): Promise<number> {
|
|
const query = 'DELETE FROM provider_exchange_mappings WHERE master_exchange_id = $1';
|
|
const result = await this.postgresClient.query(query, [exchangeId]);
|
|
|
|
logger.info('Deleted provider mappings for hidden exchange', {
|
|
exchangeId,
|
|
deletedMappings: result.rowCount,
|
|
});
|
|
|
|
return result.rowCount || 0;
|
|
}
|
|
|
|
// Providers and Statistics
|
|
async getProviders(): Promise<string[]> {
|
|
const query = `
|
|
SELECT DISTINCT provider
|
|
FROM provider_exchange_mappings
|
|
ORDER BY provider
|
|
`;
|
|
|
|
const result = await this.postgresClient.query(query);
|
|
return result.rows.map(row => row.provider);
|
|
}
|
|
|
|
async getExchangeStats(): Promise<ExchangeStats> {
|
|
const query = `
|
|
SELECT
|
|
(SELECT COUNT(*) FROM exchanges WHERE visible = true) as total_exchanges,
|
|
(SELECT COUNT(*) FROM exchanges WHERE active = true AND visible = true) as active_exchanges,
|
|
(SELECT COUNT(DISTINCT country) FROM exchanges WHERE visible = true) as countries,
|
|
(SELECT COUNT(DISTINCT currency) FROM exchanges WHERE visible = true) as currencies,
|
|
(SELECT COUNT(*) FROM provider_exchange_mappings pem JOIN exchanges e ON pem.master_exchange_id = e.id WHERE e.visible = true) as total_provider_mappings,
|
|
(SELECT COUNT(*) FROM provider_exchange_mappings pem JOIN exchanges e ON pem.master_exchange_id = e.id WHERE pem.active = true AND e.visible = true) as active_provider_mappings,
|
|
(SELECT COUNT(*) FROM provider_exchange_mappings pem JOIN exchanges e ON pem.master_exchange_id = e.id WHERE pem.verified = true AND e.visible = true) as verified_provider_mappings,
|
|
(SELECT COUNT(DISTINCT provider) FROM provider_exchange_mappings pem JOIN exchanges e ON pem.master_exchange_id = e.id WHERE e.visible = true) as providers
|
|
`;
|
|
|
|
const result = await this.postgresClient.query(query);
|
|
return result.rows[0];
|
|
}
|
|
|
|
async getUnmappedProviderExchanges(provider: string): Promise<ProviderExchange[]> {
|
|
// Get existing mappings for this provider
|
|
const existingMappingsQuery = `
|
|
SELECT provider_exchange_code
|
|
FROM provider_exchange_mappings
|
|
WHERE provider = $1
|
|
`;
|
|
const existingMappings = await this.postgresClient.query(existingMappingsQuery, [provider]);
|
|
const mappedCodes = new Set(existingMappings.rows.map(row => row.provider_exchange_code));
|
|
|
|
const db = this.mongoClient.getDatabase();
|
|
let providerExchanges: ProviderExchange[] = [];
|
|
|
|
switch (provider) {
|
|
case 'eod': {
|
|
const eodExchanges = await db.collection('eodExchanges').find({ active: true }).toArray();
|
|
providerExchanges = eodExchanges
|
|
.filter(exchange => !mappedCodes.has(exchange.Code))
|
|
.map(exchange => ({
|
|
provider_exchange_code: exchange.Code,
|
|
provider_exchange_name: exchange.Name,
|
|
country_code: exchange.CountryISO2,
|
|
currency: exchange.Currency,
|
|
symbol_count: null,
|
|
}));
|
|
break;
|
|
}
|
|
|
|
case 'ib': {
|
|
const ibExchanges = await db.collection('ibExchanges').find({}).toArray();
|
|
providerExchanges = ibExchanges
|
|
.filter(exchange => !mappedCodes.has(exchange.exchange_id))
|
|
.map(exchange => ({
|
|
provider_exchange_code: exchange.exchange_id,
|
|
provider_exchange_name: exchange.name,
|
|
country_code: exchange.country_code,
|
|
currency: null,
|
|
symbol_count: null,
|
|
}));
|
|
break;
|
|
}
|
|
|
|
case 'qm': {
|
|
const qmExchanges = await db.collection('qmExchanges').find({}).toArray();
|
|
providerExchanges = qmExchanges
|
|
.filter(exchange => !mappedCodes.has(exchange.exchangeCode))
|
|
.map(exchange => ({
|
|
provider_exchange_code: exchange.exchangeCode,
|
|
provider_exchange_name: exchange.name,
|
|
country_code: exchange.countryCode,
|
|
currency: exchange.countryCode === 'CA' ? 'CAD' : 'USD',
|
|
symbol_count: null,
|
|
}));
|
|
break;
|
|
}
|
|
|
|
|
|
default:
|
|
throw new Error(`Unknown provider: ${provider}`);
|
|
}
|
|
|
|
return providerExchanges;
|
|
}
|
|
}
|
|
|
|
// Export singleton instance
|
|
export const exchangeService = new ExchangeService(); |