added initial py analytics / rust core / ts orchestrator services
This commit is contained in:
parent
680b5fd2ae
commit
c862ed496b
62 changed files with 13459 additions and 0 deletions
180
apps/stock/orchestrator/src/api/rest/analytics.ts
Normal file
180
apps/stock/orchestrator/src/api/rest/analytics.ts
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
import { Hono } from 'hono';
|
||||
import { z } from 'zod';
|
||||
import { logger } from '@stock-bot/logger';
|
||||
import { AnalyticsService } from '../../services/AnalyticsService';
|
||||
import { container } from '../../container';
|
||||
|
||||
const DateRangeSchema = z.object({
|
||||
startDate: z.string().datetime(),
|
||||
endDate: z.string().datetime()
|
||||
});
|
||||
|
||||
const OptimizationRequestSchema = z.object({
|
||||
symbols: z.array(z.string()),
|
||||
returns: z.array(z.array(z.number())),
|
||||
constraints: z.object({
|
||||
minWeight: z.number().optional(),
|
||||
maxWeight: z.number().optional(),
|
||||
targetReturn: z.number().optional(),
|
||||
maxRisk: z.number().optional()
|
||||
}).optional()
|
||||
});
|
||||
|
||||
export function createAnalyticsRoutes(): Hono {
|
||||
const app = new Hono();
|
||||
const analyticsService = container.get('AnalyticsService') as AnalyticsService;
|
||||
|
||||
// Get performance metrics
|
||||
app.get('/performance/:portfolioId', async (c) => {
|
||||
try {
|
||||
const portfolioId = c.req.param('portfolioId');
|
||||
const query = c.req.query();
|
||||
|
||||
const { startDate, endDate } = DateRangeSchema.parse({
|
||||
startDate: query.start_date,
|
||||
endDate: query.end_date
|
||||
});
|
||||
|
||||
const metrics = await analyticsService.getPerformanceMetrics(
|
||||
portfolioId,
|
||||
new Date(startDate),
|
||||
new Date(endDate)
|
||||
);
|
||||
|
||||
return c.json(metrics);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return c.json({
|
||||
error: 'Invalid date range',
|
||||
details: error.errors
|
||||
}, 400);
|
||||
}
|
||||
|
||||
logger.error('Error getting performance metrics:', error);
|
||||
return c.json({
|
||||
error: error instanceof Error ? error.message : 'Failed to get performance metrics'
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Portfolio optimization
|
||||
app.post('/optimize', async (c) => {
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
const request = OptimizationRequestSchema.parse(body);
|
||||
|
||||
const result = await analyticsService.optimizePortfolio({
|
||||
returns: request.returns,
|
||||
constraints: request.constraints
|
||||
});
|
||||
|
||||
return c.json(result);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return c.json({
|
||||
error: 'Invalid optimization request',
|
||||
details: error.errors
|
||||
}, 400);
|
||||
}
|
||||
|
||||
logger.error('Error optimizing portfolio:', error);
|
||||
return c.json({
|
||||
error: error instanceof Error ? error.message : 'Failed to optimize portfolio'
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Get risk metrics
|
||||
app.get('/risk/:portfolioId', async (c) => {
|
||||
try {
|
||||
const portfolioId = c.req.param('portfolioId');
|
||||
const metrics = await analyticsService.getRiskMetrics(portfolioId);
|
||||
|
||||
return c.json(metrics);
|
||||
} catch (error) {
|
||||
logger.error('Error getting risk metrics:', error);
|
||||
return c.json({
|
||||
error: error instanceof Error ? error.message : 'Failed to get risk metrics'
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Market regime detection
|
||||
app.get('/regime', async (c) => {
|
||||
try {
|
||||
const regime = await analyticsService.detectMarketRegime();
|
||||
|
||||
return c.json({
|
||||
regime,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error detecting market regime:', error);
|
||||
return c.json({
|
||||
error: error instanceof Error ? error.message : 'Failed to detect market regime'
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate correlation matrix
|
||||
app.post('/correlation', async (c) => {
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
const { symbols } = z.object({
|
||||
symbols: z.array(z.string()).min(2)
|
||||
}).parse(body);
|
||||
|
||||
const matrix = await analyticsService.calculateCorrelationMatrix(symbols);
|
||||
|
||||
return c.json({
|
||||
symbols,
|
||||
matrix
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return c.json({
|
||||
error: 'Invalid correlation request',
|
||||
details: error.errors
|
||||
}, 400);
|
||||
}
|
||||
|
||||
logger.error('Error calculating correlation:', error);
|
||||
return c.json({
|
||||
error: error instanceof Error ? error.message : 'Failed to calculate correlation'
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// ML model prediction
|
||||
app.post('/predict', async (c) => {
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
const { modelId, features } = z.object({
|
||||
modelId: z.string(),
|
||||
features: z.record(z.number())
|
||||
}).parse(body);
|
||||
|
||||
const prediction = await analyticsService.predictWithModel(modelId, features);
|
||||
|
||||
if (prediction) {
|
||||
return c.json(prediction);
|
||||
} else {
|
||||
return c.json({ error: 'Model not found or prediction failed' }, 404);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return c.json({
|
||||
error: 'Invalid prediction request',
|
||||
details: error.errors
|
||||
}, 400);
|
||||
}
|
||||
|
||||
logger.error('Error making prediction:', error);
|
||||
return c.json({
|
||||
error: error instanceof Error ? error.message : 'Failed to make prediction'
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
162
apps/stock/orchestrator/src/api/rest/backtest.ts
Normal file
162
apps/stock/orchestrator/src/api/rest/backtest.ts
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
import { Hono } from 'hono';
|
||||
import { z } from 'zod';
|
||||
import { logger } from '@stock-bot/logger';
|
||||
import { BacktestConfigSchema } from '../../types';
|
||||
import { BacktestEngine } from '../../backtest/BacktestEngine';
|
||||
import { ModeManager } from '../../core/ModeManager';
|
||||
import { container } from '../../container';
|
||||
|
||||
const BacktestIdSchema = z.object({
|
||||
backtestId: z.string()
|
||||
});
|
||||
|
||||
export function createBacktestRoutes(): Hono {
|
||||
const app = new Hono();
|
||||
const backtestEngine = container.get('BacktestEngine') as BacktestEngine;
|
||||
const modeManager = container.get('ModeManager') as ModeManager;
|
||||
|
||||
// Run new backtest
|
||||
app.post('/run', async (c) => {
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
const config = BacktestConfigSchema.parse(body);
|
||||
|
||||
// Initialize backtest mode
|
||||
await modeManager.initializeMode(config);
|
||||
|
||||
// Run backtest
|
||||
const result = await backtestEngine.runBacktest(config);
|
||||
|
||||
return c.json(result, 201);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return c.json({
|
||||
error: 'Invalid backtest configuration',
|
||||
details: error.errors
|
||||
}, 400);
|
||||
}
|
||||
|
||||
logger.error('Error running backtest:', error);
|
||||
return c.json({
|
||||
error: error instanceof Error ? error.message : 'Failed to run backtest'
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Stop running backtest
|
||||
app.post('/stop', async (c) => {
|
||||
try {
|
||||
await backtestEngine.stopBacktest();
|
||||
|
||||
return c.json({
|
||||
message: 'Backtest stop requested',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error stopping backtest:', error);
|
||||
return c.json({
|
||||
error: error instanceof Error ? error.message : 'Failed to stop backtest'
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Get backtest progress
|
||||
app.get('/progress', async (c) => {
|
||||
try {
|
||||
// In real implementation, would track progress
|
||||
return c.json({
|
||||
status: 'running',
|
||||
progress: 0.5,
|
||||
processed: 10000,
|
||||
total: 20000,
|
||||
currentTime: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error getting backtest progress:', error);
|
||||
return c.json({
|
||||
error: error instanceof Error ? error.message : 'Failed to get progress'
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Stream backtest events (Server-Sent Events)
|
||||
app.get('/stream', async (c) => {
|
||||
c.header('Content-Type', 'text/event-stream');
|
||||
c.header('Cache-Control', 'no-cache');
|
||||
c.header('Connection', 'keep-alive');
|
||||
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
// Listen for backtest events
|
||||
const onProgress = (data: any) => {
|
||||
controller.enqueue(`data: ${JSON.stringify(data)}\n\n`);
|
||||
};
|
||||
|
||||
const onComplete = (data: any) => {
|
||||
controller.enqueue(`data: ${JSON.stringify({ event: 'complete', data })}\n\n`);
|
||||
controller.close();
|
||||
};
|
||||
|
||||
backtestEngine.on('progress', onProgress);
|
||||
backtestEngine.on('complete', onComplete);
|
||||
|
||||
// Cleanup on close
|
||||
c.req.raw.signal.addEventListener('abort', () => {
|
||||
backtestEngine.off('progress', onProgress);
|
||||
backtestEngine.off('complete', onComplete);
|
||||
controller.close();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return new Response(stream);
|
||||
});
|
||||
|
||||
// Validate backtest configuration
|
||||
app.post('/validate', async (c) => {
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
const config = BacktestConfigSchema.parse(body);
|
||||
|
||||
// Additional validation logic
|
||||
const validation = {
|
||||
valid: true,
|
||||
warnings: [] as string[],
|
||||
estimatedDuration: 0
|
||||
};
|
||||
|
||||
// Check data availability
|
||||
const startDate = new Date(config.startDate);
|
||||
const endDate = new Date(config.endDate);
|
||||
const days = (endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24);
|
||||
|
||||
if (days > 365) {
|
||||
validation.warnings.push('Large date range may take significant time to process');
|
||||
}
|
||||
|
||||
if (config.symbols.length > 100) {
|
||||
validation.warnings.push('Large number of symbols may impact performance');
|
||||
}
|
||||
|
||||
// Estimate duration (simplified)
|
||||
validation.estimatedDuration = days * config.symbols.length * 0.1; // seconds
|
||||
|
||||
return c.json(validation);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return c.json({
|
||||
valid: false,
|
||||
error: 'Invalid configuration',
|
||||
details: error.errors
|
||||
}, 400);
|
||||
}
|
||||
|
||||
return c.json({
|
||||
valid: false,
|
||||
error: error instanceof Error ? error.message : 'Validation failed'
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
112
apps/stock/orchestrator/src/api/rest/orders.ts
Normal file
112
apps/stock/orchestrator/src/api/rest/orders.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import { Hono } from 'hono';
|
||||
import { z } from 'zod';
|
||||
import { logger } from '@stock-bot/logger';
|
||||
import { OrderRequestSchema } from '../../types';
|
||||
import { ExecutionService } from '../../services/ExecutionService';
|
||||
import { container } from '../../container';
|
||||
|
||||
const OrderIdSchema = z.object({
|
||||
orderId: z.string()
|
||||
});
|
||||
|
||||
export function createOrderRoutes(): Hono {
|
||||
const app = new Hono();
|
||||
const executionService = container.get('ExecutionService') as ExecutionService;
|
||||
|
||||
// Submit new order
|
||||
app.post('/', async (c) => {
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
const orderRequest = OrderRequestSchema.parse(body);
|
||||
|
||||
const result = await executionService.submitOrder(orderRequest);
|
||||
|
||||
return c.json(result, 201);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return c.json({
|
||||
error: 'Invalid order request',
|
||||
details: error.errors
|
||||
}, 400);
|
||||
}
|
||||
|
||||
logger.error('Error submitting order:', error);
|
||||
return c.json({
|
||||
error: error instanceof Error ? error.message : 'Failed to submit order'
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Cancel order
|
||||
app.delete('/:orderId', async (c) => {
|
||||
try {
|
||||
const { orderId } = OrderIdSchema.parse(c.req.param());
|
||||
|
||||
const success = await executionService.cancelOrder(orderId);
|
||||
|
||||
if (success) {
|
||||
return c.json({ message: 'Order cancelled successfully' });
|
||||
} else {
|
||||
return c.json({ error: 'Order not found or already filled' }, 404);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error cancelling order:', error);
|
||||
return c.json({
|
||||
error: error instanceof Error ? error.message : 'Failed to cancel order'
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Get order status
|
||||
app.get('/:orderId', async (c) => {
|
||||
try {
|
||||
const { orderId } = OrderIdSchema.parse(c.req.param());
|
||||
|
||||
const status = await executionService.getOrderStatus(orderId);
|
||||
|
||||
if (status) {
|
||||
return c.json(status);
|
||||
} else {
|
||||
return c.json({ error: 'Order not found' }, 404);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error getting order status:', error);
|
||||
return c.json({
|
||||
error: error instanceof Error ? error.message : 'Failed to get order status'
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Batch order submission
|
||||
app.post('/batch', async (c) => {
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
const orders = z.array(OrderRequestSchema).parse(body);
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
orders.map(order => executionService.submitOrder(order))
|
||||
);
|
||||
|
||||
const response = results.map((result, index) => ({
|
||||
order: orders[index],
|
||||
result: result.status === 'fulfilled' ? result.value : { error: result.reason }
|
||||
}));
|
||||
|
||||
return c.json(response, 201);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return c.json({
|
||||
error: 'Invalid batch order request',
|
||||
details: error.errors
|
||||
}, 400);
|
||||
}
|
||||
|
||||
logger.error('Error submitting batch orders:', error);
|
||||
return c.json({
|
||||
error: error instanceof Error ? error.message : 'Failed to submit batch orders'
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
122
apps/stock/orchestrator/src/api/rest/positions.ts
Normal file
122
apps/stock/orchestrator/src/api/rest/positions.ts
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import { Hono } from 'hono';
|
||||
import { z } from 'zod';
|
||||
import { logger } from '@stock-bot/logger';
|
||||
import { ModeManager } from '../../core/ModeManager';
|
||||
import { container } from '../../container';
|
||||
|
||||
const SymbolSchema = z.object({
|
||||
symbol: z.string()
|
||||
});
|
||||
|
||||
export function createPositionRoutes(): Hono {
|
||||
const app = new Hono();
|
||||
const modeManager = container.get('ModeManager') as ModeManager;
|
||||
|
||||
// Get all positions
|
||||
app.get('/', async (c) => {
|
||||
try {
|
||||
const tradingEngine = modeManager.getTradingEngine();
|
||||
const positions = JSON.parse(tradingEngine.getAllPositions());
|
||||
|
||||
return c.json({
|
||||
mode: modeManager.getCurrentMode(),
|
||||
positions
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error getting positions:', error);
|
||||
return c.json({
|
||||
error: error instanceof Error ? error.message : 'Failed to get positions'
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Get open positions only
|
||||
app.get('/open', async (c) => {
|
||||
try {
|
||||
const tradingEngine = modeManager.getTradingEngine();
|
||||
const positions = JSON.parse(tradingEngine.getOpenPositions());
|
||||
|
||||
return c.json({
|
||||
mode: modeManager.getCurrentMode(),
|
||||
positions
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error getting open positions:', error);
|
||||
return c.json({
|
||||
error: error instanceof Error ? error.message : 'Failed to get open positions'
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Get position for specific symbol
|
||||
app.get('/:symbol', async (c) => {
|
||||
try {
|
||||
const { symbol } = SymbolSchema.parse(c.req.param());
|
||||
const tradingEngine = modeManager.getTradingEngine();
|
||||
|
||||
const positionJson = tradingEngine.getPosition(symbol);
|
||||
const position = positionJson ? JSON.parse(positionJson) : null;
|
||||
|
||||
if (position) {
|
||||
return c.json({
|
||||
mode: modeManager.getCurrentMode(),
|
||||
position
|
||||
});
|
||||
} else {
|
||||
return c.json({
|
||||
error: 'Position not found',
|
||||
symbol
|
||||
}, 404);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error getting position:', error);
|
||||
return c.json({
|
||||
error: error instanceof Error ? error.message : 'Failed to get position'
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Get P&L summary
|
||||
app.get('/pnl/summary', async (c) => {
|
||||
try {
|
||||
const tradingEngine = modeManager.getTradingEngine();
|
||||
const [realizedPnl, unrealizedPnl] = tradingEngine.getTotalPnl();
|
||||
|
||||
return c.json({
|
||||
mode: modeManager.getCurrentMode(),
|
||||
pnl: {
|
||||
realized: realizedPnl,
|
||||
unrealized: unrealizedPnl,
|
||||
total: realizedPnl + unrealizedPnl
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error getting P&L:', error);
|
||||
return c.json({
|
||||
error: error instanceof Error ? error.message : 'Failed to get P&L'
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Get risk metrics
|
||||
app.get('/risk/metrics', async (c) => {
|
||||
try {
|
||||
const tradingEngine = modeManager.getTradingEngine();
|
||||
const metrics = JSON.parse(tradingEngine.getRiskMetrics());
|
||||
|
||||
return c.json({
|
||||
mode: modeManager.getCurrentMode(),
|
||||
risk: metrics,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error getting risk metrics:', error);
|
||||
return c.json({
|
||||
error: error instanceof Error ? error.message : 'Failed to get risk metrics'
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
195
apps/stock/orchestrator/src/api/websocket/index.ts
Normal file
195
apps/stock/orchestrator/src/api/websocket/index.ts
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
import { Server as SocketIOServer, Socket } from 'socket.io';
|
||||
import { logger } from '@stock-bot/logger';
|
||||
import { z } from 'zod';
|
||||
import { MarketDataService } from '../../services/MarketDataService';
|
||||
import { ExecutionService } from '../../services/ExecutionService';
|
||||
import { ModeManager } from '../../core/ModeManager';
|
||||
import { Container } from '@stock-bot/di';
|
||||
|
||||
const SubscribeSchema = z.object({
|
||||
symbols: z.array(z.string()),
|
||||
dataTypes: z.array(z.enum(['quote', 'trade', 'bar'])).optional()
|
||||
});
|
||||
|
||||
const UnsubscribeSchema = z.object({
|
||||
symbols: z.array(z.string())
|
||||
});
|
||||
|
||||
export function setupWebSocketHandlers(io: SocketIOServer, container: Container): void {
|
||||
const marketDataService = container.get('MarketDataService') as MarketDataService;
|
||||
const executionService = container.get('ExecutionService') as ExecutionService;
|
||||
const modeManager = container.get('ModeManager') as ModeManager;
|
||||
|
||||
// Track client subscriptions
|
||||
const clientSubscriptions = new Map<string, Set<string>>();
|
||||
|
||||
io.on('connection', (socket: Socket) => {
|
||||
logger.info(`WebSocket client connected: ${socket.id}`);
|
||||
clientSubscriptions.set(socket.id, new Set());
|
||||
|
||||
// Send initial connection info
|
||||
socket.emit('connected', {
|
||||
mode: modeManager.getCurrentMode(),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Handle market data subscriptions
|
||||
socket.on('subscribe', async (data: any, callback?: Function) => {
|
||||
try {
|
||||
const { symbols, dataTypes } = SubscribeSchema.parse(data);
|
||||
const subscriptions = clientSubscriptions.get(socket.id)!;
|
||||
|
||||
for (const symbol of symbols) {
|
||||
await marketDataService.subscribeToSymbol(symbol);
|
||||
subscriptions.add(symbol);
|
||||
}
|
||||
|
||||
logger.debug(`Client ${socket.id} subscribed to: ${symbols.join(', ')}`);
|
||||
|
||||
if (callback) {
|
||||
callback({ success: true, symbols });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Subscription error:', error);
|
||||
if (callback) {
|
||||
callback({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Subscription failed'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle unsubscribe
|
||||
socket.on('unsubscribe', async (data: any, callback?: Function) => {
|
||||
try {
|
||||
const { symbols } = UnsubscribeSchema.parse(data);
|
||||
const subscriptions = clientSubscriptions.get(socket.id)!;
|
||||
|
||||
for (const symbol of symbols) {
|
||||
subscriptions.delete(symbol);
|
||||
|
||||
// Check if any other clients are subscribed
|
||||
let othersSubscribed = false;
|
||||
for (const [clientId, subs] of clientSubscriptions) {
|
||||
if (clientId !== socket.id && subs.has(symbol)) {
|
||||
othersSubscribed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!othersSubscribed) {
|
||||
await marketDataService.unsubscribeFromSymbol(symbol);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(`Client ${socket.id} unsubscribed from: ${symbols.join(', ')}`);
|
||||
|
||||
if (callback) {
|
||||
callback({ success: true, symbols });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Unsubscribe error:', error);
|
||||
if (callback) {
|
||||
callback({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unsubscribe failed'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle order submission via WebSocket
|
||||
socket.on('submitOrder', async (order: any, callback?: Function) => {
|
||||
try {
|
||||
const result = await executionService.submitOrder(order);
|
||||
if (callback) {
|
||||
callback({ success: true, result });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Order submission error:', error);
|
||||
if (callback) {
|
||||
callback({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Order submission failed'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle position queries
|
||||
socket.on('getPositions', async (callback?: Function) => {
|
||||
try {
|
||||
const tradingEngine = modeManager.getTradingEngine();
|
||||
const positions = JSON.parse(tradingEngine.getAllPositions());
|
||||
|
||||
if (callback) {
|
||||
callback({ success: true, positions });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error getting positions:', error);
|
||||
if (callback) {
|
||||
callback({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to get positions'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle disconnection
|
||||
socket.on('disconnect', async () => {
|
||||
logger.info(`WebSocket client disconnected: ${socket.id}`);
|
||||
|
||||
// Unsubscribe from all symbols for this client
|
||||
const subscriptions = clientSubscriptions.get(socket.id);
|
||||
if (subscriptions) {
|
||||
for (const symbol of subscriptions) {
|
||||
// Check if any other clients are subscribed
|
||||
let othersSubscribed = false;
|
||||
for (const [clientId, subs] of clientSubscriptions) {
|
||||
if (clientId !== socket.id && subs.has(symbol)) {
|
||||
othersSubscribed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!othersSubscribed) {
|
||||
await marketDataService.unsubscribeFromSymbol(symbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clientSubscriptions.delete(socket.id);
|
||||
});
|
||||
});
|
||||
|
||||
// Forward market data to subscribed clients
|
||||
marketDataService.on('marketData', (data: any) => {
|
||||
for (const [clientId, subscriptions] of clientSubscriptions) {
|
||||
if (subscriptions.has(data.data.symbol)) {
|
||||
io.to(clientId).emit('marketData', data);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Forward order updates to all clients
|
||||
executionService.on('orderUpdate', (update: any) => {
|
||||
io.emit('orderUpdate', update);
|
||||
});
|
||||
|
||||
// Forward fills to all clients
|
||||
executionService.on('fill', (fill: any) => {
|
||||
io.emit('fill', fill);
|
||||
});
|
||||
|
||||
// Mode change notifications
|
||||
modeManager.on('modeChanged', (config: any) => {
|
||||
io.emit('modeChanged', {
|
||||
mode: config.mode,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
logger.info('WebSocket handlers initialized');
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue