diff --git a/apps/stock/core/index.node b/apps/stock/core/index.node index ffedd0a..8e8691f 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 ee08045..1cc2e44 100644 --- a/apps/stock/core/src/api/backtest.rs +++ b/apps/stock/core/src/api/backtest.rs @@ -63,8 +63,9 @@ impl BacktestEngine { ) -> Result<()> { // 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; + // Use shorter periods for testing with low volatility mock data + let fast_period = 5; + let slow_period = 15; if let Some(engine) = self.inner.lock().as_mut() { engine.add_strategy(Box::new(SimpleSMAStrategy::new( @@ -104,19 +105,29 @@ impl BacktestEngine { #[napi] pub fn load_market_data(&self, data: Vec) -> Result<()> { + eprintln!("load_market_data called with {} items", data.len()); + // Convert JS objects to MarketData let market_data: Vec = data.into_iter() .filter_map(|obj| parse_market_data(obj).ok()) .collect(); + eprintln!("Parsed {} valid market data items", market_data.len()); + // 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::() { + eprintln!("Loading data into HistoricalDataSource"); historical_source.load_data(market_data); + eprintln!("Data loaded successfully"); + } else { + eprintln!("ERROR: Could not downcast to HistoricalDataSource"); } + } else { + eprintln!("ERROR: Engine not found"); } Ok(()) @@ -186,9 +197,21 @@ fn parse_market_data(obj: napi::JsObject) -> Result { vwap: obj.get_named_property("vwap").ok(), }) } else { + eprintln!("Unsupported market data type: {}", data_type); return Err(Error::from_reason("Unsupported market data type")); }; + // First few items + static mut COUNT: usize = 0; + unsafe { + if COUNT < 3 { + eprintln!("Parsed market data: symbol={}, timestamp={}, close={}", + symbol, timestamp, + if let crate::MarketDataType::Bar(ref bar) = data { bar.close } else { 0.0 }); + COUNT += 1; + } + } + Ok(crate::MarketUpdate { symbol, timestamp: DateTime::::from_timestamp(timestamp / 1000, 0) @@ -233,6 +256,11 @@ impl Strategy for SimpleSMAStrategy { let history = self.price_history.entry(symbol.clone()).or_insert_with(Vec::new); history.push(price); + // Debug: Log first few prices + if history.len() <= 3 { + eprintln!("Price history for {}: {:?}", symbol, history); + } + // Keep only necessary history if history.len() > self.slow_period { history.remove(0); @@ -244,6 +272,11 @@ impl Strategy for SimpleSMAStrategy { 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; + // Debug: Log SMAs periodically + if history.len() % 10 == 0 { + eprintln!("SMAs for {}: fast={:.2}, slow={:.2}, price={:.2}", symbol, fast_sma, slow_sma, price); + } + // Previous SMAs (if we have enough history) if history.len() > self.slow_period { let prev_history = &history[..history.len() - 1]; @@ -280,6 +313,11 @@ impl Strategy for SimpleSMAStrategy { eprintln!("Generated SELL signal for {} at price {}", symbol, price); } } + } else { + // Debug: Log when we don't have enough data + if history.len() == 1 || history.len() == 10 || history.len() == 20 { + eprintln!("Not enough data for {}: {} bars (need {})", symbol, history.len(), self.slow_period); + } } } diff --git a/apps/stock/core/src/backtest/engine.rs b/apps/stock/core/src/backtest/engine.rs index 11e5497..9687e8a 100644 --- a/apps/stock/core/src/backtest/engine.rs +++ b/apps/stock/core/src/backtest/engine.rs @@ -85,56 +85,74 @@ impl BacktestEngine { // Load market data self.load_market_data().await?; + eprintln!("Event queue empty: {}, length: {}", self.event_queue.read().is_empty(), self.event_queue.read().len()); + eprintln!("Current time: {}, End time: {}", self.time_provider.now(), self.config.end_time); + // Main event loop - while !self.event_queue.read().is_empty() || - self.time_provider.now() < self.config.end_time - { - // Get next batch of events - let current_time = self.time_provider.now(); - let events = self.event_queue.write().pop_until(current_time); - - for event in events { - self.process_event(event).await?; + let mut iteration = 0; + while !self.event_queue.read().is_empty() { + iteration += 1; + if iteration <= 5 || iteration % 100 == 0 { + eprintln!("Processing iteration {} at time {}", iteration, self.time_provider.now()); } - // Update portfolio value - self.update_portfolio_value(); - - // Check if we should advance time - if self.event_queue.read().is_empty() { - // Advance to next data point or end time - if let Some(next_time) = self.get_next_event_time() { - if next_time < self.config.end_time { - self.advance_time(next_time); - } else { - break; - } - } else { - break; + // Get the next event's timestamp + let next_event_time = self.event_queue.read() + .peek_next() + .map(|e| e.timestamp); + + if let Some(event_time) = next_event_time { + // Advance time to the next event + self.advance_time(event_time); + + // Get all events at this timestamp + let current_time = self.time_provider.now(); + let events = self.event_queue.write().pop_until(current_time); + + for event in events { + self.process_event(event).await?; } + + // Update portfolio value + self.update_portfolio_value(); + } else { + // No more events + break; } } + eprintln!("Backtest complete. Total trades: {}", self.total_trades); + // Generate results Ok(self.generate_results()) } async fn load_market_data(&mut self) -> Result<(), String> { + eprintln!("load_market_data: Starting"); let mut data_source = self.market_data_source.write(); + eprintln!("load_market_data: Seeking to start time: {}", self.config.start_time); // Seek to start time data_source.seek_to_time(self.config.start_time)?; + eprintln!("load_market_data: Loading data"); + let mut count = 0; // Load all data into event queue while let Some(update) = data_source.get_next_update().await { if update.timestamp > self.config.end_time { break; } + count += 1; + if count % 100 == 0 { + eprintln!("load_market_data: Loaded {} data points", count); + } + let event = BacktestEvent::market_data(update.timestamp, update); self.event_queue.write().push(event); } + eprintln!("load_market_data: Complete. Loaded {} total data points", count); Ok(()) } @@ -391,8 +409,10 @@ impl BacktestEngine { } fn get_next_event_time(&self) -> Option> { - // In a real implementation, this would look at the next market data point - None + // Get the timestamp of the next event in the queue + self.event_queue.read() + .peek_next() + .map(|event| event.timestamp) } fn generate_results(&self) -> BacktestResult { diff --git a/apps/stock/core/src/backtest/mod.rs b/apps/stock/core/src/backtest/mod.rs index 2a4b01c..06b7901 100644 --- a/apps/stock/core/src/backtest/mod.rs +++ b/apps/stock/core/src/backtest/mod.rs @@ -109,4 +109,8 @@ impl EventQueue { pub fn len(&self) -> usize { self.events.len() } + + pub fn peek_next(&self) -> Option<&BacktestEvent> { + self.events.front() + } } \ No newline at end of file diff --git a/apps/stock/orchestrator/src/backtest/RustBacktestAdapter.ts b/apps/stock/orchestrator/src/backtest/RustBacktestAdapter.ts index 4ee9110..ff5325a 100644 --- a/apps/stock/orchestrator/src/backtest/RustBacktestAdapter.ts +++ b/apps/stock/orchestrator/src/backtest/RustBacktestAdapter.ts @@ -71,6 +71,25 @@ export class RustBacktestAdapter extends EventEmitter { const resultJson = this.currentEngine.run(); const rustResult = JSON.parse(resultJson); + // Store OHLC data for each symbol + const ohlcData: Record = {}; + for (const symbol of config.symbols) { + const bars = await this.storageService.getHistoricalBars( + symbol, + new Date(config.startDate), + new Date(config.endDate), + config.dataFrequency || '1d' + ); + ohlcData[symbol] = bars.map(bar => ({ + timestamp: bar.timestamp.getTime(), + open: bar.open, + high: bar.high, + low: bar.low, + close: bar.close, + volume: bar.volume, + })); + } + // Convert Rust result to orchestrator format const result: BacktestResult = { backtestId: `rust-${Date.now()}`, @@ -109,6 +128,7 @@ export class RustBacktestAdapter extends EventEmitter { dailyReturns: this.calculateDailyReturns(rustResult.equity_curve), finalPositions: rustResult.final_positions || {}, executionTime: Date.now() - startTime, + ohlcData, }; this.emit('complete', result); @@ -199,10 +219,16 @@ export class RustBacktestAdapter extends EventEmitter { // 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; + const fastPeriod = parameters.fastPeriod || 5; + const slowPeriod = parameters.slowPeriod || 15; - // Create a simple strategy based on the name + this.container.logger.info('Registering TypeScript strategy', { + strategyName, + fastPeriod, + slowPeriod + }); + + // Create a TypeScript strategy callback const callback = (callJson: string) => { const call = JSON.parse(callJson); @@ -212,7 +238,7 @@ export class RustBacktestAdapter extends EventEmitter { // Debug log first few data points if (priceHistory.size === 0) { - console.log('First market data received:', JSON.stringify(marketData, null, 2)); + this.container.logger.debug('First market data received:', marketData); } // For SMA crossover strategy @@ -255,10 +281,12 @@ export class RustBacktestAdapter extends EventEmitter { // Golden cross - buy signal if (prevFastSMA <= prevSlowSMA && fastSMA > slowSMA && currentPosition <= 0) { + this.container.logger.info(`Golden cross detected for ${symbol} at price ${price}`); signals.push({ symbol, signal_type: 'Buy', strength: 1.0, + quantity: 100, // Fixed quantity for testing reason: 'Golden cross' }); positions.set(symbol, 1); @@ -266,10 +294,12 @@ export class RustBacktestAdapter extends EventEmitter { // Death cross - sell signal else if (prevFastSMA >= prevSlowSMA && fastSMA < slowSMA && currentPosition >= 0) { + this.container.logger.info(`Death cross detected for ${symbol} at price ${price}`); signals.push({ symbol, signal_type: 'Sell', strength: 1.0, + quantity: 100, // Fixed quantity for testing reason: 'Death cross' }); positions.set(symbol, -1); diff --git a/apps/stock/orchestrator/src/services/StorageService.ts b/apps/stock/orchestrator/src/services/StorageService.ts index 1eb766d..944c55a 100644 --- a/apps/stock/orchestrator/src/services/StorageService.ts +++ b/apps/stock/orchestrator/src/services/StorageService.ts @@ -304,18 +304,18 @@ export class StorageService { 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 + // Random walk with trend - increased volatility for testing + const trend = 0.0002; // Slight upward trend + const volatility = 0.01; // 1% volatility (increased from 0.2%) const change = (Math.random() - 0.5 + trend) * volatility; basePrice *= (1 + change); - // Generate OHLC data - const open = basePrice * (1 + (Math.random() - 0.5) * 0.001); + // Generate OHLC data with more realistic volatility + const open = basePrice * (1 + (Math.random() - 0.5) * 0.005); 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 high = Math.max(open, close) * (1 + Math.random() * 0.008); + const low = Math.min(open, close) * (1 - Math.random() * 0.008); const volume = 1000000 + Math.random() * 500000; bars.push({ diff --git a/apps/stock/web-app/src/components/charts/Chart.tsx b/apps/stock/web-app/src/components/charts/Chart.tsx index dec5c98..24fa3af 100644 --- a/apps/stock/web-app/src/components/charts/Chart.tsx +++ b/apps/stock/web-app/src/components/charts/Chart.tsx @@ -55,10 +55,44 @@ export function Chart({ // Reset zoom handler const resetZoom = useCallback(() => { - if (chartRef.current) { + if (chartRef.current && data.length > 0) { + // Get the validated data to ensure we're using the correct time values + const validateData = (rawData: any[]) => { + const seen = new Set(); + return rawData + .map(item => { + const timeInSeconds = item.time > 10000000000 + ? Math.floor(item.time / 1000) + : item.time; + return { ...item, time: timeInSeconds }; + }) + .filter(item => { + if (seen.has(item.time)) return false; + seen.add(item.time); + return true; + }) + .sort((a, b) => a.time - b.time); + }; + + const validatedData = validateData(data); + + if (validatedData.length > 0) { + const firstTime = validatedData[0].time; + const lastTime = validatedData[validatedData.length - 1].time; + + // Add some padding (5% on each side) + const timeRange = lastTime - firstTime; + const padding = timeRange * 0.05; + + chartRef.current.timeScale().setVisibleRange({ + from: (firstTime - padding) as any, + to: (lastTime + padding) as any, + }); + } + chartRef.current.timeScale().fitContent(); } - }, []); + }, [data]); useEffect(() => { if (!chartContainerRef.current || !data || !data.length) { @@ -110,6 +144,17 @@ export function Chart({ const validateAndFilterData = (rawData: any[]) => { const seen = new Set(); return rawData + .map(item => { + // Convert timestamp to seconds if it's in milliseconds + const timeInSeconds = item.time > 10000000000 + ? Math.floor(item.time / 1000) + : item.time; + + return { + ...item, + time: timeInSeconds as LightweightCharts.Time + }; + }) .filter((item, index) => { if (seen.has(item.time)) { return false; @@ -117,7 +162,7 @@ export function Chart({ seen.add(item.time); return true; }) - .sort((a, b) => a.time - b.time); // Ensure ascending time order + .sort((a, b) => (a.time as number) - (b.time as number)); // Ensure ascending time order }; // Create main series @@ -175,15 +220,17 @@ export function Chart({ }, }); - const volumeData = data - .filter(d => d.volume !== undefined) - .map(d => ({ - time: d.time, - value: d.volume!, - color: d.close && d.open ? - (d.close >= d.open ? '#10b98140' : '#ef444440') : - '#3b82f640', - })); + const volumeData = validateAndFilterData( + data + .filter(d => d.volume !== undefined) + .map(d => ({ + time: d.time, + value: d.volume!, + color: d.close && d.open ? + (d.close >= d.open ? '#10b98140' : '#ef444440') : + '#3b82f640', + })) + ); volumeSeriesRef.current.setData(volumeData); } @@ -206,15 +253,13 @@ export function Chart({ }); } - // Filter out duplicate timestamps and ensure ascending order - const sortedData = [...overlay.data].sort((a, b) => a.time - b.time); - const uniqueData = sortedData.reduce((acc: any[], curr) => { - if (!acc.length || curr.time > acc[acc.length - 1].time) { - acc.push(curr); - } - return acc; - }, []); - series.setData(uniqueData); + // Use validateAndFilterData to ensure consistent time handling + const overlayDataWithTime = overlay.data.map(d => ({ + ...d, + time: d.time // Ensure time field exists + })); + const validatedData = validateAndFilterData(overlayDataWithTime); + series.setData(validatedData); overlaySeriesRef.current.set(overlay.name, series); }); @@ -236,17 +281,29 @@ export function Chart({ // Fit content with a slight delay to ensure all series are loaded setTimeout(() => { + // First fit content to calculate proper range chart.timeScale().fitContent(); - // Also set the visible range to ensure all data is shown - if (data.length > 0) { - const firstTime = data[0].time; - const lastTime = data[data.length - 1].time; + // Get the validated data to ensure we're using the correct time values + const validatedData = validateAndFilterData(data); + + // Set visible range with some padding + if (validatedData.length > 0) { + const firstTime = validatedData[0].time; + const lastTime = validatedData[validatedData.length - 1].time; + + // Add some padding (5% on each side) + const timeRange = (lastTime as number) - (firstTime as number); + const padding = timeRange * 0.05; + chart.timeScale().setVisibleRange({ - from: firstTime as any, - to: lastTime as any, + from: ((firstTime as number) - padding) as any, + to: ((lastTime as number) + padding) as any, }); } + + // Ensure the chart fits the content properly + chart.timeScale().fitContent(); }, 100); // Enable mouse wheel zoom and touch gestures diff --git a/apps/stock/web-app/src/features/backtest/components/BacktestResults.tsx b/apps/stock/web-app/src/features/backtest/components/BacktestResults.tsx index 858b270..9337690 100644 --- a/apps/stock/web-app/src/features/backtest/components/BacktestResults.tsx +++ b/apps/stock/web-app/src/features/backtest/components/BacktestResults.tsx @@ -164,9 +164,15 @@ export function BacktestResults({ status, results, currentTime }: BacktestResult })) ); + // Convert OHLC data timestamps + const chartData = ohlcData.map((bar: any) => ({ + ...bar, + time: bar.timestamp || bar.time + })); + return (