237 lines
6.9 KiB
TypeScript
237 lines
6.9 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach, jest } from 'bun:test';
|
|
import { EventEmitter } from 'events';
|
|
import { WebSocket } from 'ws';
|
|
import { StrategyExecutionService } from '../../core/execution/StrategyExecutionService';
|
|
import { StrategyRegistry } from '../../core/strategies/StrategyRegistry';
|
|
import { MarketDataFeed } from '../../core/backtesting/MarketDataFeed';
|
|
import { BaseStrategy, BarData, Order } from '../../core/Strategy';
|
|
|
|
// Mock WebSocket to avoid actual network connections during tests
|
|
jest.mock('ws', () => {
|
|
const EventEmitter = require('events');
|
|
|
|
class MockWebSocket extends EventEmitter {
|
|
static OPEN = 1;
|
|
readyState = 1;
|
|
close = jest.fn();
|
|
send = jest.fn();
|
|
}
|
|
|
|
class MockServer extends EventEmitter {
|
|
clients = new Set();
|
|
|
|
constructor() {
|
|
super();
|
|
// Add a mock client to the set
|
|
const mockClient = new MockWebSocket();
|
|
this.clients.add(mockClient);
|
|
}
|
|
|
|
close(callback) {
|
|
callback();
|
|
}
|
|
}
|
|
|
|
return {
|
|
WebSocket: MockWebSocket,
|
|
Server: MockServer
|
|
};
|
|
});
|
|
|
|
// Mock MarketDataFeed to avoid actual API calls
|
|
jest.mock('../../core/backtesting/MarketDataFeed', () => {
|
|
return {
|
|
MarketDataFeed: class {
|
|
async getHistoricalData(symbol, resolution, startDate, endDate) {
|
|
// Return mock data
|
|
return [
|
|
{
|
|
symbol,
|
|
timestamp: new Date(),
|
|
open: 100,
|
|
high: 105,
|
|
low: 95,
|
|
close: 102,
|
|
volume: 1000
|
|
}
|
|
];
|
|
}
|
|
}
|
|
};
|
|
});
|
|
|
|
// Mock strategy for testing
|
|
class MockStrategy extends BaseStrategy {
|
|
name = 'MockStrategy';
|
|
description = 'A mock strategy for testing';
|
|
symbols = ['AAPL', 'MSFT'];
|
|
parameters = { param1: 1, param2: 2 };
|
|
|
|
constructor(id: string) {
|
|
super(id);
|
|
}
|
|
|
|
async start(): Promise<void> {}
|
|
|
|
async stop(): Promise<void> {}
|
|
|
|
onBar(bar: BarData) {
|
|
// Return a mock signal
|
|
return {
|
|
action: 'BUY',
|
|
symbol: bar.symbol,
|
|
price: bar.close,
|
|
quantity: 10,
|
|
metadata: { reason: 'Test signal' }
|
|
};
|
|
}
|
|
|
|
async onOrderFilled(order: Order): Promise<void> {}
|
|
}
|
|
|
|
// Mock StrategyRegistry
|
|
jest.mock('../../core/strategies/StrategyRegistry', () => {
|
|
const mockInstance = {
|
|
getStrategyById: jest.fn(),
|
|
getStrategyTypes: () => [{ id: 'mock-strategy', name: 'Mock Strategy' }],
|
|
getAllStrategies: () => [new MockStrategy('mock-1')]
|
|
};
|
|
|
|
return {
|
|
StrategyRegistry: {
|
|
getInstance: () => mockInstance
|
|
}
|
|
};
|
|
});
|
|
|
|
describe('StrategyExecutionService', () => {
|
|
let executionService: StrategyExecutionService;
|
|
let strategyRegistry: typeof StrategyRegistry.getInstance;
|
|
|
|
beforeEach(() => {
|
|
// Reset mocks
|
|
jest.clearAllMocks();
|
|
|
|
// Create a new execution service for each test
|
|
executionService = new StrategyExecutionService('http://localhost:3001/api', 8082);
|
|
strategyRegistry = StrategyRegistry.getInstance();
|
|
|
|
// Setup mock strategy
|
|
const mockStrategy = new MockStrategy('test-strategy');
|
|
(strategyRegistry.getStrategyById as jest.Mock).mockReturnValue(mockStrategy);
|
|
});
|
|
|
|
afterEach(() => {
|
|
executionService.shutdown();
|
|
});
|
|
|
|
it('should initialize correctly', () => {
|
|
expect(executionService).toBeDefined();
|
|
});
|
|
|
|
it('should start a strategy correctly', () => {
|
|
// Arrange & Act
|
|
const result = executionService.startStrategy('test-strategy');
|
|
|
|
// Assert
|
|
expect(result).toBe(true);
|
|
expect(strategyRegistry.getStrategyById).toHaveBeenCalledWith('test-strategy');
|
|
|
|
// Check if WebSocket broadcast happened
|
|
const ws = executionService['webSocketServer'].clients.values().next().value;
|
|
expect(ws.send).toHaveBeenCalled();
|
|
|
|
// Check the broadcast message contains the correct type
|
|
const lastCall = ws.send.mock.calls[0][0];
|
|
const message = JSON.parse(lastCall);
|
|
expect(message.type).toBe('strategy_started');
|
|
expect(message.data.strategyId).toBe('test-strategy');
|
|
});
|
|
|
|
it('should stop a strategy correctly', () => {
|
|
// Arrange
|
|
executionService.startStrategy('test-strategy');
|
|
|
|
// Act
|
|
const result = executionService.stopStrategy('test-strategy');
|
|
|
|
// Assert
|
|
expect(result).toBe(true);
|
|
|
|
// Check if WebSocket broadcast happened
|
|
const ws = executionService['webSocketServer'].clients.values().next().value;
|
|
const lastCallIndex = ws.send.mock.calls.length - 1;
|
|
const lastCall = ws.send.mock.calls[lastCallIndex][0];
|
|
const message = JSON.parse(lastCall);
|
|
|
|
expect(message.type).toBe('strategy_stopped');
|
|
expect(message.data.strategyId).toBe('test-strategy');
|
|
});
|
|
|
|
it('should pause a strategy correctly', () => {
|
|
// Arrange
|
|
executionService.startStrategy('test-strategy');
|
|
|
|
// Act
|
|
const result = executionService.pauseStrategy('test-strategy');
|
|
|
|
// Assert
|
|
expect(result).toBe(true);
|
|
|
|
// Check if WebSocket broadcast happened
|
|
const ws = executionService['webSocketServer'].clients.values().next().value;
|
|
const lastCallIndex = ws.send.mock.calls.length - 1;
|
|
const lastCall = ws.send.mock.calls[lastCallIndex][0];
|
|
const message = JSON.parse(lastCall);
|
|
|
|
expect(message.type).toBe('strategy_paused');
|
|
expect(message.data.strategyId).toBe('test-strategy');
|
|
});
|
|
|
|
it('should process market data and generate signals', async () => {
|
|
// Arrange
|
|
executionService.startStrategy('test-strategy');
|
|
|
|
// Act - Trigger market data polling manually
|
|
await executionService['pollMarketData']('test-strategy');
|
|
|
|
// Assert - Check if signal was generated and broadcast
|
|
const ws = executionService['webSocketServer'].clients.values().next().value;
|
|
|
|
// Find the strategy_signal message
|
|
const signalMessages = ws.send.mock.calls
|
|
.map(call => JSON.parse(call[0]))
|
|
.filter(msg => msg.type === 'strategy_signal');
|
|
|
|
expect(signalMessages.length).toBeGreaterThan(0);
|
|
expect(signalMessages[0].data.action).toBe('BUY');
|
|
expect(signalMessages[0].data.strategyId).toBe('test-strategy');
|
|
});
|
|
|
|
it('should handle WebSocket client connections', () => {
|
|
// Arrange
|
|
const mockWs = new WebSocket();
|
|
const mockMessage = JSON.stringify({ type: 'get_active_strategies' });
|
|
|
|
// Act - Simulate connection and message
|
|
executionService['webSocketServer'].emit('connection', mockWs);
|
|
mockWs.emit('message', mockMessage);
|
|
|
|
// Assert
|
|
expect(mockWs.send).toHaveBeenCalled();
|
|
|
|
// Check that the response is a strategy_status_list message
|
|
const lastCall = mockWs.send.mock.calls[0][0];
|
|
const message = JSON.parse(lastCall);
|
|
expect(message.type).toBe('strategy_status_list');
|
|
});
|
|
|
|
it('should shut down correctly', () => {
|
|
// Act
|
|
executionService.shutdown();
|
|
|
|
// Assert - WebSocket server should be closed
|
|
const ws = executionService['webSocketServer'].clients.values().next().value;
|
|
expect(ws.close).toHaveBeenCalled();
|
|
});
|
|
});
|