From 90168ba619f22816c2111a46d57f52031722f4dc Mon Sep 17 00:00:00 2001 From: Bojan Kucera Date: Mon, 2 Jun 2025 20:31:22 -0400 Subject: [PATCH] improved dashboard --- apps/core-services/risk-guardian/src/index.ts | 245 +++++++++++ .../strategy-orchestrator/src/index.ts | 409 ++++++++++++++++++ .../trading-dashboard/src/app/app.html | 63 ++- .../components/sidebar/sidebar.component.css | 18 + .../components/sidebar/sidebar.component.html | 9 +- .../components/sidebar/sidebar.component.ts | 20 +- .../market-data/market-data.component.html | 5 +- .../market-data/market-data.component.ts | 59 +-- 8 files changed, 781 insertions(+), 47 deletions(-) create mode 100644 apps/core-services/risk-guardian/src/index.ts create mode 100644 apps/intelligence-services/strategy-orchestrator/src/index.ts diff --git a/apps/core-services/risk-guardian/src/index.ts b/apps/core-services/risk-guardian/src/index.ts new file mode 100644 index 0000000..7f64d1c --- /dev/null +++ b/apps/core-services/risk-guardian/src/index.ts @@ -0,0 +1,245 @@ +import { Hono } from 'hono'; +import { WebSocketServer } from 'ws'; +import Redis from 'ioredis'; + +const app = new Hono(); +const redis = new Redis({ + host: process.env.REDIS_HOST || 'localhost', + port: parseInt(process.env.REDIS_PORT || '6379'), + enableReadyCheck: false, + maxRetriesPerRequest: null, +}); + +// WebSocket server for real-time risk alerts +const wss = new WebSocketServer({ port: 8081 }); + +// Risk thresholds configuration +interface RiskThresholds { + maxPositionSize: number; + maxDailyLoss: number; + maxPortfolioRisk: number; + volatilityLimit: number; +} + +const defaultThresholds: RiskThresholds = { + maxPositionSize: 100000, // $100k max position + maxDailyLoss: 10000, // $10k max daily loss + maxPortfolioRisk: 0.02, // 2% portfolio risk + volatilityLimit: 0.3 // 30% volatility limit +}; + +// Health check endpoint +app.get('/health', (c) => { + return c.json({ + service: 'risk-guardian', + status: 'healthy', + timestamp: new Date(), + version: '1.0.0', + connections: wss.clients.size + }); +}); + +// Get risk thresholds +app.get('/api/risk/thresholds', async (c) => { + try { + const thresholds = await redis.hgetall('risk:thresholds'); + const parsedThresholds = Object.keys(thresholds).length > 0 + ? Object.fromEntries( + Object.entries(thresholds).map(([k, v]) => [k, parseFloat(v as string)]) + ) + : defaultThresholds; + + return c.json({ + success: true, + data: parsedThresholds + }); + } catch (error) { + console.error('Error fetching risk thresholds:', error); + return c.json({ success: false, error: 'Failed to fetch thresholds' }, 500); + } +}); + +// Update risk thresholds +app.put('/api/risk/thresholds', async (c) => { + try { + const thresholds = await c.req.json(); + await redis.hmset('risk:thresholds', thresholds); + + // Broadcast threshold update to connected clients + const message = JSON.stringify({ + type: 'THRESHOLD_UPDATE', + data: thresholds, + timestamp: new Date() + }); + + wss.clients.forEach(client => { + if (client.readyState === 1) { // WebSocket.OPEN + client.send(message); + } + }); + + return c.json({ success: true, data: thresholds }); + } catch (error) { + console.error('Error updating risk thresholds:', error); + return c.json({ success: false, error: 'Failed to update thresholds' }, 500); + } +}); + +// Real-time risk monitoring endpoint +app.post('/api/risk/evaluate', async (c) => { + try { + const { symbol, quantity, price, portfolioValue } = await c.req.json(); + + const thresholds = await redis.hgetall('risk:thresholds'); + const activeThresholds = Object.keys(thresholds).length > 0 + ? Object.fromEntries( + Object.entries(thresholds).map(([k, v]) => [k, parseFloat(v as string)]) + ) + : defaultThresholds; + + const positionValue = quantity * price; + const positionRisk = positionValue / portfolioValue; + + const riskEvaluation = { + symbol, + positionValue, + positionRisk, + violations: [] as string[], + riskLevel: 'LOW' as 'LOW' | 'MEDIUM' | 'HIGH' + }; + + // Check risk violations + if (positionValue > activeThresholds.maxPositionSize) { + riskEvaluation.violations.push(`Position size exceeds limit: $${positionValue.toLocaleString()}`); + } + + if (positionRisk > activeThresholds.maxPortfolioRisk) { + riskEvaluation.violations.push(`Portfolio risk exceeds limit: ${(positionRisk * 100).toFixed(2)}%`); + } + + // Determine risk level + if (riskEvaluation.violations.length > 0) { + riskEvaluation.riskLevel = 'HIGH'; + } else if (positionRisk > activeThresholds.maxPortfolioRisk * 0.7) { + riskEvaluation.riskLevel = 'MEDIUM'; + } + + // Store risk evaluation + await redis.setex( + `risk:evaluation:${symbol}:${Date.now()}`, + 3600, // 1 hour TTL + JSON.stringify(riskEvaluation) + ); + + // Send real-time alert if high risk + if (riskEvaluation.riskLevel === 'HIGH') { + const alert = { + type: 'RISK_ALERT', + level: 'HIGH', + data: riskEvaluation, + timestamp: new Date() + }; + + wss.clients.forEach(client => { + if (client.readyState === 1) { + client.send(JSON.stringify(alert)); + } + }); + } + + return c.json({ success: true, data: riskEvaluation }); + } catch (error) { + console.error('Error evaluating risk:', error); + return c.json({ success: false, error: 'Failed to evaluate risk' }, 500); + } +}); + +// Get risk history +app.get('/api/risk/history', async (c) => { + try { + const keys = await redis.keys('risk:evaluation:*'); + const evaluations: any[] = []; + + for (const key of keys.slice(0, 100)) { // Limit to 100 recent evaluations + const data = await redis.get(key); + if (data) { + evaluations.push(JSON.parse(data)); + } + } + + return c.json({ + success: true, + data: evaluations.sort((a: any, b: any) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()) + }); + } catch (error) { + console.error('Error fetching risk history:', error); + return c.json({ success: false, error: 'Failed to fetch risk history' }, 500); + } +}); + +// WebSocket connection handling +wss.on('connection', (ws) => { + console.log('New risk monitoring client connected'); + + // Send welcome message + ws.send(JSON.stringify({ + type: 'CONNECTED', + message: 'Connected to Risk Guardian', + timestamp: new Date() + })); + + ws.on('close', () => { + console.log('Risk monitoring client disconnected'); + }); + + ws.on('error', (error) => { + console.error('WebSocket error:', error); + }); +}); + +// Redis event subscriptions for cross-service communication +redis.subscribe('trading:position:opened', 'trading:position:closed'); + +redis.on('message', async (channel, message) => { + try { + const data = JSON.parse(message); + + if (channel === 'trading:position:opened') { + // Auto-evaluate risk for new positions + const evaluation = await evaluatePositionRisk(data); + + // Broadcast to connected clients + wss.clients.forEach(client => { + if (client.readyState === 1) { + client.send(JSON.stringify({ + type: 'POSITION_RISK_UPDATE', + data: evaluation, + timestamp: new Date() + })); + } + }); + } + } catch (error) { + console.error('Error processing Redis message:', error); + } +}); + +async function evaluatePositionRisk(position: any) { + // Implementation would evaluate position against current thresholds + // This is a simplified version + return { + symbol: position.symbol, + riskLevel: 'LOW', + timestamp: new Date() + }; +} + +const port = parseInt(process.env.PORT || '3002'); + +console.log(`🛡️ Risk Guardian starting on port ${port}`); +console.log(`📡 WebSocket server running on port 8081`); + +export default { + port, + fetch: app.fetch, +}; diff --git a/apps/intelligence-services/strategy-orchestrator/src/index.ts b/apps/intelligence-services/strategy-orchestrator/src/index.ts new file mode 100644 index 0000000..c52f297 --- /dev/null +++ b/apps/intelligence-services/strategy-orchestrator/src/index.ts @@ -0,0 +1,409 @@ +import { Hono } from 'hono'; +import { WebSocketServer } from 'ws'; +import Redis from 'ioredis'; +import * as cron from 'node-cron'; + +const app = new Hono(); +const redis = new Redis({ + host: process.env.REDIS_HOST || 'localhost', + port: parseInt(process.env.REDIS_PORT || '6379'), + enableReadyCheck: false, + maxRetriesPerRequest: null, +}); + +// WebSocket server for real-time strategy updates +const wss = new WebSocketServer({ port: 8082 }); + +// Strategy interfaces +interface TradingStrategy { + id: string; + name: string; + description: string; + status: 'ACTIVE' | 'INACTIVE' | 'PAUSED' | 'ERROR'; + type: 'MOMENTUM' | 'MEAN_REVERSION' | 'ARBITRAGE' | 'CUSTOM'; + symbols: string[]; + parameters: Record; + performance: { + totalTrades: number; + winRate: number; + totalReturn: number; + sharpeRatio: number; + maxDrawdown: number; + }; + createdAt: Date; + updatedAt: Date; +} + +interface StrategySignal { + strategyId: string; + symbol: string; + action: 'BUY' | 'SELL' | 'HOLD'; + confidence: number; + price: number; + quantity: number; + timestamp: Date; + metadata: Record; +} + +// In-memory strategy registry (in production, this would be persisted) +const strategies = new Map(); + +// Health check endpoint +app.get('/health', (c) => { + return c.json({ + service: 'strategy-orchestrator', + status: 'healthy', + timestamp: new Date(), + version: '1.0.0', + activeStrategies: Array.from(strategies.values()).filter(s => s.status === 'ACTIVE').length, + connections: wss.clients.size + }); +}); + +// Get all strategies +app.get('/api/strategies', async (c) => { + try { + const strategiesList = Array.from(strategies.values()); + return c.json({ + success: true, + data: strategiesList + }); + } catch (error) { + console.error('Error fetching strategies:', error); + return c.json({ success: false, error: 'Failed to fetch strategies' }, 500); + } +}); + +// Get specific strategy +app.get('/api/strategies/:id', async (c) => { + try { + const id = c.req.param('id'); + const strategy = strategies.get(id); + + if (!strategy) { + return c.json({ success: false, error: 'Strategy not found' }, 404); + } + + return c.json({ success: true, data: strategy }); + } catch (error) { + console.error('Error fetching strategy:', error); + return c.json({ success: false, error: 'Failed to fetch strategy' }, 500); + } +}); + +// Create new strategy +app.post('/api/strategies', async (c) => { + try { + const strategyData = await c.req.json(); + + const strategy: TradingStrategy = { + id: `strategy_${Date.now()}`, + name: strategyData.name, + description: strategyData.description || '', + status: 'INACTIVE', + type: strategyData.type || 'CUSTOM', + symbols: strategyData.symbols || [], + parameters: strategyData.parameters || {}, + performance: { + totalTrades: 0, + winRate: 0, + totalReturn: 0, + sharpeRatio: 0, + maxDrawdown: 0 + }, + createdAt: new Date(), + updatedAt: new Date() + }; + + strategies.set(strategy.id, strategy); + + // Store in Redis for persistence + await redis.setex( + `strategy:${strategy.id}`, + 86400, // 24 hours TTL + JSON.stringify(strategy) + ); + + // Broadcast to connected clients + broadcastToClients({ + type: 'STRATEGY_CREATED', + data: strategy, + timestamp: new Date() + }); + + return c.json({ success: true, data: strategy }); + } catch (error) { + console.error('Error creating strategy:', error); + return c.json({ success: false, error: 'Failed to create strategy' }, 500); + } +}); + +// Update strategy +app.put('/api/strategies/:id', async (c) => { + try { + const id = c.req.param('id'); + const updateData = await c.req.json(); + const strategy = strategies.get(id); + + if (!strategy) { + return c.json({ success: false, error: 'Strategy not found' }, 404); + } + + const updatedStrategy = { + ...strategy, + ...updateData, + id, // Ensure ID doesn't change + updatedAt: new Date() + }; + + strategies.set(id, updatedStrategy); + + // Update in Redis + await redis.setex( + `strategy:${id}`, + 86400, + JSON.stringify(updatedStrategy) + ); + + // Broadcast update + broadcastToClients({ + type: 'STRATEGY_UPDATED', + data: updatedStrategy, + timestamp: new Date() + }); + + return c.json({ success: true, data: updatedStrategy }); + } catch (error) { + console.error('Error updating strategy:', error); + return c.json({ success: false, error: 'Failed to update strategy' }, 500); + } +}); + +// Start/Stop strategy +app.post('/api/strategies/:id/:action', async (c) => { + try { + const id = c.req.param('id'); + const action = c.req.param('action'); + const strategy = strategies.get(id); + + if (!strategy) { + return c.json({ success: false, error: 'Strategy not found' }, 404); + } + + if (!['start', 'stop', 'pause'].includes(action)) { + return c.json({ success: false, error: 'Invalid action' }, 400); + } + + const statusMap = { + start: 'ACTIVE' as const, + stop: 'INACTIVE' as const, + pause: 'PAUSED' as const + }; + + strategy.status = statusMap[action as keyof typeof statusMap]; + strategy.updatedAt = new Date(); + + strategies.set(id, strategy); + + // Update in Redis + await redis.setex( + `strategy:${id}`, + 86400, + JSON.stringify(strategy) + ); + + // Broadcast status change + broadcastToClients({ + type: 'STRATEGY_STATUS_CHANGED', + data: { id, status: strategy.status, action }, + timestamp: new Date() + }); + + return c.json({ success: true, data: strategy }); + } catch (error) { + console.error('Error changing strategy status:', error); + return c.json({ success: false, error: 'Failed to change strategy status' }, 500); + } +}); + +// Get strategy signals +app.get('/api/strategies/:id/signals', async (c) => { + try { + const id = c.req.param('id'); + const limit = parseInt(c.req.query('limit') || '50'); + + const signalKeys = await redis.keys(`signal:${id}:*`); + const signals: any[] = []; + + for (const key of signalKeys.slice(0, limit)) { + const data = await redis.get(key); + if (data) { + signals.push(JSON.parse(data)); + } + } + + return c.json({ + success: true, + data: signals.sort((a: any, b: any) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()) + }); + } catch (error) { + console.error('Error fetching strategy signals:', error); + return c.json({ success: false, error: 'Failed to fetch signals' }, 500); + } +}); + +// Generate demo signal (for testing) +app.post('/api/strategies/:id/generate-signal', async (c) => { + try { + const id = c.req.param('id'); + const strategy = strategies.get(id); + + if (!strategy) { + return c.json({ success: false, error: 'Strategy not found' }, 404); + } + + if (strategy.status !== 'ACTIVE') { + return c.json({ success: false, error: 'Strategy is not active' }, 400); + } + + // Generate demo signal + const symbol = strategy.symbols[Math.floor(Math.random() * strategy.symbols.length)] || 'AAPL'; + const signal: StrategySignal = { + strategyId: id, + symbol, + action: ['BUY', 'SELL', 'HOLD'][Math.floor(Math.random() * 3)] as any, + confidence: Math.random() * 0.4 + 0.6, // 60-100% confidence + price: 150 + Math.random() * 50, + quantity: Math.floor(Math.random() * 100) + 1, + timestamp: new Date(), + metadata: { + indicator1: Math.random(), + indicator2: Math.random(), + rsi: Math.random() * 100 + } + }; + + // Store signal + await redis.setex( + `signal:${id}:${Date.now()}`, + 3600, // 1 hour TTL + JSON.stringify(signal) + ); + + // Broadcast signal + broadcastToClients({ + type: 'STRATEGY_SIGNAL', + data: signal, + timestamp: new Date() + }); + + // Publish to trading system + await redis.publish('trading:signals', JSON.stringify(signal)); + + return c.json({ success: true, data: signal }); + } catch (error) { + console.error('Error generating signal:', error); + return c.json({ success: false, error: 'Failed to generate signal' }, 500); + } +}); + +// WebSocket connection handling +wss.on('connection', (ws) => { + console.log('New strategy monitoring client connected'); + + ws.send(JSON.stringify({ + type: 'CONNECTED', + message: 'Connected to Strategy Orchestrator', + timestamp: new Date() + })); + + ws.on('close', () => { + console.log('Strategy monitoring client disconnected'); + }); + + ws.on('error', (error) => { + console.error('WebSocket error:', error); + }); +}); + +function broadcastToClients(message: any) { + const messageStr = JSON.stringify(message); + wss.clients.forEach(client => { + if (client.readyState === 1) { // WebSocket.OPEN + client.send(messageStr); + } + }); +} + +// Scheduled tasks for strategy management +cron.schedule('*/5 * * * *', async () => { + // Every 5 minutes: Check strategy health and generate signals for active strategies + console.log('Running strategy health check...'); + + for (const [id, strategy] of strategies.entries()) { + if (strategy.status === 'ACTIVE') { + try { + // Generate signals for active strategies (demo mode) + if (Math.random() > 0.7) { // 30% chance to generate a signal + const symbol = strategy.symbols[Math.floor(Math.random() * strategy.symbols.length)] || 'AAPL'; + const signal: StrategySignal = { + strategyId: id, + symbol, + action: ['BUY', 'SELL'][Math.floor(Math.random() * 2)] as any, + confidence: Math.random() * 0.3 + 0.7, + price: 150 + Math.random() * 50, + quantity: Math.floor(Math.random() * 100) + 1, + timestamp: new Date(), + metadata: { scheduled: true } + }; + + await redis.setex( + `signal:${id}:${Date.now()}`, + 3600, + JSON.stringify(signal) + ); + + broadcastToClients({ + type: 'STRATEGY_SIGNAL', + data: signal, + timestamp: new Date() + }); + + await redis.publish('trading:signals', JSON.stringify(signal)); + } + } catch (error) { + console.error(`Error in scheduled task for strategy ${id}:`, error); + } + } + } +}); + +// Load existing strategies from Redis on startup +async function loadStrategiesFromRedis() { + try { + const strategyKeys = await redis.keys('strategy:*'); + for (const key of strategyKeys) { + const data = await redis.get(key); + if (data) { + const strategy = JSON.parse(data); + strategies.set(strategy.id, strategy); + } + } + console.log(`Loaded ${strategies.size} strategies from Redis`); + } catch (error) { + console.error('Error loading strategies from Redis:', error); + } +} + +const port = parseInt(process.env.PORT || '3003'); + +console.log(`🎯 Strategy Orchestrator starting on port ${port}`); +console.log(`📡 WebSocket server running on port 8082`); + +// Load existing strategies +loadStrategiesFromRedis(); + +export default { + port, + fetch: app.fetch, +}; diff --git a/apps/interface-services/trading-dashboard/src/app/app.html b/apps/interface-services/trading-dashboard/src/app/app.html index af1932a..953d59d 100644 --- a/apps/interface-services/trading-dashboard/src/app/app.html +++ b/apps/interface-services/trading-dashboard/src/app/app.html @@ -1,18 +1,67 @@ -
- -
- -
+
+ + + + +
+ + + + {{ title }} + + + + + + +
+ +
+