270 lines
No EOL
8.5 KiB
TypeScript
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 {}
|
|
} |