mql5/Experts/Advisors/Modules_optimised/Trade_Executor (2).mqh

727 lines
24 KiB
MQL5
Raw Permalink Normal View History

2025-08-15 23:05:57 +01:00
//+------------------------------------------------------------------+
//| 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/Trade.mqh>
//+------------------------------------------------------------------+
//| 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