diff --git a/apps/stock/core/index.node b/apps/stock/core/index.node index 81251f9..96ddc19 100755 Binary files a/apps/stock/core/index.node and b/apps/stock/core/index.node differ diff --git a/apps/stock/core/src/api/mod.rs b/apps/stock/core/src/api/mod.rs index d01710d..4fedac6 100644 --- a/apps/stock/core/src/api/mod.rs +++ b/apps/stock/core/src/api/mod.rs @@ -159,6 +159,20 @@ impl TradingEngine { #[napi] pub fn process_fill(&self, symbol: String, price: f64, quantity: f64, side: String, commission: f64) -> Result { + self.process_fill_with_metadata(symbol, price, quantity, side, commission, None, None) + } + + #[napi] + pub fn process_fill_with_metadata( + &self, + symbol: String, + price: f64, + quantity: f64, + side: String, + commission: f64, + order_id: Option, + strategy_id: Option + ) -> Result { let side = match side.as_str() { "buy" | "Buy" => Side::Buy, "sell" | "Sell" => Side::Sell, @@ -175,7 +189,7 @@ impl TradingEngine { commission, }; - let update = core.position_tracker.process_fill(&symbol, &fill, side); + let update = core.position_tracker.process_fill_with_tracking(&symbol, &fill, side, order_id, strategy_id); // Update risk engine with new position core.risk_engine.update_position(&symbol, update.resulting_position.quantity); @@ -212,12 +226,18 @@ impl TradingEngine { // Backtest-specific methods #[napi] - pub fn advance_time(&self, _to_timestamp: i64) -> Result<()> { + pub fn advance_time(&self, to_timestamp: i64) -> Result<()> { let core = self.core.lock(); if let TradingMode::Backtest { .. } = core.get_mode() { - // In real implementation, would downcast and advance time - // For now, return success in backtest mode - Ok(()) + // Downcast time provider to SimulatedTime and advance it + if let Some(simulated_time) = core.time_provider.as_any().downcast_ref::() { + let new_time = DateTime::::from_timestamp_millis(to_timestamp) + .ok_or_else(|| Error::from_reason("Invalid timestamp"))?; + simulated_time.advance_to(new_time); + Ok(()) + } else { + Err(Error::from_reason("Failed to access simulated time provider")) + } } else { Err(Error::from_reason("Can only advance time in backtest mode")) } @@ -274,6 +294,39 @@ impl TradingEngine { Ok(()) } + + #[napi] + pub fn get_trade_history(&self) -> Result { + let core = self.core.lock(); + let trades = core.position_tracker.get_trade_history(); + Ok(serde_json::to_string(&trades).unwrap()) + } + + #[napi] + pub fn get_closed_trades(&self) -> Result { + let core = self.core.lock(); + let trades = core.position_tracker.get_closed_trades(); + Ok(serde_json::to_string(&trades).unwrap()) + } + + #[napi] + pub fn get_open_trades(&self) -> Result { + let core = self.core.lock(); + let trades = core.position_tracker.get_open_trades(); + Ok(serde_json::to_string(&trades).unwrap()) + } + + #[napi] + pub fn get_trade_count(&self) -> Result { + let core = self.core.lock(); + Ok(core.position_tracker.get_trade_count() as u32) + } + + #[napi] + pub fn get_closed_trade_count(&self) -> Result { + let core = self.core.lock(); + Ok(core.position_tracker.get_closed_trade_count() as u32) + } } // Helper functions to parse JavaScript objects diff --git a/apps/stock/core/src/lib.rs b/apps/stock/core/src/lib.rs index 5d8c422..d361d2e 100644 --- a/apps/stock/core/src/lib.rs +++ b/apps/stock/core/src/lib.rs @@ -8,7 +8,7 @@ pub mod api; pub mod analytics; // Re-export commonly used types -pub use positions::{Position, PositionUpdate}; +pub use positions::{Position, PositionUpdate, TradeRecord, ClosedTrade}; pub use risk::{RiskLimits, RiskCheckResult, RiskMetrics}; use chrono::{DateTime, Utc}; diff --git a/apps/stock/core/src/positions/mod.rs b/apps/stock/core/src/positions/mod.rs index e2b0a54..48401eb 100644 --- a/apps/stock/core/src/positions/mod.rs +++ b/apps/stock/core/src/positions/mod.rs @@ -2,6 +2,8 @@ use crate::{Fill, Side}; use chrono::{DateTime, Utc}; use dashmap::DashMap; use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use parking_lot::RwLock; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Position { @@ -21,17 +23,159 @@ pub struct PositionUpdate { pub resulting_position: Position, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TradeRecord { + pub id: String, + pub symbol: String, + pub side: Side, + pub quantity: f64, + pub price: f64, + pub timestamp: DateTime, + pub commission: f64, + pub order_id: Option, + pub strategy_id: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ClosedTrade { + pub id: String, + pub symbol: String, + pub entry_time: DateTime, + pub exit_time: DateTime, + pub entry_price: f64, + pub exit_price: f64, + pub quantity: f64, + pub side: Side, // Side of the opening trade + pub pnl: f64, + pub pnl_percent: f64, + pub commission: f64, + pub duration_ms: i64, + pub entry_fill_id: String, + pub exit_fill_id: String, +} + pub struct PositionTracker { positions: DashMap, + trade_history: Arc>>, + closed_trades: Arc>>, + open_trades: DashMap>, // Track open trades by symbol + next_trade_id: Arc>, } impl PositionTracker { pub fn new() -> Self { Self { positions: DashMap::new(), + trade_history: Arc::new(RwLock::new(Vec::new())), + closed_trades: Arc::new(RwLock::new(Vec::new())), + open_trades: DashMap::new(), + next_trade_id: Arc::new(RwLock::new(1)), } } + fn generate_trade_id(&self) -> String { + let mut id = self.next_trade_id.write(); + let current_id = *id; + *id += 1; + format!("T{:08}", current_id) + } + + pub fn process_fill_with_tracking( + &self, + symbol: &str, + fill: &Fill, + side: Side, + order_id: Option, + strategy_id: Option + ) -> PositionUpdate { + // First process the fill normally + let update = self.process_fill(symbol, fill, side); + + // Create trade record + let trade_record = TradeRecord { + id: self.generate_trade_id(), + symbol: symbol.to_string(), + side, + quantity: fill.quantity, + price: fill.price, + timestamp: fill.timestamp, + commission: fill.commission, + order_id, + strategy_id, + }; + + // Add to trade history + self.trade_history.write().push(trade_record.clone()); + + // Handle trade matching for closed trades + match side { + Side::Buy => { + // For buy orders, just add to open trades + self.open_trades.entry(symbol.to_string()) + .or_insert_with(Vec::new) + .push(trade_record); + } + Side::Sell => { + // For sell orders, try to match with open buy trades + if let Some(mut open_trades) = self.open_trades.get_mut(symbol) { + let mut remaining_quantity = fill.quantity; + let mut trades_to_remove = Vec::new(); + + // FIFO matching + for (idx, open_trade) in open_trades.iter_mut().enumerate() { + if open_trade.side == Side::Buy && remaining_quantity > 0.0 { + let close_quantity = remaining_quantity.min(open_trade.quantity); + + // Create closed trade record + let closed_trade = ClosedTrade { + id: format!("CT{}", self.generate_trade_id()), + symbol: symbol.to_string(), + entry_time: open_trade.timestamp, + exit_time: fill.timestamp, + entry_price: open_trade.price, + exit_price: fill.price, + quantity: close_quantity, + side: Side::Buy, // Opening side + pnl: close_quantity * (fill.price - open_trade.price) - (open_trade.commission + fill.commission * close_quantity / fill.quantity), + pnl_percent: ((fill.price - open_trade.price) / open_trade.price) * 100.0, + commission: open_trade.commission + fill.commission * close_quantity / fill.quantity, + duration_ms: (fill.timestamp - open_trade.timestamp).num_milliseconds(), + entry_fill_id: open_trade.id.clone(), + exit_fill_id: trade_record.id.clone(), + }; + + self.closed_trades.write().push(closed_trade); + + // Update quantities + remaining_quantity -= close_quantity; + open_trade.quantity -= close_quantity; + + if open_trade.quantity <= 0.0 { + trades_to_remove.push(idx); + } + } + } + + // Remove fully closed trades + for idx in trades_to_remove.into_iter().rev() { + open_trades.remove(idx); + } + + // If we still have quantity left, it's a short position + if remaining_quantity > 0.0 { + let short_trade = TradeRecord { + quantity: remaining_quantity, + ..trade_record.clone() + }; + open_trades.push(short_trade); + } + } + } + } + + update + } + pub fn process_fill(&self, symbol: &str, fill: &Fill, side: Side) -> PositionUpdate { let mut entry = self.positions.entry(symbol.to_string()).or_insert_with(|| { Position { @@ -162,5 +306,33 @@ impl PositionTracker { pub fn reset(&self) { self.positions.clear(); + self.trade_history.write().clear(); + self.closed_trades.write().clear(); + self.open_trades.clear(); + *self.next_trade_id.write() = 1; + } + + pub fn get_trade_history(&self) -> Vec { + self.trade_history.read().clone() + } + + pub fn get_closed_trades(&self) -> Vec { + self.closed_trades.read().clone() + } + + pub fn get_open_trades(&self) -> Vec { + let mut all_open_trades = Vec::new(); + for entry in self.open_trades.iter() { + all_open_trades.extend(entry.value().clone()); + } + all_open_trades + } + + pub fn get_trade_count(&self) -> usize { + self.trade_history.read().len() + } + + pub fn get_closed_trade_count(&self) -> usize { + self.closed_trades.read().len() } } \ No newline at end of file diff --git a/apps/stock/orchestrator/src/backtest/BacktestEngine.ts b/apps/stock/orchestrator/src/backtest/BacktestEngine.ts index ffe73a9..d039aee 100644 --- a/apps/stock/orchestrator/src/backtest/BacktestEngine.ts +++ b/apps/stock/orchestrator/src/backtest/BacktestEngine.ts @@ -92,7 +92,6 @@ export class BacktestEngine extends EventEmitter { private eventQueue: BacktestEvent[] = []; private currentTime: number = 0; private equityCurve: { timestamp: number; value: number }[] = []; - private trades: any[] = []; private isRunning = false; private dataManager: DataManager; private marketSimulator: MarketSimulator; @@ -100,6 +99,8 @@ export class BacktestEngine extends EventEmitter { private microstructures: Map = new Map(); private container: IServiceContainer; private initialCapital: number = 100000; + private pendingOrders: Map = new Map(); + private ordersListenerSetup = false; constructor( container: IServiceContainer, @@ -116,6 +117,9 @@ export class BacktestEngine extends EventEmitter { latencyMs: 1 }); this.performanceAnalyzer = new PerformanceAnalyzer(); + + // Set up order listener immediately + this.setupOrderListener(); } async runBacktest(config: any): Promise { @@ -183,14 +187,41 @@ export class BacktestEngine extends EventEmitter { await this.strategyManager.initializeStrategies(strategies); this.container.logger.info(`Initialized ${strategies.length} strategies`); + // Don't setup strategy order listeners - we already listen to StrategyManager + // this.setupStrategyOrderListeners(); + // Convert market data to events this.populateEventQueue(marketData); // Main backtest loop await this.processEvents(); + // Get trade history from Rust core + const tradingEngine = this.strategyManager.getTradingEngine(); + let closedTrades: any[] = []; + let tradeCount = 0; + if (tradingEngine) { + if (tradingEngine.getClosedTrades) { + try { + const tradesJson = tradingEngine.getClosedTrades(); + closedTrades = JSON.parse(tradesJson); + this.container.logger.info(`Retrieved ${closedTrades.length} closed trades from Rust core`); + } catch (error) { + this.container.logger.warn('Failed to get closed trades from Rust core:', error); + } + } + if (tradingEngine.getTradeCount) { + try { + tradeCount = tradingEngine.getTradeCount(); + this.container.logger.info(`Total trades executed: ${tradeCount}`); + } catch (error) { + this.container.logger.warn('Failed to get trade count from Rust core:', error); + } + } + } + // Calculate final metrics - const performance = this.calculatePerformance(); + const performance = this.calculatePerformance(closedTrades); // Get final positions const finalPositions = await this.getFinalPositions(); @@ -221,7 +252,7 @@ export class BacktestEngine extends EventEmitter { sharpeRatio: performance.sharpeRatio, maxDrawdown: performance.maxDrawdown, winRate: performance.winRate, - totalTrades: performance.totalTrades, + totalTrades: tradeCount || performance.totalTrades, profitFactor: performance.profitFactor || 0, profitableTrades: Math.round(performance.totalTrades * performance.winRate / 100), avgWin: performance.avgWin || 0, @@ -243,19 +274,19 @@ export class BacktestEngine extends EventEmitter { ohlcData: this.getOHLCData(marketData, validatedConfig.symbols), // Trade history (frontend-ready) - trades: this.trades.map(trade => ({ - id: `${trade.symbol}-${trade.entryTime}`, + trades: closedTrades.map(trade => ({ + id: trade.id, symbol: trade.symbol, - entryDate: new Date(trade.entryTime).toISOString(), - exitDate: trade.exitTime ? new Date(trade.exitTime).toISOString() : null, - entryPrice: trade.entryPrice, - exitPrice: trade.exitPrice || trade.currentPrice, + entryDate: trade.entry_time, + exitDate: trade.exit_time, + entryPrice: trade.entry_price, + exitPrice: trade.exit_price, quantity: trade.quantity, - side: trade.side, - pnl: trade.pnl || 0, - pnlPercent: trade.returnPct || 0, - commission: trade.commission || 0, - duration: trade.holdingPeriod || 0 + side: trade.side === 'Buy' ? 'buy' : 'sell', + pnl: trade.pnl, + pnlPercent: trade.pnl_percent, + commission: trade.commission, + duration: trade.duration_ms })), // Final positions @@ -273,8 +304,8 @@ export class BacktestEngine extends EventEmitter { drawdownSeries: this.calculateDrawdown(), dailyReturns: this.calculateDailyReturns(), monthlyReturns: this.calculateMonthlyReturns(), - exposureTime: this.calculateExposureTime(), - riskMetrics: this.calculateRiskMetrics() + exposureTime: this.calculateExposureTime(closedTrades), + riskMetrics: this.calculateRiskMetrics(closedTrades) } }; @@ -445,6 +476,8 @@ export class BacktestEngine extends EventEmitter { let lastEquityUpdate = 0; const equityUpdateInterval = 60000; // Update equity every minute + this.container.logger.info(`[BacktestEngine] Processing ${this.eventQueue.length} events`); + while (this.eventQueue.length > 0 && this.isRunning) { const event = this.eventQueue.shift()!; @@ -452,12 +485,6 @@ export class BacktestEngine extends EventEmitter { this.currentTime = event.timestamp; if (tradingEngine) { await tradingEngine.advanceTime(this.currentTime); - - // Check for any fills - const fills = tradingEngine.getFills ? tradingEngine.getFills() : []; - for (const fill of fills) { - await this.processFill(fill); - } } // Process event based on type @@ -484,7 +511,7 @@ export class BacktestEngine extends EventEmitter { // Emit progress if (this.eventQueue.length % 1000 === 0) { this.emit('progress', { - processed: this.trades.length, + processed: this.eventQueue.length, remaining: this.eventQueue.length, currentTime: new Date(this.currentTime) }); @@ -496,8 +523,12 @@ export class BacktestEngine extends EventEmitter { } private async processMarketData(data: MarketData): Promise { + this.container.logger.info(`[BacktestEngine] processMarketData called for ${data.data.symbol} @ ${data.data.close}`); const tradingEngine = this.strategyManager.getTradingEngine(); - if (!tradingEngine) {return;} + if (!tradingEngine) { + this.container.logger.warn(`[BacktestEngine] No trading engine available`); + return; + } // Process through market simulator for realistic orderbook const orderbook = this.marketSimulator.processMarketData(data); @@ -558,6 +589,7 @@ export class BacktestEngine extends EventEmitter { } // Let strategies process the data + this.container.logger.info(`[BacktestEngine] Forwarding data to strategy manager for ${data.data.symbol}`); await this.strategyManager.onMarketData(data); // Check for any pending orders that should be filled @@ -576,43 +608,45 @@ export class BacktestEngine extends EventEmitter { } private async processFill(fill: any): Promise { + this.container.logger.info(`[BacktestEngine] Processing fill: ${fill.side} ${fill.quantity} ${fill.symbol} @ ${fill.price} with timestamp ${fill.timestamp}`); + // Process fill in trading engine for position tracking const tradingEngine = this.strategyManager.getTradingEngine(); if (tradingEngine) { - const fillResult = await tradingEngine.processFill( - fill.symbol, - fill.price, - fill.quantity, - fill.side, - fill.commission || 0 - ); - this.container.logger.debug('Fill processed:', fillResult); - } - - // Handle trade recording based on side - if (fill.side === 'buy') { - // Create new trade entry for buy orders - const trade = { - symbol: fill.symbol, - side: fill.side, - quantity: fill.quantity, - entryPrice: fill.price, - entryTime: fill.timestamp || this.currentTime, - exitPrice: null, - exitTime: null, - pnl: 0, - returnPct: 0, - commission: fill.commission || 0, - currentPrice: fill.price, - holdingPeriod: 0, - backtestTime: this.currentTime - }; + this.container.logger.info(`[BacktestEngine] Trading engine available, processing fill`); + this.container.logger.info(`[BacktestEngine] Current time in trading engine before advance: ${tradingEngine.getCurrentTime()}`); - this.trades.push(trade); - this.container.logger.info(`šŸ’µ Buy trade opened: ${fill.quantity} ${fill.symbol} @ ${fill.price}`); - } else if (fill.side === 'sell') { - // Update existing trades for sell orders - this.updateClosedTrades(fill); + // Make sure time is properly advanced + if (fill.timestamp) { + await tradingEngine.advanceTime(fill.timestamp); + this.container.logger.info(`[BacktestEngine] Advanced trading engine time to ${fill.timestamp}, current time now: ${tradingEngine.getCurrentTime()}`); + } + + // Use the new process_fill_with_metadata method if available + if (tradingEngine.processFillWithMetadata) { + const fillResult = await tradingEngine.processFillWithMetadata( + fill.symbol, + fill.price, + fill.quantity, + fill.side, + fill.commission || 0, + fill.orderId || null, + fill.strategyId || null + ); + this.container.logger.debug('Fill processed with tracking:', fillResult); + } else { + // Fallback to old method + const fillResult = await tradingEngine.processFill( + fill.symbol, + fill.price, + fill.quantity, + fill.side, + fill.commission || 0 + ); + this.container.logger.debug('Fill processed:', fillResult); + } + } else { + this.container.logger.warn(`[BacktestEngine] No trading engine available to process fill!`); } // Notify strategies of fill @@ -651,23 +685,24 @@ export class BacktestEngine extends EventEmitter { return this.initialCapital + realized + unrealized; } - private calculatePerformance(): PerformanceMetrics { + private calculatePerformance(closedTrades: any[] = []): PerformanceMetrics { // Use sophisticated performance analyzer - this.trades.forEach(trade => { + // Add closed trades from Rust core + closedTrades.forEach(trade => { this.performanceAnalyzer.addTrade({ - entryTime: new Date(trade.entryTime), - exitTime: new Date(trade.exitTime || this.currentTime), + entryTime: new Date(trade.entry_time), + exitTime: new Date(trade.exit_time), symbol: trade.symbol, - side: trade.side, - entryPrice: trade.entryPrice, - exitPrice: trade.exitPrice || trade.currentPrice, + side: trade.side === 'Buy' ? 'long' : 'short', + entryPrice: trade.entry_price, + exitPrice: trade.exit_price, quantity: trade.quantity, commission: trade.commission || 0, pnl: trade.pnl || 0, - returnPct: trade.returnPct || 0, - holdingPeriod: trade.holdingPeriod || 0, - mae: trade.mae || 0, - mfe: trade.mfe || 0 + returnPct: trade.pnl_percent || 0, + holdingPeriod: trade.duration_ms / 60000, // Convert to minutes + mae: 0, // Not tracked yet + mfe: 0 // Not tracked yet }); }); @@ -724,14 +759,12 @@ export class BacktestEngine extends EventEmitter { return monthlyReturns; } - private calculateExposureTime(): number { - if (this.trades.length === 0) return 0; + private calculateExposureTime(closedTrades: any[] = []): number { + if (closedTrades.length === 0) return 0; let totalExposureTime = 0; - for (const trade of this.trades) { - if (trade.exitTime) { - totalExposureTime += trade.exitTime - trade.entryTime; - } + for (const trade of closedTrades) { + totalExposureTime += trade.duration_ms || 0; } // Use equity curve to determine actual trading period @@ -742,7 +775,7 @@ export class BacktestEngine extends EventEmitter { return totalTime > 0 ? (totalExposureTime / totalTime) * 100 : 0; } - private calculateRiskMetrics(): Record { + private calculateRiskMetrics(closedTrades: any[] = []): Record { const returns = this.calculateDailyReturns(); // Calculate various risk metrics @@ -761,7 +794,7 @@ export class BacktestEngine extends EventEmitter { var95: this.calculateVaR(returns, 0.95), var99: this.calculateVaR(returns, 0.99), cvar95: this.calculateCVaR(returns, 0.95), - maxConsecutiveLosses: this.calculateMaxConsecutiveLosses() + maxConsecutiveLosses: this.calculateMaxConsecutiveLosses(closedTrades) }; } @@ -778,11 +811,11 @@ export class BacktestEngine extends EventEmitter { return tail.reduce((a, b) => a + b, 0) / tail.length; } - private calculateMaxConsecutiveLosses(): number { + private calculateMaxConsecutiveLosses(closedTrades: any[] = []): number { let maxLosses = 0; let currentLosses = 0; - for (const trade of this.trades) { + for (const trade of closedTrades) { if (trade.pnl && trade.pnl < 0) { currentLosses++; maxLosses = Math.max(maxLosses, currentLosses); @@ -839,7 +872,8 @@ export class BacktestEngine extends EventEmitter { this.eventQueue = []; this.currentTime = 0; this.equityCurve = []; - this.trades = []; + this.pendingOrders.clear(); + this.ordersListenerSetup = false; this.marketSimulator.reset(); } @@ -900,9 +934,21 @@ export class BacktestEngine extends EventEmitter { } async exportResults(format: 'json' | 'csv' | 'html' = 'json'): Promise { + // Get trades from Rust core + const tradingEngine = this.strategyManager.getTradingEngine(); + let closedTrades: any[] = []; + if (tradingEngine && tradingEngine.getClosedTrades) { + try { + const tradesJson = tradingEngine.getClosedTrades(); + closedTrades = JSON.parse(tradesJson); + } catch (error) { + this.container.logger.warn('Failed to get closed trades for export:', error); + } + } + const result = { - summary: this.calculatePerformance(), - trades: this.trades, + summary: this.calculatePerformance(closedTrades), + trades: closedTrades, equityCurve: this.equityCurve, drawdowns: this.calculateDrawdown(), dataQuality: this.dataManager.getDataQualityReport(), @@ -927,14 +973,14 @@ export class BacktestEngine extends EventEmitter { // Simple CSV conversion for trades const headers = ['Date', 'Symbol', 'Side', 'Entry', 'Exit', 'Quantity', 'PnL', 'Return%']; const rows = result.trades.map(t => [ - new Date(t.entryTime).toISOString(), + new Date(t.entry_time).toISOString(), t.symbol, - t.side, - t.entryPrice, - t.exitPrice, + t.side === 'Buy' ? 'buy' : 'sell', + t.entry_price, + t.exit_price, t.quantity, t.pnl, - t.returnPct + t.pnl_percent ]); return [headers, ...rows].map(row => row.join(',')).join('\n'); @@ -1021,98 +1067,100 @@ export class BacktestEngine extends EventEmitter { return ohlcData; } - private pendingOrders: Map = new Map(); - private ordersListenerSetup = false; + private setupOrderListener(): void { + if (this.ordersListenerSetup) return; + + // Listen for orders from strategy manager + this.strategyManager.on('order', async (orderEvent: any) => { + this.container.logger.info('[BacktestEngine] Order from StrategyManager:', orderEvent); + this.pendingOrders.set(orderEvent.orderId, { + ...orderEvent, + timestamp: this.currentTime + }); + }); + + this.ordersListenerSetup = true; + } + private setupStrategyOrderListeners(): void { + // Listen for orders directly from strategies + const strategies = this.strategyManager.getAllStrategies(); + this.container.logger.info(`Setting up order listeners for ${strategies.length} strategies`); + + strategies.forEach(strategy => { + this.container.logger.info(`Setting up order listener for strategy: ${strategy.config.id}`); + + const orderHandler = async (order: any) => { + const orderId = `${strategy.config.id}-${Date.now()}-${Math.random()}`; + this.container.logger.info(`[BacktestEngine] šŸ”” Order from strategy ${strategy.config.id}:`, order); + this.container.logger.info(`[BacktestEngine] Adding order ${orderId} to pendingOrders. Current size: ${this.pendingOrders.size}`); + this.pendingOrders.set(orderId, { + strategyId: strategy.config.id, + orderId, + order, + timestamp: this.currentTime + }); + this.container.logger.info(`[BacktestEngine] After adding, pendingOrders size: ${this.pendingOrders.size}`); + }; + + strategy.on('order', orderHandler); + + // Also listen for signals + strategy.on('signal', (signal: any) => { + this.container.logger.info(`[BacktestEngine] šŸ“Š Signal from strategy ${strategy.config.id}:`, signal); + }); + }); + } + private async checkAndFillOrders(data: MarketData): Promise { if (data.type !== 'bar') return; const symbol = data.data.symbol; const currentPrice = data.data.close; - // Listen for orders from strategy manager - if (!this.ordersListenerSetup) { - this.strategyManager.on('order', async (orderEvent: any) => { - this.container.logger.info('New order received:', orderEvent); - this.pendingOrders.set(orderEvent.orderId, { - ...orderEvent, - timestamp: this.currentTime - }); - }); - this.ordersListenerSetup = true; - } - // Check pending orders for this symbol + const ordersToFill: string[] = []; + + this.container.logger.info(`[checkAndFillOrders] Checking ${this.pendingOrders.size} pending orders for ${symbol}`); + for (const [orderId, orderEvent] of this.pendingOrders) { if (orderEvent.order.symbol === symbol) { // For market orders, fill immediately at current price if (orderEvent.order.orderType === 'market') { - const fillPrice = orderEvent.order.side === 'buy' ? - currentPrice * (1 + (this.marketSimulator.config.slippage || 0.0001)) : - currentPrice * (1 - (this.marketSimulator.config.slippage || 0.0001)); - - const fill = { - orderId, - symbol: orderEvent.order.symbol, - side: orderEvent.order.side, - quantity: orderEvent.order.quantity, - price: fillPrice, - timestamp: this.currentTime, - commission: orderEvent.order.quantity * fillPrice * 0.001, // 0.1% commission - strategyId: orderEvent.strategyId - }; - - this.container.logger.info(`āœ… Filling ${orderEvent.order.side} order for ${symbol} @ ${fillPrice}`); - - await this.processFill(fill); - this.pendingOrders.delete(orderId); + this.container.logger.info(`[checkAndFillOrders] Found market order ${orderId} for ${symbol}`); + ordersToFill.push(orderId); } - // TODO: Handle limit orders } } + + // Process fills + for (const orderId of ordersToFill) { + const orderEvent = this.pendingOrders.get(orderId); + if (!orderEvent) continue; + + const fillPrice = orderEvent.order.side === 'buy' ? + currentPrice * (1 + (this.marketSimulator.config.slippage || 0.0001)) : + currentPrice * (1 - (this.marketSimulator.config.slippage || 0.0001)); + + const fill = { + orderId, + symbol: orderEvent.order.symbol, + side: orderEvent.order.side, + quantity: orderEvent.order.quantity, + price: fillPrice, + timestamp: this.currentTime, + commission: orderEvent.order.quantity * fillPrice * 0.001, // 0.1% commission + strategyId: orderEvent.strategyId + }; + + this.container.logger.info(`āœ… Filling ${orderEvent.order.side} order for ${symbol} @ ${fillPrice} at timestamp ${this.currentTime} (${new Date(this.currentTime).toISOString()})`); + + // Remove from pending BEFORE processing to avoid duplicate fills + this.pendingOrders.delete(orderId); + + // Process the fill (this will update trading engine AND notify strategies) + await this.processFill(fill); + } } - private updateClosedTrades(fill: any): void { - // Find open trades for this symbol - const openTrades = this.trades.filter(t => - t.symbol === fill.symbol && - t.side === 'buy' && - !t.exitTime - ); - - if (openTrades.length > 0) { - // FIFO - close oldest trade first - const tradeToClose = openTrades[0]; - tradeToClose.exitPrice = fill.price; - tradeToClose.exitTime = fill.timestamp || this.currentTime; - tradeToClose.pnl = (tradeToClose.exitPrice - tradeToClose.entryPrice) * tradeToClose.quantity - tradeToClose.commission - (fill.commission || 0); - tradeToClose.returnPct = ((tradeToClose.exitPrice - tradeToClose.entryPrice) / tradeToClose.entryPrice) * 100; - tradeToClose.holdingPeriod = tradeToClose.exitTime - tradeToClose.entryTime; - - this.container.logger.info(`šŸ’° Trade closed: P&L ${tradeToClose.pnl.toFixed(2)}, Return ${tradeToClose.returnPct.toFixed(2)}%`); - } else { - // Log if we're trying to sell without an open position - this.container.logger.warn(`āš ļø Sell order for ${fill.symbol} but no open trades found`); - - // Still record it as a trade for tracking purposes (short position) - const trade = { - symbol: fill.symbol, - side: 'sell', - quantity: fill.quantity, - entryPrice: fill.price, - entryTime: fill.timestamp || this.currentTime, - exitPrice: null, - exitTime: null, - pnl: 0, - returnPct: 0, - commission: fill.commission || 0, - currentPrice: fill.price, - holdingPeriod: 0, - backtestTime: this.currentTime - }; - - this.trades.push(trade); - this.container.logger.info(`šŸ’µ Short trade opened: ${fill.quantity} ${fill.symbol} @ ${fill.price}`); - } - } } \ No newline at end of file diff --git a/apps/stock/orchestrator/src/strategies/BaseStrategy.ts b/apps/stock/orchestrator/src/strategies/BaseStrategy.ts index 7af4c1b..be298c2 100644 --- a/apps/stock/orchestrator/src/strategies/BaseStrategy.ts +++ b/apps/stock/orchestrator/src/strategies/BaseStrategy.ts @@ -74,7 +74,10 @@ export abstract class BaseStrategy extends EventEmitter { // Market data handling async onMarketData(data: MarketData): Promise { - if (!this.isActive) return; + if (!this.isActive) { + logger.info(`[BaseStrategy] Strategy ${this.config.id} received market data but is not active`); + return; + } try { // Update any indicators or state @@ -210,20 +213,27 @@ export abstract class BaseStrategy extends EventEmitter { // 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' && currentPosition <= 0) { + if (signal.type === 'buy') { + // Allow buying to open long or close short return { symbol: signal.symbol, side: 'buy', - quantity: this.calculatePositionSize(signal), + quantity: quantity, orderType: 'market', timeInForce: 'DAY' }; - } else if (signal.type === 'sell' && currentPosition >= 0) { + } else if (signal.type === 'sell') { + // Allow selling to close long or open short return { symbol: signal.symbol, side: 'sell', - quantity: this.calculatePositionSize(signal), + quantity: quantity, orderType: 'market', timeInForce: 'DAY' }; diff --git a/apps/stock/orchestrator/src/strategies/StrategyManager.ts b/apps/stock/orchestrator/src/strategies/StrategyManager.ts index af99f55..c869adf 100644 --- a/apps/stock/orchestrator/src/strategies/StrategyManager.ts +++ b/apps/stock/orchestrator/src/strategies/StrategyManager.ts @@ -163,6 +163,12 @@ export class StrategyManager extends EventEmitter { private async handleMarketData(data: MarketData): Promise { // Forward to all active strategies + if (this.activeStrategies.size === 0) { + this.container.logger.info(`[StrategyManager] No active strategies to process market data! All strategies: ${Array.from(this.strategies.keys()).join(', ')}`); + } else { + this.container.logger.info(`[StrategyManager] Forwarding market data to ${this.activeStrategies.size} active strategies: ${Array.from(this.activeStrategies).join(', ')}`); + } + for (const strategyId of this.activeStrategies) { const strategy = this.strategies.get(strategyId); if (strategy) { @@ -267,6 +273,10 @@ export class StrategyManager extends EventEmitter { getStrategy(strategyId: string): BaseStrategy | undefined { return this.strategies.get(strategyId); } + + getAllStrategies(): BaseStrategy[] { + return Array.from(this.strategies.values()); + } async shutdown(): Promise { this.container.logger.info('Shutting down strategy manager...'); diff --git a/apps/stock/orchestrator/src/strategies/examples/SimpleMovingAverageCrossover.ts b/apps/stock/orchestrator/src/strategies/examples/SimpleMovingAverageCrossover.ts index e20d068..dba4d7d 100644 --- a/apps/stock/orchestrator/src/strategies/examples/SimpleMovingAverageCrossover.ts +++ b/apps/stock/orchestrator/src/strategies/examples/SimpleMovingAverageCrossover.ts @@ -7,6 +7,7 @@ const logger = getLogger('SimpleMovingAverageCrossover'); export class SimpleMovingAverageCrossover extends BaseStrategy { private priceHistory = new Map(); private lastTradeTime = new Map(); + private barCount = new Map(); private totalSignals = 0; // Strategy parameters @@ -30,12 +31,17 @@ export class SimpleMovingAverageCrossover extends BaseStrategy { // Update price history if (!this.priceHistory.has(symbol)) { this.priceHistory.set(symbol, []); + this.barCount.set(symbol, 0); logger.info(`šŸ“Š Starting to track ${symbol} @ ${price}`); } const history = this.priceHistory.get(symbol)!; history.push(price); + // Increment bar count + const currentBar = (this.barCount.get(symbol) || 0) + 1; + this.barCount.set(symbol, currentBar); + // Keep only needed history if (history.length > this.SLOW_PERIOD * 2) { history.shift(); @@ -74,8 +80,9 @@ export class SimpleMovingAverageCrossover extends BaseStrategy { const timestamp = new Date(data.data.timestamp).toISOString().split('T')[0]; // Check minimum holding period + const currentBar = this.barCount.get(symbol) || 0; const lastTradeBar = this.lastTradeTime.get(symbol) || 0; - const barsSinceLastTrade = lastTradeBar > 0 ? history.length - lastTradeBar : Number.MAX_SAFE_INTEGER; + const barsSinceLastTrade = lastTradeBar > 0 ? currentBar - lastTradeBar : Number.MAX_SAFE_INTEGER; // Detect crossovers FIRST const goldenCross = prevFastMA <= prevSlowMA && fastMA > slowMA; @@ -87,7 +94,7 @@ export class SimpleMovingAverageCrossover extends BaseStrategy { const masAreClose = Math.abs(maDiffPct) < 1.0; // Within 1% if (history.length % this.DEBUG_INTERVAL === 0 || masAreClose || goldenCross || deathCross) { - logger.info(`${symbol} @ ${timestamp} [Bar ${history.length}]:`); + logger.info(`${symbol} @ ${timestamp} [Bar ${currentBar}]:`); logger.info(` Price: $${currentPrice.toFixed(2)}`); logger.info(` Fast MA (${this.FAST_PERIOD}): $${fastMA.toFixed(2)}`); logger.info(` Slow MA (${this.SLOW_PERIOD}): $${slowMA.toFixed(2)}`); @@ -131,7 +138,7 @@ export class SimpleMovingAverageCrossover extends BaseStrategy { } }; - this.lastTradeTime.set(symbol, history.length); + this.lastTradeTime.set(symbol, currentBar); this.totalSignals++; logger.info(`šŸ‘‰ Total signals generated: ${this.totalSignals}`); return signal; @@ -160,7 +167,7 @@ export class SimpleMovingAverageCrossover extends BaseStrategy { } }; - this.lastTradeTime.set(symbol, history.length); + this.lastTradeTime.set(symbol, currentBar); this.totalSignals++; logger.info(`šŸ‘‰ Total signals generated: ${this.totalSignals}`); return signal; @@ -194,7 +201,7 @@ export class SimpleMovingAverageCrossover extends BaseStrategy { } }; - this.lastTradeTime.set(symbol, history.length); + this.lastTradeTime.set(symbol, currentBar); this.totalSignals++; logger.info(`šŸ‘‰ Total signals generated: ${this.totalSignals}`); return signal; @@ -222,7 +229,7 @@ export class SimpleMovingAverageCrossover extends BaseStrategy { } }; - this.lastTradeTime.set(symbol, history.length); + this.lastTradeTime.set(symbol, currentBar); this.totalSignals++; logger.info(`šŸ‘‰ Total signals generated: ${this.totalSignals}`); return signal; diff --git a/apps/stock/orchestrator/test-backtest-simple.ts b/apps/stock/orchestrator/test-backtest-simple.ts new file mode 100644 index 0000000..fb9dfd8 --- /dev/null +++ b/apps/stock/orchestrator/test-backtest-simple.ts @@ -0,0 +1,70 @@ +#!/usr/bin/env bun + +/** + * Simple backtest test without full container + */ + +import { BacktestEngine } from './src/backtest/BacktestEngine'; +import { StrategyManager } from './src/strategies/StrategyManager'; +import { StorageService } from './src/services/StorageService'; +import { getLogger } from '@stock-bot/logger'; + +async function runSimpleBacktest() { + console.log('Running simple backtest test...\n'); + + // Create minimal container + const logger = getLogger('test'); + const container = { + logger, + custom: {} + }; + + // Create services + const storageService = new StorageService(container as any); + const strategyManager = new StrategyManager(container as any); + + // Initialize strategy + await strategyManager.initializeStrategies([{ + id: 'test-sma', + name: 'sma-crossover', + enabled: true, + symbols: ['AAPL'], + allocation: 1.0 + }]); + + // Create backtest engine + const backtestEngine = new BacktestEngine(container as any, storageService, strategyManager); + + const config = { + mode: 'backtest', + name: 'Simple SMA Test', + strategy: 'sma-crossover', + symbols: ['AAPL'], + startDate: '2023-01-01T00:00:00Z', + endDate: '2023-03-01T00:00:00Z', // Just 2 months + initialCapital: 100000, + dataFrequency: '1d', + commission: 0.001, + slippage: 0.0001 + }; + + try { + const result = await backtestEngine.runBacktest(config); + + console.log('\nBacktest Results:'); + console.log(`Total Return: ${result.metrics.totalReturn.toFixed(2)}%`); + console.log(`Total Trades: ${result.metrics.totalTrades}`); + console.log(`Trades in history: ${result.trades.length}`); + console.log(`Win Rate: ${result.metrics.winRate.toFixed(2)}%`); + + console.log('\nTrade Details:'); + result.trades.forEach((trade, i) => { + console.log(`Trade ${i + 1}: ${trade.side} ${trade.quantity} @ $${trade.entryPrice.toFixed(2)} -> $${trade.exitPrice.toFixed(2)} (P&L: $${trade.pnl.toFixed(2)})`); + }); + + } catch (error) { + console.error('Backtest failed:', error); + } +} + +runSimpleBacktest().catch(console.error); \ No newline at end of file diff --git a/apps/stock/orchestrator/test-clear-crossovers.ts b/apps/stock/orchestrator/test-clear-crossovers.ts new file mode 100755 index 0000000..8b745d4 --- /dev/null +++ b/apps/stock/orchestrator/test-clear-crossovers.ts @@ -0,0 +1,138 @@ +#!/usr/bin/env bun + +/** + * Test with very clear crossover patterns + */ + +import { BacktestEngine } from './src/backtest/BacktestEngine'; +import { StrategyManager } from './src/strategies/StrategyManager'; +import { StorageService } from './src/services/StorageService'; +import { getLogger } from '@stock-bot/logger'; +import { ModeManager } from './src/core/ModeManager'; +import { MarketDataService } from './src/services/MarketDataService'; +import { ExecutionService } from './src/services/ExecutionService'; +import { DataManager } from './src/data/DataManager'; + +async function testClearCrossovers() { + console.log('=== Test with Clear Crossovers ===\n'); + + const logger = getLogger('test'); + const container = { + logger, + custom: {} + }; + + const storageService = new StorageService(container as any); + const marketDataService = new MarketDataService(container as any); + const executionService = new ExecutionService(container as any); + const modeManager = new ModeManager(container as any, marketDataService, executionService, storageService); + + container.custom = { + ModeManager: modeManager, + MarketDataService: marketDataService, + ExecutionService: executionService + }; + + const strategyManager = new StrategyManager(container as any); + const backtestEngine = new BacktestEngine(container as any, storageService, strategyManager); + + // Override data loading to provide clear patterns + const dataManager = new DataManager(container as any, storageService); + (backtestEngine as any).dataManager = dataManager; + + (dataManager as any).loadHistoricalData = async (symbols: string[], startDate: Date, endDate: Date) => { + const data = new Map(); + const bars = []; + + console.log('Generating clear crossover patterns...'); + + // Generate 100 days of data with 3 clear crossovers + // Pattern: Start high, go low (death cross), go high (golden cross), go low (death cross) + + for (let i = 0; i < 100; i++) { + let price; + + if (i < 20) { + // Start at 100, slight upward trend + price = 100 + i * 0.5; + } else if (i < 40) { + // Sharp downtrend from 110 to 70 (death cross around day 30) + price = 110 - (i - 20) * 2; + } else if (i < 60) { + // Sharp uptrend from 70 to 110 (golden cross around day 50) + price = 70 + (i - 40) * 2; + } else if (i < 80) { + // Sharp downtrend from 110 to 70 (death cross around day 70) + price = 110 - (i - 60) * 2; + } else { + // Stabilize around 70 + price = 70 + Math.sin((i - 80) * 0.3) * 2; + } + + const timestamp = startDate.getTime() + i * 86400000; + + bars.push({ + type: 'bar', + data: { + symbol: 'AAPL', + open: price - 0.5, + high: price + 1, + low: price - 1, + close: price, + volume: 1000000, + timestamp + } + }); + + if (i % 10 === 0) { + console.log(`Day ${i + 1}: Price = $${price.toFixed(2)}`); + } + } + + console.log('\nExpected crossovers:'); + console.log('- Death cross around day 30'); + console.log('- Golden cross around day 50'); + console.log('- Death cross around day 70\n'); + + data.set('AAPL', bars); + return data; + }; + + const config = { + mode: 'backtest' as const, + name: 'Clear Crossovers Test', + strategy: 'sma-crossover', + symbols: ['AAPL'], + startDate: '2023-01-01T00:00:00Z', + endDate: '2023-04-10T00:00:00Z', // 100 days + initialCapital: 100000, + dataFrequency: '1d', + commission: 0.001, + slippage: 0.0001, + speed: 'max' as const + }; + + await modeManager.initializeMode(config); + + try { + const result = await backtestEngine.runBacktest(config); + + console.log('\n=== Backtest Results ==='); + console.log(`Total Return: ${result.metrics.totalReturn.toFixed(2)}%`); + console.log(`Total Trades: ${result.metrics.totalTrades}`); + console.log(`Trades in history: ${result.trades.length}`); + console.log(`Win Rate: ${result.metrics.winRate.toFixed(2)}%`); + + console.log('\nTrade Details:'); + result.trades.forEach((trade, i) => { + const entry = new Date(trade.entryDate).toLocaleDateString(); + const exit = trade.exitDate ? new Date(trade.exitDate).toLocaleDateString() : 'OPEN'; + console.log(`Trade ${i + 1}: ${trade.side} ${trade.quantity} @ $${trade.entryPrice.toFixed(2)} (${entry}) -> ${exit === 'OPEN' ? 'OPEN' : `$${trade.exitPrice.toFixed(2)} (${exit})`} | P&L: ${trade.pnl.toFixed(2)}`); + }); + + } catch (error) { + console.error('Backtest failed:', error); + } +} + +testClearCrossovers().catch(console.error); \ No newline at end of file diff --git a/apps/stock/orchestrator/test-full-flow.ts b/apps/stock/orchestrator/test-full-flow.ts new file mode 100644 index 0000000..bb4d23d --- /dev/null +++ b/apps/stock/orchestrator/test-full-flow.ts @@ -0,0 +1,93 @@ +#!/usr/bin/env bun + +/** + * Test the full flow from market data to trade execution + */ + +import { SimpleMovingAverageCrossover } from './src/strategies/examples/SimpleMovingAverageCrossover'; +import { BaseStrategy } from './src/strategies/BaseStrategy'; +import { MarketData } from './src/types'; +import { getLogger } from '@stock-bot/logger'; + +const logger = getLogger('test-flow'); + +async function testFullFlow() { + console.log('Testing full flow from market data to orders...\n'); + + // Create strategy + const config = { + id: 'test-sma', + name: 'Test SMA', + enabled: true, + symbols: ['AAPL'], + allocation: 1.0 + }; + + const strategy = new SimpleMovingAverageCrossover(config, null, null); + + let signalCount = 0; + let orderCount = 0; + + // Listen for signals + strategy.on('signal', (signal) => { + signalCount++; + logger.info(`Signal #${signalCount}:`, signal); + }); + + // Listen for orders + strategy.on('order', (order) => { + orderCount++; + logger.info(`Order #${orderCount}:`, order); + }); + + await strategy.start(); + + // Generate 50 days of data with a clear uptrend after day 20 + logger.info('Generating market data with uptrend...'); + + for (let day = 0; day < 50; day++) { + const basePrice = 100; + let price = basePrice; + + // Create a clear uptrend after day 20 + if (day > 20) { + price = basePrice + (day - 20) * 0.5; // 50 cents per day uptrend + } else { + price = basePrice + Math.sin(day * 0.3) * 2; // Sideways + } + + const marketData: MarketData = { + type: 'bar', + data: { + symbol: 'AAPL', + open: price - 0.5, + high: price + 0.5, + low: price - 1, + close: price, + volume: 1000000, + timestamp: Date.now() + day * 86400000 + } + }; + + // Process the data + await strategy.onMarketData(marketData); + + if (day === 19) { + logger.info(`Day 19: Last day before uptrend, price = ${price}`); + } + if (day === 25) { + logger.info(`Day 25: Should see golden cross soon, price = ${price}`); + } + } + + await strategy.stop(); + + console.log('\n=== Test Results ==='); + console.log(`Total signals generated: ${signalCount}`); + console.log(`Total orders generated: ${orderCount}`); + + const perf = strategy.getPerformance(); + console.log('Strategy performance:', perf); +} + +testFullFlow().catch(console.error); \ No newline at end of file diff --git a/apps/stock/orchestrator/test-minimal-backtest.ts b/apps/stock/orchestrator/test-minimal-backtest.ts new file mode 100755 index 0000000..b44ad64 --- /dev/null +++ b/apps/stock/orchestrator/test-minimal-backtest.ts @@ -0,0 +1,80 @@ +#!/usr/bin/env bun + +/** + * Minimal test to debug order flow + */ + +import { BacktestEngine } from './src/backtest/BacktestEngine'; +import { StrategyManager } from './src/strategies/StrategyManager'; +import { StorageService } from './src/services/StorageService'; +import { getLogger } from '@stock-bot/logger'; +import { TradingEngine } from '@stock-bot/core'; +import { ModeManager } from './src/core/ModeManager'; +import { MarketDataService } from './src/services/MarketDataService'; +import { ExecutionService } from './src/services/ExecutionService'; + +async function testMinimalBacktest() { + console.log('=== Minimal Backtest Test ===\n'); + + const logger = getLogger('test'); + const container = { + logger, + custom: {} + }; + + const storageService = new StorageService(container as any); + const marketDataService = new MarketDataService(container as any); + const executionService = new ExecutionService(container as any); + const modeManager = new ModeManager(container as any, marketDataService, executionService, storageService); + + // Add services to container + container.custom = { + ModeManager: modeManager, + MarketDataService: marketDataService, + ExecutionService: executionService + }; + + const strategyManager = new StrategyManager(container as any); + + // Add debug logging to strategy manager + const origHandleMarketData = (strategyManager as any).handleMarketData; + (strategyManager as any).handleMarketData = async function(data: any) { + console.log(`>>> StrategyManager.handleMarketData called for ${data.data.symbol} @ ${data.data.close}`); + console.log(` Active strategies: ${this.activeStrategies.size}`); + console.log(` Strategy IDs: ${Array.from(this.activeStrategies).join(', ')}`); + return origHandleMarketData.call(this, data); + }; + + const backtestEngine = new BacktestEngine(container as any, storageService, strategyManager); + + const config = { + mode: 'backtest' as const, + name: 'Minimal Test', + strategy: 'sma-crossover', + symbols: ['AAPL'], + startDate: '2023-01-01T00:00:00Z', + endDate: '2023-02-15T00:00:00Z', // 45 days to ensure we have enough data + initialCapital: 100000, + dataFrequency: '1d', + commission: 0.001, + slippage: 0.0001, + speed: 'max' as const + }; + + // Initialize mode manager with backtest config + await modeManager.initializeMode(config); + + try { + const result = await backtestEngine.runBacktest(config); + + console.log('\nResults:'); + console.log(`Total Return: ${result.metrics.totalReturn.toFixed(2)}%`); + console.log(`Total Trades: ${result.metrics.totalTrades}`); + console.log(`Trades in history: ${result.trades.length}`); + + } catch (error) { + console.error('Backtest failed:', error); + } +} + +testMinimalBacktest().catch(console.error); \ No newline at end of file diff --git a/apps/stock/orchestrator/test-order-flow.ts b/apps/stock/orchestrator/test-order-flow.ts new file mode 100755 index 0000000..3e2b9f0 --- /dev/null +++ b/apps/stock/orchestrator/test-order-flow.ts @@ -0,0 +1,122 @@ +#!/usr/bin/env bun + +/** + * Test order flow through the full backtest system + */ + +import { BacktestEngine } from './src/backtest/BacktestEngine'; +import { StrategyManager } from './src/strategies/StrategyManager'; +import { StorageService } from './src/services/StorageService'; +import { getLogger } from '@stock-bot/logger'; +import { SimpleMovingAverageCrossover } from './src/strategies/examples/SimpleMovingAverageCrossover'; + +async function testOrderFlow() { + console.log('Testing order flow in backtest...\n'); + + // Create minimal container + const logger = getLogger('test'); + const container = { + logger, + custom: {} + }; + + // Create services + const storageService = new StorageService(container as any); + const strategyManager = new StrategyManager(container as any); + + // First test the strategy directly + console.log('=== Testing Strategy Directly ==='); + const testStrategy = new SimpleMovingAverageCrossover({ + id: 'test-direct', + name: 'Direct Test', + enabled: true, + symbols: ['AAPL'], + allocation: 1.0 + }, null, null); + + let directSignals = 0; + let directOrders = 0; + + testStrategy.on('signal', (signal) => { + directSignals++; + console.log(`Direct signal #${directSignals}:`, signal); + }); + + testStrategy.on('order', (order) => { + directOrders++; + console.log(`Direct order #${directOrders}:`, order); + }); + + await testStrategy.start(); + + // Generate test data with clear crossover + for (let i = 0; i < 30; i++) { + const basePrice = 100; + const price = i < 15 ? basePrice - i * 0.5 : basePrice + (i - 15) * 0.5; + + await testStrategy.onMarketData({ + type: 'bar', + data: { + symbol: 'AAPL', + open: price, + high: price + 1, + low: price - 1, + close: price, + volume: 1000000, + timestamp: Date.now() + i * 86400000 + } + }); + } + + await testStrategy.stop(); + console.log(`\nDirect test: ${directSignals} signals, ${directOrders} orders\n`); + + // Now test through backtest engine + console.log('=== Testing Through Backtest Engine ==='); + + // Create backtest engine (it will initialize strategies) + const backtestEngine = new BacktestEngine(container as any, storageService, strategyManager); + + // Hook into backtest engine to see what's happening + const origProcessMarketData = (backtestEngine as any).processMarketData; + (backtestEngine as any).processMarketData = async function(data: any) { + console.log(`Processing market data: ${data.data.symbol} @ ${data.data.close}`); + return origProcessMarketData.call(this, data); + }; + + const origCheckAndFillOrders = (backtestEngine as any).checkAndFillOrders; + (backtestEngine as any).checkAndFillOrders = async function(data: any) { + const pendingCount = this.pendingOrders.size; + if (pendingCount > 0) { + console.log(`Checking ${pendingCount} pending orders...`); + } + return origCheckAndFillOrders.call(this, data); + }; + + const config = { + mode: 'backtest', + name: 'Order Flow Test', + strategy: 'sma-crossover', + symbols: ['AAPL'], + startDate: '2023-01-01T00:00:00Z', + endDate: '2023-02-01T00:00:00Z', // Just 1 month + initialCapital: 100000, + dataFrequency: '1d', + commission: 0.001, + slippage: 0.0001 + }; + + try { + const result = await backtestEngine.runBacktest(config); + + console.log('\nBacktest Results:'); + console.log(`Total Return: ${result.metrics.totalReturn.toFixed(2)}%`); + console.log(`Total Trades: ${result.metrics.totalTrades}`); + console.log(`Trades in history: ${result.trades.length}`); + + } catch (error) { + console.error('Backtest failed:', error); + } +} + +testOrderFlow().catch(console.error); \ No newline at end of file diff --git a/apps/stock/orchestrator/test-predictable-backtest.ts b/apps/stock/orchestrator/test-predictable-backtest.ts new file mode 100755 index 0000000..55a3cd1 --- /dev/null +++ b/apps/stock/orchestrator/test-predictable-backtest.ts @@ -0,0 +1,187 @@ +#!/usr/bin/env bun + +/** + * Test with predictable data to ensure trades are generated + */ + +import { TradingEngine } from '@stock-bot/core'; +import { getLogger } from '@stock-bot/logger'; + +async function testPredictableBacktest() { + console.log('=== Predictable Backtest Test ===\n'); + + const logger = getLogger('test'); + + // Create trading engine directly + const tradingEngine = new TradingEngine('backtest', { + startTime: new Date('2023-01-01').getTime(), + endTime: new Date('2023-03-01').getTime(), + speedMultiplier: 0 + }); + + // Set initial capital + await tradingEngine.setCapital(100000); + + // Generate predictable price data that will cause crossovers + console.log('Generating predictable market data...'); + + // Phase 1: Downtrend (days 1-25) - prices fall from 100 to 75 + for (let i = 0; i < 25; i++) { + const price = 100 - i; + const timestamp = new Date('2023-01-01').getTime() + i * 86400000; + + await tradingEngine.advanceTime(timestamp); + await tradingEngine.updateQuote('AAPL', price - 0.01, price + 0.01, 10000, 10000); + + if (i % 5 === 0) { + console.log(`Day ${i + 1}: Price = $${price}`); + } + } + + // Phase 2: Uptrend (days 26-50) - prices rise from 76 to 100 + for (let i = 25; i < 50; i++) { + const price = 76 + (i - 25); + const timestamp = new Date('2023-01-01').getTime() + i * 86400000; + + await tradingEngine.advanceTime(timestamp); + await tradingEngine.updateQuote('AAPL', price - 0.01, price + 0.01, 10000, 10000); + + if (i % 5 === 0) { + console.log(`Day ${i + 1}: Price = $${price}`); + } + } + + // Phase 3: Another downtrend (days 51-60) - prices fall from 99 to 90 + for (let i = 50; i < 60; i++) { + const price = 100 - (i - 50); + const timestamp = new Date('2023-01-01').getTime() + i * 86400000; + + await tradingEngine.advanceTime(timestamp); + await tradingEngine.updateQuote('AAPL', price - 0.01, price + 0.01, 10000, 10000); + + if (i % 5 === 0) { + console.log(`Day ${i + 1}: Price = $${price}`); + } + } + + // Test moving averages manually + console.log('\n=== Expected Crossovers ==='); + console.log('Around day 35-40: Golden cross (10 SMA crosses above 20 SMA)'); + console.log('Around day 55-60: Death cross (10 SMA crosses below 20 SMA)'); + + // Get results + const closedTrades = tradingEngine.getClosedTrades ? JSON.parse(tradingEngine.getClosedTrades()) : []; + const tradeCount = tradingEngine.getTradeCount ? tradingEngine.getTradeCount() : 0; + const [realizedPnl, unrealizedPnl] = tradingEngine.getTotalPnl(); + + console.log('\n=== Results ==='); + console.log(`Total trades: ${tradeCount}`); + console.log(`Closed trades: ${closedTrades.length}`); + console.log(`Realized P&L: $${realizedPnl.toFixed(2)}`); + console.log(`Unrealized P&L: $${unrealizedPnl.toFixed(2)}`); + + // Now let's test the full backtest with this data pattern + console.log('\n=== Running Full Backtest with SMA Strategy ==='); + + const { BacktestEngine } = await import('./src/backtest/BacktestEngine'); + const { StrategyManager } = await import('./src/strategies/StrategyManager'); + const { StorageService } = await import('./src/services/StorageService'); + const { ModeManager } = await import('./src/core/ModeManager'); + const { MarketDataService } = await import('./src/services/MarketDataService'); + const { ExecutionService } = await import('./src/services/ExecutionService'); + const { DataManager } = await import('./src/data/DataManager'); + + const container = { + logger, + custom: {} + }; + + const storageService = new StorageService(container as any); + const marketDataService = new MarketDataService(container as any); + const executionService = new ExecutionService(container as any); + const modeManager = new ModeManager(container as any, marketDataService, executionService, storageService); + + container.custom = { + ModeManager: modeManager, + MarketDataService: marketDataService, + ExecutionService: executionService + }; + + const strategyManager = new StrategyManager(container as any); + const backtestEngine = new BacktestEngine(container as any, storageService, strategyManager); + + // Override the data manager to return our predictable data + const dataManager = new DataManager(container as any, storageService); + (backtestEngine as any).dataManager = dataManager; + + // Mock the loadHistoricalData to return our pattern + (dataManager as any).loadHistoricalData = async (symbols: string[], startDate: Date, endDate: Date) => { + const data = new Map(); + const bars = []; + + // Generate the same pattern as above + for (let i = 0; i < 60; i++) { + let price; + if (i < 25) { + price = 100 - i; + } else if (i < 50) { + price = 76 + (i - 25); + } else { + price = 100 - (i - 50); + } + + const timestamp = startDate.getTime() + i * 86400000; + bars.push({ + type: 'bar', + data: { + symbol: 'AAPL', + open: price - 0.5, + high: price + 0.5, + low: price - 0.5, + close: price, + volume: 1000000, + timestamp + } + }); + } + + data.set('AAPL', bars); + return data; + }; + + const config = { + mode: 'backtest' as const, + name: 'Predictable Test', + strategy: 'sma-crossover', + symbols: ['AAPL'], + startDate: '2023-01-01', + endDate: '2023-03-01', + initialCapital: 100000, + dataFrequency: '1d', + commission: 0.001, + slippage: 0.0001, + speed: 'max' as const + }; + + await modeManager.initializeMode(config); + + try { + const result = await backtestEngine.runBacktest(config); + + console.log('\nBacktest Results:'); + console.log(`Total Return: ${result.metrics.totalReturn.toFixed(2)}%`); + console.log(`Total Trades: ${result.metrics.totalTrades}`); + console.log(`Trades in history: ${result.trades.length}`); + console.log(`Win Rate: ${result.metrics.winRate.toFixed(2)}%`); + + console.log('\nTrade Details:'); + result.trades.forEach((trade, i) => { + console.log(`Trade ${i + 1}: ${trade.side} ${trade.quantity} @ $${trade.entryPrice.toFixed(2)} -> $${trade.exitPrice?.toFixed(2) || 'OPEN'}`); + }); + + } catch (error) { + console.error('Backtest failed:', error); + } +} + +testPredictableBacktest().catch(console.error); \ No newline at end of file diff --git a/apps/stock/orchestrator/test-quick-backtest.ts b/apps/stock/orchestrator/test-quick-backtest.ts new file mode 100644 index 0000000..4d2054d --- /dev/null +++ b/apps/stock/orchestrator/test-quick-backtest.ts @@ -0,0 +1,54 @@ +#!/usr/bin/env bun + +/** + * Quick test of backtest with fixed order execution + */ + +import { createContainer } from './src/simple-container'; +import { BacktestEngine } from './src/backtest/BacktestEngine'; + +async function runQuickBacktest() { + const container = await createContainer(); + + const backtestEngine = container.resolve('backtestEngine') as BacktestEngine; + + const config = { + name: 'Quick SMA Test', + strategy: 'sma-crossover', + symbols: ['AAPL'], + startDate: '2020-01-01', + endDate: '2021-01-01', // Just 1 year for quick test + initialCapital: 100000, + dataFrequency: '1d', + commission: 0.001, + slippage: 0.0001 + }; + + console.log('Running quick backtest...\n'); + + try { + const result = await backtestEngine.runBacktest(config); + + console.log('Backtest Results:'); + console.log(`Total Return: ${result.metrics.totalReturn.toFixed(2)}%`); + console.log(`Total Trades: ${result.metrics.totalTrades}`); + console.log(`Win Rate: ${result.metrics.winRate.toFixed(2)}%`); + console.log(`Sharpe Ratio: ${result.metrics.sharpeRatio.toFixed(2)}`); + console.log(`Max Drawdown: ${result.metrics.maxDrawdown.toFixed(2)}%`); + console.log('\nTrade History:'); + console.log(`Trades in history: ${result.trades.length}`); + + result.trades.slice(0, 5).forEach(trade => { + console.log(`- ${trade.side} ${trade.quantity} @ $${trade.entryPrice} -> $${trade.exitPrice} (${trade.pnlPercent.toFixed(2)}%)`); + }); + + if (result.trades.length > 5) { + console.log(`... and ${result.trades.length - 5} more trades`); + } + + } catch (error) { + console.error('Backtest failed:', error); + } +} + +runQuickBacktest().catch(console.error); \ No newline at end of file diff --git a/apps/stock/orchestrator/test-sma-trades.ts b/apps/stock/orchestrator/test-sma-trades.ts new file mode 100755 index 0000000..061ad28 --- /dev/null +++ b/apps/stock/orchestrator/test-sma-trades.ts @@ -0,0 +1,151 @@ +#!/usr/bin/env bun + +/** + * Test SMA strategy directly to debug trading + */ + +import { SimpleMovingAverageCrossover } from './src/strategies/examples/SimpleMovingAverageCrossover'; +import { getLogger } from '@stock-bot/logger'; + +async function testSMAStrategy() { + console.log('=== Testing SMA Strategy Trading ===\n'); + + const logger = getLogger('test'); + + const config = { + id: 'test-sma', + name: 'Test SMA', + enabled: true, + symbols: ['AAPL'], + allocation: 1.0 + }; + + const strategy = new SimpleMovingAverageCrossover(config, null, null); + + let signalCount = 0; + let orderCount = 0; + const orders: any[] = []; + + strategy.on('signal', (signal) => { + signalCount++; + console.log(`\nšŸ“Š Signal #${signalCount}:`, { + type: signal.type, + symbol: signal.symbol, + strength: signal.strength, + reason: signal.reason + }); + }); + + strategy.on('order', (order) => { + orderCount++; + orders.push(order); + console.log(`\nšŸ“ˆ Order #${orderCount}:`, order); + }); + + await strategy.start(); + + // Generate clear pattern: downtrend then uptrend + console.log('Generating market data with clear trend changes...\n'); + + // Phase 1: Stable around 100 for first 10 days + for (let i = 0; i < 10; i++) { + const price = 100 + Math.sin(i * 0.5) * 2; + await strategy.onMarketData({ + type: 'bar', + data: { + symbol: 'AAPL', + open: price, + high: price + 1, + low: price - 1, + close: price, + volume: 1000000, + timestamp: Date.now() + i * 86400000 + } + }); + } + + // Phase 2: Clear downtrend from 100 to 80 (days 11-30) + for (let i = 10; i < 30; i++) { + const price = 100 - (i - 10); // Falls by $1 per day + await strategy.onMarketData({ + type: 'bar', + data: { + symbol: 'AAPL', + open: price, + high: price + 0.5, + low: price - 0.5, + close: price, + volume: 1000000, + timestamp: Date.now() + i * 86400000 + } + }); + } + + // Phase 3: Clear uptrend from 80 to 110 (days 31-60) + for (let i = 30; i < 60; i++) { + const price = 80 + (i - 30); // Rises by $1 per day + await strategy.onMarketData({ + type: 'bar', + data: { + symbol: 'AAPL', + open: price, + high: price + 0.5, + low: price - 0.5, + close: price, + volume: 1000000, + timestamp: Date.now() + i * 86400000 + } + }); + + // Simulate position update after first buy + if (i === 45 && orders.length > 0) { + console.log('\nšŸ”„ Simulating position update after buy order...'); + await strategy.onOrderUpdate({ + orderId: 'test-order-1', + symbol: 'AAPL', + side: 'buy', + status: 'filled', + fills: [{ + quantity: orders[0].quantity, + price: 95 + }] + }); + } + } + + // Phase 4: Another downtrend from 110 to 90 (days 61-80) + for (let i = 60; i < 80; i++) { + const price = 110 - (i - 60); // Falls by $1 per day + + if (i % 5 === 0) { + console.log(`\nDay ${i + 1}: Price = $${price}`); + } + + await strategy.onMarketData({ + type: 'bar', + data: { + symbol: 'AAPL', + open: price, + high: price + 0.5, + low: price - 0.5, + close: price, + volume: 1000000, + timestamp: Date.now() + i * 86400000 + } + }); + } + + await strategy.stop(); + + console.log('\n=== Test Results ==='); + console.log(`Total signals generated: ${signalCount}`); + console.log(`Total orders generated: ${orderCount}`); + console.log(`\nExpected behavior:`); + console.log(`- Golden cross around day 40-45 (when 10 SMA crosses above 20 SMA)`); + console.log(`- Death cross around day 70-75 (when 10 SMA crosses below 20 SMA)`); + + const perf = strategy.getPerformance(); + console.log('\nStrategy performance:', perf); +} + +testSMAStrategy().catch(console.error); \ No newline at end of file diff --git a/apps/stock/orchestrator/test-strategy-signals.ts b/apps/stock/orchestrator/test-strategy-signals.ts new file mode 100644 index 0000000..e09dcb8 --- /dev/null +++ b/apps/stock/orchestrator/test-strategy-signals.ts @@ -0,0 +1,89 @@ +#!/usr/bin/env bun + +/** + * Test strategy signal generation + */ + +import { SimpleMovingAverageCrossover } from './src/strategies/examples/SimpleMovingAverageCrossover'; +import { MarketData } from './src/types'; + +async function testStrategySignals() { + console.log('Testing strategy signal generation...\n'); + + // Create strategy with mock config + const config = { + id: 'test-sma', + name: 'Test SMA Strategy', + enabled: true, + symbols: ['AAPL'], + allocation: 1.0 + }; + + const strategy = new SimpleMovingAverageCrossover(config, null, null); + await strategy.start(); + + // Generate 100 days of mock data with a clear trend + const basePrice = 100; + let price = basePrice; + let signalCount = 0; + + console.log('Feeding market data to strategy...\n'); + + for (let day = 0; day < 100; day++) { + // Create uptrend for days 30-50, downtrend for days 60-80 + if (day >= 30 && day < 50) { + price += 0.5; // Uptrend + } else if (day >= 60 && day < 80) { + price -= 0.5; // Downtrend + } else { + price += (Math.random() - 0.5) * 0.2; // Small random movement + } + + const marketData: MarketData = { + type: 'bar', + data: { + symbol: 'AAPL', + open: price - 0.1, + high: price + 0.2, + low: price - 0.2, + close: price, + volume: 1000000, + timestamp: Date.now() + day * 86400000 + } + }; + + // Listen for signals + strategy.once('signal', (signal) => { + signalCount++; + console.log(`Day ${day}: Signal generated!`); + console.log(` Type: ${signal.type}`); + console.log(` Strength: ${signal.strength}`); + console.log(` Reason: ${signal.reason}`); + console.log(` Metadata:`, signal.metadata); + }); + + // Listen for orders + strategy.once('order', (order) => { + console.log(`Day ${day}: Order generated!`); + console.log(` Side: ${order.side}`); + console.log(` Quantity: ${order.quantity}`); + console.log(` Type: ${order.orderType}`); + }); + + // Process the market data + await strategy.onMarketData(marketData); + + if (day % 10 === 0) { + console.log(`Day ${day}: Price = ${price.toFixed(2)}, Total signals = ${signalCount}`); + } + } + + console.log(`\nāœ… Test completed. Total signals generated: ${signalCount}`); + + const perf = strategy.getPerformance(); + console.log('\nStrategy Performance:', perf); + + await strategy.stop(); +} + +testStrategySignals().catch(console.error); \ No newline at end of file diff --git a/apps/stock/orchestrator/test-trade-history.ts b/apps/stock/orchestrator/test-trade-history.ts new file mode 100644 index 0000000..098d243 --- /dev/null +++ b/apps/stock/orchestrator/test-trade-history.ts @@ -0,0 +1,107 @@ +#!/usr/bin/env bun + +/** + * Test the new trade history functionality + */ + +import { TradingEngine } from '@stock-bot/core'; + +async function testTradeHistory() { + console.log('Testing trade history functionality...\n'); + + // Create a trading engine in backtest mode + const config = { + startTime: Date.now() - 86400000, // 24 hours ago + endTime: Date.now(), + speedMultiplier: 1.0 + }; + + const engine = new TradingEngine('backtest', config as any); + + console.log('Trading engine created in', engine.getMode(), 'mode'); + console.log('Initial trade count:', engine.getTradeCount()); + console.log('Initial closed trades:', engine.getClosedTradeCount()); + + // Simulate some trades + console.log('\n--- Simulating trades ---'); + + // Buy 100 shares at $50 + console.log('Processing BUY: 100 shares @ $50'); + engine.processFillWithMetadata( + 'AAPL', + 50.0, + 100, + 'buy', + 1.0, + 'ORDER001', + 'STRATEGY001' + ); + + console.log('Trade count after buy:', engine.getTradeCount()); + console.log('Closed trades after buy:', engine.getClosedTradeCount()); + + // Sell 50 shares at $55 + console.log('\nProcessing SELL: 50 shares @ $55'); + engine.processFillWithMetadata( + 'AAPL', + 55.0, + 50, + 'sell', + 1.0, + 'ORDER002', + 'STRATEGY001' + ); + + console.log('Trade count after partial sell:', engine.getTradeCount()); + console.log('Closed trades after partial sell:', engine.getClosedTradeCount()); + + // Get closed trades + const closedTradesJson = engine.getClosedTrades(); + const closedTrades = JSON.parse(closedTradesJson); + + console.log('\n--- Closed Trades ---'); + closedTrades.forEach((trade: any) => { + console.log(`Trade ${trade.id}:`); + console.log(` Symbol: ${trade.symbol}`); + console.log(` Entry: ${trade.entry_price} @ ${new Date(trade.entry_time).toISOString()}`); + console.log(` Exit: ${trade.exit_price} @ ${new Date(trade.exit_time).toISOString()}`); + console.log(` Quantity: ${trade.quantity}`); + console.log(` P&L: $${trade.pnl.toFixed(2)} (${trade.pnl_percent.toFixed(2)}%)`); + console.log(` Duration: ${trade.duration_ms}ms`); + }); + + // Sell remaining 50 shares at $52 + console.log('\nProcessing SELL: 50 shares @ $52'); + engine.processFillWithMetadata( + 'AAPL', + 52.0, + 50, + 'sell', + 1.0, + 'ORDER003', + 'STRATEGY001' + ); + + console.log('Trade count after full close:', engine.getTradeCount()); + console.log('Closed trades after full close:', engine.getClosedTradeCount()); + + // Get all trade history + const allTradesJson = engine.getTradeHistory(); + const allTrades = JSON.parse(allTradesJson); + + console.log('\n--- All Trade History ---'); + console.log(`Total trades executed: ${allTrades.length}`); + allTrades.forEach((trade: any) => { + console.log(`${trade.id}: ${trade.side} ${trade.quantity} ${trade.symbol} @ ${trade.price}`); + }); + + // Get final P&L + const [realizedPnl, unrealizedPnl] = engine.getTotalPnl(); + console.log('\n--- Final P&L ---'); + console.log(`Realized P&L: $${realizedPnl.toFixed(2)}`); + console.log(`Unrealized P&L: $${unrealizedPnl.toFixed(2)}`); + + console.log('\nāœ… Trade history test completed successfully!'); +} + +testTradeHistory().catch(console.error); \ No newline at end of file