improved dashboard
This commit is contained in:
parent
114c280734
commit
90168ba619
8 changed files with 781 additions and 47 deletions
245
apps/core-services/risk-guardian/src/index.ts
Normal file
245
apps/core-services/risk-guardian/src/index.ts
Normal file
|
|
@ -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,
|
||||||
|
};
|
||||||
409
apps/intelligence-services/strategy-orchestrator/src/index.ts
Normal file
409
apps/intelligence-services/strategy-orchestrator/src/index.ts
Normal file
|
|
@ -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<string, any>;
|
||||||
|
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<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In-memory strategy registry (in production, this would be persisted)
|
||||||
|
const strategies = new Map<string, TradingStrategy>();
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
};
|
||||||
|
|
@ -1,18 +1,67 @@
|
||||||
<!-- Trading Dashboard App -->
|
<!-- Trading Dashboard App -->
|
||||||
<div class="min-h-screen bg-gray-50">
|
<div class="app-layout">
|
||||||
<app-sidebar></app-sidebar>
|
<!-- Sidebar -->
|
||||||
<main class="main-content">
|
<app-sidebar [opened]="sidenavOpened()" (navigationItemClick)="onNavigationClick($event)"></app-sidebar>
|
||||||
|
|
||||||
|
<!-- Main Content Area -->
|
||||||
|
<div class="main-content" [class.main-content-closed]="!sidenavOpened()">
|
||||||
|
<!-- Top Navigation Bar -->
|
||||||
|
<mat-toolbar class="top-toolbar">
|
||||||
|
<button mat-icon-button (click)="toggleSidenav()" class="mr-2">
|
||||||
|
<mat-icon>menu</mat-icon>
|
||||||
|
</button>
|
||||||
|
<span class="text-lg font-semibold text-gray-800">{{ title }}</span>
|
||||||
|
<span class="spacer"></span>
|
||||||
|
<button mat-icon-button>
|
||||||
|
<mat-icon>notifications</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button mat-icon-button>
|
||||||
|
<mat-icon>account_circle</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-toolbar>
|
||||||
|
|
||||||
|
<!-- Page Content -->
|
||||||
|
<div class="page-content">
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
</main>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.app-layout {
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
.main-content {
|
.main-content {
|
||||||
margin-left: 256px; /* Width of sidebar (w-64 = 16rem = 256px) */
|
flex: 1;
|
||||||
min-height: 100vh;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-left: 256px; /* Width of sidebar */
|
||||||
transition: margin-left 0.3s ease;
|
transition: margin-left 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main-content-closed {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-toolbar {
|
||||||
|
background-color: white;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 1.5rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.main-content {
|
.main-content {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,22 @@
|
||||||
/* Sidebar specific styles */
|
/* Sidebar specific styles */
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 16rem; /* 256px */
|
||||||
|
height: 100vh;
|
||||||
|
background-color: white;
|
||||||
|
border-right: 1px solid #e5e7eb;
|
||||||
|
transform: translateX(0);
|
||||||
|
transition: transform 0.3s ease-in-out;
|
||||||
|
z-index: 1000;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-closed {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
.nav-button {
|
.nav-button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
<mat-sidenav
|
<!-- Sidebar Navigation -->
|
||||||
[opened]="opened()"
|
<aside class="sidebar" [class.sidebar-closed]="!opened()">
|
||||||
mode="side"
|
|
||||||
class="w-64 bg-white border-r border-gray-200">
|
|
||||||
|
|
||||||
<!-- Logo/Brand -->
|
<!-- Logo/Brand -->
|
||||||
<div class="p-6 border-b border-gray-200">
|
<div class="p-6 border-b border-gray-200">
|
||||||
<h2 class="text-xl font-bold text-gray-900">
|
<h2 class="text-xl font-bold text-gray-900">
|
||||||
|
|
@ -30,4 +27,4 @@
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</mat-sidenav>
|
</aside>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ import { CommonModule } from '@angular/common';
|
||||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { Router } from '@angular/router';
|
import { Router, NavigationEnd } from '@angular/router';
|
||||||
|
import { filter } from 'rxjs/operators';
|
||||||
|
|
||||||
export interface NavigationItem {
|
export interface NavigationItem {
|
||||||
label: string;
|
label: string;
|
||||||
|
|
@ -37,15 +38,24 @@ export class SidebarComponent {
|
||||||
{ label: 'Settings', icon: 'settings', route: '/settings' }
|
{ label: 'Settings', icon: 'settings', route: '/settings' }
|
||||||
];
|
];
|
||||||
|
|
||||||
constructor(private router: Router) {}
|
constructor(private router: Router) {
|
||||||
|
// Listen to route changes to update active state
|
||||||
|
this.router.events.pipe(
|
||||||
|
filter(event => event instanceof NavigationEnd)
|
||||||
|
).subscribe((event: NavigationEnd) => {
|
||||||
|
this.updateActiveRoute(event.urlAfterRedirects);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onNavigationClick(route: string) {
|
onNavigationClick(route: string) {
|
||||||
this.navigationItemClick.emit(route);
|
this.navigationItemClick.emit(route);
|
||||||
this.router.navigate([route]);
|
this.router.navigate([route]);
|
||||||
|
this.updateActiveRoute(route);
|
||||||
|
}
|
||||||
|
|
||||||
// Update active state
|
private updateActiveRoute(currentRoute: string) {
|
||||||
this.navigationItems.forEach(item => {
|
this.navigationItems.forEach(item => {
|
||||||
item.active = item.route === route;
|
item.active = item.route === currentRoute;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,10 +34,9 @@
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
|
||||||
<mat-card class="p-6">
|
<mat-card class="p-6">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between"> <div>
|
||||||
<div>
|
|
||||||
<p class="text-sm text-gray-600">Last Update</p>
|
<p class="text-sm text-gray-600">Last Update</p>
|
||||||
<p class="text-lg font-semibold text-gray-900">{{ new Date().toLocaleTimeString() }}</p>
|
<p class="text-lg font-semibold text-gray-900">{{ currentTime() }}</p>
|
||||||
</div>
|
</div>
|
||||||
<mat-icon class="text-purple-600 text-3xl">access_time</mat-icon>
|
<mat-icon class="text-purple-600 text-3xl">access_time</mat-icon>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -45,45 +45,52 @@ export class MarketDataComponent {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
symbol: 'GOOGL',
|
symbol: 'GOOGL',
|
||||||
price: 138.21,
|
price: 2847.56,
|
||||||
change: -1.82,
|
change: -12.34,
|
||||||
changePercent: -1.30,
|
changePercent: -0.43,
|
||||||
volume: 23450000,
|
volume: 12450000,
|
||||||
marketCap: '1.75T',
|
marketCap: '1.78T',
|
||||||
high52Week: 152.10,
|
high52Week: 3030.93,
|
||||||
low52Week: 125.45
|
low52Week: 2193.62
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
symbol: 'MSFT',
|
symbol: 'MSFT',
|
||||||
price: 378.85,
|
price: 415.26,
|
||||||
change: 4.12,
|
change: 8.73,
|
||||||
changePercent: 1.10,
|
changePercent: 2.15,
|
||||||
volume: 34560000,
|
volume: 23180000,
|
||||||
marketCap: '2.82T',
|
marketCap: '3.08T',
|
||||||
high52Week: 384.30,
|
high52Week: 468.35,
|
||||||
low52Week: 309.45
|
low52Week: 309.45
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
symbol: 'TSLA',
|
symbol: 'TSLA',
|
||||||
price: 248.42,
|
price: 248.50,
|
||||||
change: -3.21,
|
change: -5.21,
|
||||||
changePercent: -1.28,
|
changePercent: -2.05,
|
||||||
volume: 67890000,
|
volume: 89760000,
|
||||||
marketCap: '789B',
|
marketCap: '789.2B',
|
||||||
high52Week: 299.29,
|
high52Week: 299.29,
|
||||||
low52Week: 138.80
|
low52Week: 152.37
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
symbol: 'AMZN',
|
symbol: 'AMZN',
|
||||||
price: 145.67,
|
price: 152.74,
|
||||||
change: 1.89,
|
change: 3.18,
|
||||||
changePercent: 1.31,
|
changePercent: 2.12,
|
||||||
volume: 29340000,
|
volume: 34520000,
|
||||||
marketCap: '1.52T',
|
marketCap: '1.59T',
|
||||||
high52Week: 155.20,
|
high52Week: 170.17,
|
||||||
low52Week: 118.35
|
low52Week: 118.35
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
protected displayedColumns: string[] = ['symbol', 'price', 'change', 'changePercent', 'volume', 'marketCap'];
|
protected displayedColumns: string[] = ['symbol', 'price', 'change', 'changePercent', 'volume', 'marketCap'];
|
||||||
|
|
||||||
|
protected currentTime = signal<string>(new Date().toLocaleTimeString()); constructor() {
|
||||||
|
// Update time every second
|
||||||
|
setInterval(() => {
|
||||||
|
this.currentTime.set(new Date().toLocaleTimeString());
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue