180 lines
No EOL
6.4 KiB
TypeScript
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); |