From 143e2e1678c4b7dade56482859512e3ddb56ed87 Mon Sep 17 00:00:00 2001 From: Boki Date: Thu, 3 Jul 2025 09:55:13 -0400 Subject: [PATCH] work on backtest --- .../src/backtest/BacktestEngine.ts | 147 +++++++++- .../src/strategies/StrategyManager.ts | 117 ++++++-- .../examples/SimpleMovingAverageCrossover.ts | 254 ++++++++++++++++++ apps/stock/orchestrator/src/types.ts | 8 + .../web-api/src/services/backtest.service.ts | 3 + .../components/BacktestConfiguration.tsx | 16 +- .../backtest/components/BacktestResults.tsx | 110 +++++++- .../features/backtest/hooks/useBacktest.ts | 2 +- .../backtest/services/backtestService.ts | 2 +- 9 files changed, 613 insertions(+), 46 deletions(-) create mode 100644 apps/stock/orchestrator/src/strategies/examples/SimpleMovingAverageCrossover.ts diff --git a/apps/stock/orchestrator/src/backtest/BacktestEngine.ts b/apps/stock/orchestrator/src/backtest/BacktestEngine.ts index 31c7efe..1b2f03d 100644 --- a/apps/stock/orchestrator/src/backtest/BacktestEngine.ts +++ b/apps/stock/orchestrator/src/backtest/BacktestEngine.ts @@ -167,7 +167,21 @@ export class BacktestEngine extends EventEmitter { this.container.logger.info(`Loaded ${marketData.length} market data points`); // Initialize strategies - await this.strategyManager.initializeStrategies(validatedConfig.strategies || []); + const strategies = validatedConfig.strategies || [{ + id: `${validatedConfig.strategy}-${Date.now()}`, + name: validatedConfig.strategy, + enabled: true, + parameters: { + symbols: validatedConfig.symbols, + initialCapital: validatedConfig.initialCapital, + // Add any default strategy parameters here + }, + symbols: validatedConfig.symbols, + allocation: 1.0 // Use 100% of capital + }]; + + await this.strategyManager.initializeStrategies(strategies); + this.container.logger.info(`Initialized ${strategies.length} strategies`); // Convert market data to events this.populateEventQueue(marketData); @@ -421,6 +435,12 @@ 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 @@ -523,6 +543,9 @@ export class BacktestEngine extends EventEmitter { // Let strategies process the data await this.strategyManager.onMarketData(data); + // Check for any pending orders that should be filled + await this.checkAndFillOrders(data); + // Track performance this.performanceAnalyzer.addEquityPoint( new Date(this.currentTime), @@ -536,11 +559,55 @@ export class BacktestEngine extends EventEmitter { } private async processFill(fill: any): Promise { + // 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); + } + // Record trade - this.trades.push({ - ...fill, + 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.trades.push(trade); + this.container.logger.info(`💵 Trade recorded: ${fill.side} ${fill.quantity} ${fill.symbol} @ ${fill.price}`); + + // Update existing trades if this is a closing trade + if (fill.side === 'sell') { + this.updateClosedTrades(fill); + } + + // Notify strategies of fill + const strategy = fill.strategyId ? this.strategyManager.getStrategy(fill.strategyId) : null; + if (strategy) { + await strategy.onOrderUpdate({ + orderId: fill.orderId, + symbol: fill.symbol, + side: fill.side, + status: 'filled', + fills: [fill] + }); + } // Store in database await this.storageService.storeFill(fill); @@ -934,4 +1001,76 @@ export class BacktestEngine extends EventEmitter { return ohlcData; } + + private pendingOrders: Map = new Map(); + private ordersListenerSetup = false; + + 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 + 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); + } + // TODO: Handle limit orders + } + } + } + + 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; + 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)}%`); + } + } } \ No newline at end of file diff --git a/apps/stock/orchestrator/src/strategies/StrategyManager.ts b/apps/stock/orchestrator/src/strategies/StrategyManager.ts index a3bf425..af99f55 100644 --- a/apps/stock/orchestrator/src/strategies/StrategyManager.ts +++ b/apps/stock/orchestrator/src/strategies/StrategyManager.ts @@ -72,19 +72,61 @@ export class StrategyManager extends EventEmitter { } private async createStrategy(config: StrategyConfig): Promise { - // In a real system, this would dynamically load strategy classes - // For now, create a base strategy instance - const strategy = new BaseStrategy( - config, - this.container.custom?.ModeManager, - this.container.custom?.ExecutionService - ); + // Load strategy based on name + let strategy: BaseStrategy; + + switch (config.name.toLowerCase()) { + case 'meanreversion': + case 'mean-reversion': { + const { MeanReversionStrategy } = await import('./examples/MeanReversionStrategy'); + strategy = new MeanReversionStrategy( + config, + this.container.custom?.ModeManager, + this.container.custom?.ExecutionService + ); + break; + } + case 'smacrossover': + case 'sma-crossover': + case 'moving-average': { + const { SimpleMovingAverageCrossover } = await import('./examples/SimpleMovingAverageCrossover'); + strategy = new SimpleMovingAverageCrossover( + config, + this.container.custom?.ModeManager, + this.container.custom?.ExecutionService + ); + break; + } + case 'mlenhanced': + case 'ml-enhanced': { + const { MLEnhancedStrategy } = await import('./examples/MLEnhancedStrategy'); + strategy = new MLEnhancedStrategy( + config, + this.container.custom?.ModeManager, + this.container.custom?.ExecutionService + ); + break; + } + default: + // Default to base strategy + this.container.logger.warn(`Unknown strategy: ${config.name}, using base strategy`); + strategy = new BaseStrategy( + config, + this.container.custom?.ModeManager, + this.container.custom?.ExecutionService + ); + break; + } // Set up strategy event handlers strategy.on('signal', (signal: any) => { this.handleStrategySignal(config.id, signal); }); + strategy.on('order', (order: any) => { + this.handleStrategyOrder(config.id, order); + }); + strategy.on('error', (error: Error) => { this.container.logger.error(`Strategy ${config.id} error:`, error); }); @@ -99,6 +141,7 @@ export class StrategyManager extends EventEmitter { } await strategy.initialize(); + await strategy.start(); // Start the strategy to make it active this.activeStrategies.add(strategyId); this.container.logger.info(`Enabled strategy: ${strategyId}`); } @@ -114,6 +157,10 @@ export class StrategyManager extends EventEmitter { this.container.logger.info(`Disabled strategy: ${strategyId}`); } + async onMarketData(data: MarketData): Promise { + return this.handleMarketData(data); + } + private async handleMarketData(data: MarketData): Promise { // Forward to all active strategies for (const strategyId of this.activeStrategies) { @@ -158,25 +205,49 @@ export class StrategyManager extends EventEmitter { private async handleStrategySignal(strategyId: string, signal: any): Promise { this.container.logger.info(`Strategy ${strategyId} generated signal:`, signal); + // Signals are informational - strategies will convert strong signals to orders + } + + private async handleStrategyOrder(strategyId: string, order: OrderRequest): Promise { + this.container.logger.info(`Strategy ${strategyId} generated order:`, order); - // Convert signal to order request - const orderRequest: OrderRequest = { - symbol: signal.symbol, - quantity: signal.quantity, - side: signal.side, - type: signal.orderType || 'market', - timeInForce: signal.timeInForce || 'day', - strategyId - }; - - // Submit order through execution service - const executionService = this.container.custom?.ExecutionService; - if (executionService) { + // Submit order through trading engine (for backtesting) + if (this.tradingEngine) { try { - const result = await executionService.submitOrder(orderRequest); - this.container.logger.info(`Order submitted for strategy ${strategyId}:`, result); + // Create order object for Rust API + const orderObj = { + id: `${strategyId}-${Date.now()}`, + symbol: order.symbol, + side: order.side, + quantity: order.quantity, + orderType: order.orderType, + limitPrice: order.limitPrice, + timeInForce: order.timeInForce || 'DAY' + }; + + const orderResult = await this.tradingEngine.submitOrder(orderObj); + const result = JSON.parse(orderResult); + this.container.logger.info(`Order placed for strategy ${strategyId}: ${result.order_id}`); + + // Emit order event + this.emit('order', { + strategyId, + orderId: result.order_id, + order + }); } catch (error) { - this.container.logger.error(`Failed to submit order for strategy ${strategyId}:`, error); + this.container.logger.error(`Failed to place order for strategy ${strategyId}:`, error); + } + } else { + // Use execution service for paper/live trading + const executionService = this.container.custom?.ExecutionService; + if (executionService) { + try { + const result = await executionService.submitOrder(order); + this.container.logger.info(`Order submitted for strategy ${strategyId}:`, result); + } catch (error) { + this.container.logger.error(`Failed to submit order for strategy ${strategyId}:`, error); + } } } } diff --git a/apps/stock/orchestrator/src/strategies/examples/SimpleMovingAverageCrossover.ts b/apps/stock/orchestrator/src/strategies/examples/SimpleMovingAverageCrossover.ts new file mode 100644 index 0000000..9b5503a --- /dev/null +++ b/apps/stock/orchestrator/src/strategies/examples/SimpleMovingAverageCrossover.ts @@ -0,0 +1,254 @@ +import { BaseStrategy, Signal } from '../BaseStrategy'; +import { MarketData } from '../../types'; +import { getLogger } from '@stock-bot/logger'; + +const logger = getLogger('SimpleMovingAverageCrossover'); + +export class SimpleMovingAverageCrossover extends BaseStrategy { + private priceHistory = new Map(); + private positions = new Map(); + private lastSignalTime = new Map(); + private totalSignals = 0; + + // Strategy parameters + private readonly FAST_PERIOD = 10; + private readonly SLOW_PERIOD = 20; + private readonly POSITION_SIZE = 0.1; // 10% of capital per position + private readonly MIN_SIGNAL_INTERVAL = 24 * 60 * 60 * 1000; // 1 day minimum between signals + + constructor(config: any, modeManager?: any, executionService?: any) { + super(config, modeManager, executionService); + logger.info(`SimpleMovingAverageCrossover initialized with Fast=${this.FAST_PERIOD}, Slow=${this.SLOW_PERIOD}`); + } + + protected updateIndicators(data: MarketData): void { + if (data.type !== 'bar') return; + + const symbol = data.data.symbol; + const price = data.data.close; + + // Update price history + if (!this.priceHistory.has(symbol)) { + this.priceHistory.set(symbol, []); + logger.info(`📊 Starting to track ${symbol} @ ${price}`); + } + + const history = this.priceHistory.get(symbol)!; + history.push(price); + + // Keep only needed history + if (history.length > this.SLOW_PERIOD * 2) { + history.shift(); + } + + // Log when we have enough data to start trading + if (history.length === this.SLOW_PERIOD) { + logger.info(`✅ ${symbol} now has enough history (${history.length} bars) to start trading`); + } + } + + protected async generateSignal(data: MarketData): Promise { + if (data.type !== 'bar') return null; + + const symbol = data.data.symbol; + const history = this.priceHistory.get(symbol); + + if (!history || history.length < this.SLOW_PERIOD) { + if (history && history.length % 5 === 0) { + logger.debug(`${symbol} - Not enough history: ${history.length}/${this.SLOW_PERIOD} bars`); + } + return null; + } + + // Calculate moving averages + const fastMA = this.calculateSMA(history, this.FAST_PERIOD); + const slowMA = this.calculateSMA(history, this.SLOW_PERIOD); + + // Get previous MAs for crossover detection + const prevHistory = history.slice(0, -1); + const prevFastMA = this.calculateSMA(prevHistory, this.FAST_PERIOD); + const prevSlowMA = this.calculateSMA(prevHistory, this.SLOW_PERIOD); + + const currentPosition = this.positions.get(symbol) || 0; + const currentPrice = data.data.close; + const timestamp = new Date(data.data.timestamp).toISOString().split('T')[0]; + + // Log every 50 bars to track MA values and crossover conditions + if (history.length % 50 === 0) { + logger.info(`${symbol} @ ${timestamp} - Price: ${currentPrice.toFixed(2)}, Fast MA: ${fastMA.toFixed(2)}, Slow MA: ${slowMA.toFixed(2)}, Position: ${currentPosition}`); + logger.debug(`${symbol} - Prev Fast MA: ${prevFastMA.toFixed(2)}, Prev Slow MA: ${prevSlowMA.toFixed(2)}`); + logger.debug(`${symbol} - Fast > Slow: ${fastMA > slowMA}, Prev Fast <= Prev Slow: ${prevFastMA <= prevSlowMA}`); + } + + // Detect crossovers with detailed logging + const goldenCross = prevFastMA <= prevSlowMA && fastMA > slowMA; + const deathCross = prevFastMA >= prevSlowMA && fastMA < slowMA; + + if (goldenCross && currentPosition === 0) { + // Golden cross - buy signal + logger.info(`🟢 Golden cross detected for ${symbol} @ ${timestamp}`); + logger.info(` Price: ${currentPrice.toFixed(2)}, Fast MA: ${fastMA.toFixed(2)} > Slow MA: ${slowMA.toFixed(2)}`); + logger.info(` Prev Fast MA: ${prevFastMA.toFixed(2)} <= Prev Slow MA: ${prevSlowMA.toFixed(2)}`); + + // Calculate position size + const positionSize = this.calculatePositionSize(currentPrice); + logger.info(` Position size: ${positionSize} shares`); + + const signal: Signal = { + type: 'buy', + symbol, + strength: 0.8, + reason: 'Golden cross - Fast MA crossed above Slow MA', + metadata: { + fastMA, + slowMA, + prevFastMA, + prevSlowMA, + crossoverType: 'golden', + price: currentPrice, + quantity: positionSize + } + }; + + // Track signal time + this.lastSignalTime.set(symbol, Date.now()); + this.totalSignals++; + logger.info(`👉 Total signals generated: ${this.totalSignals}`); + + return signal; + } else if (deathCross && currentPosition > 0) { + // Death cross - sell signal + logger.info(`🔴 Death cross detected for ${symbol} @ ${timestamp}`); + logger.info(` Price: ${currentPrice.toFixed(2)}, Fast MA: ${fastMA.toFixed(2)} < Slow MA: ${slowMA.toFixed(2)}`); + logger.info(` Prev Fast MA: ${prevFastMA.toFixed(2)} >= Prev Slow MA: ${prevSlowMA.toFixed(2)}`); + logger.info(` Current position: ${currentPosition} shares`); + + const signal: Signal = { + type: 'sell', + symbol, + strength: 0.8, + reason: 'Death cross - Fast MA crossed below Slow MA', + metadata: { + fastMA, + slowMA, + prevFastMA, + prevSlowMA, + crossoverType: 'death', + price: currentPrice, + quantity: currentPosition + } + }; + + // Track signal time + this.lastSignalTime.set(symbol, Date.now()); + this.totalSignals++; + logger.info(`👉 Total signals generated: ${this.totalSignals}`); + + return signal; + } + + // Log near-crossover conditions + const fastApproachingSlow = Math.abs(fastMA - slowMA) / slowMA < 0.01; // Within 1% + if (fastApproachingSlow && history.length % 20 === 0) { + logger.debug(`${symbol} - MAs converging: Fast MA ${fastMA.toFixed(2)} ~ Slow MA ${slowMA.toFixed(2)} (${((Math.abs(fastMA - slowMA) / slowMA) * 100).toFixed(2)}% diff)`); + } + + return null; + } + + private calculateSMA(prices: number[], period: number): number { + if (prices.length < period) { + logger.warn(`Not enough data for SMA calculation: ${prices.length} < ${period}`); + return 0; + } + + const slice = prices.slice(-period); + const sum = slice.reduce((a, b) => a + b, 0); + const sma = sum / period; + + // Sanity check + if (isNaN(sma) || !isFinite(sma)) { + logger.error(`Invalid SMA calculation: sum=${sum}, period=${period}, prices=${slice.length}`); + return 0; + } + + return sma; + } + + private calculatePositionSize(price: number): number { + // Get account balance from trading engine + const tradingEngine = this.modeManager?.getTradingEngine(); + if (!tradingEngine) { + logger.warn('No trading engine available, using default position size'); + return 100; + } + + // Try to get account balance from trading engine + let accountBalance = 100000; // Default + try { + if (tradingEngine.getAccountBalance) { + accountBalance = tradingEngine.getAccountBalance(); + } else if (tradingEngine.getTotalPnl) { + const [realized, unrealized] = tradingEngine.getTotalPnl(); + accountBalance = 100000 + realized + unrealized; // Assuming 100k initial + } + } catch (error) { + logger.warn('Could not get account balance:', error); + } + + const positionValue = accountBalance * this.POSITION_SIZE; + const shares = Math.floor(positionValue / price); + + logger.debug(`Position sizing: Balance=$${accountBalance}, Position Size=${this.POSITION_SIZE}, Price=$${price}, Shares=${shares}`); + + return Math.max(1, shares); // At least 1 share + } + + protected onOrderFilled(fill: any): void { + const { symbol, side, quantity, price } = fill; + + const currentPosition = this.positions.get(symbol) || 0; + + if (side === 'buy') { + this.positions.set(symbol, currentPosition + quantity); + logger.info(`✅ BUY filled: ${symbol} - ${quantity} shares @ ${price}`); + } else { + this.positions.set(symbol, Math.max(0, currentPosition - quantity)); + logger.info(`✅ SELL filled: ${symbol} - ${quantity} shares @ ${price}`); + } + + logger.info(`Position updated for ${symbol}: ${this.positions.get(symbol)} shares`); + } + + // Override to provide custom order generation + protected async signalToOrder(signal: Signal): Promise { + logger.info(`🔄 Converting signal to order:`, signal); + + // Get position sizing from metadata or calculate + const quantity = signal.metadata?.quantity || 100; + + if (signal.type === 'buy') { + const order: OrderRequest = { + symbol: signal.symbol, + side: 'buy', + quantity, + orderType: 'market', + timeInForce: 'DAY' + }; + logger.info(`📈 Generated BUY order:`, order); + return order; + } else if (signal.type === 'sell') { + const order: OrderRequest = { + symbol: signal.symbol, + side: 'sell', + quantity, + orderType: 'market', + timeInForce: 'DAY' + }; + logger.info(`📉 Generated SELL order:`, order); + return order; + } + + return null; + } +} \ No newline at end of file diff --git a/apps/stock/orchestrator/src/types.ts b/apps/stock/orchestrator/src/types.ts index 1a0dc8b..549040b 100644 --- a/apps/stock/orchestrator/src/types.ts +++ b/apps/stock/orchestrator/src/types.ts @@ -12,6 +12,14 @@ export const BacktestConfigSchema = z.object({ symbols: z.array(z.string()), initialCapital: z.number().positive(), dataFrequency: z.enum(['1m', '5m', '15m', '1h', '1d']), + strategy: z.string(), + strategies: z.array(z.object({ + name: z.string(), + enabled: z.boolean().default(true), + config: z.record(z.any()).optional() + })).optional(), + commission: z.number().optional(), + slippage: z.number().optional(), fillModel: z.object({ slippage: z.enum(['zero', 'conservative', 'realistic', 'aggressive']), marketImpact: z.boolean(), diff --git a/apps/stock/web-api/src/services/backtest.service.ts b/apps/stock/web-api/src/services/backtest.service.ts index f2f3288..e95fddd 100644 --- a/apps/stock/web-api/src/services/backtest.service.ts +++ b/apps/stock/web-api/src/services/backtest.service.ts @@ -65,8 +65,11 @@ export class BacktestService { startDate: new Date(request.startDate).toISOString(), endDate: new Date(request.endDate).toISOString(), symbols: request.symbols, + strategy: request.strategy, initialCapital: request.initialCapital, dataFrequency: '1d', // Default to daily + commission: request.config?.commission, + slippage: request.config?.slippage, speed: 'max', // Default speed fillModel: { slippage: 'realistic', diff --git a/apps/stock/web-app/src/features/backtest/components/BacktestConfiguration.tsx b/apps/stock/web-app/src/features/backtest/components/BacktestConfiguration.tsx index 474a3e0..70cf6f2 100644 --- a/apps/stock/web-app/src/features/backtest/components/BacktestConfiguration.tsx +++ b/apps/stock/web-app/src/features/backtest/components/BacktestConfiguration.tsx @@ -13,7 +13,7 @@ export function BacktestConfiguration({ onSubmit, disabled }: BacktestConfigurat endDate: new Date(), initialCapital: 100000, symbols: [], - strategy: 'momentum', + strategy: 'sma-crossover', speedMultiplier: 1, commission: 0.001, // 0.1% slippage: 0.0005, // 0.05% @@ -185,11 +185,19 @@ export function BacktestConfiguration({ onSubmit, disabled }: BacktestConfigurat className="w-full px-3 py-2 bg-background border border-border rounded-md text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500" disabled={disabled} > - + - - + + + +

+ {formData.strategy === 'sma-crossover' && 'Trades on 10/20 day moving average crossovers'} + {formData.strategy === 'mean-reversion' && 'Trades when price deviates from mean by 2+ std devs'} + {formData.strategy === 'ml-enhanced' && 'Uses machine learning for signal generation'} + {formData.strategy === 'momentum' && 'Follows strong price trends'} + {formData.strategy === 'pairs-trading' && 'Trades correlated asset pairs'} +

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 56d6560..e20820c 100644 --- a/apps/stock/web-app/src/features/backtest/components/BacktestResults.tsx +++ b/apps/stock/web-app/src/features/backtest/components/BacktestResults.tsx @@ -2,7 +2,6 @@ import type { BacktestStatus } from '../types'; import type { BacktestResult } from '../services/backtestApi'; import { MetricsCard } from './MetricsCard'; import { PositionsTable } from './PositionsTable'; -import { TradeLog } from './TradeLog'; import { Chart } from '../../../components/charts'; import { useState, useMemo } from 'react'; @@ -171,22 +170,107 @@ export function BacktestResults({ status, results, currentTime }: BacktestResult })()}
- {/* Trade Log */} + {/* Trade History Table */} {results.trades && results.trades.length > 0 && (

- Trade History + Trade History ({results.trades.length} trades)

- ({ - id: trade.id, - timestamp: trade.exitDate || trade.entryDate, - symbol: trade.symbol, - side: trade.side as 'buy' | 'sell', - quantity: trade.quantity, - price: trade.exitPrice, - commission: trade.commission, - pnl: trade.pnl - }))} /> +
+ + + + + + + + + + + + + + + + {results.trades.slice().reverse().map((trade) => { + const formatDate = (dateStr: string) => { + const date = new Date(dateStr); + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: '2-digit' + }); + }; + + const formatDuration = (ms: number) => { + const days = Math.floor(ms / (1000 * 60 * 60 * 24)); + if (days > 0) return `${days}d`; + const hours = Math.floor(ms / (1000 * 60 * 60)); + if (hours > 0) return `${hours}h`; + return '<1h'; + }; + + return ( + + + + + + + + + + + + ); + })} + + + + + + + + + +
DateSymbolSideQtyEntryExitP&LReturnDuration
+ {formatDate(trade.entryDate)} + + {trade.symbol} + + + {trade.side.toUpperCase()} + + + {trade.quantity} + + ${trade.entryPrice.toFixed(2)} + + ${trade.exitPrice.toFixed(2)} + = 0 ? 'text-success' : 'text-error' + }`}> + {trade.pnl >= 0 ? '+' : ''}${trade.pnl.toFixed(2)} + = 0 ? 'text-success' : 'text-error' + }`}> + {trade.pnlPercent >= 0 ? '+' : ''}{trade.pnlPercent.toFixed(2)}% + + {formatDuration(trade.duration)} +
+ Total + sum + t.pnl, 0) >= 0 ? 'text-success' : 'text-error' + }`}> + ${results.trades.reduce((sum, t) => sum + t.pnl, 0).toFixed(2)} + + Avg: {(results.trades.reduce((sum, t) => sum + t.pnlPercent, 0) / results.trades.length).toFixed(2)}% +
+
)} diff --git a/apps/stock/web-app/src/features/backtest/hooks/useBacktest.ts b/apps/stock/web-app/src/features/backtest/hooks/useBacktest.ts index 922e6ce..7b0802e 100644 --- a/apps/stock/web-app/src/features/backtest/hooks/useBacktest.ts +++ b/apps/stock/web-app/src/features/backtest/hooks/useBacktest.ts @@ -75,7 +75,7 @@ export function useBacktest(): UseBacktestReturn { setIsPolling(true); pollingIntervalRef.current = setInterval(() => { pollStatus(newBacktest.id); - }, 2000); // Poll every 2 seconds + }, 200); // Poll every 2 seconds } catch (err) { setError(err instanceof Error ? err.message : 'Failed to create backtest'); diff --git a/apps/stock/web-app/src/features/backtest/services/backtestService.ts b/apps/stock/web-app/src/features/backtest/services/backtestService.ts index 72152de..b320512 100644 --- a/apps/stock/web-app/src/features/backtest/services/backtestService.ts +++ b/apps/stock/web-app/src/features/backtest/services/backtestService.ts @@ -141,7 +141,7 @@ export class BacktestService { static async pollBacktestUpdates( id: string, onUpdate: (status: BacktestStatus, progress?: number, currentTime?: number) => void, - interval: number = 1000 + interval: number = 200 ): Promise<() => void> { let isPolling = true;