import { EventEmitter } from 'events'; import { getLogger } from '@stock-bot/logger'; const logger = getLogger('BaseStrategy'); import { MarketData, StrategyConfig, OrderRequest } from '../types'; import { ModeManager } from '../core/ModeManager'; import { ExecutionService } from '../services/ExecutionService'; export interface Signal { type: 'buy' | 'sell' | 'close'; symbol: string; strength: number; // -1 to 1 reason?: string; metadata?: Record; } export abstract class BaseStrategy extends EventEmitter { protected config: StrategyConfig; protected isActive = false; protected positions = new Map(); protected pendingOrders = new Map(); protected performance = { trades: 0, wins: 0, losses: 0, totalPnl: 0, maxDrawdown: 0, currentDrawdown: 0, peakEquity: 0 }; constructor( config: StrategyConfig, protected modeManager: ModeManager, protected executionService: ExecutionService ) { super(); this.config = config; } async initialize(): Promise { logger.info(`Initializing strategy: ${this.config.name}`); // Subscribe to symbols for (const symbol of this.config.symbols) { // Note: In real implementation, would subscribe through market data service logger.debug(`Strategy ${this.config.id} subscribed to ${symbol}`); } } async start(): Promise { this.isActive = true; logger.info(`Started strategy: ${this.config.name}`); this.onStart(); } async stop(): Promise { this.isActive = false; // Cancel pending orders for (const [orderId, order] of this.pendingOrders) { await this.executionService.cancelOrder(orderId); } this.pendingOrders.clear(); logger.info(`Stopped strategy: ${this.config.name}`); this.onStop(); } async shutdown(): Promise { await this.stop(); this.removeAllListeners(); logger.info(`Shutdown strategy: ${this.config.name}`); } // Market data handling async onMarketData(data: MarketData): Promise { if (!this.isActive) { logger.info(`[BaseStrategy] Strategy ${this.config.id} received market data but is not active`); return; } try { // Update any indicators or state this.updateIndicators(data); // Generate signals const signal = await this.generateSignal(data); if (signal) { this.emit('signal', signal); // Convert signal to order if strong enough const order = await this.signalToOrder(signal); if (order) { this.emit('order', order); } } } catch (error) { logger.error(`Strategy ${this.config.id} error:`, error); } } async onMarketDataBatch(batch: MarketData[]): Promise { // Default implementation processes individually // Strategies can override for more efficient batch processing for (const data of batch) { await this.onMarketData(data); } } // Order and fill handling async onOrderUpdate(update: any): Promise { logger.debug(`Strategy ${this.config.id} order update:`, update); if (update.status === 'filled') { // Remove from pending this.pendingOrders.delete(update.orderId); // Update position tracking const fill = update.fills[0]; // Assuming single fill for simplicity if (fill) { const currentPos = this.positions.get(update.symbol) || 0; const newPos = update.side === 'buy' ? currentPos + fill.quantity : currentPos - fill.quantity; logger.info(`[BaseStrategy] Position update for ${update.symbol}: ${currentPos} -> ${newPos} (${update.side} ${fill.quantity})`); if (Math.abs(newPos) < 0.0001) { this.positions.delete(update.symbol); } else { this.positions.set(update.symbol, newPos); } } } else if (update.status === 'rejected' || update.status === 'cancelled') { this.pendingOrders.delete(update.orderId); } } async onOrderError(order: OrderRequest, error: any): Promise { logger.error(`Strategy ${this.config.id} order error:`, error); // Strategies can override to handle errors } async onFill(fill: any): Promise { // Update performance metrics this.performance.trades++; if (fill.pnl > 0) { this.performance.wins++; } else if (fill.pnl < 0) { this.performance.losses++; } this.performance.totalPnl += fill.pnl; // Update drawdown const currentEquity = this.getEquity(); if (currentEquity > this.performance.peakEquity) { this.performance.peakEquity = currentEquity; this.performance.currentDrawdown = 0; } else { this.performance.currentDrawdown = (this.performance.peakEquity - currentEquity) / this.performance.peakEquity; this.performance.maxDrawdown = Math.max(this.performance.maxDrawdown, this.performance.currentDrawdown); } } // Configuration async updateConfig(updates: Partial): Promise { this.config = { ...this.config, ...updates }; logger.info(`Updated config for strategy ${this.config.id}`); // Strategies can override to handle specific config changes this.onConfigUpdate(updates); } // Helper methods isInterestedInSymbol(symbol: string): boolean { return this.config.symbols.includes(symbol); } hasPosition(symbol: string): boolean { return this.positions.has(symbol) && Math.abs(this.positions.get(symbol)!) > 0.0001; } getPosition(symbol: string): number { return this.positions.get(symbol) || 0; } getPerformance(): any { const winRate = this.performance.trades > 0 ? (this.performance.wins / this.performance.trades) * 100 : 0; return { ...this.performance, winRate, averagePnl: this.performance.trades > 0 ? this.performance.totalPnl / this.performance.trades : 0 }; } protected getEquity(): number { // Get initial capital from config parameters or default const initialCapital = this.config.parameters?.initialCapital || 100000; return initialCapital + this.performance.totalPnl; } protected async signalToOrder(signal: Signal): Promise { // Only act on strong signals if (Math.abs(signal.strength) < 0.7) return null; // Check if we already have a position const currentPosition = this.getPosition(signal.symbol); // Use quantity from signal metadata if provided const quantity = signal.metadata?.quantity || this.calculatePositionSize(signal); logger.info(`[BaseStrategy] Converting signal to order: ${signal.type} ${quantity} ${signal.symbol}, current position: ${currentPosition}`); // Simple logic - can be overridden by specific strategies if (signal.type === 'buy') { // Allow buying to open long or close short return { symbol: signal.symbol, side: 'buy', quantity: quantity, orderType: 'market', timeInForce: 'DAY' }; } else if (signal.type === 'sell') { // Allow selling to close long or open short return { symbol: signal.symbol, side: 'sell', quantity: quantity, orderType: 'market', timeInForce: 'DAY' }; } else if (signal.type === 'close' && currentPosition !== 0) { return { symbol: signal.symbol, side: currentPosition > 0 ? 'sell' : 'buy', quantity: Math.abs(currentPosition), orderType: 'market', timeInForce: 'DAY' }; } return null; } protected calculatePositionSize(signal: Signal): number { // Simple fixed size - strategies should override with proper position sizing const baseSize = 100; // 100 shares const allocation = this.config.allocation || 1.0; return Math.floor(baseSize * allocation * Math.abs(signal.strength)); } // Abstract methods that strategies must implement protected abstract updateIndicators(data: MarketData): void; protected abstract generateSignal(data: MarketData): Promise; // Optional hooks for strategies to override protected onStart(): void {} protected onStop(): void {} protected onConfigUpdate(updates: Partial): void {} }