added initial py analytics / rust core / ts orchestrator services
This commit is contained in:
parent
680b5fd2ae
commit
c862ed496b
62 changed files with 13459 additions and 0 deletions
255
apps/stock/orchestrator/src/strategies/BaseStrategy.ts
Normal file
255
apps/stock/orchestrator/src/strategies/BaseStrategy.ts
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
import { EventEmitter } from 'events';
|
||||
import { logger } from '@stock-bot/logger';
|
||||
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) 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;
|
||||
|
||||
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 {
|
||||
// Simplified - in reality would calculate based on positions and market values
|
||||
return 100000 + this.performance.totalPnl; // Assuming 100k starting capital
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Simple logic - can be overridden by specific strategies
|
||||
if (signal.type === 'buy' && currentPosition <= 0) {
|
||||
return {
|
||||
symbol: signal.symbol,
|
||||
side: 'buy',
|
||||
quantity: this.calculatePositionSize(signal),
|
||||
orderType: 'market',
|
||||
timeInForce: 'DAY'
|
||||
};
|
||||
} else if (signal.type === 'sell' && currentPosition >= 0) {
|
||||
return {
|
||||
symbol: signal.symbol,
|
||||
side: 'sell',
|
||||
quantity: this.calculatePositionSize(signal),
|
||||
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 {}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue