added initial py analytics / rust core / ts orchestrator services

This commit is contained in:
Boki 2025-07-01 11:16:25 -04:00
parent 680b5fd2ae
commit c862ed496b
62 changed files with 13459 additions and 0 deletions

View 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 {}
}

View 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();
}
}

View file

@ -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 };
}
}
}

View file

@ -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);
}
}