Spaces:
No application file
No application file
| //+------------------------------------------------------------------+ | |
| //| PythonBridgeEA.mq5 | | |
| //| MQL5 Client for AFML Bridge | | |
| //+------------------------------------------------------------------+ | |
| #property copyright "AFML Integration" | |
| #property version "1.00" | |
| #property strict | |
| #include <Trade\Trade.mqh> | |
| //--- Input parameters | |
| input string PythonHost = "127.0.0.1"; // Python server host (NO http://) | |
| input int PythonPort = 5056; // Python server port | |
| input double RiskPercent = 1.0; // Risk per trade (%) | |
| input int MagicNumber = 12345; // EA magic number | |
| input bool EnableTrading = true; // Enable actual trading | |
| input int HeartbeatInterval = 5000; // Heartbeat interval (ms) | |
| input int DataSendInterval = 1000; // Market data send interval (ms) | |
| //--- Global variables | |
| int socket_handle = INVALID_HANDLE; | |
| CTrade trade; | |
| datetime last_heartbeat = 0; | |
| datetime last_data_send = 0; | |
| bool is_connected = false; | |
| //--- Message buffer | |
| uchar message_buffer[]; | |
| int buffer_size = 0; | |
| //+------------------------------------------------------------------+ | |
| //| Expert initialization function | | |
| //+------------------------------------------------------------------+ | |
| int OnInit() | |
| { | |
| // Setup trade object | |
| trade.SetExpertMagicNumber(MagicNumber); | |
| trade.SetDeviationInPoints(10); | |
| trade.SetTypeFilling(ORDER_FILLING_FOK); | |
| // Initialize buffer | |
| ArrayResize(message_buffer, 0); | |
| // Connect to Python bridge | |
| if(!ConnectToPython()) | |
| { | |
| Print("Failed to connect to Python bridge - will retry"); | |
| } | |
| Print("Python Bridge EA initialized successfully"); | |
| return(INIT_SUCCEEDED); | |
| } | |
| //+------------------------------------------------------------------+ | |
| //| Expert deinitialization function | | |
| //+------------------------------------------------------------------+ | |
| void OnDeinit(const int reason) | |
| { | |
| DisconnectFromPython(); | |
| Print("Python Bridge EA deinitialized"); | |
| } | |
| //+------------------------------------------------------------------+ | |
| //| Expert tick function | | |
| //+------------------------------------------------------------------+ | |
| void OnTick() | |
| { | |
| static datetime last_reconnect = 0; | |
| // Check connection | |
| if(!is_connected) | |
| { | |
| // Try reconnect every 5 seconds | |
| if(TimeCurrent() - last_reconnect >= 5) | |
| { | |
| Print("Attempting to reconnect to Python..."); | |
| ConnectToPython(); | |
| last_reconnect = TimeCurrent(); | |
| } | |
| return; | |
| } | |
| // Send heartbeat | |
| if(TimeCurrent() - last_heartbeat >= HeartbeatInterval / 1000) | |
| { | |
| SendHeartbeat(); | |
| last_heartbeat = TimeCurrent(); | |
| } | |
| // Send market data | |
| if(TimeCurrent() - last_data_send >= DataSendInterval / 1000) | |
| { | |
| SendMarketData(); | |
| last_data_send = TimeCurrent(); | |
| } | |
| // Check for incoming signals | |
| ProcessIncomingMessages(); | |
| } | |
| //+------------------------------------------------------------------+ | |
| //| Connect to Python bridge server | | |
| //+------------------------------------------------------------------+ | |
| bool ConnectToPython() | |
| { | |
| if(socket_handle != INVALID_HANDLE) | |
| { | |
| SocketClose(socket_handle); | |
| } | |
| // Create socket | |
| socket_handle = SocketCreate(); | |
| if(socket_handle == INVALID_HANDLE) | |
| { | |
| return false; | |
| } | |
| // Connect to server | |
| ResetLastError(); | |
| if(!SocketConnect(socket_handle, PythonHost, PythonPort, 5000)) | |
| { | |
| int error = GetLastError(); | |
| Print("Failed to connect to Python server at ", PythonHost, ":", PythonPort, | |
| " error=", error); | |
| if(error == 4014) | |
| { | |
| Print("SocketConnect is not allowed. Open Tools > Options > Expert Advisors, ", | |
| "enable Algo Trading, and add 127.0.0.1 to the allowed WebRequest/Socket URLs. ", | |
| "Also make sure this file is attached as an Expert Advisor, not an Indicator or Tester debug run."); | |
| } | |
| SocketClose(socket_handle); | |
| socket_handle = INVALID_HANDLE; | |
| return false; | |
| } | |
| is_connected = true; | |
| Print("Connected to Python bridge at ", PythonHost, ":", PythonPort); | |
| Print("Socket handle: ", socket_handle); | |
| Print("Testing initial communication..."); | |
| // Test the connection immediately | |
| SendHeartbeat(); | |
| // Request any pending signals | |
| RequestPendingSignals(); | |
| return true; | |
| } | |
| //+------------------------------------------------------------------+ | |
| //| Disconnect from Python bridge | | |
| //+------------------------------------------------------------------+ | |
| void DisconnectFromPython() | |
| { | |
| if(socket_handle != INVALID_HANDLE) | |
| { | |
| SocketClose(socket_handle); | |
| socket_handle = INVALID_HANDLE; | |
| } | |
| is_connected = false; | |
| } | |
| //+------------------------------------------------------------------+ | |
| //| Escape string for JSON | | |
| //+------------------------------------------------------------------+ | |
| string JsonEscape(string value) | |
| { | |
| StringReplace(value, "\\", "\\\\"); | |
| StringReplace(value, "\"", "\\\""); | |
| StringReplace(value, "\r", "\\r"); | |
| StringReplace(value, "\n", "\\n"); | |
| StringReplace(value, "\t", "\\t"); | |
| return value; | |
| } | |
| //+------------------------------------------------------------------+ | |
| //| JSON pair helpers | | |
| //+------------------------------------------------------------------+ | |
| string JsonString(string key, string value) | |
| { | |
| return "\"" + key + "\":\"" + JsonEscape(value) + "\""; | |
| } | |
| string JsonNumber(string key, double value) | |
| { | |
| return "\"" + key + "\":" + DoubleToString(value, 8); | |
| } | |
| //+------------------------------------------------------------------+ | |
| //| Extract a simple string value from JSON | | |
| //+------------------------------------------------------------------+ | |
| string JsonGetString(string json, string key, string default_value = "") | |
| { | |
| string marker = "\"" + key + "\""; | |
| int pos = StringFind(json, marker); | |
| if(pos < 0) | |
| return default_value; | |
| pos = StringFind(json, ":", pos + StringLen(marker)); | |
| if(pos < 0) | |
| return default_value; | |
| pos++; | |
| while(pos < StringLen(json) && StringGetCharacter(json, pos) <= 32) | |
| pos++; | |
| if(pos >= StringLen(json) || StringGetCharacter(json, pos) != 34) | |
| return default_value; | |
| pos++; | |
| string out = ""; | |
| while(pos < StringLen(json)) | |
| { | |
| ushort ch = StringGetCharacter(json, pos); | |
| if(ch == 92) // backslash | |
| { | |
| pos++; | |
| if(pos < StringLen(json)) | |
| out += ShortToString(StringGetCharacter(json, pos)); | |
| } | |
| else if(ch == 34) // double quote | |
| return out; | |
| else | |
| out += ShortToString(ch); | |
| pos++; | |
| } | |
| return default_value; | |
| } | |
| //+------------------------------------------------------------------+ | |
| //| Extract a simple numeric value from JSON | | |
| //+------------------------------------------------------------------+ | |
| double JsonGetDouble(string json, string key, double default_value = 0.0) | |
| { | |
| string marker = "\"" + key + "\""; | |
| int pos = StringFind(json, marker); | |
| if(pos < 0) | |
| return default_value; | |
| pos = StringFind(json, ":", pos + StringLen(marker)); | |
| if(pos < 0) | |
| return default_value; | |
| pos++; | |
| while(pos < StringLen(json) && StringGetCharacter(json, pos) <= 32) | |
| pos++; | |
| int start = pos; | |
| while(pos < StringLen(json)) | |
| { | |
| ushort ch = StringGetCharacter(json, pos); | |
| if((ch >= 48 && ch <= 57) || ch == 45 || ch == 43 || ch == 46 || ch == 101 || ch == 69) | |
| pos++; | |
| else | |
| break; | |
| } | |
| if(pos <= start) | |
| return default_value; | |
| return StringToDouble(StringSubstr(json, start, pos - start)); | |
| } | |
| //+------------------------------------------------------------------+ | |
| //| Send JSON message to Python with length prefix | | |
| //+------------------------------------------------------------------+ | |
| bool SendMessage(string json_message) | |
| { | |
| if(socket_handle == INVALID_HANDLE || !is_connected) | |
| { | |
| return false; | |
| } | |
| // Convert string to UTF-8 byte array | |
| uchar data[]; | |
| int str_len = StringToCharArray(json_message, data, 0, WHOLE_ARRAY, CP_UTF8); | |
| // Remove null terminator if present | |
| if(str_len > 0 && data[str_len - 1] == 0) | |
| str_len--; | |
| ArrayResize(data, str_len); | |
| // Create length prefix (4 bytes, little-endian) | |
| uint length = str_len; | |
| uchar length_bytes[4]; | |
| length_bytes[0] = (uchar)(length & 0xFF); | |
| length_bytes[1] = (uchar)((length >> 8) & 0xFF); | |
| length_bytes[2] = (uchar)((length >> 16) & 0xFF); | |
| length_bytes[3] = (uchar)((length >> 24) & 0xFF); | |
| // Send length prefix | |
| if(SocketSend(socket_handle, length_bytes, 4) != 4) | |
| { | |
| is_connected = false; | |
| return false; | |
| } | |
| // Send data | |
| if(SocketSend(socket_handle, data, str_len) != str_len) | |
| { | |
| is_connected = false; | |
| return false; | |
| } | |
| return true; | |
| } | |
| //+------------------------------------------------------------------+ | |
| //| Send heartbeat to Python | | |
| //+------------------------------------------------------------------+ | |
| void SendHeartbeat() | |
| { | |
| string json = "{"; | |
| json += JsonString("type", "heartbeat") + ","; | |
| json += JsonString("timestamp", TimeToString(TimeCurrent(), TIME_DATE | TIME_SECONDS)); | |
| json += "}"; | |
| SendMessage(json); | |
| } | |
| //+------------------------------------------------------------------+ | |
| //| Send market data to Python | | |
| //+------------------------------------------------------------------+ | |
| void SendMarketData() | |
| { | |
| MqlTick tick; | |
| if(!SymbolInfoTick(_Symbol, tick)) | |
| { | |
| return; | |
| } | |
| string json = "{"; | |
| json += JsonString("type", "market_data") + ","; | |
| json += JsonString("timestamp", TimeToString(TimeCurrent(), TIME_DATE | TIME_SECONDS)) + ","; | |
| json += JsonString("symbol", _Symbol) + ","; | |
| json += JsonNumber("bid", tick.bid) + ","; | |
| json += JsonNumber("ask", tick.ask) + ","; | |
| json += JsonNumber("volume", (double)tick.volume) + ","; | |
| json += JsonNumber("spread", (tick.ask - tick.bid) / _Point); | |
| // Add recent bars as array | |
| MqlRates rates[]; | |
| int copied = CopyRates(_Symbol, PERIOD_CURRENT, 0, 20, rates); | |
| if(copied > 0) | |
| { | |
| json += ",\"bars\":["; | |
| for(int i = 0; i < copied; i++) | |
| { | |
| if(i > 0) | |
| json += ","; | |
| json += "{"; | |
| json += JsonString("time", TimeToString(rates[i].time, TIME_DATE | TIME_SECONDS)) + ","; | |
| json += JsonNumber("open", rates[i].open) + ","; | |
| json += JsonNumber("high", rates[i].high) + ","; | |
| json += JsonNumber("low", rates[i].low) + ","; | |
| json += JsonNumber("close", rates[i].close) + ","; | |
| json += JsonNumber("volume", (double)rates[i].tick_volume); | |
| json += "}"; | |
| } | |
| json += "]"; | |
| } | |
| json += "}"; | |
| SendMessage(json); | |
| } | |
| //+------------------------------------------------------------------+ | |
| //| Request pending signals from Python | | |
| //+------------------------------------------------------------------+ | |
| void RequestPendingSignals() | |
| { | |
| string json = "{"; | |
| json += JsonString("type", "request_signals") + ","; | |
| json += JsonString("timestamp", TimeToString(TimeCurrent(), TIME_DATE | TIME_SECONDS)); | |
| json += "}"; | |
| SendMessage(json); | |
| } | |
| //+------------------------------------------------------------------+ | |
| //| Process incoming messages from Python | | |
| //+------------------------------------------------------------------+ | |
| void ProcessIncomingMessages() | |
| { | |
| if(socket_handle == INVALID_HANDLE || !is_connected) | |
| { | |
| return; | |
| } | |
| // Read available data | |
| uchar temp_buffer[4096]; | |
| int received = SocketRead(socket_handle, temp_buffer, 4096, 0); | |
| if(received == -1) | |
| { | |
| int error = GetLastError(); | |
| if(error == 4014) | |
| { | |
| // Normal - no data available yet | |
| return; | |
| } | |
| else if(error != 0) | |
| { | |
| is_connected = false; | |
| } | |
| return; | |
| } | |
| if(received == 0) | |
| { | |
| return; | |
| } | |
| // Append to buffer | |
| int old_size = buffer_size; | |
| buffer_size += received; | |
| ArrayResize(message_buffer, buffer_size); | |
| ArrayCopy(message_buffer, temp_buffer, old_size, 0, received); | |
| // Process complete messages | |
| while(buffer_size >= 4) | |
| { | |
| // Read length prefix (little-endian) | |
| uint message_length = | |
| message_buffer[0] | | |
| (message_buffer[1] << 8) | | |
| (message_buffer[2] << 16) | | |
| (message_buffer[3] << 24); | |
| // Check if we have complete message | |
| if(buffer_size < (int)(4 + message_length)) | |
| { | |
| break; // Wait for more data | |
| } | |
| // Extract message | |
| uchar msg_data[]; | |
| ArrayResize(msg_data, (int)message_length); | |
| ArrayCopy(msg_data, message_buffer, 0, 4, (int)message_length); | |
| string message = CharArrayToString(msg_data, 0, (int)message_length, CP_UTF8); | |
| // Remove processed message from buffer | |
| int remaining = buffer_size - (4 + (int)message_length); | |
| if(remaining > 0) | |
| { | |
| uchar temp[]; | |
| ArrayResize(temp, remaining); | |
| ArrayCopy(temp, message_buffer, 0, 4 + (int)message_length, remaining); | |
| ArrayResize(message_buffer, remaining); | |
| ArrayCopy(message_buffer, temp, 0, 0, remaining); | |
| buffer_size = remaining; | |
| } | |
| else | |
| { | |
| ArrayResize(message_buffer, 0); | |
| buffer_size = 0; | |
| } | |
| // Process message | |
| ProcessMessage(message); | |
| } | |
| } | |
| //+------------------------------------------------------------------+ | |
| //| Process single JSON message from Python | | |
| //+------------------------------------------------------------------+ | |
| void ProcessMessage(string json_string) | |
| { | |
| string msg_type = JsonGetString(json_string, "type", ""); | |
| if(msg_type == "signal") | |
| { | |
| ProcessSignal(json_string); | |
| } | |
| else if(msg_type == "heartbeat_response") | |
| { | |
| // Connection alive - no action needed | |
| } | |
| else | |
| { | |
| Print("Unknown message type: ", msg_type); | |
| } | |
| } | |
| //+------------------------------------------------------------------+ | |
| //| Process trading signal from Python | | |
| //+------------------------------------------------------------------+ | |
| void ProcessSignal(string json_string) | |
| { | |
| // Check if data field exists | |
| if(StringFind(json_string, "\"data\"") < 0) | |
| { | |
| Print("Signal message missing 'data' field"); | |
| return; | |
| } | |
| // Extract signal fields with defaults | |
| string signal_type = JsonGetString(json_string, "signal_type", ""); | |
| string symbol = JsonGetString(json_string, "symbol", ""); | |
| double entry_price = JsonGetDouble(json_string, "entry_price", 0.0); | |
| double stop_loss = JsonGetDouble(json_string, "stop_loss", 0.0); | |
| double take_profit = JsonGetDouble(json_string, "take_profit", 0.0); | |
| double position_size = JsonGetDouble(json_string, "position_size", 0.0); | |
| string strategy_name = JsonGetString(json_string, "strategy_name", "unknown"); | |
| if(signal_type == "" || symbol == "") | |
| { | |
| Print("Missing required fields in signal data"); | |
| return; | |
| } | |
| Print("Received ", signal_type, " signal for ", symbol, | |
| " from strategy: ", strategy_name); | |
| if(!EnableTrading) | |
| { | |
| Print("Trading disabled - signal ignored"); | |
| SendExecutionReport(signal_type, "ignored", 0.0); | |
| return; | |
| } | |
| // Execute signal | |
| bool result = false; | |
| double execution_price = 0.0; | |
| if(signal_type == "BUY") | |
| { | |
| result = ExecuteBuy(symbol, position_size, stop_loss, take_profit); | |
| execution_price = SymbolInfoDouble(symbol, SYMBOL_ASK); | |
| } | |
| else if(signal_type == "SELL") | |
| { | |
| result = ExecuteSell(symbol, position_size, stop_loss, take_profit); | |
| execution_price = SymbolInfoDouble(symbol, SYMBOL_BID); | |
| } | |
| else if(signal_type == "CLOSE") | |
| { | |
| result = ClosePosition(symbol); | |
| execution_price = 0.0; | |
| } | |
| // Send execution report | |
| string status = result ? "executed" : "failed"; | |
| SendExecutionReport(signal_type, status, execution_price); | |
| } | |
| //+------------------------------------------------------------------+ | |
| //| Execute buy order | | |
| //+------------------------------------------------------------------+ | |
| bool ExecuteBuy(string symbol, double size, double sl, double tp) | |
| { | |
| double volume = CalculateVolume(size); | |
| double price = SymbolInfoDouble(symbol, SYMBOL_ASK); | |
| // Normalize SL/TP | |
| if(sl > 0) | |
| sl = NormalizeDouble(sl, _Digits); | |
| if(tp > 0) | |
| tp = NormalizeDouble(tp, _Digits); | |
| bool result = trade.Buy(volume, symbol, price, sl, tp, "Python Signal"); | |
| if(result) | |
| { | |
| Print("BUY order executed: ", volume, " lots at ", price); | |
| } | |
| else | |
| { | |
| Print("BUY order failed: ", trade.ResultRetcodeDescription()); | |
| } | |
| return result; | |
| } | |
| //+------------------------------------------------------------------+ | |
| //| Execute sell order | | |
| //+------------------------------------------------------------------+ | |
| bool ExecuteSell(string symbol, double size, double sl, double tp) | |
| { | |
| double volume = CalculateVolume(size); | |
| double price = SymbolInfoDouble(symbol, SYMBOL_BID); | |
| // Normalize SL/TP | |
| if(sl > 0) | |
| sl = NormalizeDouble(sl, _Digits); | |
| if(tp > 0) | |
| tp = NormalizeDouble(tp, _Digits); | |
| bool result = trade.Sell(volume, symbol, price, sl, tp, "Python Signal"); | |
| if(result) | |
| { | |
| Print("SELL order executed: ", volume, " lots at ", price); | |
| } | |
| else | |
| { | |
| Print("SELL order failed: ", trade.ResultRetcodeDescription()); | |
| } | |
| return result; | |
| } | |
| //+------------------------------------------------------------------+ | |
| //| Close position for symbol | | |
| //+------------------------------------------------------------------+ | |
| bool ClosePosition(string symbol) | |
| { | |
| for(int i = PositionsTotal() - 1; i >= 0; i--) | |
| { | |
| ulong ticket = PositionGetTicket(i); | |
| if(PositionSelectByTicket(ticket)) | |
| { | |
| if(PositionGetString(POSITION_SYMBOL) == symbol && | |
| PositionGetInteger(POSITION_MAGIC) == MagicNumber) | |
| { | |
| bool result = trade.PositionClose(ticket); | |
| if(result) | |
| { | |
| Print("Position closed: ", ticket); | |
| } | |
| else | |
| { | |
| Print("Failed to close position: ", trade.ResultRetcodeDescription()); | |
| } | |
| return result; | |
| } | |
| } | |
| } | |
| Print("No position found for ", symbol); | |
| return false; | |
| } | |
| //+------------------------------------------------------------------+ | |
| //| Calculate position volume based on risk | | |
| //+------------------------------------------------------------------+ | |
| double CalculateVolume(double position_size) | |
| { | |
| if(position_size > 0) | |
| { | |
| // Use provided size | |
| return NormalizeDouble(position_size, 2); | |
| } | |
| // Calculate based on risk percentage | |
| double balance = AccountInfoDouble(ACCOUNT_BALANCE); | |
| double risk_amount = balance * RiskPercent / 100.0; | |
| // Simple volume calculation (can be enhanced) | |
| double volume = 0.01; // Minimum volume | |
| // Normalize to broker's allowed values | |
| double min_volume = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); | |
| double max_volume = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); | |
| double volume_step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); | |
| volume = MathMax(volume, min_volume); | |
| volume = MathMin(volume, max_volume); | |
| volume = MathFloor(volume / volume_step) * volume_step; | |
| return NormalizeDouble(volume, 2); | |
| } | |
| //+------------------------------------------------------------------+ | |
| //| Send execution report to Python | | |
| //+------------------------------------------------------------------+ | |
| void SendExecutionReport(string signal_type, string status, double execution_price) | |
| { | |
| string json = "{"; | |
| json += JsonString("type", "execution_report") + ","; | |
| json += JsonString("timestamp", TimeToString(TimeCurrent(), TIME_DATE | TIME_SECONDS)) + ","; | |
| json += JsonString("signal_id", IntegerToString(TimeCurrent())) + ","; | |
| json += JsonString("signal_type", signal_type) + ","; | |
| json += JsonString("status", status) + ","; | |
| json += JsonNumber("execution_price", execution_price) + ","; | |
| json += JsonString("symbol", _Symbol) + ","; | |
| json += JsonNumber("magic_number", (double)MagicNumber); | |
| json += "}"; | |
| SendMessage(json); | |
| } | |
| //+------------------------------------------------------------------+ | |