stock-bot/apps/stock/orchestrator/src/strategies/BaseStrategy.ts

270 lines
No EOL
8.5 KiB
TypeScript

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<string, any>;
}
export abstract class BaseStrategy extends EventEmitter {
protected config: StrategyConfig;
protected isActive = false;
protected positions = new Map<string, number>();
protected pendingOrders = new Map<string, OrderRequest>();
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<void> {
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<void> {
this.isActive = true;
logger.info(`Started strategy: ${this.config.name}`);
this.onStart();
}
async stop(): Promise<void> {
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<void> {
await this.stop();
this.removeAllListeners();
logger.info(`Shutdown strategy: ${this.config.name}`);
}
// Market data handling
async onMarketData(data: MarketData): Promise<void> {
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<void> {
// 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<void> {
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<void> {
logger.error(`Strategy ${this.config.id} order error:`, error);
// Strategies can override to handle errors
}
async onFill(fill: any): Promise<void> {
// 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<StrategyConfig>): Promise<void> {
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<OrderRequest | null> {
// 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<Signal | null>;
// Optional hooks for strategies to override
protected onStart(): void {}
protected onStop(): void {}
protected onConfigUpdate(updates: Partial<StrategyConfig>): void {}
}