added new exchanges system
This commit is contained in:
parent
95eda4a842
commit
263e9513b7
98 changed files with 4643 additions and 1496 deletions
130
apps/web-api/src/index.ts
Normal file
130
apps/web-api/src/index.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
/**
|
||||
* Stock Bot Web API - REST API service for web application
|
||||
*/
|
||||
import { Hono } from 'hono';
|
||||
import { cors } from 'hono/cors';
|
||||
import { loadEnvVariables } from '@stock-bot/config';
|
||||
import { getLogger, shutdownLoggers } from '@stock-bot/logger';
|
||||
import { connectMongoDB, disconnectMongoDB } from '@stock-bot/mongodb-client';
|
||||
import { connectPostgreSQL, disconnectPostgreSQL } from '@stock-bot/postgres-client';
|
||||
import { Shutdown } from '@stock-bot/shutdown';
|
||||
// Import routes
|
||||
import { exchangeRoutes } from './routes/exchange.routes';
|
||||
import { healthRoutes } from './routes/health.routes';
|
||||
|
||||
// Load environment variables
|
||||
loadEnvVariables();
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
// Add CORS middleware
|
||||
app.use(
|
||||
'*',
|
||||
cors({
|
||||
origin: ['http://localhost:4200', 'http://localhost:3000'], // React dev server ports
|
||||
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
||||
allowHeaders: ['Content-Type', 'Authorization'],
|
||||
credentials: true,
|
||||
})
|
||||
);
|
||||
|
||||
const logger = getLogger('web-api');
|
||||
const PORT = parseInt(process.env.WEB_API_PORT || '4000');
|
||||
let server: ReturnType<typeof Bun.serve> | null = null;
|
||||
|
||||
// Initialize shutdown manager
|
||||
const shutdown = Shutdown.getInstance({ timeout: 15000 });
|
||||
|
||||
// Add routes
|
||||
app.route('/health', healthRoutes);
|
||||
app.route('/api/exchanges', exchangeRoutes);
|
||||
|
||||
// Basic API info endpoint
|
||||
app.get('/', c => {
|
||||
return c.json({
|
||||
name: 'Stock Bot Web API',
|
||||
version: '1.0.0',
|
||||
status: 'running',
|
||||
timestamp: new Date().toISOString(),
|
||||
endpoints: {
|
||||
health: '/health',
|
||||
exchanges: '/api/exchanges',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize services
|
||||
async function initializeServices() {
|
||||
logger.info('Initializing web API service...');
|
||||
|
||||
try {
|
||||
// Initialize MongoDB client
|
||||
logger.info('Connecting to MongoDB...');
|
||||
await connectMongoDB();
|
||||
logger.info('MongoDB connected');
|
||||
|
||||
// Initialize PostgreSQL client
|
||||
logger.info('Connecting to PostgreSQL...');
|
||||
await connectPostgreSQL();
|
||||
logger.info('PostgreSQL connected');
|
||||
|
||||
logger.info('All services initialized successfully');
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize services', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Start server
|
||||
async function startServer() {
|
||||
await initializeServices();
|
||||
|
||||
server = Bun.serve({
|
||||
port: PORT,
|
||||
fetch: app.fetch,
|
||||
development: process.env.NODE_ENV === 'development',
|
||||
});
|
||||
|
||||
logger.info(`Stock Bot Web API started on port ${PORT}`);
|
||||
}
|
||||
|
||||
// Register shutdown handlers
|
||||
shutdown.onShutdown(async () => {
|
||||
if (server) {
|
||||
logger.info('Stopping HTTP server...');
|
||||
try {
|
||||
server.stop();
|
||||
logger.info('HTTP server stopped');
|
||||
} catch (error) {
|
||||
logger.error('Error stopping HTTP server', { error });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
shutdown.onShutdown(async () => {
|
||||
logger.info('Disconnecting from databases...');
|
||||
try {
|
||||
await disconnectMongoDB();
|
||||
await disconnectPostgreSQL();
|
||||
logger.info('Database connections closed');
|
||||
} catch (error) {
|
||||
logger.error('Error closing database connections', { error });
|
||||
}
|
||||
});
|
||||
|
||||
shutdown.onShutdown(async () => {
|
||||
try {
|
||||
await shutdownLoggers();
|
||||
process.stdout.write('Web API loggers shut down\n');
|
||||
} catch (error) {
|
||||
process.stderr.write(`Error shutting down loggers: ${error}\n`);
|
||||
}
|
||||
});
|
||||
|
||||
// Start the service
|
||||
startServer().catch(error => {
|
||||
logger.error('Failed to start web API service', { error });
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
logger.info('Web API service startup initiated');
|
||||
688
apps/web-api/src/routes/exchange.routes.ts
Normal file
688
apps/web-api/src/routes/exchange.routes.ts
Normal file
|
|
@ -0,0 +1,688 @@
|
|||
/**
|
||||
* Exchange management routes
|
||||
*/
|
||||
import { Hono } from 'hono';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import { getPostgreSQLClient } from '@stock-bot/postgres-client';
|
||||
import { getMongoDBClient } from '@stock-bot/mongodb-client';
|
||||
|
||||
const logger = getLogger('exchange-routes');
|
||||
export const exchangeRoutes = new Hono();
|
||||
|
||||
// Get all exchanges with provider mapping counts
|
||||
exchangeRoutes.get('/', async c => {
|
||||
try {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
e.id,
|
||||
e.code,
|
||||
e.name,
|
||||
e.country,
|
||||
e.currency,
|
||||
e.active,
|
||||
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
|
||||
GROUP BY e.id, e.code, e.name, e.country, e.currency, e.active, e.created_at, e.updated_at
|
||||
ORDER BY e.code
|
||||
`;
|
||||
|
||||
const result = await postgresClient.query(query);
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
data: result.rows,
|
||||
total: result.rows.length,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get exchanges', { error });
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Get exchange by ID with detailed provider mappings
|
||||
exchangeRoutes.get('/:id', async c => {
|
||||
try {
|
||||
const exchangeId = c.req.param('id');
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
|
||||
// Get exchange details
|
||||
const exchangeQuery = 'SELECT * FROM exchanges WHERE id = $1';
|
||||
const exchangeResult = await postgresClient.query(exchangeQuery, [exchangeId]);
|
||||
|
||||
if (exchangeResult.rows.length === 0) {
|
||||
return c.json({ success: false, error: 'Exchange not found' }, 404);
|
||||
}
|
||||
|
||||
// Get provider mappings for this exchange
|
||||
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 postgresClient.query(mappingsQuery, [exchangeId]);
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
data: {
|
||||
exchange: exchangeResult.rows[0],
|
||||
provider_mappings: mappingsResult.rows,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get exchange details', { error });
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Update exchange (activate/deactivate, rename, etc.)
|
||||
exchangeRoutes.patch('/:id', async c => {
|
||||
try {
|
||||
const exchangeId = c.req.param('id');
|
||||
const body = await c.req.json();
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
|
||||
const updateFields = [];
|
||||
const values = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// Build dynamic update query
|
||||
if (body.active !== undefined) {
|
||||
updateFields.push(`active = $${paramIndex++}`);
|
||||
values.push(body.active);
|
||||
}
|
||||
|
||||
if (body.name !== undefined) {
|
||||
updateFields.push(`name = $${paramIndex++}`);
|
||||
values.push(body.name);
|
||||
}
|
||||
|
||||
if (body.country !== undefined) {
|
||||
updateFields.push(`country = $${paramIndex++}`);
|
||||
values.push(body.country);
|
||||
}
|
||||
|
||||
if (body.currency !== undefined) {
|
||||
updateFields.push(`currency = $${paramIndex++}`);
|
||||
values.push(body.currency);
|
||||
}
|
||||
|
||||
if (updateFields.length === 0) {
|
||||
return c.json({ success: false, error: 'No valid fields to update' }, 400);
|
||||
}
|
||||
|
||||
updateFields.push(`updated_at = NOW()`);
|
||||
values.push(exchangeId);
|
||||
|
||||
const query = `
|
||||
UPDATE exchanges
|
||||
SET ${updateFields.join(', ')}
|
||||
WHERE id = $${paramIndex}
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const result = await postgresClient.query(query, values);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return c.json({ success: false, error: 'Exchange not found' }, 404);
|
||||
}
|
||||
|
||||
logger.info('Exchange updated', { exchangeId, updates: body });
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
data: result.rows[0],
|
||||
message: 'Exchange updated successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to update exchange', { error });
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Get all provider mappings
|
||||
exchangeRoutes.get('/provider-mappings/all', async c => {
|
||||
try {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
|
||||
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
|
||||
ORDER BY pem.provider, pem.provider_exchange_code
|
||||
`;
|
||||
|
||||
const result = await postgresClient.query(query);
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
data: result.rows,
|
||||
total: result.rows.length,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get provider mappings', { error });
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Get provider mappings by provider
|
||||
exchangeRoutes.get('/provider-mappings/:provider', async c => {
|
||||
try {
|
||||
const provider = c.req.param('provider');
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
|
||||
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
|
||||
ORDER BY pem.provider_exchange_code
|
||||
`;
|
||||
|
||||
const result = await postgresClient.query(query, [provider]);
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
data: result.rows,
|
||||
total: result.rows.length,
|
||||
provider,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get provider mappings', { error });
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Update provider mapping (activate/deactivate, verify, change confidence)
|
||||
exchangeRoutes.patch('/provider-mappings/:id', async c => {
|
||||
try {
|
||||
const mappingId = c.req.param('id');
|
||||
const body = await c.req.json();
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
|
||||
const updateFields = [];
|
||||
const values = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// Build dynamic update query
|
||||
if (body.active !== undefined) {
|
||||
updateFields.push(`active = $${paramIndex++}`);
|
||||
values.push(body.active);
|
||||
}
|
||||
|
||||
if (body.verified !== undefined) {
|
||||
updateFields.push(`verified = $${paramIndex++}`);
|
||||
values.push(body.verified);
|
||||
}
|
||||
|
||||
if (body.confidence !== undefined) {
|
||||
updateFields.push(`confidence = $${paramIndex++}`);
|
||||
values.push(body.confidence);
|
||||
}
|
||||
|
||||
if (body.master_exchange_id !== undefined) {
|
||||
updateFields.push(`master_exchange_id = $${paramIndex++}`);
|
||||
values.push(body.master_exchange_id);
|
||||
}
|
||||
|
||||
if (updateFields.length === 0) {
|
||||
return c.json({ success: false, error: 'No valid fields to update' }, 400);
|
||||
}
|
||||
|
||||
updateFields.push(`updated_at = NOW()`);
|
||||
updateFields.push(`auto_mapped = false`); // Mark as manually managed
|
||||
values.push(mappingId);
|
||||
|
||||
const query = `
|
||||
UPDATE provider_exchange_mappings
|
||||
SET ${updateFields.join(', ')}
|
||||
WHERE id = $${paramIndex}
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const result = await postgresClient.query(query, values);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return c.json({ success: false, error: 'Provider mapping not found' }, 404);
|
||||
}
|
||||
|
||||
logger.info('Provider mapping updated', { mappingId, updates: body });
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
data: result.rows[0],
|
||||
message: 'Provider mapping updated successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to update provider mapping', { error });
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Create new provider mapping
|
||||
exchangeRoutes.post('/provider-mappings', async c => {
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
|
||||
const {
|
||||
provider,
|
||||
provider_exchange_code,
|
||||
provider_exchange_name,
|
||||
master_exchange_id,
|
||||
country_code,
|
||||
currency,
|
||||
confidence = 1.0,
|
||||
active = false,
|
||||
verified = false,
|
||||
} = body;
|
||||
|
||||
if (!provider || !provider_exchange_code || !master_exchange_id) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Missing required fields: provider, provider_exchange_code, master_exchange_id',
|
||||
},
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
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 postgresClient.query(query, [
|
||||
provider,
|
||||
provider_exchange_code,
|
||||
provider_exchange_name,
|
||||
master_exchange_id,
|
||||
country_code,
|
||||
currency,
|
||||
confidence,
|
||||
active,
|
||||
verified,
|
||||
]);
|
||||
|
||||
logger.info('Provider mapping created', {
|
||||
provider,
|
||||
provider_exchange_code,
|
||||
master_exchange_id,
|
||||
});
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: true,
|
||||
data: result.rows[0],
|
||||
message: 'Provider mapping created successfully',
|
||||
},
|
||||
201
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('Failed to create provider mapping', { error });
|
||||
|
||||
// Handle unique constraint violations
|
||||
if (error instanceof Error && error.message.includes('duplicate key')) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Provider mapping already exists for this provider and exchange code',
|
||||
},
|
||||
409
|
||||
);
|
||||
}
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Get all available providers
|
||||
exchangeRoutes.get('/providers/list', async c => {
|
||||
try {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
|
||||
const query = `
|
||||
SELECT DISTINCT provider
|
||||
FROM provider_exchange_mappings
|
||||
ORDER BY provider
|
||||
`;
|
||||
|
||||
const result = await postgresClient.query(query);
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
data: result.rows.map(row => row.provider),
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get providers list', { error });
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Get all provider exchanges from MongoDB with mapping status
|
||||
exchangeRoutes.get('/provider-exchanges/all', async c => {
|
||||
try {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
|
||||
const mongoClient = getMongoDBClient();
|
||||
const db = mongoClient.getDatabase();
|
||||
|
||||
// Get all provider exchanges from different MongoDB collections
|
||||
const [eodExchanges, ibExchanges, qmExchanges, unifiedExchanges] = await Promise.all([
|
||||
db.collection('eodExchanges').find({ active: true }).toArray(),
|
||||
db.collection('ibExchanges').find({}).toArray(),
|
||||
db.collection('qmExchanges').find({}).toArray(),
|
||||
db.collection('exchanges').find({}).toArray(),
|
||||
]);
|
||||
|
||||
// Get existing mappings to mark which are already mapped
|
||||
const existingMappingsQuery = `
|
||||
SELECT provider, provider_exchange_code, master_exchange_id, e.code as master_exchange_code
|
||||
FROM provider_exchange_mappings pem
|
||||
JOIN exchanges e ON pem.master_exchange_id = e.id
|
||||
`;
|
||||
const existingMappings = await postgresClient.query(existingMappingsQuery);
|
||||
const mappingLookup = new Map();
|
||||
existingMappings.rows.forEach(row => {
|
||||
mappingLookup.set(`${row.provider}:${row.provider_exchange_code}`, {
|
||||
mapped: true,
|
||||
master_exchange_id: row.master_exchange_id,
|
||||
master_exchange_code: row.master_exchange_code,
|
||||
});
|
||||
});
|
||||
|
||||
// Combine all provider exchanges
|
||||
const allProviderExchanges = [];
|
||||
|
||||
// EOD exchanges
|
||||
eodExchanges.forEach(exchange => {
|
||||
const key = `eod:${exchange.Code}`;
|
||||
const mapping = mappingLookup.get(key);
|
||||
allProviderExchanges.push({
|
||||
provider: 'eod',
|
||||
provider_exchange_code: exchange.Code,
|
||||
provider_exchange_name: exchange.Name,
|
||||
country_code: exchange.CountryISO2,
|
||||
currency: exchange.Currency,
|
||||
symbol_count: null,
|
||||
is_mapped: !!mapping,
|
||||
mapped_to_exchange_id: mapping?.master_exchange_id || null,
|
||||
mapped_to_exchange_code: mapping?.master_exchange_code || null,
|
||||
});
|
||||
});
|
||||
|
||||
// IB exchanges
|
||||
ibExchanges.forEach(exchange => {
|
||||
const key = `ib:${exchange.exchange_id}`;
|
||||
const mapping = mappingLookup.get(key);
|
||||
allProviderExchanges.push({
|
||||
provider: 'ib',
|
||||
provider_exchange_code: exchange.exchange_id,
|
||||
provider_exchange_name: exchange.name,
|
||||
country_code: exchange.country_code,
|
||||
currency: null,
|
||||
symbol_count: null,
|
||||
is_mapped: !!mapping,
|
||||
mapped_to_exchange_id: mapping?.master_exchange_id || null,
|
||||
mapped_to_exchange_code: mapping?.master_exchange_code || null,
|
||||
});
|
||||
});
|
||||
|
||||
// QM exchanges
|
||||
qmExchanges.forEach(exchange => {
|
||||
const key = `qm:${exchange.exchangeCode}`;
|
||||
const mapping = mappingLookup.get(key);
|
||||
allProviderExchanges.push({
|
||||
provider: 'qm',
|
||||
provider_exchange_code: exchange.exchangeCode,
|
||||
provider_exchange_name: exchange.name,
|
||||
country_code: exchange.countryCode,
|
||||
currency: exchange.countryCode === 'CA' ? 'CAD' : 'USD',
|
||||
symbol_count: null,
|
||||
is_mapped: !!mapping,
|
||||
mapped_to_exchange_id: mapping?.master_exchange_id || null,
|
||||
mapped_to_exchange_code: mapping?.master_exchange_code || null,
|
||||
});
|
||||
});
|
||||
|
||||
// Unified exchanges
|
||||
unifiedExchanges.forEach(exchange => {
|
||||
const key = `unified:${exchange.sourceCode || exchange.code}`;
|
||||
const mapping = mappingLookup.get(key);
|
||||
allProviderExchanges.push({
|
||||
provider: 'unified',
|
||||
provider_exchange_code: exchange.sourceCode || exchange.code,
|
||||
provider_exchange_name: exchange.sourceName || exchange.name,
|
||||
country_code: null,
|
||||
currency: null,
|
||||
symbol_count: null,
|
||||
is_mapped: !!mapping,
|
||||
mapped_to_exchange_id: mapping?.master_exchange_id || null,
|
||||
mapped_to_exchange_code: mapping?.master_exchange_code || null,
|
||||
});
|
||||
});
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
data: allProviderExchanges,
|
||||
total: allProviderExchanges.length,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get provider exchanges', { error });
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Get unmapped provider exchanges by provider
|
||||
exchangeRoutes.get('/provider-exchanges/unmapped/:provider', async c => {
|
||||
try {
|
||||
const provider = c.req.param('provider');
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
|
||||
const mongoClient = getMongoDBClient();
|
||||
const db = mongoClient.getDatabase();
|
||||
|
||||
// Get existing mappings for this provider
|
||||
const existingMappingsQuery = `
|
||||
SELECT provider_exchange_code
|
||||
FROM provider_exchange_mappings
|
||||
WHERE provider = $1
|
||||
`;
|
||||
const existingMappings = await postgresClient.query(existingMappingsQuery, [provider]);
|
||||
const mappedCodes = new Set(existingMappings.rows.map(row => row.provider_exchange_code));
|
||||
|
||||
let providerExchanges = [];
|
||||
|
||||
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;
|
||||
|
||||
case 'unified':
|
||||
const unifiedExchanges = await db.collection('exchanges').find({}).toArray();
|
||||
providerExchanges = unifiedExchanges
|
||||
.filter(exchange => !mappedCodes.has(exchange.sourceCode || exchange.code))
|
||||
.map(exchange => ({
|
||||
provider_exchange_code: exchange.sourceCode || exchange.code,
|
||||
provider_exchange_name: exchange.sourceName || exchange.name,
|
||||
country_code: null,
|
||||
currency: null,
|
||||
symbol_count: null,
|
||||
}));
|
||||
break;
|
||||
|
||||
default:
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: `Unknown provider: ${provider}`,
|
||||
},
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
data: providerExchanges,
|
||||
total: providerExchanges.length,
|
||||
provider,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get unmapped provider exchanges', { error });
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Get exchange statistics
|
||||
exchangeRoutes.get('/stats/summary', async c => {
|
||||
try {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM exchanges) as total_exchanges,
|
||||
(SELECT COUNT(*) FROM exchanges WHERE active = true) as active_exchanges,
|
||||
(SELECT COUNT(DISTINCT country) FROM exchanges) as countries,
|
||||
(SELECT COUNT(DISTINCT currency) FROM exchanges) as currencies,
|
||||
(SELECT COUNT(*) FROM provider_exchange_mappings) as total_provider_mappings,
|
||||
(SELECT COUNT(*) FROM provider_exchange_mappings WHERE active = true) as active_provider_mappings,
|
||||
(SELECT COUNT(*) FROM provider_exchange_mappings WHERE verified = true) as verified_provider_mappings,
|
||||
(SELECT COUNT(DISTINCT provider) FROM provider_exchange_mappings) as providers
|
||||
`;
|
||||
|
||||
const result = await postgresClient.query(query);
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
data: result.rows[0],
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get exchange statistics', { error });
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
69
apps/web-api/src/routes/health.routes.ts
Normal file
69
apps/web-api/src/routes/health.routes.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
/**
|
||||
* Health check routes
|
||||
*/
|
||||
import { Hono } from 'hono';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import { getMongoDBClient } from '@stock-bot/mongodb-client';
|
||||
import { getPostgreSQLClient } from '@stock-bot/postgres-client';
|
||||
|
||||
const logger = getLogger('health-routes');
|
||||
export const healthRoutes = new Hono();
|
||||
|
||||
// Basic health check
|
||||
healthRoutes.get('/', c => {
|
||||
return c.json({
|
||||
status: 'healthy',
|
||||
service: 'web-api',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
// Detailed health check with database connectivity
|
||||
healthRoutes.get('/detailed', async c => {
|
||||
const health = {
|
||||
status: 'healthy',
|
||||
service: 'web-api',
|
||||
timestamp: new Date().toISOString(),
|
||||
checks: {
|
||||
mongodb: { status: 'unknown', message: '' },
|
||||
postgresql: { status: 'unknown', message: '' },
|
||||
},
|
||||
};
|
||||
|
||||
// Check MongoDB
|
||||
try {
|
||||
const mongoClient = getMongoDBClient();
|
||||
if (mongoClient.connected) {
|
||||
// Try a simple operation
|
||||
const db = mongoClient.getDatabase();
|
||||
await db.admin().ping();
|
||||
health.checks.mongodb = { status: 'healthy', message: 'Connected and responsive' };
|
||||
} else {
|
||||
health.checks.mongodb = { status: 'unhealthy', message: 'Not connected' };
|
||||
}
|
||||
} catch (error) {
|
||||
health.checks.mongodb = {
|
||||
status: 'unhealthy',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
}
|
||||
|
||||
// Check PostgreSQL
|
||||
try {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
await postgresClient.query('SELECT 1');
|
||||
health.checks.postgresql = { status: 'healthy', message: 'Connected and responsive' };
|
||||
} catch (error) {
|
||||
health.checks.postgresql = {
|
||||
status: 'unhealthy',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
}
|
||||
|
||||
// Overall status
|
||||
const allHealthy = Object.values(health.checks).every(check => check.status === 'healthy');
|
||||
health.status = allHealthy ? 'healthy' : 'unhealthy';
|
||||
|
||||
const statusCode = allHealthy ? 200 : 503;
|
||||
return c.json(health, statusCode);
|
||||
});
|
||||
5
apps/web-api/src/routes/index.ts
Normal file
5
apps/web-api/src/routes/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
/**
|
||||
* Routes index - exports all route modules
|
||||
*/
|
||||
export { exchangeRoutes } from './exchange.routes';
|
||||
export { healthRoutes } from './health.routes';
|
||||
Loading…
Add table
Add a link
Reference in a new issue