import { config } from '../config/index.js'; import { IExchangeConnector } from '../connectors/types.js'; import { RiskEngine, RiskProfile } from './riskEngine.js'; import { MarketContext, RuleResult, SignalDirection } from '../strategies/rules/types.js'; import logger from '../utils/logger.js'; import { Notifier } from './notifier.js'; import { ApiServer } from './apiServer.js'; import { SymbolMapper } from '../utils/symbolMapper.js'; import * as runtimeOrderRepository from './runtimeOrderRepository.js'; let deprecationWarned = false; /** * @deprecated Legacy execution manager retained only for backward compatibility. * Use TradeExecutor + AutoTrader for all new execution flows. */ export class ExecutionManager { private riskEngine: RiskEngine; private notifier: Notifier; private activeTraders: Map = new Map(); // Symbol -> Position Details private cooldowns: Map = new Map(); // Symbol -> Timestamp of last exit constructor( private exchange: IExchangeConnector, private apiServer?: ApiServer, private userId: string = 'global' // Pass 'global' or user UUID ) { const allowLegacy = String(process.env.ALLOW_LEGACY_EXECUTION_MANAGER || '').trim().toLowerCase() === 'true'; if (!allowLegacy) { throw new Error( '[ExecutionManager] CRITICAL: This class is DEPRECATED and disabled by default. ' + 'Set ALLOW_LEGACY_EXECUTION_MANAGER=true only for controlled test tooling. ' + 'Use TradeExecutor and AutoTrader for runtime flows.' ); } this.riskEngine = new RiskEngine(); this.notifier = new Notifier(); if (!deprecationWarned) { logger.warn('[ExecutionManager] Legacy execution manager enabled for compatibility/testing only.'); deprecationWarned = true; } } /** * Attempts to execute a trade based on a strategy result. */ /** * Attempts to execute a trade based on a strategy result. */ public async handleSignal(symbol: string, result: RuleResult, context: MarketContext) { if (!config.ENABLE_TRADING) { logger.info(`[Execution] 💡 Trading is DISABLED. Alert only for ${symbol}.`); return; } const activePos = this.activeTraders.get(symbol); // --- EXIT LOGIC (If in trade) --- if (activePos) { // 1. Exit on Signal Flip (Trend Reversal) const isOpposite = (activePos.side === SignalDirection.BUY && result.signal === SignalDirection.SELL) || (activePos.side === SignalDirection.SELL && result.signal === SignalDirection.BUY); if (isOpposite) { await this.executeExit(symbol, context.currentPrice, 'Strategy Signal Flip'); return; } // 2. Exit on Trend Dissipation (Neutral after Profit Target) const profitPercent = ((context.currentPrice - activePos.entryPrice) / activePos.entryPrice) * 100 * (activePos.side === SignalDirection.BUY ? 1 : -1); if (profitPercent >= 1.0 && result.signal === SignalDirection.NONE) { await this.executeExit(symbol, context.currentPrice, 'Trend Neutralized (Profit Target Met)'); return; } // Update price peaks for trailing logic if (activePos.side === SignalDirection.BUY) { activePos.peakPrice = Math.max(activePos.peakPrice || 0, context.currentPrice); } else { activePos.peakPrice = Math.min(activePos.peakPrice || 999999, context.currentPrice); } return; // Still in valid trade } if (result.signal === SignalDirection.NONE || !result.passed) { return; } // --- ENTRY LOGIC --- // 1. Check Global Max Open Trades Limit if (this.activeTraders.size >= config.MAX_OPEN_TRADES) { logger.info(`[Execution] 🛑 Max open trades reached (${config.MAX_OPEN_TRADES}). Skipping entry for ${symbol}.`); return; } // 2. Check Cooldown (Default 1 hour after exit) const lastExit = this.cooldowns.get(symbol) || 0; const cooldownPeriod = 3600000; // 1 Hour if (Date.now() - lastExit < cooldownPeriod) { logger.info(`[Execution] 💤 ${symbol} is in cooldown. Skipping entry.`); return; } // 3. Check for existing position on exchange (Safety Lock) try { const tradeSymbol = SymbolMapper.toTradeSymbol(symbol, config.EXECUTION_PROVIDER); const position = await this.exchange.getPosition(tradeSymbol); if (position) { logger.warn(`[Execution] ⚠️ Found existing position on exchange for ${symbol} (Mapped: ${tradeSymbol}). Locking local state.`); this.activeTraders.set(symbol, { side: position.side?.toUpperCase(), entryPrice: parseFloat(position.avg_entry_price), size: parseFloat(position.qty), stopLoss: 0, takeProfit: 0, peakPrice: parseFloat(position.avg_entry_price) }); return; } } catch (e) { logger.error(`[Execution] Error checking position for ${symbol}: ${e}`); } // 4. Calculate Risk-Adjusted Position const riskProfile = await this.riskEngine.calculateRiskProfile(symbol, result.signal as SignalDirection, context); if (!riskProfile) { logger.warn(`[Execution] ❌ Failed to calculate risk profile for ${symbol}.`); return; } // 5. Place Order try { const tradeSymbol = SymbolMapper.toTradeSymbol(symbol, config.EXECUTION_PROVIDER); logger.info(`[Execution] 🚀 Executing ${riskProfile.action} for ${symbol} (Mapped: ${tradeSymbol})...`); logger.info(`[Execution] Details: Qty=${riskProfile.positionSize.toFixed(4)}, SL=${riskProfile.stopLoss.toFixed(2)}, TP Trigger=${riskProfile.takeProfit.toFixed(2)}`); const order = await this.exchange.placeOrder( tradeSymbol, riskProfile.action === SignalDirection.BUY ? 'buy' : 'sell', riskProfile.positionSize, 'market', undefined, undefined, undefined ); if (order) { const posData = { side: riskProfile.action, entryPrice: context.currentPrice, size: riskProfile.positionSize, stopLoss: riskProfile.stopLoss, takeProfit: riskProfile.takeProfit, // Trigger for profit guard peakPrice: context.currentPrice }; this.activeTraders.set(symbol, posData); // Log order to database if (this.userId !== 'global') { runtimeOrderRepository.logOrder({ user_id: this.userId, order_id: order.id || undefined, symbol, type: 'Market', side: riskProfile.action, qty: riskProfile.positionSize, price: context.currentPrice, status: 'Filled', timestamp: Date.now() }); } // Update Dashboard Orders if (this.apiServer) { this.apiServer.updateOrders([{ id: Math.random().toString(36).substring(7), symbol: symbol, type: 'Market', side: riskProfile.action, qty: riskProfile.positionSize, price: context.currentPrice, status: 'Filled', timestamp: Date.now() }]); } await this.notifier.sendAlert(`🚀 **TRADE EXECUTED**\nSymbol: ${symbol}\nAction: ${riskProfile.action}\nQty: ${riskProfile.positionSize.toFixed(4)}\nSL: ${riskProfile.stopLoss.toFixed(2)}\nMonitoring for 1%+ Runs...`); } } catch (error) { logger.error(`[Execution] ❌ Failed to execute trade for ${symbol}: ${error}`); await this.notifier.sendAlert(`❌ **EXECUTION FAILED**\nSymbol: ${symbol}\nError: ${error}`); } } /** * Forcefully exits a trade (Market Order). */ public async executeExit(symbol: string, currentPrice: number, reason: string = 'Target Reached') { const pos = this.activeTraders.get(symbol); if (!pos) return; try { const tradeSymbol = SymbolMapper.toTradeSymbol(symbol, config.EXECUTION_PROVIDER); logger.info(`[Execution] 🚪 EXITING ${symbol} (Mapped: ${tradeSymbol}) | Reason: ${reason} | Price: ${currentPrice}`); const order = await this.exchange.placeOrder( tradeSymbol, pos.side === SignalDirection.BUY ? 'sell' : 'buy', pos.size, 'market', undefined, undefined, undefined ); if (order) { await this.notifier.sendAlert(`🚪 **TRADE CLOSED**\nSymbol: ${symbol}\nReason: ${reason}\nPrice: ${currentPrice}`); this.markTradeComplete(symbol, currentPrice, reason); } } catch (error) { logger.error(`[Execution] ❌ Failed to execute exit for ${symbol}: ${error}`); await this.notifier.sendAlert(`❌ **EXIT FAILED**\nSymbol: ${symbol}\nError: ${error}`); } } /** * Call this when a trade exit is detected (via TradeMonitor or Manual) */ public markTradeComplete(symbol: string, exitPrice?: number, reason: string = 'Target Reached') { const pos = this.activeTraders.get(symbol); if (pos && exitPrice && this.apiServer) { const pnl = (exitPrice - pos.entryPrice) * pos.size * (pos.side === SignalDirection.BUY ? 1 : -1); const pnlPercent = ((exitPrice - pos.entryPrice) / pos.entryPrice) * 100 * (pos.side === SignalDirection.BUY ? 1 : -1); this.apiServer.addHistory({ symbol, side: pos.side, entryPrice: pos.entryPrice, exitPrice, size: pos.size, pnl, pnlPercent, reason: reason, timestamp: Date.now() }); } if (pos && exitPrice) { const pnl = (exitPrice - pos.entryPrice) * pos.size * (pos.side === SignalDirection.BUY ? 1 : -1); const pnlPercent = ((exitPrice - pos.entryPrice) / pos.entryPrice) * 100 * (pos.side === SignalDirection.BUY ? 1 : -1); // Log to Supabase if (this.userId !== 'global') { runtimeOrderRepository.logTransaction({ user_id: this.userId, symbol, side: pos.side, entry_price: pos.entryPrice, exit_price: exitPrice, size: pos.size, pnl, pnl_percent: pnlPercent, reason, timestamp: Date.now() }); } } this.activeTraders.delete(symbol); this.cooldowns.set(symbol, Date.now()); logger.info(`[Execution] ✅ Trade complete for ${symbol}. Cooldown started.`); } /** * Synchronizes local state with exchange positions (Startup Recovery) */ public async syncPositions(symbols: string[]) { logger.info(`[Execution] 🔄 Synchronizing positions on startup for ${symbols.length} symbols...`); for (const symbol of symbols) { try { const tradeSymbol = SymbolMapper.toTradeSymbol(symbol, config.EXECUTION_PROVIDER); logger.info(`[Execution] Checking exchange for ${symbol} (Mapped: ${tradeSymbol})...`); const position = await this.exchange.getPosition(tradeSymbol); if (position) { const side = position.side?.toLowerCase(); const finalSide = (side === 'long' || side === 'buy') ? SignalDirection.BUY : SignalDirection.SELL; logger.info(`[Execution] ✅ Found existing ${side} position for ${symbol}. Recovering as ${finalSide} | Price: ${position.avg_entry_price} | Qty: ${position.qty}`); this.activeTraders.set(symbol, { side: finalSide, entryPrice: parseFloat(position.avg_entry_price), size: parseFloat(position.qty), stopLoss: 0, // Recalculated locally during loop takeProfit: 0, peakPrice: parseFloat(position.avg_entry_price) }); } else { logger.info(`[Execution] No active position found for ${symbol} on exchange.`); } } catch (e) { logger.error(`[Execution] Failed to sync ${symbol}: ${e}`); } } } public isSymbolLocked(symbol: string): boolean { return this.activeTraders.has(symbol); } public getActiveSymbols(): string[] { return Array.from(this.activeTraders.keys()); } /** * Executes a manual trade triggered via API/Dashboard. */ public async executeManualTrade(symbol: string, side: 'buy' | 'sell', qty: number, type: 'market' | 'limit' = 'market', price?: number) { logger.info(`[Execution] 🖐️ Manual Trade Request: ${side.toUpperCase()} ${symbol} | Qty: ${qty} | Type: ${type}`); try { const tradeSymbol = SymbolMapper.toTradeSymbol(symbol, config.EXECUTION_PROVIDER); // Check for existing position if opening a new one // (Optional: You might want to allow adding to a position manually, so we won't strictly block it, but we update tracking) const order = await this.exchange.placeOrder( tradeSymbol, side, qty, type, price, undefined, undefined, undefined ); if (order) { const signalSide = side === 'buy' ? SignalDirection.BUY : SignalDirection.SELL; // If this is an ENTRY or Adding to position const currentPrice = price || order.filled_avg_price || 0; // Fallback if market order filling isn't instant // Update local tracking this.activeTraders.set(symbol, { side: signalSide, entryPrice: currentPrice, // Note: This might overwrite existing average entry price tracking, effectively averaging it loosely size: qty, // Simplified: In a real system we'd sum it up stopLoss: 0, takeProfit: 0, peakPrice: currentPrice }); // Log to Supabase if (this.userId !== 'global') { runtimeOrderRepository.logOrder({ user_id: this.userId, order_id: order.id, symbol, type: type === 'market' ? 'Market' : 'Limit', side: signalSide, qty: qty, price: currentPrice, status: 'Filled', // Simplified assumption for market orders timestamp: Date.now() }); } // Update Dashboard if (this.apiServer) { this.apiServer.updateOrders([{ id: order.id || Math.random().toString(36).substring(7), symbol: symbol, type: type === 'market' ? 'Market' : 'Limit', side: signalSide, qty: qty, price: currentPrice, status: order.repliesstatus || 'Filled', timestamp: Date.now() }]); } await this.notifier.sendAlert(`🖐️ **MANUAL TRADE EXECUTED**\nSymbol: ${symbol}\nSide: ${side.toUpperCase()}\nQty: ${qty}`); return { success: true, orderId: order.id }; } } catch (error: any) { logger.error(`[Execution] ❌ Manual Execution Failed: ${error.message}`); return { success: false, error: error.message }; } } public getActivePosition(symbol: string): any { return this.activeTraders.get(symbol) || null; } }