stock-bot/apps/intelligence-services/strategy-orchestrator/src/index.ts

706 lines
20 KiB
TypeScript

import { Hono } from 'hono';
import { WebSocketServer } from 'ws';
import Redis from 'ioredis';
import * as cron from 'node-cron';
import { BaseStrategy } from './core/Strategy';
import { StrategyRegistry } from './core/strategies/StrategyRegistry';
import { BacktestService, BacktestRequest } from './core/backtesting/BacktestService';
import { BacktestResult } from './core/backtesting/BacktestEngine';
import { PerformanceAnalytics } from './core/backtesting/PerformanceAnalytics';
import { StrategyController } from './controllers/StrategyController';
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 });
// Initialize strategy registry and backtest service
const strategyRegistry = StrategyRegistry.getInstance();
const backtestService = new BacktestService();
// 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>();
// Initialize strategy controller
const strategyController = new StrategyController();
// 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,
registeredStrategies: strategyRegistry.getAllStrategies().length,
connections: wss.clients.size
});
});
// API Routes
// Strategy management endpoints
app.get('/api/strategy-types', async (c) => {
try {
const types = Object.values(strategyRegistry.getStrategyTypes());
return c.json({ success: true, data: types });
} catch (error) {
console.error('Error getting strategy types:', error);
return c.json({ success: false, error: 'Failed to get strategy types' }, 500);
}
});
app.get('/api/strategies', async (c) => {
try {
const strategies = strategyRegistry.getAllStrategies();
const serializedStrategies = strategies.map(strategy => ({
id: strategy.id,
name: strategy.name,
description: strategy.description,
symbols: strategy.symbols,
parameters: strategy.parameters,
type: strategyRegistry.getStrategyType(strategy)
}));
return c.json({ success: true, data: serializedStrategies });
} catch (error) {
console.error('Error fetching strategies:', error);
return c.json({ success: false, error: 'Failed to fetch strategies' }, 500);
}
});
app.get('/api/strategies/:id', async (c) => {
try {
const id = c.req.param('id');
const strategy = strategyRegistry.getStrategyById(id);
if (!strategy) {
return c.json({ success: false, error: `Strategy with ID ${id} not found` }, 404);
}
const type = strategyRegistry.getStrategyType(strategy);
return c.json({
success: true,
data: {
id: strategy.id,
name: strategy.name,
description: strategy.description,
symbols: strategy.symbols,
parameters: strategy.parameters,
type
}
});
} catch (error) {
console.error('Error fetching strategy:', error);
return c.json({ success: false, error: 'Failed to fetch strategy' }, 500);
}
});
app.post('/api/strategies', async (c) => {
try {
const { name, description, symbols, parameters, type } = await c.req.json();
if (!type) {
return c.json({
success: false,
error: 'Invalid strategy type'
}, 400);
}
const strategy = strategyRegistry.createStrategy(
type,
`strategy_${Date.now()}`, // Generate an ID
name || `New ${type} Strategy`,
description || `Generated ${type} strategy`,
symbols || [],
parameters || {}
);
return c.json({
success: true,
data: {
id: strategy.id,
name: strategy.name,
description: strategy.description,
symbols: strategy.symbols,
parameters: strategy.parameters,
type
}
}, 201);
} catch (error) {
console.error('Error creating strategy:', error);
return c.json({ success: false, error: (error as Error).message }, 500);
}
});
app.put('/api/strategies/:id', async (c) => {
try {
const id = c.req.param('id');
const { name, description, symbols, parameters } = await c.req.json();
const strategy = strategyRegistry.getStrategyById(id);
if (!strategy) {
return c.json({ success: false, error: `Strategy with ID ${id} not found` }, 404);
}
// Update properties
if (name !== undefined) strategy.name = name;
if (description !== undefined) strategy.description = description;
if (symbols !== undefined) (strategy as any).symbols = symbols; // Hack since symbols is readonly
if (parameters !== undefined) strategy.parameters = parameters;
return c.json({
success: true,
data: {
id: strategy.id,
name: strategy.name,
description: strategy.description,
symbols: strategy.symbols,
parameters: strategy.parameters,
type: strategyRegistry.getStrategyType(strategy)
}
});
} catch (error) {
console.error('Error updating strategy:', error);
return c.json({ success: false, error: (error as Error).message }, 500);
}
});
app.delete('/api/strategies/:id', async (c) => {
try {
const id = c.req.param('id');
const success = strategyRegistry.deleteStrategy(id);
if (!success) {
return c.json({ success: false, error: `Strategy with ID ${id} not found` }, 404);
}
return c.json({ success: true, data: { id } });
} catch (error) {
console.error('Error deleting strategy:', error);
return c.json({ success: false, error: (error as Error).message }, 500);
}
});
// Backtesting endpoints
app.post('/api/backtest', async (c) => {
try {
const backtestRequest = await c.req.json() as BacktestRequest;
// Validate request
if (!backtestRequest.strategyType) {
return c.json({ success: false, error: 'Strategy type is required' }, 400);
}
if (!backtestRequest.symbols || backtestRequest.symbols.length === 0) {
return c.json({ success: false, error: 'At least one symbol is required' }, 400);
}
// Run the backtest
const result = await backtestService.runBacktest(backtestRequest);
// Enhance results with additional metrics
const enhancedResult = PerformanceAnalytics.enhanceResults(result);
// Calculate additional analytics
const monthlyReturns = PerformanceAnalytics.calculateMonthlyReturns(result.dailyReturns);
const drawdowns = PerformanceAnalytics.analyzeDrawdowns(result.dailyReturns);
return c.json({
success: true,
data: {
...enhancedResult,
monthlyReturns,
drawdowns
}
});
} catch (error) {
console.error('Backtest error:', error);
return c.json({ success: false, error: (error as Error).message }, 500);
}
});
app.post('/api/optimize', async (c) => {
try {
const { baseRequest, parameterGrid } = await c.req.json();
// Validate request
if (!baseRequest || !parameterGrid) {
return c.json({ success: false, error: 'Base request and parameter grid are required' }, 400);
}
// Run optimization
const results = await backtestService.optimizeStrategy(baseRequest, parameterGrid);
return c.json({ success: true, data: results });
} catch (error) {
console.error('Strategy optimization error:', error);
return c.json({ success: false, error: (error as Error).message }, 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);
}
});
// Get strategy trades
app.get('/api/strategies/:id/trades', async (c) => {
try {
const id = c.req.param('id');
const limit = parseInt(c.req.query('limit') || '50');
const tradeKeys = await redis.keys(`trade:${id}:*`);
const trades: any[] = [];
for (const key of tradeKeys.slice(0, limit)) {
const data = await redis.get(key);
if (data) {
trades.push(JSON.parse(data));
}
}
return c.json({
success: true,
data: trades.sort((a: any, b: any) => new Date(b.exitTime || b.timestamp).getTime() - new Date(a.exitTime || a.timestamp).getTime())
});
} catch (error) {
console.error('Error fetching strategy trades:', error);
return c.json({ success: false, error: 'Failed to fetch trades' }, 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);
}
}
}
});
// Backtesting API endpoints
app.post('/api/backtest', async (c) => {
try {
const request = await c.req.json() as BacktestRequest;
console.log('Received backtest request:', request);
const result = await backtestService.runBacktest(request);
const enhancedResult = PerformanceAnalytics.enhanceResults(result);
// Store backtest result in Redis for persistence
await redis.setex(
`backtest:${result.strategyId}`,
86400 * 7, // 7 days TTL
JSON.stringify(enhancedResult)
);
return c.json({ success: true, data: enhancedResult });
} catch (error) {
console.error('Backtest error:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
app.post('/api/backtest/optimize', async (c) => {
try {
const { baseRequest, parameterGrid } = await c.req.json() as {
baseRequest: BacktestRequest,
parameterGrid: Record<string, any[]>
};
console.log('Received optimization request:', baseRequest, parameterGrid);
const results = await backtestService.optimizeStrategy(baseRequest, parameterGrid);
return c.json({ success: true, data: results });
} catch (error) {
console.error('Optimization error:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
app.get('/api/backtest/:id', async (c) => {
try {
const id = c.req.param('id');
const data = await redis.get(`backtest:${id}`);
if (!data) {
return c.json({ success: false, error: 'Backtest not found' }, 404);
}
const result = JSON.parse(data) as BacktestResult;
return c.json({ success: true, data: result });
} catch (error) {
console.error('Error fetching backtest:', error);
return c.json({ success: false, error: 'Failed to fetch backtest' }, 500);
}
});
app.get('/api/strategy-types', (c) => {
const types = strategyRegistry.getStrategyTypes();
return c.json({ success: true, data: types });
});
app.get('/api/strategy-parameters/:type', (c) => {
try {
const type = c.req.param('type') as any;
if (!strategyRegistry.hasStrategyType(type)) {
return c.json({ success: false, error: 'Strategy type not found' }, 404);
}
const params = strategyRegistry.getDefaultParameters(type);
return c.json({ success: true, data: params });
} catch (error) {
console.error('Error fetching strategy parameters:', error);
return c.json({ success: false, error: 'Failed to fetch parameters' }, 500);
}
});
// 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 || '4001');
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,
};