//+------------------------------------------------------------------+ //| TradeExecutor.mqh v1.0 | //| Centralized Trade Execution Engine | //| Optimized with retry logic and error handling | //+------------------------------------------------------------------+ #ifndef TRADE_EXECUTOR_MQH #define TRADE_EXECUTOR_MQH #include "DataTypes.mqh" #include "SymbolManager.mqh" #include //+------------------------------------------------------------------+ //| Trade execution result structure | //+------------------------------------------------------------------+ struct ExecutionResult { bool success; ulong ticket; double executed_price; double executed_volume; double slippage; int error_code; string error_message; int retries; datetime execution_time; int execution_ms; }; //+------------------------------------------------------------------+ //| Trade Executor Class | //+------------------------------------------------------------------+ class CTradeExecutor { private: //--- Configuration int m_magic_number; int m_max_slippage; int m_max_retries; int m_retry_delay_ms; bool m_async_mode; //--- Trade object CTrade m_trade; //--- Symbol manager reference CSymbolManager *m_symbol_manager; //--- Statistics int m_trades_executed; int m_trades_failed; double m_total_slippage; int m_total_retries; //--- Error handling int m_last_error; string m_last_error_message; //--- Helper methods bool PrepareTradeRequest(MqlTradeRequest &request, const TradeRequest &trade_req); bool ValidateTradeRequest(const MqlTradeRequest &request); ExecutionResult ExecuteWithRetry(MqlTradeRequest &request); void HandleTradeError(int error_code, MqlTradeRequest &request); double CalculateSlippage(double requested_price, double executed_price, string symbol); void LogExecution(const ExecutionResult &result, const MqlTradeRequest &request); bool WaitForExecution(ulong order_ticket, int timeout_ms = 5000); public: CTradeExecutor(); ~CTradeExecutor(); //--- Initialization bool Initialize(int magic_number, int max_slippage = 10); void SetSymbolManager(CSymbolManager *manager) { m_symbol_manager = manager; } //--- Configuration void SetMaxRetries(int retries) { m_max_retries = retries; } void SetRetryDelay(int delay_ms) { m_retry_delay_ms = delay_ms; } void SetAsyncMode(bool async) { m_async_mode = async; } //--- Trade execution ExecutionResult OpenPosition(const TradeRequest &request); ExecutionResult ClosePosition(ulong ticket, double volume = 0, string comment = ""); ExecutionResult ClosePositionBySymbol(string symbol, ENUM_POSITION_TYPE type = -1); ExecutionResult ModifyPosition(ulong ticket, double sl, double tp); //--- Batch operations int CloseAllPositions(int magic = -1); int CloseAllBySymbol(string symbol); int CloseAllProfitable(); int CloseAllLosing(); //--- Pending orders ExecutionResult PlacePendingOrder(const TradeRequest &request); ExecutionResult DeletePendingOrder(ulong ticket); ExecutionResult ModifyPendingOrder(ulong ticket, double price, double sl, double tp); //--- Utility functions bool IsTradeAllowed(); double GetMaxVolume(string symbol); double GetMinVolume(string symbol); bool CheckMarginForTrade(string symbol, ENUM_ORDER_TYPE type, double volume); //--- Statistics int GetTradesExecuted() { return m_trades_executed; } int GetTradesFailed() { return m_trades_failed; } double GetAverageSlippage() { return (m_trades_executed > 0) ? m_total_slippage / m_trades_executed : 0; } double GetSuccessRate() { return (m_trades_executed + m_trades_failed > 0) ? (double)m_trades_executed / (m_trades_executed + m_trades_failed) * 100 : 0; } //--- Error handling int GetLastError() { return m_last_error; } string GetLastErrorMessage() { return m_last_error_message; } }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CTradeExecutor::CTradeExecutor() { m_magic_number = 0; m_max_slippage = 10; m_max_retries = 3; m_retry_delay_ms = 500; m_async_mode = false; m_trades_executed = 0; m_trades_failed = 0; m_total_slippage = 0; m_total_retries = 0; m_last_error = 0; m_last_error_message = ""; m_symbol_manager = NULL; } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CTradeExecutor::~CTradeExecutor() { } //+------------------------------------------------------------------+ //| Initialize executor | //+------------------------------------------------------------------+ bool CTradeExecutor::Initialize(int magic_number, int max_slippage) { m_magic_number = magic_number; m_max_slippage = max_slippage; //--- Configure trade object m_trade.SetExpertMagicNumber(magic_number); m_trade.SetDeviationInPoints(max_slippage); m_trade.SetTypeFilling(ORDER_FILLING_RETURN); m_trade.SetAsyncMode(m_async_mode); //--- Get symbol manager instance if(m_symbol_manager == NULL) m_symbol_manager = CSymbolManager::GetInstance(); Print("TradeExecutor initialized: Magic=", magic_number, ", MaxSlippage=", max_slippage, " points"); return true; } //+------------------------------------------------------------------+ //| Open new position with full error handling | //+------------------------------------------------------------------+ ExecutionResult CTradeExecutor::OpenPosition(const TradeRequest &request) { ExecutionResult result; ZeroMemory(result); datetime start_time = GetTickCount(); //--- Check if trading allowed if(!IsTradeAllowed()) { result.success = false; result.error_code = ERR_TRADE_NOT_ALLOWED; result.error_message = "Trading not allowed"; return result; } //--- Prepare MQL trade request MqlTradeRequest mql_request; if(!PrepareTradeRequest(mql_request, request)) { result.success = false; result.error_code = m_last_error; result.error_message = m_last_error_message; return result; } //--- Validate request if(!ValidateTradeRequest(mql_request)) { result.success = false; result.error_code = m_last_error; result.error_message = m_last_error_message; return result; } //--- Execute with retry logic result = ExecuteWithRetry(mql_request); //--- Calculate execution time result.execution_ms = (int)(GetTickCount() - start_time); //--- Update statistics if(result.success) { m_trades_executed++; m_total_slippage += result.slippage; } else { m_trades_failed++; } m_total_retries += result.retries; //--- Log execution LogExecution(result, mql_request); return result; } //+------------------------------------------------------------------+ //| Prepare trade request | //+------------------------------------------------------------------+ bool CTradeExecutor::PrepareTradeRequest(MqlTradeRequest &request, const TradeRequest &trade_req) { ZeroMemory(request); //--- Get symbol info if(m_symbol_manager == NULL) { m_last_error = ERR_INVALID_POINTER; m_last_error_message = "Symbol manager not initialized"; return false; } SymbolCacheEntry symbol_info; if(!m_symbol_manager.GetSymbolInfo(trade_req.symbol, symbol_info)) { m_last_error = ERR_UNKNOWN_SYMBOL; m_last_error_message = "Invalid symbol: " + trade_req.symbol; return false; } //--- Basic parameters request.action = TRADE_ACTION_DEAL; request.symbol = trade_req.symbol; request.volume = m_symbol_manager.NormalizeLots(trade_req.symbol, trade_req.volume); request.type = trade_req.type; request.type_filling = ORDER_FILLING_RETURN; request.magic = m_magic_number; request.comment = trade_req.comment; request.deviation = m_max_slippage; //--- Get current price if(trade_req.type == ORDER_TYPE_BUY) { request.price = m_symbol_manager.GetCurrentAsk(trade_req.symbol); if(trade_req.sl_distance > 0) request.sl = m_symbol_manager.NormalizePrice(trade_req.symbol, request.price - trade_req.sl_distance); if(trade_req.tp_distance > 0) request.tp = m_symbol_manager.NormalizePrice(trade_req.symbol, request.price + trade_req.tp_distance); } else if(trade_req.type == ORDER_TYPE_SELL) { request.price = m_symbol_manager.GetCurrentBid(trade_req.symbol); if(trade_req.sl_distance > 0) request.sl = m_symbol_manager.NormalizePrice(trade_req.symbol, request.price + trade_req.sl_distance); if(trade_req.tp_distance > 0) request.tp = m_symbol_manager.NormalizePrice(trade_req.symbol, request.price - trade_req.tp_distance); } else { m_last_error = ERR_INVALID_ORDER_TYPE; m_last_error_message = "Invalid order type"; return false; } return true; } //+------------------------------------------------------------------+ //| Validate trade request | //+------------------------------------------------------------------+ bool CTradeExecutor::ValidateTradeRequest(const MqlTradeRequest &request) { //--- Check volume double min_lot = m_symbol_manager.GetMinLot(request.symbol); double max_lot = m_symbol_manager.GetMaxLot(request.symbol); if(request.volume < min_lot || request.volume > max_lot) { m_last_error = ERR_INVALID_VOLUME; m_last_error_message = StringFormat("Invalid volume: %.2f (min: %.2f, max: %.2f)", request.volume, min_lot, max_lot); return false; } //--- Check stop levels if(!m_symbol_manager.ValidateStopLevels(request.symbol, request.price, request.sl, request.tp)) { m_last_error = ERR_INVALID_STOPS; m_last_error_message = "Invalid stop levels"; return false; } //--- Check margin if(!CheckMarginForTrade(request.symbol, request.type, request.volume)) { m_last_error = ERR_NOT_ENOUGH_MONEY; m_last_error_message = "Insufficient margin"; return false; } return true; } //+------------------------------------------------------------------+ //| Execute trade with retry logic | //+------------------------------------------------------------------+ ExecutionResult CTradeExecutor::ExecuteWithRetry(MqlTradeRequest &request) { ExecutionResult result; ZeroMemory(result); MqlTradeResult trade_result; for(int retry = 0; retry <= m_max_retries; retry++) { result.retries = retry; //--- Refresh price before each attempt if(retry > 0) { if(request.type == ORDER_TYPE_BUY) request.price = m_symbol_manager.GetCurrentAsk(request.symbol); else request.price = m_symbol_manager.GetCurrentBid(request.symbol); Sleep(m_retry_delay_ms); } //--- Send order ResetLastError(); if(OrderSend(request, trade_result)) { result.success = true; result.ticket = trade_result.order; result.executed_price = trade_result.price; result.executed_volume = trade_result.volume; result.execution_time = TimeCurrent(); //--- Calculate slippage result.slippage = CalculateSlippage(request.price, result.executed_price, request.symbol); //--- Wait for position to appear (for async mode) if(m_async_mode) { if(!WaitForExecution(result.ticket)) { result.success = false; result.error_code = ERR_TRADE_TIMEOUT; result.error_message = "Trade execution timeout"; } } break; } else { int error = GetLastError(); result.error_code = error; result.error_message = trade_result.comment; //--- Handle specific errors HandleTradeError(error, request); //--- Check if should retry bool should_retry = false; switch(error) { case ERR_REQUOTE: case ERR_PRICE_CHANGED: case ERR_OFF_QUOTES: case ERR_BROKER_BUSY: case ERR_TRADE_CONTEXT_BUSY: should_retry = true; break; default: should_retry = false; break; } if(!should_retry || retry == m_max_retries) { break; } } } return result; } //+------------------------------------------------------------------+ //| Handle trade error | //+------------------------------------------------------------------+ void CTradeExecutor::HandleTradeError(int error_code, MqlTradeRequest &request) { m_last_error = error_code; switch(error_code) { case ERR_REQUOTE: m_last_error_message = "Requote - prices changed"; break; case ERR_INVALID_STOPS: m_last_error_message = StringFormat("Invalid stops - SL: %.5f, TP: %.5f", request.sl, request.tp); break; case ERR_INVALID_VOLUME: m_last_error_message = StringFormat("Invalid volume: %.2f lots", request.volume); break; case ERR_NOT_ENOUGH_MONEY: m_last_error_message = "Insufficient margin for trade"; break; case ERR_TRADE_NOT_ALLOWED: m_last_error_message = "Trading not allowed - check AutoTrading"; break; case ERR_MARKET_CLOSED: m_last_error_message = "Market is closed"; break; default: m_last_error_message = StringFormat("Trade error %d", error_code); break; } } //+------------------------------------------------------------------+ //| Calculate slippage in points | //+------------------------------------------------------------------+ double CTradeExecutor::CalculateSlippage(double requested_price, double executed_price, string symbol) { double point = m_symbol_manager.GetPoint(symbol); if(point == 0) return 0; return MathAbs(executed_price - requested_price) / point; } //+------------------------------------------------------------------+ //| Log execution result | //+------------------------------------------------------------------+ void CTradeExecutor::LogExecution(const ExecutionResult &result, const MqlTradeRequest &request) { if(result.success) { Print(StringFormat("TradeExecutor: %s #%d opened - %s %.2f @ %.5f (slippage: %.1f points, %d ms)", (request.type == ORDER_TYPE_BUY) ? "BUY" : "SELL", result.ticket, request.symbol, result.executed_volume, result.executed_price, result.slippage, result.execution_ms)); } else { Print(StringFormat("TradeExecutor: Failed to open %s %s %.2f - Error %d: %s (retries: %d)", (request.type == ORDER_TYPE_BUY) ? "BUY" : "SELL", request.symbol, request.volume, result.error_code, result.error_message, result.retries)); } } //+------------------------------------------------------------------+ //| Close position | //+------------------------------------------------------------------+ ExecutionResult CTradeExecutor::ClosePosition(ulong ticket, double volume, string comment) { ExecutionResult result; ZeroMemory(result); datetime start_time = GetTickCount(); //--- Select position if(!PositionSelectByTicket(ticket)) { result.success = false; result.error_code = ERR_TRADE_POSITION_NOT_FOUND; result.error_message = "Position not found"; return result; } //--- Get position info string symbol = PositionGetString(POSITION_SYMBOL); double position_volume = PositionGetDouble(POSITION_VOLUME); ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); //--- Determine volume to close if(volume == 0 || volume >= position_volume) volume = position_volume; else volume = m_symbol_manager.NormalizeLots(symbol, volume); //--- Prepare request MqlTradeRequest request; ZeroMemory(request); request.action = TRADE_ACTION_DEAL; request.symbol = symbol; request.volume = volume; request.type = (type == POSITION_TYPE_BUY) ? ORDER_TYPE_SELL : ORDER_TYPE_BUY; request.position = ticket; request.deviation = m_max_slippage; request.magic = m_magic_number; request.comment = comment; //--- Get current price request.price = (type == POSITION_TYPE_BUY) ? m_symbol_manager.GetCurrentBid(symbol) : m_symbol_manager.GetCurrentAsk(symbol); //--- Execute result = ExecuteWithRetry(request); //--- Update execution time result.execution_ms = (int)(GetTickCount() - start_time); //--- Update statistics if(result.success) { m_trades_executed++; m_total_slippage += result.slippage; } else { m_trades_failed++; } return result; } //+------------------------------------------------------------------+ //| Modify position stops | //+------------------------------------------------------------------+ ExecutionResult CTradeExecutor::ModifyPosition(ulong ticket, double sl, double tp) { ExecutionResult result; ZeroMemory(result); //--- Select position if(!PositionSelectByTicket(ticket)) { result.success = false; result.error_code = ERR_TRADE_POSITION_NOT_FOUND; result.error_message = "Position not found"; return result; } //--- Get position info string symbol = PositionGetString(POSITION_SYMBOL); double current_sl = PositionGetDouble(POSITION_SL); double current_tp = PositionGetDouble(POSITION_TP); //--- Check if modification needed sl = m_symbol_manager.NormalizePrice(symbol, sl); tp = m_symbol_manager.NormalizePrice(symbol, tp); if(sl == current_sl && tp == current_tp) { result.success = true; result.ticket = ticket; return result; } //--- Validate stops double current_price = m_symbol_manager.GetCurrentPrice(symbol, (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE)); if(!m_symbol_manager.ValidateStopLevels(symbol, current_price, sl, tp)) { result.success = false; result.error_code = ERR_INVALID_STOPS; result.error_message = "Invalid stop levels"; return result; } //--- Modify position if(m_trade.PositionModify(ticket, sl, tp)) { result.success = true; result.ticket = ticket; result.execution_time = TimeCurrent(); } else { result.success = false; result.error_code = GetLastError(); result.error_message = m_trade.ResultComment(); } return result; } //+------------------------------------------------------------------+ //| Check if trading is allowed | //+------------------------------------------------------------------+ bool CTradeExecutor::IsTradeAllowed() { //--- Check terminal if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) return false; //--- Check EA if(!MQLInfoInteger(MQL_TRADE_ALLOWED)) return false; //--- Check account if(!AccountInfoInteger(ACCOUNT_TRADE_ALLOWED)) return false; if(!AccountInfoInteger(ACCOUNT_TRADE_EXPERT)) return false; return true; } //+------------------------------------------------------------------+ //| Check margin for trade | //+------------------------------------------------------------------+ bool CTradeExecutor::CheckMarginForTrade(string symbol, ENUM_ORDER_TYPE type, double volume) { double price = (type == ORDER_TYPE_BUY) ? m_symbol_manager.GetCurrentAsk(symbol) : m_symbol_manager.GetCurrentBid(symbol); double margin = 0; if(!OrderCalcMargin(type, symbol, volume, price, margin)) return false; double free_margin = AccountInfoDouble(ACCOUNT_MARGIN_FREE); return (margin > 0 && free_margin > margin); } //+------------------------------------------------------------------+ //| Wait for order execution (async mode) | //+------------------------------------------------------------------+ bool CTradeExecutor::WaitForExecution(ulong order_ticket, int timeout_ms) { datetime start_time = GetTickCount(); while(GetTickCount() - start_time < timeout_ms) { //--- Check if position exists if(PositionSelectByTicket(order_ticket)) return true; //--- Check if order still pending if(OrderSelect(order_ticket)) { Sleep(10); continue; } //--- Order doesn't exist - might be rejected break; } return false; } //+------------------------------------------------------------------+ //| Close all positions | //+------------------------------------------------------------------+ int CTradeExecutor::CloseAllPositions(int magic) { int closed = 0; for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(ticket == 0) continue; if(PositionSelectByTicket(ticket)) { //--- Check magic number if(magic >= 0 && PositionGetInteger(POSITION_MAGIC) != magic) continue; ExecutionResult result = ClosePosition(ticket, 0, "Close All"); if(result.success) closed++; } } return closed; } #endif // TRADE_EXECUTOR_MQH