stock-bot/apps/stock/orchestrator/test-mean-reversion.ts

180 lines
No EOL
6.4 KiB
TypeScript

import { RustBacktestAdapter } from './src/backtest/RustBacktestAdapter';
import { IServiceContainer } from '@stock-bot/di';
import { BacktestConfig } from './src/types';
// Mock container
const mockContainer: IServiceContainer = {
logger: {
info: console.log,
error: console.error,
warn: console.warn,
debug: console.log,
},
mongodb: {} as any,
postgres: {} as any,
redis: {} as any,
custom: {},
} as IServiceContainer;
// Mock storage service that returns test data
class MockStorageService {
async getHistoricalBars(symbol: string, startDate: Date, endDate: Date, frequency: string) {
console.log(`MockStorageService: Getting bars for ${symbol} from ${startDate} to ${endDate}`);
// Generate test data with mean reverting behavior
const bars = [];
const startTime = startDate.getTime();
const endTime = endDate.getTime();
const dayMs = 24 * 60 * 60 * 1000;
let time = startTime;
let dayIndex = 0;
// Base prices for different symbols
const basePrices = {
'AAPL': 150,
'GOOGL': 2800,
'MSFT': 400,
};
const basePrice = basePrices[symbol as keyof typeof basePrices] || 100;
while (time <= endTime) {
// Create mean reverting price movement
// Price oscillates around the base price with increasing then decreasing deviations
const cycleLength = 40; // 40 day cycle
const positionInCycle = dayIndex % cycleLength;
const halfCycle = cycleLength / 2;
let deviation;
if (positionInCycle < halfCycle) {
// First half: price moves away from mean
deviation = (positionInCycle / halfCycle) * 0.1; // Up to 10% deviation
} else {
// Second half: price reverts to mean
deviation = ((cycleLength - positionInCycle) / halfCycle) * 0.1;
}
// Alternate between above and below mean
const cycleNumber = Math.floor(dayIndex / cycleLength);
const multiplier = cycleNumber % 2 === 0 ? 1 : -1;
const price = basePrice * (1 + multiplier * deviation);
// Add some noise
const noise = (Math.random() - 0.5) * 0.02 * basePrice;
const finalPrice = price + noise;
bars.push({
timestamp: new Date(time),
open: finalPrice * 0.99,
high: finalPrice * 1.01,
low: finalPrice * 0.98,
close: finalPrice,
volume: 1000000,
vwap: finalPrice,
});
time += dayMs;
dayIndex++;
}
console.log(`Generated ${bars.length} bars for ${symbol}, first close: ${bars[0].close.toFixed(2)}, last close: ${bars[bars.length - 1].close.toFixed(2)}`);
return bars;
}
}
// Test the backtest
async function testMeanReversionBacktest() {
console.log('=== Testing Mean Reversion Backtest ===\n');
// Create adapter with mock storage
const adapter = new RustBacktestAdapter(mockContainer);
(adapter as any).storageService = new MockStorageService();
const config: BacktestConfig = {
name: 'Mean Reversion Test',
strategy: 'mean_reversion',
symbols: ['AAPL', 'GOOGL', 'MSFT'],
startDate: '2024-01-01T00:00:00Z',
endDate: '2024-06-01T00:00:00Z',
initialCapital: 100000,
commission: 0.001,
slippage: 0.0001,
dataFrequency: '1d',
config: {
lookbackPeriod: 20,
entryThreshold: 2.0,
positionSize: 100,
},
};
try {
console.log('Starting backtest...\n');
const result = await adapter.runBacktest(config);
console.log('\n=== Backtest Results ===');
console.log(`Status: ${result.status}`);
console.log(`Total Trades: ${result.metrics.totalTrades}`);
console.log(`Profitable Trades: ${result.metrics.profitableTrades}`);
console.log(`Win Rate: ${result.metrics.winRate.toFixed(2)}%`);
console.log(`Total Return: ${result.metrics.totalReturn.toFixed(2)}%`);
console.log(`Sharpe Ratio: ${result.metrics.sharpeRatio.toFixed(2)}`);
console.log(`Max Drawdown: ${result.metrics.maxDrawdown.toFixed(2)}%`);
console.log('\n=== Trade Analysis ===');
console.log(`Number of completed trades: ${result.trades.length}`);
// Analyze trades by symbol
const tradesBySymbol: Record<string, any[]> = {};
result.trades.forEach(trade => {
if (!tradesBySymbol[trade.symbol]) {
tradesBySymbol[trade.symbol] = [];
}
tradesBySymbol[trade.symbol].push(trade);
});
Object.entries(tradesBySymbol).forEach(([symbol, trades]) => {
console.log(`\n${symbol}: ${trades.length} trades`);
const longTrades = trades.filter(t => t.side === 'long');
const shortTrades = trades.filter(t => t.side === 'short');
console.log(` - Long trades: ${longTrades.length}`);
console.log(` - Short trades: ${shortTrades.length}`);
// Count buy/sell pairs
const buyTrades = trades.filter(t => t.side === 'buy');
const sellTrades = trades.filter(t => t.side === 'sell');
console.log(` - Buy trades: ${buyTrades.length}`);
console.log(` - Sell trades: ${sellTrades.length}`);
// Show first few trades
console.log(` - First 3 trades:`);
trades.slice(0, 3).forEach((trade, idx) => {
console.log(` ${idx + 1}. ${trade.side} - Price: $${trade.price.toFixed(2)}, Quantity: ${trade.quantity}${trade.pnl ? `, PnL: $${trade.pnl.toFixed(2)}` : ''}`);
});
});
// Check position distribution
const allDurations = result.trades.map(t => t.duration / 86400); // Convert to days
const avgDuration = allDurations.reduce((a, b) => a + b, 0) / allDurations.length;
const minDuration = Math.min(...allDurations);
const maxDuration = Math.max(...allDurations);
console.log('\n=== Duration Analysis ===');
console.log(`Average trade duration: ${avgDuration.toFixed(1)} days`);
console.log(`Min duration: ${minDuration.toFixed(1)} days`);
console.log(`Max duration: ${maxDuration.toFixed(1)} days`);
// Final positions
console.log('\n=== Final Positions ===');
Object.entries(result.finalPositions).forEach(([symbol, position]) => {
console.log(`${symbol}: ${position}`);
});
} catch (error) {
console.error('Backtest failed:', error);
}
}
// Run the test
testMeanReversionBacktest().catch(console.error);