stock-bot/apps/strategy-service/src/backtesting/modes/hybrid-mode.ts
2025-06-13 13:38:02 -04:00

424 lines
15 KiB
TypeScript

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<string, number[]> = 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<void> {
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<BacktestResult> {
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<BacktestResult> {
// 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<BacktestResult> {
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<BacktestResult> {
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<void> {
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<string, number> = {};
for (const [name, values] of this.precomputedIndicators.entries()) {
if (index < values.length) {
indicators[name] = values[index];
}
}
return indicators;
};
}
private async estimateDataSize(): Promise<number> {
// 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<any[]> {
// 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<void> {
await super.cleanup();
await this.eventMode.cleanup();
await this.vectorizedMode.cleanup();
this.precomputedIndicators.clear();
this.logger.info('Hybrid mode cleanup completed');
}
}
export default HybridMode;