/** * Position Manager * * Tracks active trades and manages automatic exits */ import { getDriftService } from '../drift/client' import { closePosition } from '../drift/orders' import { getPythPriceMonitor, PriceUpdate } from '../pyth/price-monitor' import { getMergedConfig, TradingConfig } from '../../config/trading' export interface ActiveTrade { id: string positionId: string // Transaction signature symbol: string direction: 'long' | 'short' // Entry details entryPrice: number entryTime: number positionSize: number leverage: number // Targets stopLossPrice: number tp1Price: number tp2Price: number emergencyStopPrice: number // State currentSize: number // Changes after TP1 tp1Hit: boolean slMovedToBreakeven: boolean slMovedToProfit: boolean // P&L tracking realizedPnL: number unrealizedPnL: number peakPnL: number // Monitoring priceCheckCount: number lastPrice: number lastUpdateTime: number } export interface ExitResult { success: boolean reason: 'TP1' | 'TP2' | 'SL' | 'emergency' | 'manual' | 'error' closePrice?: number closedSize?: number realizedPnL?: number transactionSignature?: string error?: string } export class PositionManager { private activeTrades: Map = new Map() private config: TradingConfig private isMonitoring: boolean = false constructor(config?: Partial) { this.config = getMergedConfig(config) console.log('✅ Position manager created') } /** * Add a new trade to monitor */ async addTrade(trade: ActiveTrade): Promise { console.log(`📊 Adding trade to monitor: ${trade.symbol} ${trade.direction}`) this.activeTrades.set(trade.id, trade) console.log(`✅ Trade added. Active trades: ${this.activeTrades.size}`) // Start monitoring if not already running if (!this.isMonitoring && this.activeTrades.size > 0) { await this.startMonitoring() } } /** * Remove a trade from monitoring */ removeTrade(tradeId: string): void { const trade = this.activeTrades.get(tradeId) if (trade) { console.log(`🗑️ Removing trade: ${trade.symbol}`) this.activeTrades.delete(tradeId) // Stop monitoring if no more trades if (this.activeTrades.size === 0 && this.isMonitoring) { this.stopMonitoring() } } } /** * Get all active trades */ getActiveTrades(): ActiveTrade[] { return Array.from(this.activeTrades.values()) } /** * Get specific trade */ getTrade(tradeId: string): ActiveTrade | null { return this.activeTrades.get(tradeId) || null } /** * Start price monitoring for all active trades */ private async startMonitoring(): Promise { if (this.isMonitoring) { return } // Get unique symbols from active trades const symbols = [...new Set( Array.from(this.activeTrades.values()).map(trade => trade.symbol) )] if (symbols.length === 0) { return } console.log('🚀 Starting price monitoring for:', symbols) const priceMonitor = getPythPriceMonitor() await priceMonitor.start({ symbols, onPriceUpdate: async (update: PriceUpdate) => { await this.handlePriceUpdate(update) }, onError: (error: Error) => { console.error('❌ Price monitor error:', error) }, }) this.isMonitoring = true console.log('✅ Position monitoring active') } /** * Stop price monitoring */ private async stopMonitoring(): Promise { if (!this.isMonitoring) { return } console.log('🛑 Stopping position monitoring...') const priceMonitor = getPythPriceMonitor() await priceMonitor.stop() this.isMonitoring = false console.log('✅ Position monitoring stopped') } /** * Handle price update for all relevant trades */ private async handlePriceUpdate(update: PriceUpdate): Promise { // Find all trades for this symbol const tradesForSymbol = Array.from(this.activeTrades.values()) .filter(trade => trade.symbol === update.symbol) for (const trade of tradesForSymbol) { try { await this.checkTradeConditions(trade, update.price) } catch (error) { console.error(`❌ Error checking trade ${trade.id}:`, error) } } } /** * Check if any exit conditions are met for a trade */ private async checkTradeConditions( trade: ActiveTrade, currentPrice: number ): Promise { // Update trade data trade.lastPrice = currentPrice trade.lastUpdateTime = Date.now() trade.priceCheckCount++ // Calculate P&L const profitPercent = this.calculateProfitPercent( trade.entryPrice, currentPrice, trade.direction ) const accountPnL = profitPercent * trade.leverage trade.unrealizedPnL = (trade.currentSize * profitPercent) / 100 // Track peak P&L if (trade.unrealizedPnL > trade.peakPnL) { trade.peakPnL = trade.unrealizedPnL } // Log status every 10 checks (~20 seconds) if (trade.priceCheckCount % 10 === 0) { console.log( `📊 ${trade.symbol} | ` + `Price: ${currentPrice.toFixed(4)} | ` + `P&L: ${profitPercent.toFixed(2)}% (${accountPnL.toFixed(1)}% acct) | ` + `Unrealized: $${trade.unrealizedPnL.toFixed(2)} | ` + `Peak: $${trade.peakPnL.toFixed(2)}` ) } // Check exit conditions (in order of priority) // 1. Emergency stop (-2%) if (this.shouldEmergencyStop(currentPrice, trade)) { console.log(`🚨 EMERGENCY STOP: ${trade.symbol}`) await this.executeExit(trade, 100, 'emergency', currentPrice) return } // 2. Stop loss if (!trade.tp1Hit && this.shouldStopLoss(currentPrice, trade)) { console.log(`🔴 STOP LOSS: ${trade.symbol} at ${profitPercent.toFixed(2)}%`) await this.executeExit(trade, 100, 'SL', currentPrice) return } // 3. Take profit 1 (50%) if (!trade.tp1Hit && this.shouldTakeProfit1(currentPrice, trade)) { console.log(`🎉 TP1 HIT: ${trade.symbol} at ${profitPercent.toFixed(2)}%`) await this.executeExit(trade, 50, 'TP1', currentPrice) // Move SL to breakeven trade.tp1Hit = true trade.currentSize = trade.positionSize * 0.5 trade.stopLossPrice = this.calculatePrice( trade.entryPrice, 0.15, // +0.15% to cover fees trade.direction ) trade.slMovedToBreakeven = true console.log(`🔒 SL moved to breakeven: ${trade.stopLossPrice.toFixed(4)}`) return } // 4. Profit lock trigger if ( trade.tp1Hit && !trade.slMovedToProfit && profitPercent >= this.config.profitLockTriggerPercent ) { console.log(`🔐 Profit lock trigger: ${trade.symbol}`) trade.stopLossPrice = this.calculatePrice( trade.entryPrice, this.config.profitLockPercent, trade.direction ) trade.slMovedToProfit = true console.log(`🎯 SL moved to +${this.config.profitLockPercent}%: ${trade.stopLossPrice.toFixed(4)}`) } // 5. Take profit 2 (remaining 50%) if (trade.tp1Hit && this.shouldTakeProfit2(currentPrice, trade)) { console.log(`🎊 TP2 HIT: ${trade.symbol} at ${profitPercent.toFixed(2)}%`) await this.executeExit(trade, 100, 'TP2', currentPrice) return } } /** * Execute exit (close position) */ private async executeExit( trade: ActiveTrade, percentToClose: number, reason: ExitResult['reason'], currentPrice: number ): Promise { try { console.log(`🔴 Executing ${reason} for ${trade.symbol} (${percentToClose}%)`) const result = await closePosition({ symbol: trade.symbol, percentToClose, slippageTolerance: this.config.slippageTolerance, }) if (!result.success) { console.error(`❌ Failed to close ${trade.symbol}:`, result.error) return } // Update trade state if (percentToClose >= 100) { // Full close - remove from monitoring trade.realizedPnL += result.realizedPnL || 0 this.removeTrade(trade.id) console.log(`✅ Position closed | P&L: $${trade.realizedPnL.toFixed(2)} | Reason: ${reason}`) } else { // Partial close (TP1) trade.realizedPnL += result.realizedPnL || 0 trade.currentSize -= result.closedSize || 0 console.log(`✅ 50% closed | Realized: $${result.realizedPnL?.toFixed(2)} | Remaining: ${trade.currentSize}`) } // TODO: Save to database // TODO: Send notification } catch (error) { console.error(`❌ Error executing exit for ${trade.symbol}:`, error) } } /** * Decision helpers */ private shouldEmergencyStop(price: number, trade: ActiveTrade): boolean { if (trade.direction === 'long') { return price <= trade.emergencyStopPrice } else { return price >= trade.emergencyStopPrice } } private shouldStopLoss(price: number, trade: ActiveTrade): boolean { if (trade.direction === 'long') { return price <= trade.stopLossPrice } else { return price >= trade.stopLossPrice } } private shouldTakeProfit1(price: number, trade: ActiveTrade): boolean { if (trade.direction === 'long') { return price >= trade.tp1Price } else { return price <= trade.tp1Price } } private shouldTakeProfit2(price: number, trade: ActiveTrade): boolean { if (trade.direction === 'long') { return price >= trade.tp2Price } else { return price <= trade.tp2Price } } /** * Calculate profit percentage */ private calculateProfitPercent( entryPrice: number, currentPrice: number, direction: 'long' | 'short' ): number { if (direction === 'long') { return ((currentPrice - entryPrice) / entryPrice) * 100 } else { return ((entryPrice - currentPrice) / entryPrice) * 100 } } /** * Calculate price based on percentage */ private calculatePrice( entryPrice: number, percent: number, direction: 'long' | 'short' ): number { if (direction === 'long') { return entryPrice * (1 + percent / 100) } else { return entryPrice * (1 - percent / 100) } } /** * Emergency close all positions */ async closeAll(): Promise { console.log('🚨 EMERGENCY: Closing all positions') const trades = Array.from(this.activeTrades.values()) for (const trade of trades) { await this.executeExit(trade, 100, 'emergency', trade.lastPrice) } console.log('✅ All positions closed') } /** * Get monitoring status */ getStatus(): { isMonitoring: boolean activeTradesCount: number symbols: string[] } { const symbols = [...new Set( Array.from(this.activeTrades.values()).map(t => t.symbol) )] return { isMonitoring: this.isMonitoring, activeTradesCount: this.activeTrades.size, symbols, } } } // Singleton instance let positionManagerInstance: PositionManager | null = null export function getPositionManager(): PositionManager { if (!positionManagerInstance) { positionManagerInstance = new PositionManager() } return positionManagerInstance }