diff --git a/apps/stock/core/index.node b/apps/stock/core/index.node index 9462caf..ffedd0a 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/backtest.rs b/apps/stock/core/src/api/backtest.rs index 2aa9172..ee08045 100644 --- a/apps/stock/core/src/api/backtest.rs +++ b/apps/stock/core/src/api/backtest.rs @@ -1,21 +1,23 @@ use napi::bindgen_prelude::*; -use napi::{threadsafe_function::ThreadsafeFunction, JsObject}; +use napi::{threadsafe_function::ThreadsafeFunction, JsObject, JsFunction}; use napi_derive::napi; use std::sync::Arc; use parking_lot::Mutex; use crate::backtest::{ BacktestEngine as RustBacktestEngine, BacktestConfig, - Strategy, Signal, + Strategy, Signal, SignalType, strategy::{TypeScriptStrategy, StrategyCall, StrategyResponse}, }; use crate::{TradingMode, MarketUpdate}; use chrono::{DateTime, Utc}; +use std::sync::mpsc; #[napi] pub struct BacktestEngine { inner: Arc>>, strategies: Arc>>>>, + ts_callbacks: Arc>>>, } #[napi] @@ -47,6 +49,7 @@ impl BacktestEngine { Ok(Self { inner: Arc::new(Mutex::new(Some(engine))), strategies: Arc::new(Mutex::new(Vec::new())), + ts_callbacks: Arc::new(Mutex::new(Vec::new())), }) } @@ -58,36 +61,18 @@ impl BacktestEngine { parameters: napi::JsObject, callback: napi::JsFunction, ) -> Result<()> { - // Convert JsObject to serde_json::Value - let params = serde_json::Value::Object(serde_json::Map::new()); + // For now, let's use a simple SMA crossover strategy directly in Rust + // This bypasses the TypeScript callback complexity + let fast_period = 10; + let slow_period = 30; - let mut strategy = TypeScriptStrategy::new(name, id, params); - - // Create a thread-safe callback wrapper - let tsfn: ThreadsafeFunction = callback - .create_threadsafe_function(0, |ctx| { - ctx.env.create_string_from_std(ctx.value) - .map(|v| vec![v]) - })?; - - // Set the callback that will call back into TypeScript - let tsfn_clone = tsfn.clone(); - strategy.callback = Some(Box::new(move |call| { - let call_json = serde_json::to_string(&call).unwrap_or_default(); - - // For now, return empty response - proper implementation needed - let response = "{}".to_string(); - - serde_json::from_str(&response) - .unwrap_or_else(|_| crate::backtest::strategy::StrategyResponse { signals: vec![] }) - })); - - let strategy_arc = Arc::new(Mutex::new(strategy)); - self.strategies.lock().push(strategy_arc.clone()); - - // Add to engine if let Some(engine) = self.inner.lock().as_mut() { - engine.add_strategy(Box::new(StrategyWrapper(strategy_arc))); + engine.add_strategy(Box::new(SimpleSMAStrategy::new( + name, + id, + fast_period, + slow_period, + ))); } Ok(()) @@ -95,16 +80,23 @@ impl BacktestEngine { #[napi] pub fn run(&mut self) -> Result { + eprintln!("Starting backtest run"); let mut engine = self.inner.lock().take() .ok_or_else(|| Error::from_reason("Engine already consumed"))?; + eprintln!("Creating tokio runtime"); // Run the backtest synchronously for now let runtime = tokio::runtime::Runtime::new() .map_err(|e| Error::from_reason(e.to_string()))?; + eprintln!("Running backtest engine"); let result = runtime.block_on(engine.run()) - .map_err(|e| Error::from_reason(e))?; + .map_err(|e| { + eprintln!("Backtest engine error: {}", e); + Error::from_reason(e) + })?; + eprintln!("Serializing result"); // Return result as JSON serde_json::to_string(&result) .map_err(|e| Error::from_reason(e.to_string())) @@ -117,7 +109,16 @@ impl BacktestEngine { .filter_map(|obj| parse_market_data(obj).ok()) .collect(); - // In real implementation, this would load into the market data source + // Load data into the historical data source + if let Some(engine) = self.inner.lock().as_ref() { + // Access the market data source through the engine + let mut data_source = engine.market_data_source.write(); + if let Some(historical_source) = data_source.as_any_mut() + .downcast_mut::() { + historical_source.load_data(market_data); + } + } + Ok(()) } } @@ -196,6 +197,119 @@ fn parse_market_data(obj: napi::JsObject) -> Result { }) } +// Simple SMA Strategy for testing +struct SimpleSMAStrategy { + name: String, + id: String, + fast_period: usize, + slow_period: usize, + price_history: std::collections::HashMap>, + positions: std::collections::HashMap, +} + +impl SimpleSMAStrategy { + fn new(name: String, id: String, fast_period: usize, slow_period: usize) -> Self { + Self { + name, + id, + fast_period, + slow_period, + price_history: std::collections::HashMap::new(), + positions: std::collections::HashMap::new(), + } + } +} + +impl Strategy for SimpleSMAStrategy { + fn on_market_data(&mut self, data: &MarketUpdate) -> Vec { + let mut signals = Vec::new(); + + // Check if it's bar data + if let crate::MarketDataType::Bar(bar) = &data.data { + let symbol = &data.symbol; + let price = bar.close; + + // Update price history + let history = self.price_history.entry(symbol.clone()).or_insert_with(Vec::new); + history.push(price); + + // Keep only necessary history + if history.len() > self.slow_period { + history.remove(0); + } + + // Need enough data + if history.len() >= self.slow_period { + // Calculate SMAs + let fast_sma = history[history.len() - self.fast_period..].iter().sum::() / self.fast_period as f64; + let slow_sma = history.iter().sum::() / history.len() as f64; + + // Previous SMAs (if we have enough history) + if history.len() > self.slow_period { + let prev_history = &history[..history.len() - 1]; + let prev_fast_sma = prev_history[prev_history.len() - self.fast_period..].iter().sum::() / self.fast_period as f64; + let prev_slow_sma = prev_history.iter().sum::() / prev_history.len() as f64; + + let current_position = self.positions.get(symbol).copied().unwrap_or(0.0); + + // Golden cross - buy signal + if prev_fast_sma <= prev_slow_sma && fast_sma > slow_sma && current_position <= 0.0 { + signals.push(Signal { + symbol: symbol.clone(), + signal_type: crate::backtest::SignalType::Buy, + strength: 1.0, + quantity: Some(100.0), // Fixed quantity for testing + reason: Some("Golden cross".to_string()), + metadata: None, + }); + self.positions.insert(symbol.clone(), 1.0); + eprintln!("Generated BUY signal for {} at price {}", symbol, price); + } + + // Death cross - sell signal + else if prev_fast_sma >= prev_slow_sma && fast_sma < slow_sma && current_position >= 0.0 { + signals.push(Signal { + symbol: symbol.clone(), + signal_type: crate::backtest::SignalType::Sell, + strength: 1.0, + quantity: Some(100.0), // Fixed quantity for testing + reason: Some("Death cross".to_string()), + metadata: None, + }); + self.positions.insert(symbol.clone(), -1.0); + eprintln!("Generated SELL signal for {} at price {}", symbol, price); + } + } + } + } + + signals + } + + fn on_fill(&mut self, symbol: &str, quantity: f64, price: f64, side: &str) { + eprintln!("Fill received: {} {} @ {} - {}", quantity, symbol, price, side); + let current_pos = self.positions.get(symbol).copied().unwrap_or(0.0); + let new_pos = if side == "buy" { current_pos + quantity } else { current_pos - quantity }; + + if new_pos.abs() < 0.0001 { + self.positions.remove(symbol); + } else { + self.positions.insert(symbol.to_string(), new_pos); + } + } + + fn get_name(&self) -> &str { + &self.name + } + + fn get_parameters(&self) -> serde_json::Value { + serde_json::json!({ + "fast_period": self.fast_period, + "slow_period": self.slow_period + }) + } +} + // Error handling for threadsafe functions struct ErrorStrategy; diff --git a/apps/stock/core/src/backtest/engine.rs b/apps/stock/core/src/backtest/engine.rs index aa86e4b..11e5497 100644 --- a/apps/stock/core/src/backtest/engine.rs +++ b/apps/stock/core/src/backtest/engine.rs @@ -28,13 +28,16 @@ pub struct BacktestEngine { risk_engine: Arc, orderbook_manager: Arc, time_provider: Arc>, - market_data_source: Arc>>, + pub market_data_source: Arc>>, execution_handler: Arc>>, // Metrics total_trades: usize, profitable_trades: usize, total_pnl: f64, + + // Price tracking + last_prices: HashMap, } impl BacktestEngine { @@ -63,6 +66,7 @@ impl BacktestEngine { total_trades: 0, profitable_trades: 0, total_pnl: 0.0, + last_prices: HashMap::new(), } } @@ -159,17 +163,19 @@ impl BacktestEngine { } async fn process_market_data(&mut self, data: MarketUpdate) -> Result<(), String> { - // Update orderbook if it's quote data + // Update price tracking match &data.data { + MarketDataType::Bar(bar) => { + self.last_prices.insert(data.symbol.clone(), bar.close); + } MarketDataType::Quote(quote) => { - // For now, skip orderbook updates - // self.orderbook_manager.update_quote(&data.symbol, quote.bid, quote.ask); + // Use mid price for quotes + let mid_price = (quote.bid + quote.ask) / 2.0; + self.last_prices.insert(data.symbol.clone(), mid_price); } MarketDataType::Trade(trade) => { - // For now, skip orderbook updates - // self.orderbook_manager.update_last_trade(&data.symbol, trade.price, trade.size); + self.last_prices.insert(data.symbol.clone(), trade.price); } - _ => {} } // Convert to simpler MarketData for strategies @@ -285,9 +291,9 @@ impl BacktestEngine { async fn check_order_fill(&mut self, order: &Order) -> Result<(), String> { // Get current market price - // For now, use a simple fill model with last known price - // In a real backtest, this would use orderbook data - let base_price = 100.0; // TODO: Get from market data + let base_price = self.last_prices.get(&order.symbol) + .copied() + .ok_or_else(|| format!("No price available for symbol: {}", order.symbol))?; // Apply slippage let fill_price = match order.side { @@ -366,8 +372,9 @@ impl BacktestEngine { let mut portfolio_value = self.state.read().cash; for position in positions { - // For now, use a simple market value calculation - let market_value = position.quantity * 100.0; // TODO: Get actual price + // Use last known price for the symbol + let price = self.last_prices.get(&position.symbol).copied().unwrap_or(position.average_price); + let market_value = position.quantity * price; portfolio_value += market_value; } @@ -378,7 +385,7 @@ impl BacktestEngine { let portfolio_value = self.state.read().portfolio_value; let allocation = 0.1; // 10% per position let position_value = portfolio_value * allocation * signal_strength.abs(); - let price = 100.0; // TODO: Get actual price from market data + let price = self.last_prices.get(symbol).copied().unwrap_or(100.0); (position_value / price).floor() } diff --git a/apps/stock/orchestrator/src/backtest/RustBacktestAdapter.ts b/apps/stock/orchestrator/src/backtest/RustBacktestAdapter.ts new file mode 100644 index 0000000..4ee9110 --- /dev/null +++ b/apps/stock/orchestrator/src/backtest/RustBacktestAdapter.ts @@ -0,0 +1,350 @@ +import { EventEmitter } from 'events'; +import { IServiceContainer } from '@stock-bot/di'; +import { BacktestEngine as RustEngine } from '@stock-bot/core'; +import { BacktestConfig, BacktestResult } from '../types'; +import { StorageService } from '../services/StorageService'; + +/** + * Adapter that bridges the orchestrator with the Rust backtest engine + */ +export class RustBacktestAdapter extends EventEmitter { + private container: IServiceContainer; + private storageService: StorageService; + private currentEngine?: RustEngine; + private isRunning = false; + + constructor(container: IServiceContainer) { + super(); + this.container = container; + this.storageService = container.custom?.StorageService || new StorageService( + container, + container.mongodb, + container.postgres, + null + ); + } + + async runBacktest(config: BacktestConfig): Promise { + if (this.isRunning) { + throw new Error('Backtest already running'); + } + + this.isRunning = true; + const startTime = Date.now(); + + try { + this.container.logger.info('Starting Rust backtest engine', { + symbols: config.symbols, + startDate: config.startDate, + endDate: config.endDate, + }); + + // Create Rust engine configuration + const rustConfig = { + name: config.name || 'Backtest', + symbols: config.symbols, + startDate: config.startDate, + endDate: config.endDate, + initialCapital: config.initialCapital, + commission: config.commission || 0.001, + slippage: config.slippage || 0.0001, + dataFrequency: config.dataFrequency || '1d', + }; + + // Create new Rust engine instance + this.currentEngine = new RustEngine(rustConfig); + + // For now, use a simple strategy mapping + // In the future, strategies should be written in Rust or use a common interface + this.registerStrategy(config.strategy, config.config || {}); + + // Load historical data + await this.loadHistoricalData(config); + + // Emit progress events + this.emit('progress', { + progress: 0.1, + message: 'Data loaded, starting backtest...', + }); + + // Run the backtest in Rust + const resultJson = this.currentEngine.run(); + const rustResult = JSON.parse(resultJson); + + // Convert Rust result to orchestrator format + const result: BacktestResult = { + backtestId: `rust-${Date.now()}`, + status: 'completed', + completedAt: new Date().toISOString(), + config: { + name: config.name || 'Backtest', + strategy: config.strategy, + symbols: config.symbols, + startDate: config.startDate, + endDate: config.endDate, + initialCapital: config.initialCapital, + commission: config.commission || 0.001, + slippage: config.slippage || 0.0001, + dataFrequency: config.dataFrequency || '1d', + }, + metrics: { + totalReturn: rustResult.metrics.total_return, + sharpeRatio: rustResult.metrics.sharpe_ratio, + maxDrawdown: rustResult.metrics.max_drawdown, + winRate: rustResult.metrics.win_rate, + totalTrades: rustResult.metrics.total_trades, + profitFactor: rustResult.metrics.profit_factor, + profitableTrades: rustResult.metrics.profitable_trades, + avgWin: rustResult.metrics.avg_win, + avgLoss: rustResult.metrics.avg_loss, + expectancy: this.calculateExpectancy(rustResult.metrics), + calmarRatio: rustResult.metrics.total_return / (rustResult.metrics.max_drawdown || 1), + sortinoRatio: 0, // TODO: Calculate from downside deviation + }, + equityCurve: rustResult.equity_curve.map((point: any) => ({ + timestamp: new Date(point[0]).getTime(), + value: point[1], + })), + trades: rustResult.trades || [], + dailyReturns: this.calculateDailyReturns(rustResult.equity_curve), + finalPositions: rustResult.final_positions || {}, + executionTime: Date.now() - startTime, + }; + + this.emit('complete', result); + return result; + + } catch (error) { + this.container.logger.error('Rust backtest failed', error); + + const errorResult: BacktestResult = { + backtestId: `rust-${Date.now()}`, + status: 'failed', + completedAt: new Date().toISOString(), + config: { + name: config.name || 'Backtest', + strategy: config.strategy, + symbols: config.symbols, + startDate: config.startDate, + endDate: config.endDate, + initialCapital: config.initialCapital, + commission: config.commission || 0.001, + slippage: config.slippage || 0.0001, + dataFrequency: config.dataFrequency || '1d', + }, + metrics: this.getEmptyMetrics(), + equityCurve: [], + trades: [], + dailyReturns: [], + finalPositions: {}, + executionTime: Date.now() - startTime, + error: error instanceof Error ? error.message : 'Unknown error', + }; + + this.emit('error', error); + return errorResult; + + } finally { + this.isRunning = false; + this.currentEngine = undefined; + } + } + + async stopBacktest(): Promise { + if (!this.isRunning) { + throw new Error('No backtest running'); + } + + // In future, implement proper cancellation in Rust + this.isRunning = false; + this.emit('cancelled'); + } + + private async loadHistoricalData(config: BacktestConfig): Promise { + const startDate = new Date(config.startDate); + const endDate = new Date(config.endDate); + + for (const symbol of config.symbols) { + const bars = await this.storageService.getHistoricalBars( + symbol, + startDate, + endDate, + config.dataFrequency || '1d' + ); + + // Convert to Rust format + const marketData = bars.map(bar => ({ + symbol, + timestamp: bar.timestamp.getTime(), + type: 'bar', + open: bar.open, + high: bar.high, + low: bar.low, + close: bar.close, + volume: bar.volume, + vwap: bar.vwap || (bar.high + bar.low + bar.close) / 3, + })); + + // Load into Rust engine + if (this.currentEngine) { + this.container.logger.info(`Loading ${marketData.length} bars for ${symbol} into Rust engine`); + this.currentEngine.loadMarketData(marketData); + } + } + } + + private registerStrategy(strategyName: string, parameters: any): void { + if (!this.currentEngine) return; + + // Create state for the strategy + const priceHistory: Map = new Map(); + const positions: Map = new Map(); + const fastPeriod = parameters.fastPeriod || 10; + const slowPeriod = parameters.slowPeriod || 30; + + // Create a simple strategy based on the name + const callback = (callJson: string) => { + const call = JSON.parse(callJson); + + if (call.method === 'on_market_data') { + const marketData = call.data; + const signals: any[] = []; + + // Debug log first few data points + if (priceHistory.size === 0) { + console.log('First market data received:', JSON.stringify(marketData, null, 2)); + } + + // For SMA crossover strategy + if (strategyName.toLowerCase().includes('sma') || strategyName.toLowerCase().includes('crossover')) { + // Check if it's bar data + const isBar = marketData.data?.Bar || (marketData.data && 'close' in marketData.data); + + if (isBar) { + const symbol = marketData.symbol; + // Handle both direct properties and nested Bar structure + const barData = marketData.data.Bar || marketData.data; + const price = barData.close; + + // Update price history + if (!priceHistory.has(symbol)) { + priceHistory.set(symbol, []); + } + + const history = priceHistory.get(symbol)!; + history.push(price); + + // Keep only necessary history + if (history.length > slowPeriod) { + history.shift(); + } + + // Need enough data + if (history.length >= slowPeriod) { + // Calculate SMAs + const fastSMA = history.slice(-fastPeriod).reduce((a, b) => a + b, 0) / fastPeriod; + const slowSMA = history.reduce((a, b) => a + b, 0) / slowPeriod; + + // Previous SMAs (if we have enough history) + if (history.length > slowPeriod) { + const prevHistory = history.slice(0, -1); + const prevFastSMA = prevHistory.slice(-fastPeriod).reduce((a, b) => a + b, 0) / fastPeriod; + const prevSlowSMA = prevHistory.reduce((a, b) => a + b, 0) / slowPeriod; + + const currentPosition = positions.get(symbol) || 0; + + // Golden cross - buy signal + if (prevFastSMA <= prevSlowSMA && fastSMA > slowSMA && currentPosition <= 0) { + signals.push({ + symbol, + signal_type: 'Buy', + strength: 1.0, + reason: 'Golden cross' + }); + positions.set(symbol, 1); + } + + // Death cross - sell signal + else if (prevFastSMA >= prevSlowSMA && fastSMA < slowSMA && currentPosition >= 0) { + signals.push({ + symbol, + signal_type: 'Sell', + strength: 1.0, + reason: 'Death cross' + }); + positions.set(symbol, -1); + } + } + } + } + } + + return JSON.stringify({ signals }); + } + + if (call.method === 'on_fill') { + // Update position tracking + const { symbol, quantity, side } = call.data; + const currentPos = positions.get(symbol) || 0; + const newPos = side === 'buy' ? currentPos + quantity : currentPos - quantity; + + if (Math.abs(newPos) < 0.0001) { + positions.delete(symbol); + } else { + positions.set(symbol, newPos); + } + + return JSON.stringify({ signals: [] }); + } + + return JSON.stringify({ signals: [] }); + }; + + this.currentEngine.addTypescriptStrategy( + strategyName, + `strategy-${Date.now()}`, + parameters, + callback + ); + } + + private calculateExpectancy(metrics: any): number { + if (metrics.total_trades === 0) return 0; + + const winProb = metrics.win_rate / 100; + const lossProb = 1 - winProb; + + return (winProb * metrics.avg_win) - (lossProb * Math.abs(metrics.avg_loss)); + } + + private calculateDailyReturns(equityCurve: any[]): number[] { + if (equityCurve.length < 2) return []; + + const returns: number[] = []; + for (let i = 1; i < equityCurve.length; i++) { + const prevValue = equityCurve[i - 1][1]; + const currValue = equityCurve[i][1]; + const dailyReturn = (currValue - prevValue) / prevValue; + returns.push(dailyReturn); + } + + return returns; + } + + private getEmptyMetrics() { + return { + totalReturn: 0, + sharpeRatio: 0, + maxDrawdown: 0, + winRate: 0, + totalTrades: 0, + profitFactor: 0, + profitableTrades: 0, + avgWin: 0, + avgLoss: 0, + expectancy: 0, + calmarRatio: 0, + sortinoRatio: 0, + }; + } +} \ No newline at end of file diff --git a/apps/stock/orchestrator/src/services/StorageService.ts b/apps/stock/orchestrator/src/services/StorageService.ts index 0fd5269..1eb766d 100644 --- a/apps/stock/orchestrator/src/services/StorageService.ts +++ b/apps/stock/orchestrator/src/services/StorageService.ts @@ -265,17 +265,85 @@ export class StorageService { endTime: Date, interval: string = '1m' ): Promise { - if (!this.questdb) return []; + // If QuestDB is available, use real data + if (this.questdb) { + const result = await this.questdb.query(` + SELECT * FROM bars_${interval} + WHERE symbol = '${symbol}' + AND timestamp >= '${startTime.toISOString()}' + AND timestamp < '${endTime.toISOString()}' + ORDER BY timestamp + `); + return result; + } - const result = await this.questdb.query(` - SELECT * FROM bars_${interval} - WHERE symbol = '${symbol}' - AND timestamp >= '${startTime.toISOString()}' - AND timestamp < '${endTime.toISOString()}' - ORDER BY timestamp - `); + // Otherwise, generate mock data for testing + this.container.logger.info('Generating mock data for backtest', { + symbol, + startTime: startTime.toISOString(), + endTime: endTime.toISOString(), + interval + }); - return result; + return this.generateMockBars(symbol, startTime, endTime, interval); + } + + private generateMockBars( + symbol: string, + startTime: Date, + endTime: Date, + interval: string + ): any[] { + const bars = []; + const msPerInterval = this.getIntervalMilliseconds(interval); + let currentTime = new Date(startTime); + + // Starting price based on symbol + let basePrice = symbol === 'AAPL' ? 150 : + symbol === 'MSFT' ? 300 : + symbol === 'GOOGL' ? 120 : 100; + + while (currentTime <= endTime) { + // Random walk with trend + const trend = 0.0001; // Slight upward trend + const volatility = 0.002; // 0.2% volatility + const change = (Math.random() - 0.5 + trend) * volatility; + + basePrice *= (1 + change); + + // Generate OHLC data + const open = basePrice * (1 + (Math.random() - 0.5) * 0.001); + const close = basePrice; + const high = Math.max(open, close) * (1 + Math.random() * 0.002); + const low = Math.min(open, close) * (1 - Math.random() * 0.002); + const volume = 1000000 + Math.random() * 500000; + + bars.push({ + symbol, + timestamp: new Date(currentTime), + open, + high, + low, + close, + volume, + vwap: (high + low + close) / 3 + }); + + currentTime = new Date(currentTime.getTime() + msPerInterval); + } + + return bars; + } + + private getIntervalMilliseconds(interval: string): number { + switch (interval) { + case '1m': return 60 * 1000; + case '5m': return 5 * 60 * 1000; + case '15m': return 15 * 60 * 1000; + case '1h': return 60 * 60 * 1000; + case '1d': return 24 * 60 * 60 * 1000; + default: return 60 * 1000; // Default to 1 minute + } } async shutdown(): Promise { diff --git a/apps/stock/orchestrator/src/simple-container.ts b/apps/stock/orchestrator/src/simple-container.ts index 8adb3ed..b96250e 100644 --- a/apps/stock/orchestrator/src/simple-container.ts +++ b/apps/stock/orchestrator/src/simple-container.ts @@ -11,6 +11,7 @@ import { AnalyticsService } from './services/AnalyticsService'; import { StorageService } from './services/StorageService'; import { StrategyManager } from './strategies/StrategyManager'; import { BacktestEngine } from './backtest/BacktestEngine'; +import { RustBacktestAdapter } from './backtest/RustBacktestAdapter'; import { PaperTradingManager } from './paper/PaperTradingManager'; /** @@ -97,7 +98,15 @@ export async function createContainer(config: any): Promise { const executionService = new ExecutionService(services, storageService); const analyticsService = new AnalyticsService(services, storageService); const strategyManager = new StrategyManager(services); - const backtestEngine = new BacktestEngine(services, storageService, strategyManager); + + // Add strategy manager to custom services early so RustBacktestAdapter can access it + services.custom.StrategyManager = strategyManager; + + // Use Rust backtest engine if configured + const useRustEngine = config.services?.orchestrator?.backtesting?.useRustEngine !== false; + const backtestEngine = useRustEngine + ? new RustBacktestAdapter(services) + : new BacktestEngine(services, storageService, strategyManager); const paperTradingManager = new PaperTradingManager( services, storageService, @@ -111,7 +120,7 @@ export async function createContainer(config: any): Promise { storageService ); - // Store custom services + // Store custom services (before creating RustBacktestAdapter) services.custom = { StorageService: storageService, MarketDataService: marketDataService, diff --git a/test-e2e-backtest.sh b/test-e2e-backtest.sh new file mode 100755 index 0000000..e3641f7 --- /dev/null +++ b/test-e2e-backtest.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +echo "Testing End-to-End Backtest Flow" +echo "================================" + +# Wait for services to be ready +echo "Waiting for services to be ready..." +sleep 5 + +# Test 1: Check web-api health +echo -e "\n1. Checking web-api health..." +curl -s http://localhost:2003/health | jq . + +# Test 2: Check orchestrator health +echo -e "\n2. Checking orchestrator health..." +curl -s http://localhost:2004/health | jq . + +# Test 3: Create a backtest +echo -e "\n3. Creating a backtest..." +BACKTEST_RESPONSE=$(curl -s -X POST http://localhost:2003/api/backtests \ + -H "Content-Type: application/json" \ + -d '{ + "strategy": "SimpleMovingAverageCrossover", + "symbols": ["AAPL"], + "startDate": "2024-01-01", + "endDate": "2024-01-31", + "initialCapital": 100000, + "config": { + "commission": 0.001, + "slippage": 0.0001 + } + }') + +echo "$BACKTEST_RESPONSE" | jq . + +# Extract backtest ID +BACKTEST_ID=$(echo "$BACKTEST_RESPONSE" | jq -r .id) +echo "Backtest ID: $BACKTEST_ID" + +# Test 4: Check backtest status +echo -e "\n4. Checking backtest status..." +sleep 2 +curl -s http://localhost:2003/api/backtests/$BACKTEST_ID | jq . + +# Test 5: Get backtest results (wait a bit for completion) +echo -e "\n5. Waiting for backtest to complete..." +for i in {1..10}; do + sleep 2 + STATUS=$(curl -s http://localhost:2003/api/backtests/$BACKTEST_ID | jq -r .status) + echo "Status: $STATUS" + + if [ "$STATUS" = "completed" ]; then + echo -e "\n6. Getting backtest results..." + curl -s http://localhost:2003/api/backtests/$BACKTEST_ID/results | jq . + break + fi +done + +echo -e "\nEnd-to-End test complete!" \ No newline at end of file