adding data-services
This commit is contained in:
parent
e3bfd05b90
commit
405b818c86
139 changed files with 55943 additions and 416 deletions
|
|
@ -0,0 +1,139 @@
|
|||
import { describe, test, expect, beforeEach } from 'bun:test';
|
||||
import { BacktestService, BacktestRequest } from '../../core/backtesting/BacktestService';
|
||||
import { StrategyRegistry, StrategyType } from '../../core/strategies/StrategyRegistry';
|
||||
import { MarketDataFeed } from '../../core/backtesting/MarketDataFeed';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('../../core/backtesting/MarketDataFeed');
|
||||
|
||||
describe('BacktestService', () => {
|
||||
let backtestService: BacktestService;
|
||||
let mockRequest: BacktestRequest;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset mocks and create fresh service instance
|
||||
jest.clearAllMocks();
|
||||
backtestService = new BacktestService('http://test.api');
|
||||
|
||||
// Create a standard backtest request for tests
|
||||
mockRequest = {
|
||||
strategyType: 'MEAN_REVERSION' as StrategyType,
|
||||
strategyParams: {
|
||||
lookback: 20,
|
||||
entryDeviation: 1.5,
|
||||
exitDeviation: 0.5,
|
||||
lookbackPeriod: 100
|
||||
},
|
||||
symbols: ['AAPL'],
|
||||
startDate: new Date('2023-01-01'),
|
||||
endDate: new Date('2023-02-01'),
|
||||
initialCapital: 100000,
|
||||
dataResolution: '1d',
|
||||
commission: 0.001,
|
||||
slippage: 0.001,
|
||||
mode: 'vector'
|
||||
};
|
||||
|
||||
// Mock the MarketDataFeed implementation
|
||||
(MarketDataFeed.prototype.getHistoricalData as jest.Mock).mockResolvedValue([
|
||||
// Generate some sample data
|
||||
...Array(30).fill(0).map((_, i) => ({
|
||||
symbol: 'AAPL',
|
||||
timestamp: new Date(`2023-01-${(i + 1).toString().padStart(2, '0')}`),
|
||||
open: 150 + Math.random() * 10,
|
||||
high: 155 + Math.random() * 10,
|
||||
low: 145 + Math.random() * 10,
|
||||
close: 150 + Math.random() * 10,
|
||||
volume: 1000000 + Math.random() * 500000
|
||||
}))
|
||||
]);
|
||||
});
|
||||
|
||||
test('should run a backtest successfully', async () => {
|
||||
// Act
|
||||
const result = await backtestService.runBacktest(mockRequest);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeDefined();
|
||||
expect(result.strategyId).toBeDefined();
|
||||
expect(result.initialCapital).toBe(100000);
|
||||
expect(result.trades).toBeDefined();
|
||||
expect(result.dailyReturns).toBeDefined();
|
||||
|
||||
// Verify market data was requested
|
||||
expect(MarketDataFeed.prototype.getHistoricalData).toHaveBeenCalledTimes(mockRequest.symbols.length);
|
||||
});
|
||||
|
||||
test('should optimize strategy parameters', async () => {
|
||||
// Arrange
|
||||
const parameterGrid = {
|
||||
lookback: [10, 20],
|
||||
entryDeviation: [1.0, 1.5, 2.0]
|
||||
};
|
||||
|
||||
// We should get 2×3 = 6 combinations
|
||||
|
||||
// Act
|
||||
const results = await backtestService.optimizeStrategy(mockRequest, parameterGrid);
|
||||
|
||||
// Assert
|
||||
expect(results).toHaveLength(6);
|
||||
expect(results[0].parameters).toBeDefined();
|
||||
|
||||
// Check that results are sorted by performance (sharpe ratio)
|
||||
for (let i = 0; i < results.length - 1; i++) {
|
||||
expect(results[i].sharpeRatio).toBeGreaterThanOrEqual(results[i + 1].sharpeRatio);
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle errors during backtest', async () => {
|
||||
// Arrange
|
||||
(MarketDataFeed.prototype.getHistoricalData as jest.Mock).mockRejectedValue(
|
||||
new Error('Data source error')
|
||||
);
|
||||
|
||||
// Act & Assert
|
||||
await expect(backtestService.runBacktest(mockRequest))
|
||||
.rejects
|
||||
.toThrow();
|
||||
});
|
||||
|
||||
test('should generate correct parameter combinations', () => {
|
||||
// Arrange
|
||||
const grid = {
|
||||
param1: [1, 2],
|
||||
param2: ['a', 'b'],
|
||||
param3: [true, false]
|
||||
};
|
||||
|
||||
// Act
|
||||
const combinations = (backtestService as any).generateParameterCombinations(grid, Object.keys(grid));
|
||||
|
||||
// Assert - should get 2×2×2 = 8 combinations
|
||||
expect(combinations).toHaveLength(8);
|
||||
|
||||
// Check that all combinations are generated
|
||||
expect(combinations).toContainEqual({ param1: 1, param2: 'a', param3: true });
|
||||
expect(combinations).toContainEqual({ param1: 1, param2: 'a', param3: false });
|
||||
expect(combinations).toContainEqual({ param1: 1, param2: 'b', param3: true });
|
||||
expect(combinations).toContainEqual({ param1: 1, param2: 'b', param3: false });
|
||||
expect(combinations).toContainEqual({ param1: 2, param2: 'a', param3: true });
|
||||
expect(combinations).toContainEqual({ param1: 2, param2: 'a', param3: false });
|
||||
expect(combinations).toContainEqual({ param1: 2, param2: 'b', param3: true });
|
||||
expect(combinations).toContainEqual({ param1: 2, param2: 'b', param3: false });
|
||||
});
|
||||
|
||||
test('should track active backtests', () => {
|
||||
// Arrange
|
||||
const activeBacktests = (backtestService as any).activeBacktests;
|
||||
|
||||
// Act
|
||||
let promise = backtestService.runBacktest(mockRequest);
|
||||
|
||||
// Assert
|
||||
expect(activeBacktests.size).toBe(1);
|
||||
|
||||
// Clean up
|
||||
return promise;
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
import { describe, test, expect } from 'bun:test';
|
||||
import { PerformanceAnalytics } from '../../core/backtesting/PerformanceAnalytics';
|
||||
import { BacktestResult } from '../../core/backtesting/BacktestEngine';
|
||||
|
||||
describe('PerformanceAnalytics', () => {
|
||||
// Sample backtest result for testing
|
||||
const sampleResult: BacktestResult = {
|
||||
strategyId: 'test-strategy',
|
||||
startDate: new Date('2023-01-01'),
|
||||
endDate: new Date('2023-12-31'),
|
||||
duration: 31536000000, // 1 year in ms
|
||||
initialCapital: 100000,
|
||||
finalCapital: 125000,
|
||||
totalReturn: 0.25, // 25% return
|
||||
annualizedReturn: 0.25,
|
||||
sharpeRatio: 1.5,
|
||||
maxDrawdown: 0.10, // 10% drawdown
|
||||
maxDrawdownDuration: 30, // 30 days
|
||||
winRate: 0.6, // 60% win rate
|
||||
totalTrades: 50,
|
||||
winningTrades: 30,
|
||||
losingTrades: 20,
|
||||
averageWinningTrade: 0.05, // 5% average win
|
||||
averageLosingTrade: -0.03, // 3% average loss
|
||||
profitFactor: 2.5,
|
||||
dailyReturns: [
|
||||
// Generate 365 days of sample daily returns with some randomness
|
||||
...Array(365).fill(0).map((_, i) => ({
|
||||
date: new Date(new Date('2023-01-01').getTime() + i * 24 * 3600 * 1000),
|
||||
return: 0.001 + (Math.random() - 0.45) * 0.01 // Mean positive return with noise
|
||||
}))
|
||||
],
|
||||
trades: [
|
||||
// Generate sample trades
|
||||
...Array(50).fill(0).map((_, i) => ({
|
||||
symbol: 'AAPL',
|
||||
entryTime: new Date(`2023-${Math.floor(i / 4) + 1}-${(i % 28) + 1}`),
|
||||
entryPrice: 150 + Math.random() * 10,
|
||||
exitTime: new Date(`2023-${Math.floor(i / 4) + 1}-${(i % 28) + 5}`),
|
||||
exitPrice: 155 + Math.random() * 10,
|
||||
quantity: 10,
|
||||
pnl: 500 * (Math.random() - 0.3), // Some wins, some losses
|
||||
pnlPercent: 0.05 * (Math.random() - 0.3)
|
||||
}))
|
||||
]
|
||||
};
|
||||
|
||||
test('should calculate advanced metrics', () => {
|
||||
// Act
|
||||
const enhancedResult = PerformanceAnalytics.enhanceResults(sampleResult);
|
||||
|
||||
// Assert
|
||||
expect(enhancedResult.sortinoRatio).toBeDefined();
|
||||
expect(enhancedResult.calmarRatio).toBeDefined();
|
||||
expect(enhancedResult.omegaRatio).toBeDefined();
|
||||
expect(enhancedResult.cagr).toBeDefined();
|
||||
expect(enhancedResult.volatility).toBeDefined();
|
||||
expect(enhancedResult.ulcerIndex).toBeDefined();
|
||||
|
||||
// Check that the original result properties are preserved
|
||||
expect(enhancedResult.strategyId).toBe(sampleResult.strategyId);
|
||||
expect(enhancedResult.totalReturn).toBe(sampleResult.totalReturn);
|
||||
|
||||
// Validate some calculations
|
||||
expect(enhancedResult.calmarRatio).toBeCloseTo(sampleResult.annualizedReturn / sampleResult.maxDrawdown);
|
||||
expect(typeof enhancedResult.sortinoRatio).toBe('number');
|
||||
});
|
||||
|
||||
test('should calculate monthly returns', () => {
|
||||
// Act
|
||||
const monthlyReturns = PerformanceAnalytics.calculateMonthlyReturns(sampleResult.dailyReturns);
|
||||
|
||||
// Assert
|
||||
expect(monthlyReturns).toBeDefined();
|
||||
expect(monthlyReturns.length).toBe(12); // 12 months in a year
|
||||
expect(monthlyReturns[0].year).toBe(2023);
|
||||
expect(monthlyReturns[0].month).toBe(0); // January is 0
|
||||
|
||||
// Verify sorting
|
||||
let lastDate = { year: 0, month: 0 };
|
||||
for (const mr of monthlyReturns) {
|
||||
expect(mr.year >= lastDate.year).toBeTruthy();
|
||||
if (mr.year === lastDate.year) {
|
||||
expect(mr.month >= lastDate.month).toBeTruthy();
|
||||
}
|
||||
lastDate = { year: mr.year, month: mr.month };
|
||||
}
|
||||
});
|
||||
|
||||
test('should analyze drawdowns', () => {
|
||||
// Act
|
||||
const drawdowns = PerformanceAnalytics.analyzeDrawdowns(sampleResult.dailyReturns);
|
||||
|
||||
// Assert
|
||||
expect(drawdowns).toBeDefined();
|
||||
expect(drawdowns.length).toBeGreaterThan(0);
|
||||
|
||||
// Check drawdown properties
|
||||
for (const dd of drawdowns) {
|
||||
expect(dd.startDate).toBeInstanceOf(Date);
|
||||
expect(dd.endDate).toBeInstanceOf(Date);
|
||||
expect(dd.drawdown).toBeGreaterThan(0);
|
||||
expect(dd.durationDays).toBeGreaterThanOrEqual(0);
|
||||
|
||||
// Recovery date and days might be null for ongoing drawdowns
|
||||
if (dd.recoveryDate) {
|
||||
expect(dd.recoveryDate).toBeInstanceOf(Date);
|
||||
expect(dd.recoveryDays).toBeGreaterThanOrEqual(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Check sorting by drawdown magnitude
|
||||
for (let i = 0; i < drawdowns.length - 1; i++) {
|
||||
expect(drawdowns[i].drawdown).toBeGreaterThanOrEqual(drawdowns[i + 1].drawdown);
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle empty inputs', () => {
|
||||
// Act & Assert
|
||||
expect(() => PerformanceAnalytics.calculateMonthlyReturns([])).not.toThrow();
|
||||
expect(() => PerformanceAnalytics.analyzeDrawdowns([])).not.toThrow();
|
||||
|
||||
const emptyMonthlyReturns = PerformanceAnalytics.calculateMonthlyReturns([]);
|
||||
const emptyDrawdowns = PerformanceAnalytics.analyzeDrawdowns([]);
|
||||
|
||||
expect(emptyMonthlyReturns).toEqual([]);
|
||||
expect(emptyDrawdowns).toEqual([]);
|
||||
});
|
||||
|
||||
test('should calculate special cases correctly', () => {
|
||||
// Case with no negative returns
|
||||
const allPositiveReturns = {
|
||||
dailyReturns: Array(30).fill(0).map((_, i) => ({
|
||||
date: new Date(`2023-01-${i + 1}`),
|
||||
return: 0.01 // Always positive
|
||||
}))
|
||||
};
|
||||
|
||||
// Case with no recovery from drawdown
|
||||
const noRecoveryReturns = {
|
||||
dailyReturns: [
|
||||
...Array(30).fill(0).map((_, i) => ({
|
||||
date: new Date(`2023-01-${i + 1}`),
|
||||
return: 0.01 // Positive returns
|
||||
})),
|
||||
...Array(30).fill(0).map((_, i) => ({
|
||||
date: new Date(`2023-02-${i + 1}`),
|
||||
return: -0.005 // Negative returns with no recovery
|
||||
}))
|
||||
]
|
||||
};
|
||||
|
||||
// Act
|
||||
const positiveMetrics = PerformanceAnalytics.enhanceResults({
|
||||
...sampleResult,
|
||||
dailyReturns: allPositiveReturns.dailyReturns
|
||||
});
|
||||
|
||||
const noRecoveryDrawdowns = PerformanceAnalytics.analyzeDrawdowns(noRecoveryReturns.dailyReturns);
|
||||
|
||||
// Assert
|
||||
expect(positiveMetrics.sortinoRatio).toBe(Infinity); // No downside risk
|
||||
|
||||
// Last drawdown should have no recovery
|
||||
const lastDrawdown = noRecoveryDrawdowns[noRecoveryDrawdowns.length - 1];
|
||||
expect(lastDrawdown.recoveryDate).toBeNull();
|
||||
expect(lastDrawdown.recoveryDays).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,237 @@
|
|||
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();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
import { describe, test, expect, beforeEach, mock } from 'bun:test';
|
||||
import { MeanReversionStrategy } from '../../core/strategies/MeanReversionStrategy';
|
||||
import { BarData } from '../../core/Strategy';
|
||||
|
||||
describe('MeanReversionStrategy', () => {
|
||||
let strategy: MeanReversionStrategy;
|
||||
let mockData: BarData[];
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a strategy instance with test parameters
|
||||
strategy = new MeanReversionStrategy(
|
||||
'test_id',
|
||||
'Test Mean Reversion',
|
||||
'A test strategy',
|
||||
['AAPL'],
|
||||
{
|
||||
lookback: 20,
|
||||
entryDeviation: 1.5,
|
||||
exitDeviation: 0.5,
|
||||
lookbackPeriod: 100,
|
||||
positionSize: 0.2,
|
||||
stopLoss: 0.02,
|
||||
takeProfit: 0.05,
|
||||
useBollingerBands: true,
|
||||
bollingerPeriod: 20,
|
||||
bollingerDeviation: 2,
|
||||
rsiPeriod: 14,
|
||||
rsiOverbought: 70,
|
||||
rsiOversold: 30,
|
||||
useRsi: true
|
||||
}
|
||||
);
|
||||
|
||||
// Create mock price data
|
||||
const now = new Date();
|
||||
mockData = [];
|
||||
|
||||
// Create 100 bars of data with a mean-reverting pattern
|
||||
let price = 100;
|
||||
for (let i = 0; i < 100; i++) {
|
||||
// Add some mean reversion pattern (oscillating around 100)
|
||||
price = price + Math.sin(i / 10) * 5 + (Math.random() - 0.5) * 2;
|
||||
|
||||
mockData.push({
|
||||
symbol: 'AAPL',
|
||||
timestamp: new Date(now.getTime() - (100 - i) * 60000), // 1-minute bars
|
||||
open: price - 0.5,
|
||||
high: price + 1,
|
||||
low: price - 1,
|
||||
close: price,
|
||||
volume: 1000 + Math.random() * 1000
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('should initialize with correct parameters', () => {
|
||||
expect(strategy.id).toBe('test_id');
|
||||
expect(strategy.name).toBe('Test Mean Reversion');
|
||||
expect(strategy.description).toBe('A test strategy');
|
||||
expect(strategy.symbols).toEqual(['AAPL']);
|
||||
expect(strategy.parameters.lookback).toBe(20);
|
||||
expect(strategy.parameters.entryDeviation).toBe(1.5);
|
||||
});
|
||||
|
||||
test('should generate signals with vectorized calculation', async () => {
|
||||
// Arrange a price series with fake mean reversion
|
||||
const results = await strategy.runVectorized({
|
||||
symbols: ['AAPL'],
|
||||
data: { 'AAPL': mockData },
|
||||
initialCapital: 10000,
|
||||
startIndex: 20, // Skip the first 20 bars for indicator warmup
|
||||
endIndex: mockData.length - 1
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(results).toBeDefined();
|
||||
expect(results.positions).toBeDefined();
|
||||
// Should generate at least one trade in this artificial data
|
||||
expect(results.trades.length).toBeGreaterThan(0);
|
||||
expect(results.equityCurve.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should calculate correct entry and exit signals', () => {
|
||||
// Mock the indicator calculations to test logic directly
|
||||
// We'll create a simple scenario where price is 2 standard deviations away
|
||||
const mockBar: BarData = {
|
||||
symbol: 'AAPL',
|
||||
timestamp: new Date(),
|
||||
open: 100,
|
||||
high: 102,
|
||||
low: 98,
|
||||
close: 100,
|
||||
volume: 1000
|
||||
};
|
||||
|
||||
// Mock the calculation context
|
||||
const context = {
|
||||
mean: 100,
|
||||
stdDev: 5,
|
||||
upperBand: 110,
|
||||
lowerBand: 90,
|
||||
rsi: 25, // Oversold
|
||||
shouldEnterLong: true,
|
||||
shouldExitLong: false,
|
||||
shouldEnterShort: false,
|
||||
shouldExitShort: false
|
||||
};
|
||||
|
||||
// Call the internal signal generation logic via a protected method
|
||||
// (For testing purposes, we're accessing a protected method)
|
||||
const result = (strategy as any).calculateSignals('AAPL', mockBar, context);
|
||||
|
||||
// Assert the signals based on our scenario
|
||||
expect(result).toBeDefined();
|
||||
expect(result.action).toBe('BUY'); // Should buy in oversold condition
|
||||
});
|
||||
|
||||
test('should handle empty data correctly', async () => {
|
||||
// Act & Assert
|
||||
await expect(async () => {
|
||||
await strategy.runVectorized({
|
||||
symbols: ['AAPL'],
|
||||
data: { 'AAPL': [] },
|
||||
initialCapital: 10000,
|
||||
startIndex: 0,
|
||||
endIndex: 0
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,256 @@
|
|||
import { describe, test, expect, beforeEach } from 'bun:test';
|
||||
import { BaseStrategy } from '../../core/Strategy';
|
||||
import { StrategyRegistry, StrategyType } from '../../core/strategies/StrategyRegistry';
|
||||
import { MovingAverageCrossover } from '../../core/strategies/MovingAverageCrossover';
|
||||
import { MeanReversionStrategy } from '../../core/strategies/MeanReversionStrategy';
|
||||
import { VectorizedStrategy } from '../../core/strategies/VectorizedStrategy';
|
||||
|
||||
describe('Strategy Registry', () => {
|
||||
let registry: StrategyRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset the singleton for testing
|
||||
(StrategyRegistry as any).instance = null;
|
||||
registry = StrategyRegistry.getInstance();
|
||||
});
|
||||
|
||||
test('should create a MovingAverageCrossover strategy', () => {
|
||||
// Arrange
|
||||
const id = 'test_id';
|
||||
const name = 'Test Strategy';
|
||||
const description = 'A test strategy';
|
||||
const symbols = ['AAPL', 'MSFT'];
|
||||
const parameters = { fastPeriod: 10, slowPeriod: 30 };
|
||||
|
||||
// Act
|
||||
const strategy = registry.createStrategy(
|
||||
'MOVING_AVERAGE_CROSSOVER',
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
symbols,
|
||||
parameters
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(strategy).toBeInstanceOf(MovingAverageCrossover);
|
||||
expect(strategy.id).toEqual(id);
|
||||
expect(strategy.name).toEqual(name);
|
||||
expect(strategy.description).toEqual(description);
|
||||
expect(strategy.symbols).toEqual(symbols);
|
||||
expect(strategy.parameters).toMatchObject(parameters);
|
||||
});
|
||||
|
||||
test('should create a MeanReversion strategy', () => {
|
||||
// Arrange
|
||||
const id = 'test_id';
|
||||
const name = 'Test Strategy';
|
||||
const description = 'A test strategy';
|
||||
const symbols = ['AAPL', 'MSFT'];
|
||||
const parameters = { lookback: 20, entryDeviation: 1.5 };
|
||||
|
||||
// Act
|
||||
const strategy = registry.createStrategy(
|
||||
'MEAN_REVERSION',
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
symbols,
|
||||
parameters
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(strategy).toBeInstanceOf(MeanReversionStrategy);
|
||||
expect(strategy.id).toEqual(id);
|
||||
expect(strategy.name).toEqual(name);
|
||||
expect(strategy.description).toEqual(description);
|
||||
expect(strategy.symbols).toEqual(symbols);
|
||||
expect(strategy.parameters).toMatchObject(parameters);
|
||||
});
|
||||
|
||||
test('should throw error for invalid strategy type', () => {
|
||||
// Arrange
|
||||
const id = 'test_id';
|
||||
const name = 'Test Strategy';
|
||||
const description = 'A test strategy';
|
||||
const symbols = ['AAPL', 'MSFT'];
|
||||
const parameters = {};
|
||||
|
||||
// Act & Assert
|
||||
expect(() => {
|
||||
registry.createStrategy(
|
||||
'INVALID_TYPE' as StrategyType,
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
symbols,
|
||||
parameters
|
||||
);
|
||||
}).toThrow("Strategy type 'INVALID_TYPE' is not registered");
|
||||
});
|
||||
|
||||
test('should register a custom strategy', () => {
|
||||
// Arrange
|
||||
const mockStrategyFactory = (
|
||||
id: string,
|
||||
name: string,
|
||||
description: string,
|
||||
symbols: string[],
|
||||
parameters: any
|
||||
) => {
|
||||
return new MovingAverageCrossover(id, name, description, symbols, parameters);
|
||||
};
|
||||
|
||||
// Act
|
||||
registry.registerStrategy('CUSTOM' as StrategyType, mockStrategyFactory);
|
||||
|
||||
// Assert
|
||||
expect(registry.hasStrategyType('CUSTOM')).toBe(true);
|
||||
|
||||
const strategy = registry.createStrategy(
|
||||
'CUSTOM',
|
||||
'custom_id',
|
||||
'Custom Strategy',
|
||||
'A custom strategy',
|
||||
['BTC/USD'],
|
||||
{}
|
||||
);
|
||||
|
||||
expect(strategy).toBeInstanceOf(MovingAverageCrossover);
|
||||
});
|
||||
|
||||
test('should get default parameters for a strategy type', () => {
|
||||
// Act
|
||||
const macParams = registry.getDefaultParameters('MOVING_AVERAGE_CROSSOVER');
|
||||
const mrParams = registry.getDefaultParameters('MEAN_REVERSION');
|
||||
|
||||
// Assert
|
||||
expect(macParams).toHaveProperty('fastPeriod');
|
||||
expect(macParams).toHaveProperty('slowPeriod');
|
||||
expect(mrParams).toHaveProperty('lookback');
|
||||
expect(mrParams).toHaveProperty('entryDeviation');
|
||||
});
|
||||
|
||||
test('should return empty object for unknown strategy default parameters', () => {
|
||||
// Act
|
||||
const params = registry.getDefaultParameters('CUSTOM' as StrategyType);
|
||||
|
||||
// Assert
|
||||
expect(params).toEqual({});
|
||||
});
|
||||
|
||||
test('should get all registered strategy types', () => {
|
||||
// Act
|
||||
const types = registry.getStrategyTypes();
|
||||
|
||||
// Assert
|
||||
expect(types).toContain('MOVING_AVERAGE_CROSSOVER');
|
||||
expect(types).toContain('MEAN_REVERSION');
|
||||
});
|
||||
|
||||
test('should check if strategy type is registered', () => {
|
||||
// Act & Assert
|
||||
expect(registry.hasStrategyType('MOVING_AVERAGE_CROSSOVER')).toBe(true);
|
||||
expect(registry.hasStrategyType('INVALID_TYPE' as StrategyType)).toBe(false);
|
||||
});
|
||||
|
||||
test('should get all registered strategies', () => {
|
||||
// Arrange
|
||||
registry.createStrategy(
|
||||
'MOVING_AVERAGE_CROSSOVER',
|
||||
'mac_id',
|
||||
'MAC Strategy',
|
||||
'MAC strategy',
|
||||
['AAPL'],
|
||||
{}
|
||||
);
|
||||
|
||||
registry.createStrategy(
|
||||
'MEAN_REVERSION',
|
||||
'mr_id',
|
||||
'MR Strategy',
|
||||
'MR strategy',
|
||||
['MSFT'],
|
||||
{}
|
||||
);
|
||||
|
||||
// Act
|
||||
const strategies = registry.getAllStrategies();
|
||||
|
||||
// Assert
|
||||
expect(strategies).toHaveLength(2);
|
||||
expect(strategies[0].id).toEqual('mac_id');
|
||||
expect(strategies[1].id).toEqual('mr_id');
|
||||
});
|
||||
|
||||
test('should get strategy by ID', () => {
|
||||
// Arrange
|
||||
registry.createStrategy(
|
||||
'MOVING_AVERAGE_CROSSOVER',
|
||||
'mac_id',
|
||||
'MAC Strategy',
|
||||
'MAC strategy',
|
||||
['AAPL'],
|
||||
{}
|
||||
);
|
||||
|
||||
// Act
|
||||
const strategy = registry.getStrategyById('mac_id');
|
||||
const nonExistent = registry.getStrategyById('non_existent');
|
||||
|
||||
// Assert
|
||||
expect(strategy).not.toBeNull();
|
||||
expect(strategy?.id).toEqual('mac_id');
|
||||
expect(nonExistent).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should delete strategy by ID', () => {
|
||||
// Arrange
|
||||
registry.createStrategy(
|
||||
'MOVING_AVERAGE_CROSSOVER',
|
||||
'mac_id',
|
||||
'MAC Strategy',
|
||||
'MAC strategy',
|
||||
['AAPL'],
|
||||
{}
|
||||
);
|
||||
|
||||
// Act
|
||||
const result1 = registry.deleteStrategy('mac_id');
|
||||
const result2 = registry.deleteStrategy('non_existent');
|
||||
|
||||
// Assert
|
||||
expect(result1).toBe(true);
|
||||
expect(result2).toBe(false);
|
||||
expect(registry.getStrategyById('mac_id')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should identify strategy type from instance', () => {
|
||||
// Arrange
|
||||
const macStrategy = registry.createStrategy(
|
||||
'MOVING_AVERAGE_CROSSOVER',
|
||||
'mac_id',
|
||||
'MAC Strategy',
|
||||
'MAC strategy',
|
||||
['AAPL'],
|
||||
{}
|
||||
);
|
||||
|
||||
const mrStrategy = registry.createStrategy(
|
||||
'MEAN_REVERSION',
|
||||
'mr_id',
|
||||
'MR Strategy',
|
||||
'MR strategy',
|
||||
['MSFT'],
|
||||
{}
|
||||
);
|
||||
|
||||
// Act
|
||||
const macType = registry.getStrategyType(macStrategy);
|
||||
const mrType = registry.getStrategyType(mrStrategy);
|
||||
|
||||
// Assert
|
||||
expect(macType).toEqual('MOVING_AVERAGE_CROSSOVER');
|
||||
expect(mrType).toEqual('MEAN_REVERSION');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue