diff --git a/apps/stock/core/index.node b/apps/stock/core/index.node index e647a4e..42b2603 100755 Binary files a/apps/stock/core/index.node and b/apps/stock/core/index.node differ diff --git a/apps/stock/orchestrator/src/backtest/BacktestEngine.ts b/apps/stock/orchestrator/src/backtest/BacktestEngine.ts index 8f72103..e8141f6 100644 --- a/apps/stock/orchestrator/src/backtest/BacktestEngine.ts +++ b/apps/stock/orchestrator/src/backtest/BacktestEngine.ts @@ -668,10 +668,17 @@ export class BacktestEngine extends EventEmitter { private async updateEquityCurve(): Promise { const totalEquity = await this.getPortfolioValue(); - this.equityCurve.push({ - timestamp: this.currentTime, - value: totalEquity - }); + // Don't add duplicate points at the same timestamp + const lastPoint = this.equityCurve[this.equityCurve.length - 1]; + if (lastPoint && lastPoint.timestamp === this.currentTime) { + // Update the value instead of adding a duplicate + lastPoint.value = totalEquity; + } else { + this.equityCurve.push({ + timestamp: this.currentTime, + value: totalEquity + }); + } } private async getPortfolioValue(): Promise { @@ -680,9 +687,23 @@ export class BacktestEngine extends EventEmitter { return this.initialCapital; } - // Get current P&L - const [realized, unrealized] = tradingEngine.getTotalPnl(); - return this.initialCapital + realized + unrealized; + try { + // Get current P&L + const [realized, unrealized] = tradingEngine.getTotalPnl(); + const totalValue = this.initialCapital + realized + unrealized; + + // Ensure we never return 0 or negative values that would cause spikes + // This handles the case where getTotalPnl might not be initialized yet + if (totalValue <= 0 || isNaN(totalValue)) { + return this.initialCapital; + } + + return totalValue; + } catch (error) { + // If getTotalPnl fails, return initial capital + this.container.logger.warn('Failed to get total P&L, using initial capital:', error); + return this.initialCapital; + } } private calculatePerformance(closedTrades: any[] = []): PerformanceMetrics { diff --git a/apps/stock/orchestrator/test-equity-curve-spike.ts b/apps/stock/orchestrator/test-equity-curve-spike.ts new file mode 100644 index 0000000..7b415af --- /dev/null +++ b/apps/stock/orchestrator/test-equity-curve-spike.ts @@ -0,0 +1,95 @@ +import { createContainer } from './src/simple-container'; +import { BacktestEngine } from './src/backtest/BacktestEngine'; +import { StrategyManager } from './src/strategies/StrategyManager'; +import { StorageService } from './src/services/StorageService'; + +async function testEquityCurveSpikeFixture() { + console.log('Testing equity curve spike fix...\n'); + + // Create minimal container + const container = await createContainer({ + database: { + mongodb: { enabled: false, uri: '' }, + postgres: { enabled: false, uri: '' }, + questdb: { enabled: false, host: '', port: 0 } + } + }); + + // Create services + const storageService = new StorageService(container); + const strategyManager = new StrategyManager(container); + + // Create backtest engine + const backtestEngine = new BacktestEngine(container, storageService, strategyManager); + + // Run a quick backtest + const config = { + mode: 'backtest', + name: 'Equity Curve Spike Test', + strategy: 'sma-crossover', + symbols: ['AAPL'], + startDate: '2023-01-01T00:00:00Z', + endDate: '2023-01-31T00:00:00Z', // Just one month + initialCapital: 100000, + commission: 0.001, + slippage: 0.0001, + dataFrequency: '1d', + speed: 'max' + }; + + console.log('Running backtest...'); + const result = await backtestEngine.runBacktest(config); + + // Check the equity curve + console.log('\n=== Equity Curve Analysis ==='); + console.log(`Initial Capital: $${config.initialCapital}`); + console.log(`Total equity points: ${result.equity.length}`); + + if (result.equity.length > 0) { + // Check first few points + console.log('\nFirst 5 equity curve points:'); + result.equity.slice(0, 5).forEach((point, index) => { + console.log(`${index + 1}. ${point.date}: $${point.value.toFixed(2)}`); + }); + + // Check for spike + const firstValue = result.equity[0].value; + const secondValue = result.equity.length > 1 ? result.equity[1].value : firstValue; + + console.log(`\nFirst equity value: $${firstValue.toFixed(2)}`); + console.log(`Second equity value: $${secondValue.toFixed(2)}`); + + // Check if there's a spike from 0 to initial capital + if (firstValue === 0 || firstValue < config.initialCapital * 0.5) { + console.log('\n❌ SPIKE DETECTED: First equity value is too low!'); + } else if (Math.abs(firstValue - config.initialCapital) < 1) { + console.log('\n✅ NO SPIKE: First equity value correctly starts at initial capital!'); + } else { + console.log(`\n⚠️ First equity value differs from initial capital by $${Math.abs(firstValue - config.initialCapital).toFixed(2)}`); + } + + // Check for duplicate timestamps + const timestamps = new Set(); + let duplicates = 0; + result.equity.forEach(point => { + if (timestamps.has(point.date)) { + duplicates++; + } + timestamps.add(point.date); + }); + + if (duplicates > 0) { + console.log(`\n⚠️ Found ${duplicates} duplicate timestamps in equity curve`); + } else { + console.log('\n✅ No duplicate timestamps found'); + } + } + + console.log('\n=== Test Complete ==='); + process.exit(0); +} + +testEquityCurveSpikeFixture().catch(error => { + console.error('Test failed:', error); + process.exit(1); +}); \ No newline at end of file diff --git a/apps/stock/orchestrator/test-equity-spike-simple.ts b/apps/stock/orchestrator/test-equity-spike-simple.ts new file mode 100644 index 0000000..f02a7ad --- /dev/null +++ b/apps/stock/orchestrator/test-equity-spike-simple.ts @@ -0,0 +1,105 @@ +import { BacktestEngine } from './src/backtest/BacktestEngine'; +import { StrategyManager } from './src/strategies/StrategyManager'; +import { StorageService } from './src/services/StorageService'; +import { ModeManager } from './src/core/ModeManager'; +import { MarketDataService } from './src/services/MarketDataService'; +import { ExecutionService } from './src/services/ExecutionService'; +import { IServiceContainer } from '@stock-bot/di'; + +async function testEquitySpike() { + console.log('Testing equity curve spike fix...\n'); + + // Create minimal service container + const container: IServiceContainer = { + logger: { + info: (msg: string, ...args: any[]) => console.log('[INFO]', msg, ...args), + error: (msg: string, ...args: any[]) => console.error('[ERROR]', msg, ...args), + warn: (msg: string, ...args: any[]) => console.warn('[WARN]', msg, ...args), + debug: (msg: string, ...args: any[]) => console.log('[DEBUG]', msg, ...args), + } as any, + custom: {} + }; + + // Initialize services + const storageService = new StorageService(); + const marketDataService = new MarketDataService(container); + const executionService = new ExecutionService(container); + const modeManager = new ModeManager(container, marketDataService, executionService, storageService); + const strategyManager = new StrategyManager(container); + + // Set services in container + container.custom = { + MarketDataService: marketDataService, + ExecutionService: executionService, + ModeManager: modeManager, + StorageService: storageService + }; + + // Initialize backtest mode + await modeManager.initializeMode({ + mode: 'backtest', + startDate: '2023-01-01T00:00:00Z', + endDate: '2023-01-10T00:00:00Z', // Just 10 days + speed: 'max', + symbols: ['TEST'], + initialCapital: 100000, + dataFrequency: '1d', + strategy: 'sma-crossover' + }); + + // Create backtest engine + const backtestEngine = new BacktestEngine(container, storageService, strategyManager); + + // Run backtest + const config = { + mode: 'backtest', + name: 'Equity Spike Test', + strategy: 'sma-crossover', + symbols: ['TEST'], + startDate: '2023-01-01T00:00:00Z', + endDate: '2023-01-10T00:00:00Z', + initialCapital: 100000, + commission: 0.001, + slippage: 0.0001, + dataFrequency: '1d', + speed: 'max' + }; + + console.log('Running backtest...'); + const result = await backtestEngine.runBacktest(config); + + // Analyze equity curve + console.log('\n=== EQUITY CURVE ANALYSIS ==='); + console.log(`Initial Capital: $${config.initialCapital.toLocaleString()}`); + console.log(`Equity points: ${result.equity.length}`); + + if (result.equity.length > 0) { + console.log('\nFirst 3 equity points:'); + result.equity.slice(0, 3).forEach((point, i) => { + console.log(` ${i + 1}. ${point.date} => $${point.value.toFixed(2)}`); + }); + + const firstValue = result.equity[0].value; + const hasSpike = firstValue === 0 || firstValue < config.initialCapital * 0.9; + + console.log(`\nFirst value: $${firstValue.toFixed(2)}`); + console.log(`Expected: $${config.initialCapital.toFixed(2)}`); + console.log(`Difference: $${Math.abs(firstValue - config.initialCapital).toFixed(2)}`); + + if (hasSpike) { + console.log('\n❌ SPIKE DETECTED! Portfolio value starts at 0 or too low.'); + } else if (Math.abs(firstValue - config.initialCapital) < 1) { + console.log('\n✅ SUCCESS! Portfolio value correctly starts at initial capital.'); + } else { + console.log('\n⚠️ WARNING: Small difference detected, but no major spike.'); + } + } + + console.log('\n=== TEST COMPLETE ==='); + process.exit(0); +} + +testEquitySpike().catch(error => { + console.error('Test failed:', error); + process.exit(1); +}); \ No newline at end of file