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 {}
|
||||
}
|
||||
276
apps/stock/orchestrator/src/strategies/StrategyManager.ts
Normal file
276
apps/stock/orchestrator/src/strategies/StrategyManager.ts
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
import { logger } from '@stock-bot/logger';
|
||||
import { EventEmitter } from 'events';
|
||||
import { MarketData, StrategyConfig, OrderRequest } from '../types';
|
||||
import { BaseStrategy } from './BaseStrategy';
|
||||
import { ModeManager } from '../core/ModeManager';
|
||||
import { MarketDataService } from '../services/MarketDataService';
|
||||
import { ExecutionService } from '../services/ExecutionService';
|
||||
import { TradingEngine } from '../../core';
|
||||
|
||||
export class StrategyManager extends EventEmitter {
|
||||
private strategies = new Map<string, BaseStrategy>();
|
||||
private activeStrategies = new Set<string>();
|
||||
private tradingEngine: TradingEngine | null = null;
|
||||
|
||||
constructor(
|
||||
private modeManager: ModeManager,
|
||||
private marketDataService: MarketDataService,
|
||||
private executionService: ExecutionService
|
||||
) {
|
||||
super();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
private setupEventListeners(): void {
|
||||
// Listen for market data
|
||||
this.marketDataService.on('marketData', (data: MarketData) => {
|
||||
this.handleMarketData(data);
|
||||
});
|
||||
|
||||
// Listen for market data batches (more efficient)
|
||||
this.marketDataService.on('marketDataBatch', (batch: MarketData[]) => {
|
||||
this.handleMarketDataBatch(batch);
|
||||
});
|
||||
|
||||
// Listen for fills
|
||||
this.executionService.on('fill', (fill: any) => {
|
||||
this.handleFill(fill);
|
||||
});
|
||||
}
|
||||
|
||||
async initializeStrategies(configs: StrategyConfig[]): Promise<void> {
|
||||
// Clear existing strategies
|
||||
for (const [id, strategy] of this.strategies) {
|
||||
await strategy.shutdown();
|
||||
}
|
||||
this.strategies.clear();
|
||||
this.activeStrategies.clear();
|
||||
|
||||
// Get trading engine from mode manager
|
||||
this.tradingEngine = this.modeManager.getTradingEngine();
|
||||
|
||||
// Initialize new strategies
|
||||
for (const config of configs) {
|
||||
try {
|
||||
const strategy = await this.createStrategy(config);
|
||||
this.strategies.set(config.id, strategy);
|
||||
|
||||
if (config.enabled) {
|
||||
await this.enableStrategy(config.id);
|
||||
}
|
||||
|
||||
logger.info(`Initialized strategy: ${config.name} (${config.id})`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to initialize strategy ${config.name}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async createStrategy(config: StrategyConfig): Promise<BaseStrategy> {
|
||||
// In a real system, this would dynamically load strategy classes
|
||||
// For now, create a base strategy instance
|
||||
const strategy = new BaseStrategy(
|
||||
config,
|
||||
this.modeManager,
|
||||
this.executionService
|
||||
);
|
||||
|
||||
// Set up strategy event handlers
|
||||
strategy.on('signal', (signal: any) => {
|
||||
this.handleStrategySignal(config.id, signal);
|
||||
});
|
||||
|
||||
strategy.on('order', (order: OrderRequest) => {
|
||||
this.handleStrategyOrder(config.id, order);
|
||||
});
|
||||
|
||||
await strategy.initialize();
|
||||
|
||||
return strategy;
|
||||
}
|
||||
|
||||
async enableStrategy(strategyId: string): Promise<void> {
|
||||
const strategy = this.strategies.get(strategyId);
|
||||
if (!strategy) {
|
||||
throw new Error(`Strategy ${strategyId} not found`);
|
||||
}
|
||||
|
||||
await strategy.start();
|
||||
this.activeStrategies.add(strategyId);
|
||||
logger.info(`Enabled strategy: ${strategyId}`);
|
||||
}
|
||||
|
||||
async disableStrategy(strategyId: string): Promise<void> {
|
||||
const strategy = this.strategies.get(strategyId);
|
||||
if (!strategy) {
|
||||
throw new Error(`Strategy ${strategyId} not found`);
|
||||
}
|
||||
|
||||
await strategy.stop();
|
||||
this.activeStrategies.delete(strategyId);
|
||||
logger.info(`Disabled strategy: ${strategyId}`);
|
||||
}
|
||||
|
||||
private async handleMarketData(data: MarketData): Promise<void> {
|
||||
// Forward to active strategies
|
||||
for (const strategyId of this.activeStrategies) {
|
||||
const strategy = this.strategies.get(strategyId);
|
||||
if (strategy && strategy.isInterestedInSymbol(data.data.symbol)) {
|
||||
try {
|
||||
await strategy.onMarketData(data);
|
||||
} catch (error) {
|
||||
logger.error(`Strategy ${strategyId} error processing market data:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async handleMarketDataBatch(batch: MarketData[]): Promise<void> {
|
||||
// Group by symbol for efficiency
|
||||
const bySymbol = new Map<string, MarketData[]>();
|
||||
|
||||
for (const data of batch) {
|
||||
const symbol = data.data.symbol;
|
||||
if (!bySymbol.has(symbol)) {
|
||||
bySymbol.set(symbol, []);
|
||||
}
|
||||
bySymbol.get(symbol)!.push(data);
|
||||
}
|
||||
|
||||
// Forward to strategies
|
||||
for (const strategyId of this.activeStrategies) {
|
||||
const strategy = this.strategies.get(strategyId);
|
||||
if (!strategy) continue;
|
||||
|
||||
const relevantData: MarketData[] = [];
|
||||
for (const [symbol, data] of bySymbol) {
|
||||
if (strategy.isInterestedInSymbol(symbol)) {
|
||||
relevantData.push(...data);
|
||||
}
|
||||
}
|
||||
|
||||
if (relevantData.length > 0) {
|
||||
try {
|
||||
await strategy.onMarketDataBatch(relevantData);
|
||||
} catch (error) {
|
||||
logger.error(`Strategy ${strategyId} error processing batch:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async handleFill(fill: any): Promise<void> {
|
||||
// Notify relevant strategies about fills
|
||||
for (const strategyId of this.activeStrategies) {
|
||||
const strategy = this.strategies.get(strategyId);
|
||||
if (strategy && strategy.hasPosition(fill.symbol)) {
|
||||
try {
|
||||
await strategy.onFill(fill);
|
||||
} catch (error) {
|
||||
logger.error(`Strategy ${strategyId} error processing fill:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async handleStrategySignal(strategyId: string, signal: any): Promise<void> {
|
||||
logger.debug(`Strategy ${strategyId} generated signal:`, signal);
|
||||
|
||||
// Emit for monitoring/logging
|
||||
this.emit('strategySignal', {
|
||||
strategyId,
|
||||
signal,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
private async handleStrategyOrder(strategyId: string, order: OrderRequest): Promise<void> {
|
||||
logger.info(`Strategy ${strategyId} placing order:`, order);
|
||||
|
||||
try {
|
||||
// Submit order through execution service
|
||||
const result = await this.executionService.submitOrder(order);
|
||||
|
||||
// Notify strategy of order result
|
||||
const strategy = this.strategies.get(strategyId);
|
||||
if (strategy) {
|
||||
await strategy.onOrderUpdate(result);
|
||||
}
|
||||
|
||||
// Emit for monitoring
|
||||
this.emit('strategyOrder', {
|
||||
strategyId,
|
||||
order,
|
||||
result,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`Failed to submit order from strategy ${strategyId}:`, error);
|
||||
|
||||
// Notify strategy of failure
|
||||
const strategy = this.strategies.get(strategyId);
|
||||
if (strategy) {
|
||||
await strategy.onOrderError(order, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async onMarketData(data: MarketData): Promise<void> {
|
||||
// Called by backtest engine
|
||||
await this.handleMarketData(data);
|
||||
}
|
||||
|
||||
getTradingEngine(): TradingEngine | null {
|
||||
return this.tradingEngine;
|
||||
}
|
||||
|
||||
getStrategy(strategyId: string): BaseStrategy | undefined {
|
||||
return this.strategies.get(strategyId);
|
||||
}
|
||||
|
||||
getAllStrategies(): Map<string, BaseStrategy> {
|
||||
return new Map(this.strategies);
|
||||
}
|
||||
|
||||
getActiveStrategies(): Set<string> {
|
||||
return new Set(this.activeStrategies);
|
||||
}
|
||||
|
||||
async updateStrategyConfig(strategyId: string, updates: Partial<StrategyConfig>): Promise<void> {
|
||||
const strategy = this.strategies.get(strategyId);
|
||||
if (!strategy) {
|
||||
throw new Error(`Strategy ${strategyId} not found`);
|
||||
}
|
||||
|
||||
await strategy.updateConfig(updates);
|
||||
logger.info(`Updated configuration for strategy ${strategyId}`);
|
||||
}
|
||||
|
||||
async getStrategyPerformance(strategyId: string): Promise<any> {
|
||||
const strategy = this.strategies.get(strategyId);
|
||||
if (!strategy) {
|
||||
throw new Error(`Strategy ${strategyId} not found`);
|
||||
}
|
||||
|
||||
return strategy.getPerformance();
|
||||
}
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
logger.info('Shutting down strategy manager...');
|
||||
|
||||
// Disable all strategies
|
||||
for (const strategyId of this.activeStrategies) {
|
||||
await this.disableStrategy(strategyId);
|
||||
}
|
||||
|
||||
// Shutdown all strategies
|
||||
for (const [id, strategy] of this.strategies) {
|
||||
await strategy.shutdown();
|
||||
}
|
||||
|
||||
this.strategies.clear();
|
||||
this.activeStrategies.clear();
|
||||
this.removeAllListeners();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,414 @@
|
|||
import { BaseStrategy, Signal } from '../BaseStrategy';
|
||||
import { MarketData } from '../../types';
|
||||
import { logger } from '@stock-bot/logger';
|
||||
import * as tf from '@tensorflow/tfjs-node';
|
||||
|
||||
interface MLModelConfig {
|
||||
modelPath?: string;
|
||||
features: string[];
|
||||
lookbackPeriod: number;
|
||||
updateFrequency: number; // How often to retrain in minutes
|
||||
minTrainingSize: number;
|
||||
}
|
||||
|
||||
export class MLEnhancedStrategy extends BaseStrategy {
|
||||
private model: tf.LayersModel | null = null;
|
||||
private featureBuffer: Map<string, number[][]> = new Map();
|
||||
private predictions: Map<string, number> = new Map();
|
||||
private lastUpdate: number = 0;
|
||||
private trainingData: { features: number[][]; labels: number[] } = { features: [], labels: [] };
|
||||
|
||||
// Feature extractors
|
||||
private indicators: Map<string, any> = new Map();
|
||||
|
||||
// ML Configuration
|
||||
private mlConfig: MLModelConfig = {
|
||||
features: [
|
||||
'returns_20', 'returns_50', 'volatility_20', 'rsi_14',
|
||||
'volume_ratio', 'price_position', 'macd_signal'
|
||||
],
|
||||
lookbackPeriod: 50,
|
||||
updateFrequency: 1440, // Daily
|
||||
minTrainingSize: 1000
|
||||
};
|
||||
|
||||
protected async onStart(): Promise<void> {
|
||||
logger.info('ML Enhanced Strategy starting...');
|
||||
|
||||
// Try to load existing model
|
||||
if (this.mlConfig.modelPath) {
|
||||
try {
|
||||
this.model = await tf.loadLayersModel(`file://${this.mlConfig.modelPath}`);
|
||||
logger.info('Loaded existing ML model');
|
||||
} catch (error) {
|
||||
logger.warn('Could not load model, will train new one');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize feature buffers for each symbol
|
||||
this.config.symbols.forEach(symbol => {
|
||||
this.featureBuffer.set(symbol, []);
|
||||
this.indicators.set(symbol, {
|
||||
prices: [],
|
||||
volumes: [],
|
||||
returns: [],
|
||||
sma20: 0,
|
||||
sma50: 0,
|
||||
rsi: 50,
|
||||
macd: 0,
|
||||
signal: 0
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected updateIndicators(data: MarketData): void {
|
||||
if (data.type !== 'bar') return;
|
||||
|
||||
const symbol = data.data.symbol;
|
||||
const indicators = this.indicators.get(symbol);
|
||||
if (!indicators) return;
|
||||
|
||||
// Update price and volume history
|
||||
indicators.prices.push(data.data.close);
|
||||
indicators.volumes.push(data.data.volume);
|
||||
|
||||
if (indicators.prices.length > 200) {
|
||||
indicators.prices.shift();
|
||||
indicators.volumes.shift();
|
||||
}
|
||||
|
||||
// Calculate returns
|
||||
if (indicators.prices.length >= 2) {
|
||||
const ret = (data.data.close - indicators.prices[indicators.prices.length - 2]) /
|
||||
indicators.prices[indicators.prices.length - 2];
|
||||
indicators.returns.push(ret);
|
||||
|
||||
if (indicators.returns.length > 50) {
|
||||
indicators.returns.shift();
|
||||
}
|
||||
}
|
||||
|
||||
// Update technical indicators
|
||||
if (indicators.prices.length >= 20) {
|
||||
indicators.sma20 = this.calculateSMA(indicators.prices, 20);
|
||||
indicators.volatility20 = this.calculateVolatility(indicators.returns, 20);
|
||||
}
|
||||
|
||||
if (indicators.prices.length >= 50) {
|
||||
indicators.sma50 = this.calculateSMA(indicators.prices, 50);
|
||||
}
|
||||
|
||||
if (indicators.prices.length >= 14) {
|
||||
indicators.rsi = this.calculateRSI(indicators.prices, 14);
|
||||
}
|
||||
|
||||
// Extract features
|
||||
const features = this.extractFeatures(symbol, data);
|
||||
if (features) {
|
||||
const buffer = this.featureBuffer.get(symbol)!;
|
||||
buffer.push(features);
|
||||
|
||||
if (buffer.length > this.mlConfig.lookbackPeriod) {
|
||||
buffer.shift();
|
||||
}
|
||||
|
||||
// Make prediction if we have enough data
|
||||
if (buffer.length === this.mlConfig.lookbackPeriod && this.model) {
|
||||
this.makePrediction(symbol, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we should update the model
|
||||
const now = Date.now();
|
||||
if (now - this.lastUpdate > this.mlConfig.updateFrequency * 60 * 1000) {
|
||||
this.updateModel();
|
||||
this.lastUpdate = now;
|
||||
}
|
||||
}
|
||||
|
||||
protected async generateSignal(data: MarketData): Promise<Signal | null> {
|
||||
if (data.type !== 'bar') return null;
|
||||
|
||||
const symbol = data.data.symbol;
|
||||
const prediction = this.predictions.get(symbol);
|
||||
|
||||
if (!prediction || Math.abs(prediction) < 0.01) {
|
||||
return null; // No strong signal
|
||||
}
|
||||
|
||||
const position = this.getPosition(symbol);
|
||||
const indicators = this.indicators.get(symbol);
|
||||
|
||||
// Risk management checks
|
||||
const volatility = indicators?.volatility20 || 0.02;
|
||||
const maxPositionRisk = 0.02; // 2% max risk per position
|
||||
const positionSize = this.calculatePositionSize(volatility, maxPositionRisk);
|
||||
|
||||
// Generate signals based on ML predictions
|
||||
if (prediction > 0.02 && position <= 0) {
|
||||
// Strong bullish prediction
|
||||
return {
|
||||
type: 'buy',
|
||||
symbol,
|
||||
strength: Math.min(prediction * 50, 1), // Scale prediction to 0-1
|
||||
reason: `ML prediction: ${(prediction * 100).toFixed(2)}% expected return`,
|
||||
metadata: {
|
||||
prediction,
|
||||
confidence: this.calculateConfidence(symbol),
|
||||
features: this.getLatestFeatures(symbol)
|
||||
}
|
||||
};
|
||||
} else if (prediction < -0.02 && position >= 0) {
|
||||
// Strong bearish prediction
|
||||
return {
|
||||
type: 'sell',
|
||||
symbol,
|
||||
strength: Math.min(Math.abs(prediction) * 50, 1),
|
||||
reason: `ML prediction: ${(prediction * 100).toFixed(2)}% expected return`,
|
||||
metadata: {
|
||||
prediction,
|
||||
confidence: this.calculateConfidence(symbol),
|
||||
features: this.getLatestFeatures(symbol)
|
||||
}
|
||||
};
|
||||
} else if (position !== 0 && Math.sign(position) !== Math.sign(prediction)) {
|
||||
// Exit if prediction reverses
|
||||
return {
|
||||
type: 'close',
|
||||
symbol,
|
||||
strength: 1,
|
||||
reason: 'ML prediction reversed',
|
||||
metadata: { prediction }
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private extractFeatures(symbol: string, data: MarketData): number[] | null {
|
||||
const indicators = this.indicators.get(symbol);
|
||||
if (!indicators || indicators.prices.length < 50) return null;
|
||||
|
||||
const features: number[] = [];
|
||||
|
||||
// Price returns
|
||||
const currentPrice = indicators.prices[indicators.prices.length - 1];
|
||||
features.push((currentPrice / indicators.prices[indicators.prices.length - 20] - 1)); // 20-day return
|
||||
features.push((currentPrice / indicators.prices[indicators.prices.length - 50] - 1)); // 50-day return
|
||||
|
||||
// Volatility
|
||||
features.push(indicators.volatility20 || 0);
|
||||
|
||||
// RSI
|
||||
features.push((indicators.rsi - 50) / 50); // Normalize to -1 to 1
|
||||
|
||||
// Volume ratio
|
||||
const avgVolume = indicators.volumes.slice(-20).reduce((a, b) => a + b, 0) / 20;
|
||||
features.push(data.data.volume / avgVolume - 1);
|
||||
|
||||
// Price position in daily range
|
||||
const pricePosition = (data.data.close - data.data.low) / (data.data.high - data.data.low);
|
||||
features.push(pricePosition * 2 - 1); // Normalize to -1 to 1
|
||||
|
||||
// MACD signal
|
||||
if (indicators.macd && indicators.signal) {
|
||||
features.push((indicators.macd - indicators.signal) / currentPrice);
|
||||
} else {
|
||||
features.push(0);
|
||||
}
|
||||
|
||||
// Store for training
|
||||
if (indicators.returns.length >= 21) {
|
||||
const futureReturn = indicators.returns[indicators.returns.length - 1];
|
||||
this.trainingData.features.push([...features]);
|
||||
this.trainingData.labels.push(futureReturn);
|
||||
|
||||
// Limit training data size
|
||||
if (this.trainingData.features.length > 10000) {
|
||||
this.trainingData.features.shift();
|
||||
this.trainingData.labels.shift();
|
||||
}
|
||||
}
|
||||
|
||||
return features;
|
||||
}
|
||||
|
||||
private async makePrediction(symbol: string, featureBuffer: number[][]): Promise<void> {
|
||||
if (!this.model) return;
|
||||
|
||||
try {
|
||||
// Prepare input tensor
|
||||
const input = tf.tensor3d([featureBuffer]);
|
||||
|
||||
// Make prediction
|
||||
const prediction = await this.model.predict(input) as tf.Tensor;
|
||||
const value = (await prediction.data())[0];
|
||||
|
||||
this.predictions.set(symbol, value);
|
||||
|
||||
// Cleanup tensors
|
||||
input.dispose();
|
||||
prediction.dispose();
|
||||
} catch (error) {
|
||||
logger.error('ML prediction error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async updateModel(): Promise<void> {
|
||||
if (this.trainingData.features.length < this.mlConfig.minTrainingSize) {
|
||||
logger.info('Not enough training data yet');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('Updating ML model...');
|
||||
|
||||
try {
|
||||
// Create or update model
|
||||
if (!this.model) {
|
||||
this.model = this.createModel();
|
||||
}
|
||||
|
||||
// Prepare training data
|
||||
const features = tf.tensor2d(this.trainingData.features);
|
||||
const labels = tf.tensor1d(this.trainingData.labels);
|
||||
|
||||
// Train model
|
||||
await this.model.fit(features, labels, {
|
||||
epochs: 50,
|
||||
batchSize: 32,
|
||||
validationSplit: 0.2,
|
||||
shuffle: true,
|
||||
callbacks: {
|
||||
onEpochEnd: (epoch, logs) => {
|
||||
if (epoch % 10 === 0) {
|
||||
logger.debug(`Epoch ${epoch}: loss = ${logs?.loss.toFixed(4)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
logger.info('Model updated successfully');
|
||||
|
||||
// Cleanup tensors
|
||||
features.dispose();
|
||||
labels.dispose();
|
||||
|
||||
// Save model if path provided
|
||||
if (this.mlConfig.modelPath) {
|
||||
await this.model.save(`file://${this.mlConfig.modelPath}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Model update error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private createModel(): tf.LayersModel {
|
||||
const model = tf.sequential({
|
||||
layers: [
|
||||
// LSTM layer for sequence processing
|
||||
tf.layers.lstm({
|
||||
units: 64,
|
||||
returnSequences: true,
|
||||
inputShape: [this.mlConfig.lookbackPeriod, this.mlConfig.features.length]
|
||||
}),
|
||||
tf.layers.dropout({ rate: 0.2 }),
|
||||
|
||||
// Second LSTM layer
|
||||
tf.layers.lstm({
|
||||
units: 32,
|
||||
returnSequences: false
|
||||
}),
|
||||
tf.layers.dropout({ rate: 0.2 }),
|
||||
|
||||
// Dense layers
|
||||
tf.layers.dense({
|
||||
units: 16,
|
||||
activation: 'relu'
|
||||
}),
|
||||
tf.layers.dropout({ rate: 0.1 }),
|
||||
|
||||
// Output layer
|
||||
tf.layers.dense({
|
||||
units: 1,
|
||||
activation: 'tanh' // Output between -1 and 1
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
model.compile({
|
||||
optimizer: tf.train.adam(0.001),
|
||||
loss: 'meanSquaredError',
|
||||
metrics: ['mae']
|
||||
});
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
private calculateConfidence(symbol: string): number {
|
||||
// Simple confidence based on prediction history accuracy
|
||||
// In practice, would track actual vs predicted returns
|
||||
const prediction = this.predictions.get(symbol) || 0;
|
||||
return Math.min(Math.abs(prediction) * 10, 1);
|
||||
}
|
||||
|
||||
private getLatestFeatures(symbol: string): Record<string, number> {
|
||||
const buffer = this.featureBuffer.get(symbol);
|
||||
if (!buffer || buffer.length === 0) return {};
|
||||
|
||||
const latest = buffer[buffer.length - 1];
|
||||
return {
|
||||
returns_20: latest[0],
|
||||
returns_50: latest[1],
|
||||
volatility_20: latest[2],
|
||||
rsi_normalized: latest[3],
|
||||
volume_ratio: latest[4],
|
||||
price_position: latest[5],
|
||||
macd_signal: latest[6]
|
||||
};
|
||||
}
|
||||
|
||||
private calculateVolatility(returns: number[], period: number): number {
|
||||
if (returns.length < period) return 0;
|
||||
|
||||
const recentReturns = returns.slice(-period);
|
||||
const mean = recentReturns.reduce((a, b) => a + b, 0) / period;
|
||||
const variance = recentReturns.reduce((sum, r) => sum + Math.pow(r - mean, 2), 0) / period;
|
||||
|
||||
return Math.sqrt(variance * 252); // Annualized
|
||||
}
|
||||
|
||||
private calculatePositionSize(volatility: number, maxRisk: number): number {
|
||||
// Kelly-inspired position sizing with volatility adjustment
|
||||
const targetVolatility = 0.15; // 15% annual target
|
||||
const volAdjustment = targetVolatility / volatility;
|
||||
|
||||
return Math.min(volAdjustment, 2.0); // Max 2x leverage
|
||||
}
|
||||
|
||||
protected onStop(): void {
|
||||
logger.info('ML Enhanced Strategy stopped');
|
||||
|
||||
// Save final model if configured
|
||||
if (this.model && this.mlConfig.modelPath) {
|
||||
this.model.save(`file://${this.mlConfig.modelPath}`)
|
||||
.then(() => logger.info('Model saved'))
|
||||
.catch(err => logger.error('Failed to save model:', err));
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
this.featureBuffer.clear();
|
||||
this.predictions.clear();
|
||||
this.indicators.clear();
|
||||
if (this.model) {
|
||||
this.model.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
protected onConfigUpdate(updates: any): void {
|
||||
logger.info('ML Enhanced Strategy config updated:', updates);
|
||||
|
||||
if (updates.mlConfig) {
|
||||
this.mlConfig = { ...this.mlConfig, ...updates.mlConfig };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
import { BaseStrategy, Signal } from '../BaseStrategy';
|
||||
import { MarketData } from '../../types';
|
||||
import { logger } from '@stock-bot/logger';
|
||||
|
||||
interface MeanReversionIndicators {
|
||||
sma20: number;
|
||||
sma50: number;
|
||||
stdDev: number;
|
||||
zScore: number;
|
||||
rsi: number;
|
||||
}
|
||||
|
||||
export class MeanReversionStrategy extends BaseStrategy {
|
||||
private priceHistory = new Map<string, number[]>();
|
||||
private indicators = new Map<string, MeanReversionIndicators>();
|
||||
|
||||
// Strategy parameters
|
||||
private readonly LOOKBACK_PERIOD = 20;
|
||||
private readonly Z_SCORE_ENTRY = 2.0;
|
||||
private readonly Z_SCORE_EXIT = 0.5;
|
||||
private readonly RSI_OVERSOLD = 30;
|
||||
private readonly RSI_OVERBOUGHT = 70;
|
||||
private readonly MIN_VOLUME = 1000000; // $1M daily volume
|
||||
|
||||
protected updateIndicators(data: MarketData): void {
|
||||
if (data.type !== 'bar') return;
|
||||
|
||||
const symbol = data.data.symbol;
|
||||
const price = data.data.close;
|
||||
|
||||
// Update price history
|
||||
if (!this.priceHistory.has(symbol)) {
|
||||
this.priceHistory.set(symbol, []);
|
||||
}
|
||||
|
||||
const history = this.priceHistory.get(symbol)!;
|
||||
history.push(price);
|
||||
|
||||
// Keep only needed history
|
||||
if (history.length > this.LOOKBACK_PERIOD * 3) {
|
||||
history.shift();
|
||||
}
|
||||
|
||||
// Calculate indicators if we have enough data
|
||||
if (history.length >= this.LOOKBACK_PERIOD) {
|
||||
const indicators = this.calculateIndicators(history);
|
||||
this.indicators.set(symbol, indicators);
|
||||
}
|
||||
}
|
||||
|
||||
private calculateIndicators(prices: number[]): MeanReversionIndicators {
|
||||
const len = prices.length;
|
||||
|
||||
// Calculate SMAs
|
||||
const sma20 = this.calculateSMA(prices, 20);
|
||||
const sma50 = len >= 50 ? this.calculateSMA(prices, 50) : sma20;
|
||||
|
||||
// Calculate standard deviation
|
||||
const stdDev = this.calculateStdDev(prices.slice(-20), sma20);
|
||||
|
||||
// Calculate Z-score
|
||||
const currentPrice = prices[len - 1];
|
||||
const zScore = stdDev > 0 ? (currentPrice - sma20) / stdDev : 0;
|
||||
|
||||
// Calculate RSI
|
||||
const rsi = this.calculateRSI(prices, 14);
|
||||
|
||||
return { sma20, sma50, stdDev, zScore, rsi };
|
||||
}
|
||||
|
||||
protected async generateSignal(data: MarketData): Promise<Signal | null> {
|
||||
if (data.type !== 'bar') return null;
|
||||
|
||||
const symbol = data.data.symbol;
|
||||
const indicators = this.indicators.get(symbol);
|
||||
|
||||
if (!indicators) return null;
|
||||
|
||||
// Check volume filter
|
||||
if (data.data.volume * data.data.close < this.MIN_VOLUME) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const position = this.getPosition(symbol);
|
||||
const { zScore, rsi, sma20, sma50 } = indicators;
|
||||
|
||||
// Entry signals
|
||||
if (position === 0) {
|
||||
// Long entry: Oversold conditions
|
||||
if (zScore < -this.Z_SCORE_ENTRY && rsi < this.RSI_OVERSOLD && sma20 > sma50) {
|
||||
return {
|
||||
type: 'buy',
|
||||
symbol,
|
||||
strength: Math.min(Math.abs(zScore) / 3, 1),
|
||||
reason: `Mean reversion long: Z-score ${zScore.toFixed(2)}, RSI ${rsi.toFixed(0)}`,
|
||||
metadata: { indicators }
|
||||
};
|
||||
}
|
||||
|
||||
// Short entry: Overbought conditions
|
||||
if (zScore > this.Z_SCORE_ENTRY && rsi > this.RSI_OVERBOUGHT && sma20 < sma50) {
|
||||
return {
|
||||
type: 'sell',
|
||||
symbol,
|
||||
strength: Math.min(Math.abs(zScore) / 3, 1),
|
||||
reason: `Mean reversion short: Z-score ${zScore.toFixed(2)}, RSI ${rsi.toFixed(0)}`,
|
||||
metadata: { indicators }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Exit signals
|
||||
if (position > 0) {
|
||||
// Exit long: Price reverted to mean or stop loss
|
||||
if (zScore > -this.Z_SCORE_EXIT || zScore > this.Z_SCORE_ENTRY) {
|
||||
return {
|
||||
type: 'close',
|
||||
symbol,
|
||||
strength: 1,
|
||||
reason: `Exit long: Z-score ${zScore.toFixed(2)}`,
|
||||
metadata: { indicators }
|
||||
};
|
||||
}
|
||||
} else if (position < 0) {
|
||||
// Exit short: Price reverted to mean or stop loss
|
||||
if (zScore < this.Z_SCORE_EXIT || zScore < -this.Z_SCORE_ENTRY) {
|
||||
return {
|
||||
type: 'close',
|
||||
symbol,
|
||||
strength: 1,
|
||||
reason: `Exit short: Z-score ${zScore.toFixed(2)}`,
|
||||
metadata: { indicators }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private calculateSMA(prices: number[], period: number): number {
|
||||
const relevantPrices = prices.slice(-period);
|
||||
return relevantPrices.reduce((sum, p) => sum + p, 0) / relevantPrices.length;
|
||||
}
|
||||
|
||||
private calculateStdDev(prices: number[], mean: number): number {
|
||||
const squaredDiffs = prices.map(p => Math.pow(p - mean, 2));
|
||||
const variance = squaredDiffs.reduce((sum, d) => sum + d, 0) / prices.length;
|
||||
return Math.sqrt(variance);
|
||||
}
|
||||
|
||||
private calculateRSI(prices: number[], period: number = 14): number {
|
||||
if (prices.length < period + 1) return 50;
|
||||
|
||||
let gains = 0;
|
||||
let losses = 0;
|
||||
|
||||
// Calculate initial average gain/loss
|
||||
for (let i = 1; i <= period; i++) {
|
||||
const change = prices[prices.length - i] - prices[prices.length - i - 1];
|
||||
if (change > 0) {
|
||||
gains += change;
|
||||
} else {
|
||||
losses += Math.abs(change);
|
||||
}
|
||||
}
|
||||
|
||||
const avgGain = gains / period;
|
||||
const avgLoss = losses / period;
|
||||
|
||||
if (avgLoss === 0) return 100;
|
||||
|
||||
const rs = avgGain / avgLoss;
|
||||
const rsi = 100 - (100 / (1 + rs));
|
||||
|
||||
return rsi;
|
||||
}
|
||||
|
||||
protected onStart(): void {
|
||||
logger.info(`Mean Reversion Strategy started with symbols: ${this.config.symbols.join(', ')}`);
|
||||
}
|
||||
|
||||
protected onStop(): void {
|
||||
logger.info('Mean Reversion Strategy stopped');
|
||||
// Clear indicators
|
||||
this.priceHistory.clear();
|
||||
this.indicators.clear();
|
||||
}
|
||||
|
||||
protected onConfigUpdate(updates: any): void {
|
||||
logger.info('Mean Reversion Strategy config updated:', updates);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue