import { DataFrame } from '@stock-bot/data-frame'; import { EventBus } from '@stock-bot/event-bus'; import { getLogger } from '@stock-bot/logger'; import { VectorEngine, VectorizedBacktestResult } from '@stock-bot/vector-engine'; import { BacktestContext, BacktestResult, ExecutionMode } from '../framework/execution-mode'; import { EventMode } from './event-mode'; import VectorizedMode from './vectorized-mode'; export interface HybridModeConfig { vectorizedThreshold: number; // Switch to vectorized if data points > threshold warmupPeriod: number; // Number of periods for initial vectorized calculation eventDrivenRealtime: boolean; // Use event-driven for real-time portions optimizeIndicators: boolean; // Pre-calculate indicators vectorized batchSize: number; // Size of batches for hybrid processing } export class HybridMode extends ExecutionMode { private vectorEngine: VectorEngine; private eventMode: EventMode; private vectorizedMode: VectorizedMode; private config: HybridModeConfig; private precomputedIndicators: Map = new Map(); private currentIndex: number = 0; constructor(context: BacktestContext, eventBus: EventBus, config: HybridModeConfig = {}) { super(context, eventBus); this.config = { vectorizedThreshold: 50000, warmupPeriod: 1000, eventDrivenRealtime: true, optimizeIndicators: true, batchSize: 10000, ...config, }; this.vectorEngine = new VectorEngine(); this.eventMode = new EventMode(context, eventBus); this.vectorizedMode = new VectorizedMode(context, eventBus); this.logger = getLogger('hybrid-mode'); } async initialize(): Promise { await super.initialize(); // Initialize both modes await this.eventMode.initialize(); await this.vectorizedMode.initialize(); this.logger.info('Hybrid mode initialized', { backtestId: this.context.backtestId, config: this.config, }); } async execute(): Promise { const startTime = Date.now(); this.logger.info('Starting hybrid backtest execution'); try { // Determine execution strategy based on data size const dataSize = await this.estimateDataSize(); if (dataSize <= this.config.vectorizedThreshold) { // Small dataset: use pure vectorized approach this.logger.info('Using pure vectorized approach for small dataset', { dataSize }); return await this.vectorizedMode.execute(); } // Large dataset: use hybrid approach this.logger.info('Using hybrid approach for large dataset', { dataSize }); return await this.executeHybrid(startTime); } catch (error) { this.logger.error('Hybrid backtest failed', { error, backtestId: this.context.backtestId, }); await this.eventBus.publishBacktestUpdate(this.context.backtestId, 0, { status: 'failed', error: error.message, }); throw error; } } private async executeHybrid(startTime: number): Promise { // Phase 1: Vectorized warmup and indicator pre-computation const warmupResult = await this.executeWarmupPhase(); // Phase 2: Event-driven processing with pre-computed indicators const eventResult = await this.executeEventPhase(warmupResult); // Phase 3: Combine results const combinedResult = this.combineResults(warmupResult, eventResult, startTime); await this.eventBus.publishBacktestUpdate(this.context.backtestId, 100, { status: 'completed', result: combinedResult, }); this.logger.info('Hybrid backtest completed', { backtestId: this.context.backtestId, duration: Date.now() - startTime, totalTrades: combinedResult.trades.length, warmupTrades: warmupResult.trades.length, eventTrades: eventResult.trades.length, }); return combinedResult; } private async executeWarmupPhase(): Promise { this.logger.info('Executing vectorized warmup phase', { warmupPeriod: this.config.warmupPeriod, }); // Load warmup data const warmupData = await this.loadWarmupData(); const dataFrame = this.createDataFrame(warmupData); // Pre-compute indicators for entire dataset if optimization is enabled if (this.config.optimizeIndicators) { await this.precomputeIndicators(dataFrame); } // Run vectorized backtest on warmup period const strategyCode = this.generateStrategyCode(); const vectorResult = await this.vectorEngine.executeVectorizedStrategy( dataFrame.head(this.config.warmupPeriod), strategyCode ); // Convert to standard format return this.convertVectorizedResult(vectorResult, Date.now()); } private async executeEventPhase(warmupResult: BacktestResult): Promise { this.logger.info('Executing event-driven phase'); // Set up event mode with warmup context this.currentIndex = this.config.warmupPeriod; // Create modified context for event phase const eventContext: BacktestContext = { ...this.context, initialPortfolio: this.extractFinalPortfolio(warmupResult), }; // Execute event-driven backtest for remaining data const eventMode = new EventMode(eventContext, this.eventBus); await eventMode.initialize(); // Override indicator calculations to use pre-computed values if (this.config.optimizeIndicators) { this.overrideIndicatorCalculations(eventMode); } return await eventMode.execute(); } private async precomputeIndicators(dataFrame: DataFrame): Promise { this.logger.info('Pre-computing indicators vectorized'); const close = dataFrame.getColumn('close'); const high = dataFrame.getColumn('high'); const low = dataFrame.getColumn('low'); // Import technical indicators from vector engine const { TechnicalIndicators } = await import('@stock-bot/vector-engine'); // Pre-compute common indicators this.precomputedIndicators.set('sma_20', TechnicalIndicators.sma(close, 20)); this.precomputedIndicators.set('sma_50', TechnicalIndicators.sma(close, 50)); this.precomputedIndicators.set('ema_12', TechnicalIndicators.ema(close, 12)); this.precomputedIndicators.set('ema_26', TechnicalIndicators.ema(close, 26)); this.precomputedIndicators.set('rsi', TechnicalIndicators.rsi(close)); this.precomputedIndicators.set('atr', TechnicalIndicators.atr(high, low, close)); const macd = TechnicalIndicators.macd(close); this.precomputedIndicators.set('macd', macd.macd); this.precomputedIndicators.set('macd_signal', macd.signal); this.precomputedIndicators.set('macd_histogram', macd.histogram); const bb = TechnicalIndicators.bollingerBands(close); this.precomputedIndicators.set('bb_upper', bb.upper); this.precomputedIndicators.set('bb_middle', bb.middle); this.precomputedIndicators.set('bb_lower', bb.lower); this.logger.info('Indicators pre-computed', { indicators: Array.from(this.precomputedIndicators.keys()), }); } private overrideIndicatorCalculations(eventMode: EventMode): void { // Override the event mode's indicator calculations to use pre-computed values // This is a simplified approach - in production you'd want a more sophisticated interface const _originalCalculateIndicators = (eventMode as any).calculateIndicators; (eventMode as any).calculateIndicators = (symbol: string, index: number) => { const indicators: Record = {}; for (const [name, values] of this.precomputedIndicators.entries()) { if (index < values.length) { indicators[name] = values[index]; } } return indicators; }; } private async estimateDataSize(): Promise { // Estimate the number of data points for the backtest period const startTime = new Date(this.context.startDate).getTime(); const endTime = new Date(this.context.endDate).getTime(); const timeRange = endTime - startTime; // Assume 1-minute intervals (60000ms) const estimatedPoints = Math.floor(timeRange / 60000); this.logger.debug('Estimated data size', { timeRange, estimatedPoints, threshold: this.config.vectorizedThreshold, }); return estimatedPoints; } private async loadWarmupData(): Promise { // Load historical data for warmup phase // This should load more data than just the warmup period for indicator calculations const data = []; const startTime = new Date(this.context.startDate).getTime(); const warmupEndTime = startTime + this.config.warmupPeriod * 60000; // Add extra lookback for indicator calculations const lookbackTime = startTime - 200 * 60000; // 200 periods lookback for (let timestamp = lookbackTime; timestamp <= warmupEndTime; timestamp += 60000) { const basePrice = 100 + Math.sin(timestamp / 1000000) * 10; const volatility = 0.02; const open = basePrice + (Math.random() - 0.5) * volatility * basePrice; const close = open + (Math.random() - 0.5) * volatility * basePrice; const high = Math.max(open, close) + Math.random() * volatility * basePrice; const low = Math.min(open, close) - Math.random() * volatility * basePrice; const volume = Math.floor(Math.random() * 10000) + 1000; data.push({ timestamp, symbol: this.context.symbol, open, high, low, close, volume, }); } return data; } private createDataFrame(data: any[]): DataFrame { return new DataFrame(data, { columns: ['timestamp', 'symbol', 'open', 'high', 'low', 'close', 'volume'], dtypes: { timestamp: 'number', symbol: 'string', open: 'number', high: 'number', low: 'number', close: 'number', volume: 'number', }, }); } private generateStrategyCode(): string { // Generate strategy code based on context const strategy = this.context.strategy; if (strategy.type === 'sma_crossover') { return 'sma_crossover'; } return strategy.code || 'sma_crossover'; } private convertVectorizedResult( vectorResult: VectorizedBacktestResult, startTime: number ): BacktestResult { return { backtestId: this.context.backtestId, strategy: this.context.strategy, symbol: this.context.symbol, startDate: this.context.startDate, endDate: this.context.endDate, mode: 'hybrid-vectorized', duration: Date.now() - startTime, trades: vectorResult.trades.map(trade => ({ id: `trade_${trade.entryIndex}_${trade.exitIndex}`, symbol: this.context.symbol, side: trade.side, entryTime: vectorResult.timestamps[trade.entryIndex], exitTime: vectorResult.timestamps[trade.exitIndex], entryPrice: trade.entryPrice, exitPrice: trade.exitPrice, quantity: trade.quantity, pnl: trade.pnl, commission: 0, slippage: 0, })), performance: { totalReturn: vectorResult.metrics.totalReturns, sharpeRatio: vectorResult.metrics.sharpeRatio, maxDrawdown: vectorResult.metrics.maxDrawdown, winRate: vectorResult.metrics.winRate, profitFactor: vectorResult.metrics.profitFactor, totalTrades: vectorResult.metrics.totalTrades, winningTrades: vectorResult.trades.filter(t => t.pnl > 0).length, losingTrades: vectorResult.trades.filter(t => t.pnl <= 0).length, avgTrade: vectorResult.metrics.avgTrade, avgWin: vectorResult.trades.filter(t => t.pnl > 0).reduce((sum, t) => sum + t.pnl, 0) / vectorResult.trades.filter(t => t.pnl > 0).length || 0, avgLoss: vectorResult.trades.filter(t => t.pnl <= 0).reduce((sum, t) => sum + t.pnl, 0) / vectorResult.trades.filter(t => t.pnl <= 0).length || 0, largestWin: Math.max(...vectorResult.trades.map(t => t.pnl), 0), largestLoss: Math.min(...vectorResult.trades.map(t => t.pnl), 0), }, equity: vectorResult.equity, drawdown: vectorResult.metrics.drawdown, metadata: { mode: 'hybrid-vectorized', dataPoints: vectorResult.timestamps.length, signals: Object.keys(vectorResult.signals), optimizations: ['vectorized_warmup', 'precomputed_indicators'], }, }; } private extractFinalPortfolio(warmupResult: BacktestResult): any { // Extract the final portfolio state from warmup phase const finalEquity = warmupResult.equity[warmupResult.equity.length - 1] || 10000; return { cash: finalEquity, positions: [], // Simplified - in production would track actual positions equity: finalEquity, }; } private combineResults( warmupResult: BacktestResult, eventResult: BacktestResult, startTime: number ): BacktestResult { // Combine results from both phases const combinedTrades = [...warmupResult.trades, ...eventResult.trades]; const combinedEquity = [...warmupResult.equity, ...eventResult.equity]; const combinedDrawdown = [...(warmupResult.drawdown || []), ...(eventResult.drawdown || [])]; // Recalculate combined performance metrics const totalPnL = combinedTrades.reduce((sum, trade) => sum + trade.pnl, 0); const winningTrades = combinedTrades.filter(t => t.pnl > 0); const losingTrades = combinedTrades.filter(t => t.pnl <= 0); const grossProfit = winningTrades.reduce((sum, t) => sum + t.pnl, 0); const grossLoss = Math.abs(losingTrades.reduce((sum, t) => sum + t.pnl, 0)); return { backtestId: this.context.backtestId, strategy: this.context.strategy, symbol: this.context.symbol, startDate: this.context.startDate, endDate: this.context.endDate, mode: 'hybrid', duration: Date.now() - startTime, trades: combinedTrades, performance: { totalReturn: (combinedEquity[combinedEquity.length - 1] - combinedEquity[0]) / combinedEquity[0], sharpeRatio: eventResult.performance.sharpeRatio, // Use event result for more accurate calculation maxDrawdown: Math.max(...combinedDrawdown), winRate: winningTrades.length / combinedTrades.length, profitFactor: grossLoss !== 0 ? grossProfit / grossLoss : Infinity, totalTrades: combinedTrades.length, winningTrades: winningTrades.length, losingTrades: losingTrades.length, avgTrade: totalPnL / combinedTrades.length, avgWin: grossProfit / winningTrades.length || 0, avgLoss: grossLoss / losingTrades.length || 0, largestWin: Math.max(...combinedTrades.map(t => t.pnl), 0), largestLoss: Math.min(...combinedTrades.map(t => t.pnl), 0), }, equity: combinedEquity, drawdown: combinedDrawdown, metadata: { mode: 'hybrid', phases: ['vectorized-warmup', 'event-driven'], warmupPeriod: this.config.warmupPeriod, optimizations: ['precomputed_indicators', 'hybrid_execution'], warmupTrades: warmupResult.trades.length, eventTrades: eventResult.trades.length, }, }; } async cleanup(): Promise { await super.cleanup(); await this.eventMode.cleanup(); await this.vectorizedMode.cleanup(); this.precomputedIndicators.clear(); this.logger.info('Hybrid mode cleanup completed'); } } export default HybridMode;