--- id: "ET-TRD-007" title: "Especificación Técnica - Paper Trading Engine" type: "Technical Specification" status: "Done" priority: "Alta" epic: "OQI-003" project: "trading-platform" version: "1.0.0" created_date: "2025-12-05" updated_date: "2026-01-04" --- # ET-TRD-007: Especificación Técnica - Paper Trading Engine **Version:** 1.0.0 **Fecha:** 2025-12-05 **Estado:** Pendiente **Épica:** [OQI-003](../_MAP.md) **Requerimiento:** RF-TRD-007 --- ## Resumen Esta especificación detalla la implementación del motor de paper trading simulado, incluyendo ejecución de órdenes, gestión de posiciones, cálculo de PnL en tiempo real, simulación de slippage y comisiones realistas. --- ## Arquitectura ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ PAPER TRADING ENGINE │ │ │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ Order Execution Layer │ │ │ │ ┌────────────────┐ ┌──────────────────┐ ┌────────────────┐ │ │ │ │ │ Order Matcher │─▶│ Slippage │─▶│ Fill Generator │ │ │ │ │ │ │ │ Calculator │ │ │ │ │ │ │ └────────────────┘ └──────────────────┘ └────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ Position Management │ │ │ │ ┌────────────────┐ ┌──────────────────┐ ┌────────────────┐ │ │ │ │ │ Position │ │ PnL Calculator │ │ Risk │ │ │ │ │ │ Manager │ │ │ │ Manager │ │ │ │ │ └────────────────┘ └──────────────────┘ └────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ Balance Management │ │ │ │ ┌────────────────┐ ┌──────────────────┐ ┌────────────────┐ │ │ │ │ │ Balance │ │ Commission │ │ Margin │ │ │ │ │ │ Tracker │ │ Calculator │ │ Calculator │ │ │ │ │ └────────────────┘ └──────────────────┘ └────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ DATABASE (PostgreSQL) │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │paper_orders │ │paper_positions│ │paper_balances│ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ## Componentes Principales ### 1. Order Execution Service **Ubicación:** `apps/backend/src/modules/trading/services/order-execution.service.ts` ```typescript import { PaperOrder, OrderSide, OrderType, OrderStatus } from '../types'; import { BinanceService } from './binance.service'; import { BalanceService } from './balance.service'; import { PositionService } from './position.service'; import { db } from '@/db'; export interface OrderExecutionParams { userId: string; symbol: string; side: OrderSide; type: OrderType; quantity: number; price?: number; stopPrice?: number; stopLoss?: number; takeProfit?: number; } export interface ExecutionResult { order: PaperOrder; fills: Trade[]; position?: PaperPosition; } export class OrderExecutionService { private binanceService: BinanceService; private balanceService: BalanceService; private positionService: PositionService; constructor() { this.binanceService = new BinanceService(); this.balanceService = new BalanceService(); this.positionService = new PositionService(); } /** * Execute order placement */ async placeOrder(params: OrderExecutionParams): Promise { // Validar orden await this.validateOrder(params); // Crear orden en estado pending const order = await this.createOrder(params); try { // Ejecutar orden según tipo const result = await this.executeOrder(order); return result; } catch (error) { // Marcar orden como rejected await this.rejectOrder(order.id, error.message); throw error; } } /** * Validate order before execution */ private async validateOrder(params: OrderExecutionParams): Promise { const { userId, symbol, side, quantity, price, type } = params; // Verificar símbolo válido const exchangeInfo = await this.binanceService.getExchangeInfo(symbol); if (!exchangeInfo.symbols.find((s) => s.symbol === symbol)) { throw new Error('Invalid trading symbol'); } // Verificar cantidad mínima const symbolInfo = exchangeInfo.symbols[0]; const minQty = parseFloat( symbolInfo.filters.find((f) => f.filterType === 'LOT_SIZE')?.minQty || '0' ); if (quantity < minQty) { throw new Error(`Minimum order quantity is ${minQty}`); } // Verificar balance disponible const quoteAsset = symbolInfo.quoteAsset; const balance = await this.balanceService.getBalance(userId, quoteAsset); const requiredBalance = this.calculateRequiredBalance( side, quantity, price || (await this.getCurrentPrice(symbol)), type ); if (balance.available < requiredBalance) { throw new Error( `Insufficient balance. Required: ${requiredBalance}, Available: ${balance.available}` ); } } /** * Create order record */ private async createOrder(params: OrderExecutionParams): Promise { const { userId, symbol, side, type, quantity, price, stopPrice, } = params; const currentPrice = await this.getCurrentPrice(symbol); const quoteQuantity = quantity * (price || currentPrice); const result = await db.query( ` INSERT INTO trading.paper_orders ( user_id, symbol, side, type, status, quantity, remaining_quantity, price, stop_price, quote_quantity, time_in_force, placed_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, NOW()) RETURNING * `, [ userId, symbol, side, type, 'pending', quantity, quantity, price, stopPrice, quoteQuantity, 'GTC', ] ); return result.rows[0]; } /** * Execute order based on type */ private async executeOrder(order: PaperOrder): Promise { switch (order.type) { case 'market': return await this.executeMarketOrder(order); case 'limit': return await this.executeLimitOrder(order); case 'stop_loss': return await this.executeStopLossOrder(order); default: throw new Error(`Unsupported order type: ${order.type}`); } } /** * Execute market order immediately */ private async executeMarketOrder(order: PaperOrder): Promise { const currentPrice = await this.getCurrentPrice(order.symbol); // Simular slippage const slippage = this.calculateSlippage(order.quantity, order.symbol); const executionPrice = this.applySlippage( currentPrice, order.side, slippage ); // Calcular comisión const commission = this.calculateCommission( order.quantity, executionPrice, false // market orders are takers ); // Crear trade (fill) const trade = await this.createTrade({ orderId: order.id, userId: order.userId, symbol: order.symbol, side: order.side, price: executionPrice, quantity: order.quantity, commission, marketPrice: currentPrice, slippage, isMaker: false, }); // Actualizar orden a filled await this.updateOrder(order.id, { status: 'filled', filledQuantity: order.quantity, remainingQuantity: 0, averageFillPrice: executionPrice, filledQuoteQuantity: order.quantity * executionPrice, commission, filledAt: new Date(), }); // Actualizar balance await this.updateBalanceAfterTrade( order.userId, order.symbol, order.side, order.quantity, executionPrice, commission ); // Crear o actualizar posición const position = await this.positionService.processTradeForPosition(trade); return { order: { ...order, status: 'filled' }, fills: [trade], position, }; } /** * Execute limit order (check if price matches) */ private async executeLimitOrder(order: PaperOrder): Promise { const currentPrice = await this.getCurrentPrice(order.symbol); // Verificar si el precio límite se cumple const shouldFill = (order.side === 'buy' && currentPrice <= order.price!) || (order.side === 'sell' && currentPrice >= order.price!); if (!shouldFill) { // Orden permanece abierta await this.updateOrder(order.id, { status: 'open' }); // Bloquear balance await this.balanceService.lockBalance( order.userId, this.getQuoteAsset(order.symbol), order.quoteQuantity ); return { order: { ...order, status: 'open' }, fills: [], }; } // Ejecutar como si fuera market order pero sin slippage const executionPrice = order.price!; const commission = this.calculateCommission( order.quantity, executionPrice, true // limit orders can be makers ); const trade = await this.createTrade({ orderId: order.id, userId: order.userId, symbol: order.symbol, side: order.side, price: executionPrice, quantity: order.quantity, commission, marketPrice: currentPrice, slippage: 0, isMaker: true, }); await this.updateOrder(order.id, { status: 'filled', filledQuantity: order.quantity, remainingQuantity: 0, averageFillPrice: executionPrice, filledQuoteQuantity: order.quantity * executionPrice, commission, filledAt: new Date(), }); await this.updateBalanceAfterTrade( order.userId, order.symbol, order.side, order.quantity, executionPrice, commission ); const position = await this.positionService.processTradeForPosition(trade); return { order: { ...order, status: 'filled' }, fills: [trade], position, }; } /** * Stop loss orders are monitored and executed when price reaches stop */ private async executeStopLossOrder(order: PaperOrder): Promise { // Similar to limit order but triggers at stop price // This would be handled by a monitoring service that checks prices await this.updateOrder(order.id, { status: 'open' }); return { order: { ...order, status: 'open' }, fills: [], }; } /** * Calculate slippage based on order size */ private calculateSlippage(quantity: number, symbol: string): number { // Simplified slippage model // Larger orders have more slippage const baseSlippage = 0.0001; // 0.01% const volumeFactor = Math.min(quantity / 10, 0.001); // Max 0.1% return baseSlippage + volumeFactor; } /** * Apply slippage to price */ private applySlippage( price: number, side: OrderSide, slippage: number ): number { if (side === 'buy') { // Buy orders get worse price (higher) return price * (1 + slippage); } else { // Sell orders get worse price (lower) return price * (1 - slippage); } } /** * Calculate trading commission */ private calculateCommission( quantity: number, price: number, isMaker: boolean ): number { const commissionRate = isMaker ? 0.001 : 0.001; // 0.1% for both (Binance standard) return quantity * price * commissionRate; } /** * Create trade record */ private async createTrade(params: { orderId: string; userId: string; symbol: string; side: OrderSide; price: number; quantity: number; commission: number; marketPrice: number; slippage: number; isMaker: boolean; }): Promise { const result = await db.query( ` INSERT INTO trading.paper_trades ( user_id, order_id, symbol, side, type, price, quantity, quote_quantity, commission, commission_asset, market_price, slippage, is_maker, executed_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, NOW()) RETURNING * `, [ params.userId, params.orderId, params.symbol, params.side, 'entry', // Will be determined by position service params.price, params.quantity, params.quantity * params.price, params.commission, 'USDT', params.marketPrice, params.slippage, params.isMaker, ] ); return result.rows[0]; } /** * Update balance after trade execution */ private async updateBalanceAfterTrade( userId: string, symbol: string, side: OrderSide, quantity: number, price: number, commission: number ): Promise { const [baseAsset, quoteAsset] = this.parseSymbol(symbol); if (side === 'buy') { // Deduct quote asset (e.g., USDT) await this.balanceService.deduct( userId, quoteAsset, quantity * price + commission ); // Add base asset (e.g., BTC) await this.balanceService.add(userId, baseAsset, quantity); } else { // Deduct base asset await this.balanceService.deduct(userId, baseAsset, quantity); // Add quote asset (minus commission) await this.balanceService.add( userId, quoteAsset, quantity * price - commission ); } } /** * Get current market price */ private async getCurrentPrice(symbol: string): Promise { const price = await this.binanceService.getCurrentPrice(symbol); return parseFloat(price); } /** * Cancel open order */ async cancelOrder(orderId: string, userId: string): Promise { const order = await this.getOrder(orderId); if (order.userId !== userId) { throw new Error('Unauthorized'); } if (!['pending', 'open'].includes(order.status)) { throw new Error('Order cannot be cancelled'); } // Liberar balance bloqueado if (order.status === 'open' && order.quoteQuantity > 0) { const quoteAsset = this.getQuoteAsset(order.symbol); await this.balanceService.unlockBalance( userId, quoteAsset, order.remainingQuantity * (order.price || 0) ); } await this.updateOrder(orderId, { status: 'cancelled', cancelledAt: new Date(), }); return { ...order, status: 'cancelled' }; } // Helper methods private parseSymbol(symbol: string): [string, string] { // BTCUSDT -> [BTC, USDT] // Simplified, should use exchange info return [symbol.replace('USDT', ''), 'USDT']; } private getQuoteAsset(symbol: string): string { return 'USDT'; // Simplified } private calculateRequiredBalance( side: OrderSide, quantity: number, price: number, type: OrderType ): number { if (side === 'buy') { return quantity * price * 1.001; // Include commission buffer } else { return 0; // For sell orders, we need base asset (not quote) } } private async getOrder(orderId: string): Promise { const result = await db.query( 'SELECT * FROM trading.paper_orders WHERE id = $1', [orderId] ); if (result.rows.length === 0) { throw new Error('Order not found'); } return result.rows[0]; } private async updateOrder( orderId: string, updates: Partial ): Promise { const fields = Object.keys(updates); const values = Object.values(updates); const setClause = fields .map((field, index) => `${field} = $${index + 2}`) .join(', '); await db.query( `UPDATE trading.paper_orders SET ${setClause} WHERE id = $1`, [orderId, ...values] ); } private async rejectOrder(orderId: string, reason: string): Promise { await db.query( ` UPDATE trading.paper_orders SET status = 'rejected', notes = $2, updated_at = NOW() WHERE id = $1 `, [orderId, reason] ); } } ``` --- ### 2. Position Service **Ubicación:** `apps/backend/src/modules/trading/services/position.service.ts` ```typescript import { PaperPosition, Trade, PositionSide, PositionStatus } from '../types'; import { db } from '@/db'; export class PositionService { /** * Process trade and create/update position */ async processTradeForPosition(trade: Trade): Promise { // Buscar posición abierta existente const existingPosition = await this.getOpenPosition( trade.userId, trade.symbol ); if (!existingPosition) { // Crear nueva posición return await this.createPosition(trade); } // Determinar si es aumento o reducción de posición const isIncreasingPosition = (existingPosition.side === 'long' && trade.side === 'buy') || (existingPosition.side === 'short' && trade.side === 'sell'); if (isIncreasingPosition) { return await this.increasePosition(existingPosition, trade); } else { return await this.reducePosition(existingPosition, trade); } } /** * Create new position */ private async createPosition(trade: Trade): Promise { const side: PositionSide = trade.side === 'buy' ? 'long' : 'short'; const result = await db.query( ` INSERT INTO trading.paper_positions ( user_id, symbol, side, status, entry_price, entry_quantity, entry_value, entry_order_id, current_quantity, average_entry_price, total_commission, opened_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, NOW()) RETURNING * `, [ trade.userId, trade.symbol, side, 'open', trade.price, trade.quantity, trade.quantity * trade.price, trade.orderId, trade.quantity, trade.price, trade.commission, ] ); // Actualizar tipo de trade await this.updateTradeType(trade.id, 'entry', result.rows[0].id); return result.rows[0]; } /** * Increase existing position */ private async increasePosition( position: PaperPosition, trade: Trade ): Promise { // Calcular nuevo precio promedio const totalValue = position.currentQuantity * position.averageEntryPrice + trade.quantity * trade.price; const newQuantity = position.currentQuantity + trade.quantity; const newAveragePrice = totalValue / newQuantity; await db.query( ` UPDATE trading.paper_positions SET current_quantity = $2, average_entry_price = $3, total_commission = total_commission + $4, updated_at = NOW() WHERE id = $1 `, [position.id, newQuantity, newAveragePrice, trade.commission] ); await this.updateTradeType(trade.id, 'entry', position.id); return { ...position, currentQuantity: newQuantity, averageEntryPrice: newAveragePrice }; } /** * Reduce or close position */ private async reducePosition( position: PaperPosition, trade: Trade ): Promise { const quantityReduced = Math.min(trade.quantity, position.currentQuantity); const newQuantity = position.currentQuantity - quantityReduced; // Calcular PnL realizado const realizedPnl = this.calculateRealizedPnL( position.side, position.averageEntryPrice, trade.price, quantityReduced ); if (newQuantity === 0) { // Cerrar posición completamente await db.query( ` UPDATE trading.paper_positions SET status = 'closed', current_quantity = 0, exit_price = $2, exit_quantity = $3, exit_value = $4, exit_order_id = $5, realized_pnl = realized_pnl + $6, total_pnl = realized_pnl + $6, total_commission = total_commission + $7, closed_at = NOW(), updated_at = NOW() WHERE id = $1 `, [ position.id, trade.price, quantityReduced, quantityReduced * trade.price, trade.orderId, realizedPnl, trade.commission, ] ); await this.updateTradeType(trade.id, 'exit', position.id); } else { // Reducir posición parcialmente await db.query( ` UPDATE trading.paper_positions SET current_quantity = $2, realized_pnl = realized_pnl + $3, total_commission = total_commission + $4, updated_at = NOW() WHERE id = $1 `, [position.id, newQuantity, realizedPnl, trade.commission] ); await this.updateTradeType(trade.id, 'partial', position.id); } return { ...position, currentQuantity: newQuantity, realizedPnl: position.realizedPnl + realizedPnl, }; } /** * Calculate realized PnL */ private calculateRealizedPnL( side: PositionSide, entryPrice: number, exitPrice: number, quantity: number ): number { if (side === 'long') { return (exitPrice - entryPrice) * quantity; } else { return (entryPrice - exitPrice) * quantity; } } /** * Update unrealized PnL for all open positions */ async updateUnrealizedPnL( userId: string, symbol: string, currentPrice: number ): Promise { await db.query( ` UPDATE trading.paper_positions SET unrealized_pnl = CASE WHEN side = 'long' THEN (${currentPrice} - average_entry_price) * current_quantity ELSE (average_entry_price - ${currentPrice}) * current_quantity END, total_pnl = realized_pnl + CASE WHEN side = 'long' THEN (${currentPrice} - average_entry_price) * current_quantity ELSE (average_entry_price - ${currentPrice}) * current_quantity END, pnl_percentage = ( (realized_pnl + CASE WHEN side = 'long' THEN (${currentPrice} - average_entry_price) * current_quantity ELSE (average_entry_price - ${currentPrice}) * current_quantity END) / entry_value ) * 100, updated_at = NOW() WHERE user_id = $1 AND symbol = $2 AND status = 'open' `, [userId, symbol] ); } /** * Get open position for symbol */ private async getOpenPosition( userId: string, symbol: string ): Promise { const result = await db.query( ` SELECT * FROM trading.paper_positions WHERE user_id = $1 AND symbol = $2 AND status = 'open' LIMIT 1 `, [userId, symbol] ); return result.rows[0] || null; } /** * Update trade type and link to position */ private async updateTradeType( tradeId: string, type: 'entry' | 'exit' | 'partial', positionId: string ): Promise { await db.query( 'UPDATE trading.paper_trades SET type = $2, position_id = $3 WHERE id = $1', [tradeId, type, positionId] ); } } ``` --- ### 3. Balance Service **Ubicación:** `apps/backend/src/modules/trading/services/balance.service.ts` ```typescript import { PaperBalance } from '../types'; import { db } from '@/db'; export class BalanceService { /** * Get balance for specific asset */ async getBalance(userId: string, asset: string): Promise { const result = await db.query( 'SELECT * FROM trading.paper_balances WHERE user_id = $1 AND asset = $2', [userId, asset] ); if (result.rows.length === 0) { // Create initial balance if doesn't exist return await this.createBalance(userId, asset, 0); } return result.rows[0]; } /** * Add to balance */ async add(userId: string, asset: string, amount: number): Promise { await db.query( ` INSERT INTO trading.paper_balances (user_id, asset, total, available) VALUES ($1, $2, $3, $3) ON CONFLICT (user_id, asset) DO UPDATE SET total = trading.paper_balances.total + $3, available = trading.paper_balances.available + $3, updated_at = NOW() `, [userId, asset, amount] ); } /** * Deduct from balance */ async deduct(userId: string, asset: string, amount: number): Promise { const result = await db.query( ` UPDATE trading.paper_balances SET total = total - $3, available = available - $3, updated_at = NOW() WHERE user_id = $1 AND asset = $2 AND available >= $3 RETURNING * `, [userId, asset, amount] ); if (result.rows.length === 0) { throw new Error('Insufficient balance'); } } /** * Lock balance (for open orders) */ async lockBalance( userId: string, asset: string, amount: number ): Promise { const result = await db.query( ` UPDATE trading.paper_balances SET available = available - $3, locked = locked + $3, updated_at = NOW() WHERE user_id = $1 AND asset = $2 AND available >= $3 RETURNING * `, [userId, asset, amount] ); if (result.rows.length === 0) { throw new Error('Insufficient available balance'); } } /** * Unlock balance (when order cancelled) */ async unlockBalance( userId: string, asset: string, amount: number ): Promise { await db.query( ` UPDATE trading.paper_balances SET available = available + $3, locked = locked - $3, updated_at = NOW() WHERE user_id = $1 AND asset = $2 `, [userId, asset, amount] ); } /** * Reset all balances to initial state */ async resetBalances( userId: string, initialAmount: number = 10000 ): Promise { // Delete all balances await db.query('DELETE FROM trading.paper_balances WHERE user_id = $1', [ userId, ]); // Create initial USDT balance await this.createBalance(userId, 'USDT', initialAmount); } private async createBalance( userId: string, asset: string, amount: number ): Promise { const result = await db.query( ` INSERT INTO trading.paper_balances (user_id, asset, total, available, locked) VALUES ($1, $2, $3, $3, 0) RETURNING * `, [userId, asset, amount] ); return result.rows[0]; } } ``` --- ## Configuración ```typescript // config/trading.config.ts export const tradingConfig = { // Comisiones commission: { maker: 0.001, // 0.1% taker: 0.001, // 0.1% }, // Slippage simulation slippage: { base: 0.0001, // 0.01% base slippage maxVolumeFactor: 0.001, // Additional 0.1% for large orders }, // Balance inicial initialBalance: { USDT: 10000, }, // Límites de orden orderLimits: { minOrderValue: 10, // USDT maxOrderValue: 100000, // USDT }, // Ejecución de órdenes execution: { delayMs: 100, // Simular delay de red }, }; ``` --- ## Testing ```typescript describe('OrderExecutionService', () => { let service: OrderExecutionService; let userId: string; beforeEach(async () => { service = new OrderExecutionService(); userId = await createTestUser(); await initializeBalance(userId, 10000); }); describe('Market Orders', () => { it('should execute buy market order', async () => { const result = await service.placeOrder({ userId, symbol: 'BTCUSDT', side: 'buy', type: 'market', quantity: 0.1, }); expect(result.order.status).toBe('filled'); expect(result.fills.length).toBe(1); expect(result.position).toBeDefined(); expect(result.position.side).toBe('long'); }); it('should apply slippage to market orders', async () => { // Mock current price at 50000 jest.spyOn(service as any, 'getCurrentPrice').mockResolvedValue(50000); const result = await service.placeOrder({ userId, symbol: 'BTCUSDT', side: 'buy', type: 'market', quantity: 0.1, }); expect(result.fills[0].price).toBeGreaterThan(50000); expect(result.fills[0].slippage).toBeGreaterThan(0); }); it('should reject order with insufficient balance', async () => { await expect( service.placeOrder({ userId, symbol: 'BTCUSDT', side: 'buy', type: 'market', quantity: 10, // Too large }) ).rejects.toThrow('Insufficient balance'); }); }); describe('Position Management', () => { it('should create position on first buy', async () => { const result = await service.placeOrder({ userId, symbol: 'BTCUSDT', side: 'buy', type: 'market', quantity: 0.1, }); expect(result.position.currentQuantity).toBe(0.1); expect(result.position.side).toBe('long'); }); it('should average entry price when adding to position', async () => { // First buy at 50000 await service.placeOrder({ userId, symbol: 'BTCUSDT', side: 'buy', type: 'market', quantity: 0.1, }); // Second buy at 51000 jest.spyOn(service as any, 'getCurrentPrice').mockResolvedValue(51000); const result = await service.placeOrder({ userId, symbol: 'BTCUSDT', side: 'buy', type: 'market', quantity: 0.1, }); expect(result.position.currentQuantity).toBe(0.2); expect(result.position.averageEntryPrice).toBeCloseTo(50500, 0); }); it('should calculate realized PnL when closing position', async () => { // Buy at 50000 await service.placeOrder({ userId, symbol: 'BTCUSDT', side: 'buy', type: 'market', quantity: 0.1, }); // Sell at 52000 jest.spyOn(service as any, 'getCurrentPrice').mockResolvedValue(52000); const result = await service.placeOrder({ userId, symbol: 'BTCUSDT', side: 'sell', type: 'market', quantity: 0.1, }); expect(result.position.status).toBe('closed'); expect(result.position.realizedPnl).toBeGreaterThan(0); }); }); }); ``` --- ## Referencias - [Order Matching Algorithms](https://en.wikipedia.org/wiki/Order_matching_system) - [Position Sizing and Risk Management](https://www.investopedia.com/articles/trading/09/determine-position-size.asp) - [Market Microstructure](https://www.investopedia.com/terms/m/microstructure.asp)