import { EventEmitter } from 'events'; import { IServiceContainer } from '@stock-bot/di'; import { OrderRequest, Position } from '../types'; import { StorageService } from '../services/StorageService'; import { MarketDataService } from '../services/MarketDataService'; import { ExecutionService } from '../services/ExecutionService'; interface VirtualAccount { balance: number; buyingPower: number; positions: Map; orders: Map; trades: VirtualTrade[]; equity: number; marginUsed: number; } interface VirtualPosition { symbol: string; quantity: number; averagePrice: number; marketValue: number; unrealizedPnl: number; realizedPnl: number; } interface VirtualOrder { id: string; symbol: string; side: 'buy' | 'sell'; quantity: number; orderType: string; limitPrice?: number; status: string; submittedAt: Date; } interface VirtualTrade { orderId: string; symbol: string; side: 'buy' | 'sell'; quantity: number; price: number; commission: number; timestamp: Date; pnl?: number; } export class PaperTradingManager extends EventEmitter { private account: VirtualAccount; private marketPrices = new Map(); private readonly COMMISSION_RATE = 0.001; // 0.1% private readonly MARGIN_REQUIREMENT = 0.25; // 25% margin requirement private container: IServiceContainer; constructor( container: IServiceContainer, private storageService: StorageService, private marketDataService: MarketDataService, private executionService: ExecutionService, initialBalance: number = 100000 ) { super(); this.container = container; this.account = { balance: initialBalance, buyingPower: initialBalance * (1 / this.MARGIN_REQUIREMENT), positions: new Map(), orders: new Map(), trades: [], equity: initialBalance, marginUsed: 0 }; this.setupEventListeners(); } private setupEventListeners(): void { // Listen for market data updates to track prices // In real implementation, would connect to market data service } updateMarketPrice(symbol: string, bid: number, ask: number): void { this.marketPrices.set(symbol, { bid, ask }); // Update position values const position = this.account.positions.get(symbol); if (position) { const midPrice = (bid + ask) / 2; position.marketValue = position.quantity * midPrice; position.unrealizedPnl = position.quantity * (midPrice - position.averagePrice); } // Update account equity this.updateAccountEquity(); } async executeOrder(order: OrderRequest): Promise { // Validate order const validation = this.validateOrder(order); if (!validation.valid) { return { status: 'rejected', reason: validation.reason }; } // Check buying power const requiredCapital = this.calculateRequiredCapital(order); if (requiredCapital > this.account.buyingPower) { return { status: 'rejected', reason: 'Insufficient buying power' }; } // Create virtual order const virtualOrder: VirtualOrder = { id: `paper_${Date.now()}`, symbol: order.symbol, side: order.side, quantity: order.quantity, orderType: order.orderType, limitPrice: order.limitPrice, status: 'pending', submittedAt: new Date() }; this.account.orders.set(virtualOrder.id, virtualOrder); // Simulate order execution based on type if (order.orderType === 'market') { await this.executeMarketOrder(virtualOrder); } else if (order.orderType === 'limit') { // Limit orders would be checked periodically virtualOrder.status = 'accepted'; } return { orderId: virtualOrder.id, status: virtualOrder.status }; } private async executeMarketOrder(order: VirtualOrder): Promise { const marketPrice = this.marketPrices.get(order.symbol); if (!marketPrice) { order.status = 'rejected'; this.emit('orderUpdate', { orderId: order.id, status: 'rejected', reason: 'No market data available' }); return; } // Simulate realistic fill with slippage const fillPrice = order.side === 'buy' ? marketPrice.ask * (1 + this.getSlippage(order.quantity)) : marketPrice.bid * (1 - this.getSlippage(order.quantity)); const commission = fillPrice * order.quantity * this.COMMISSION_RATE; // Create trade const trade: VirtualTrade = { orderId: order.id, symbol: order.symbol, side: order.side, quantity: order.quantity, price: fillPrice, commission, timestamp: new Date() }; // Update position this.updatePosition(trade); // Update account const totalCost = (fillPrice * order.quantity) + commission; if (order.side === 'buy') { this.account.balance -= totalCost; } else { this.account.balance += (fillPrice * order.quantity) - commission; } // Record trade this.account.trades.push(trade); order.status = 'filled'; // Update buying power and margin this.updateBuyingPower(); // Emit events this.emit('fill', { orderId: order.id, symbol: order.symbol, side: order.side, quantity: order.quantity, price: fillPrice, commission, timestamp: new Date() }); this.emit('orderUpdate', { orderId: order.id, status: 'filled' }); } private updatePosition(trade: VirtualTrade): void { const position = this.account.positions.get(trade.symbol) || { symbol: trade.symbol, quantity: 0, averagePrice: 0, marketValue: 0, unrealizedPnl: 0, realizedPnl: 0 }; const oldQuantity = position.quantity; const oldAvgPrice = position.averagePrice; if (trade.side === 'buy') { // Adding to position const newQuantity = oldQuantity + trade.quantity; position.averagePrice = oldQuantity >= 0 ? ((oldQuantity * oldAvgPrice) + (trade.quantity * trade.price)) / newQuantity : trade.price; position.quantity = newQuantity; } else { // Reducing position const newQuantity = oldQuantity - trade.quantity; if (oldQuantity > 0) { // Realize P&L on closed portion const realizedPnl = trade.quantity * (trade.price - oldAvgPrice) - trade.commission; position.realizedPnl += realizedPnl; trade.pnl = realizedPnl; } position.quantity = newQuantity; if (Math.abs(newQuantity) < 0.0001) { // Position closed this.account.positions.delete(trade.symbol); return; } } this.account.positions.set(trade.symbol, position); } private validateOrder(order: OrderRequest): { valid: boolean; reason?: string } { if (order.quantity <= 0) { return { valid: false, reason: 'Invalid quantity' }; } if (order.orderType === 'limit' && !order.limitPrice) { return { valid: false, reason: 'Limit price required for limit orders' }; } return { valid: true }; } private calculateRequiredCapital(order: OrderRequest): number { const marketPrice = this.marketPrices.get(order.symbol); if (!marketPrice) return Infinity; const price = order.side === 'buy' ? marketPrice.ask : marketPrice.bid; const notional = price * order.quantity; const commission = notional * this.COMMISSION_RATE; const marginRequired = notional * this.MARGIN_REQUIREMENT; return order.side === 'buy' ? marginRequired + commission : commission; } private updateBuyingPower(): void { let totalMarginUsed = 0; for (const position of this.account.positions.values()) { totalMarginUsed += Math.abs(position.marketValue) * this.MARGIN_REQUIREMENT; } this.account.marginUsed = totalMarginUsed; this.account.buyingPower = (this.account.equity - totalMarginUsed) / this.MARGIN_REQUIREMENT; } private updateAccountEquity(): void { let totalUnrealizedPnl = 0; for (const position of this.account.positions.values()) { totalUnrealizedPnl += position.unrealizedPnl; } this.account.equity = this.account.balance + totalUnrealizedPnl; } private getSlippage(quantity: number): number { // Simple slippage model - increases with order size const baseSlippage = 0.0001; // 1 basis point const sizeImpact = quantity / 10000; // Impact increases with size return baseSlippage + (sizeImpact * 0.0001); } checkLimitOrders(): void { // Called periodically to check if limit orders can be filled for (const [orderId, order] of this.account.orders) { if (order.status !== 'accepted' || order.orderType !== 'limit') continue; const marketPrice = this.marketPrices.get(order.symbol); if (!marketPrice) continue; const canFill = order.side === 'buy' ? marketPrice.ask <= order.limitPrice! : marketPrice.bid >= order.limitPrice!; if (canFill) { this.executeMarketOrder(order); } } } getAccount(): VirtualAccount { return { ...this.account }; } getPosition(symbol: string): VirtualPosition | undefined { return this.account.positions.get(symbol); } getAllPositions(): VirtualPosition[] { return Array.from(this.account.positions.values()); } getPerformanceMetrics(): any { const totalTrades = this.account.trades.length; const winningTrades = this.account.trades.filter(t => t.pnl && t.pnl > 0); const losingTrades = this.account.trades.filter(t => t.pnl && t.pnl < 0); const totalPnl = this.account.trades.reduce((sum, t) => sum + (t.pnl || 0), 0); const totalCommission = this.account.trades.reduce((sum, t) => sum + t.commission, 0); return { totalTrades, winningTrades: winningTrades.length, losingTrades: losingTrades.length, winRate: totalTrades > 0 ? (winningTrades.length / totalTrades) * 100 : 0, totalPnl, totalCommission, netPnl: totalPnl - totalCommission, currentEquity: this.account.equity, currentPositions: this.account.positions.size }; } reset(): void { const initialBalance = this.account.balance + Array.from(this.account.positions.values()) .reduce((sum, p) => sum + p.marketValue, 0); this.account = { balance: initialBalance, buyingPower: initialBalance * (1 / this.MARGIN_REQUIREMENT), positions: new Map(), orders: new Map(), trades: [], equity: initialBalance, marginUsed: 0 }; this.container.logger.info('Paper trading account reset'); } }