Changes: - Add FAL_KEY and GEMINI_API_KEY to .env.example - Update picture-it to use ~/.config/opencode/.env (unified creds) - Remove shodh-memory skill (no longer used) - Remove alphaear-* skills (deprecated) - Remove thai-frontend-dev skill (replaced by website-creator) - Remove theme-factory skill - Add mql-developer skill (MQL5 trading) - Add ecommerce-astro skill (Astro e-commerce) - Add website-creator skill (Next.js + Payload CMS) - Update install script for new skills
1058 lines
32 KiB
Markdown
1058 lines
32 KiB
Markdown
# External & Internal Communication
|
|
|
|
Reference for WebRequest, REST API integration, JSON handling, Node.js patterns, and inter-program communication in MQL4/MQL5.
|
|
|
|
## Table of Contents
|
|
|
|
- [WebRequest - REST API Communication](#webrequest---rest-api-communication)
|
|
- [JSON Handling](#json-handling)
|
|
- [Node.js Integration Patterns](#nodejs-integration-patterns)
|
|
- [Network Error Handling](#network-error-handling)
|
|
- [Internal Communication (Between MQL Programs)](#internal-communication-between-mql-programs)
|
|
- [MQL5 Sockets (Advanced)](#mql5-sockets-advanced)
|
|
- [Communication Method Comparison](#communication-method-comparison)
|
|
|
|
---
|
|
|
|
## WebRequest - REST API Communication
|
|
|
|
### Setup Requirements
|
|
|
|
- **Whitelist URLs**: Tools > Options > Expert Advisors > "Allow WebRequest for listed URL"
|
|
- Only available in **EAs and Scripts** (NOT indicators)
|
|
- **Not available** during backtesting in Strategy Tester
|
|
- **Synchronous/blocking**: freezes EA execution until response received or timeout expires
|
|
- Each call blocks the entire EA thread; keep timeouts reasonable (5000-10000 ms)
|
|
|
|
### MQL4 WebRequest Signatures
|
|
|
|
MQL4 provides two overloaded variants:
|
|
|
|
```mql4
|
|
// Variant 1: cookie/referer (simpler, limited)
|
|
int WebRequest(
|
|
const string method, // "GET", "POST", "PUT", "DELETE"
|
|
const string url, // Full URL
|
|
const string cookie, // Cookie string (can be "")
|
|
const string referer, // Referer header (can be "")
|
|
int timeout, // Timeout in milliseconds
|
|
const char &data[], // Request body (char array)
|
|
int data_size, // Size of data array
|
|
char &result[], // Response body (output)
|
|
string &result_headers // Response headers (output)
|
|
);
|
|
|
|
// Variant 2: custom headers (preferred for REST APIs)
|
|
int WebRequest(
|
|
const string method, // "GET", "POST", "PUT", "DELETE"
|
|
const string url, // Full URL
|
|
const string headers, // Custom headers, each ending with \r\n
|
|
int timeout, // Timeout in milliseconds
|
|
const char &data[], // Request body (char array)
|
|
char &result[], // Response body (output)
|
|
string &result_headers // Response headers (output)
|
|
);
|
|
```
|
|
|
|
### MQL4 GET Request Example
|
|
|
|
```mql4
|
|
string HttpGet(string url, int timeout = 5000)
|
|
{
|
|
char data[];
|
|
char result[];
|
|
string resultHeaders;
|
|
string headers = "Content-Type: application/json\r\n";
|
|
|
|
ResetLastError();
|
|
int statusCode = WebRequest("GET", url, headers, timeout, data, result, resultHeaders);
|
|
|
|
if(statusCode == -1)
|
|
{
|
|
int error = GetLastError();
|
|
if(error == 4014)
|
|
Print("ERROR: URL not allowed. Add to Tools > Options > Expert Advisors: ", url);
|
|
else if(error == 4060)
|
|
Print("ERROR: WebRequest not allowed in this context (indicator or tester)");
|
|
else
|
|
Print("ERROR: WebRequest failed. Error code: ", error);
|
|
return "";
|
|
}
|
|
|
|
if(statusCode != 200)
|
|
{
|
|
Print("HTTP Error: ", statusCode, " Response: ", CharArrayToString(result));
|
|
return "";
|
|
}
|
|
|
|
return CharArrayToString(result);
|
|
}
|
|
```
|
|
|
|
### MQL4 POST Request Example
|
|
|
|
```mql4
|
|
string HttpPost(string url, string jsonBody, int timeout = 5000)
|
|
{
|
|
char data[];
|
|
char result[];
|
|
string resultHeaders;
|
|
string headers = "Content-Type: application/json\r\n";
|
|
|
|
// CRITICAL: Use StringLen() to avoid including the null terminator
|
|
StringToCharArray(jsonBody, data, 0, StringLen(jsonBody));
|
|
|
|
ResetLastError();
|
|
int statusCode = WebRequest("POST", url, headers, timeout, data, result, resultHeaders);
|
|
|
|
if(statusCode == -1)
|
|
{
|
|
int error = GetLastError();
|
|
Print("ERROR: WebRequest POST failed. Error: ", error);
|
|
return "";
|
|
}
|
|
|
|
if(statusCode != 200 && statusCode != 201)
|
|
{
|
|
Print("HTTP Error: ", statusCode, " Response: ", CharArrayToString(result));
|
|
return "";
|
|
}
|
|
|
|
return CharArrayToString(result);
|
|
}
|
|
```
|
|
|
|
### MQL5 WebRequest
|
|
|
|
MQL5 uses the same `WebRequest()` function with identical signatures. The key difference is encoding support:
|
|
|
|
```mql5
|
|
// MQL5 POST with explicit UTF-8 encoding
|
|
string HttpPostMQL5(string url, string jsonBody, int timeout = 5000)
|
|
{
|
|
char data[];
|
|
char result[];
|
|
string resultHeaders;
|
|
string headers = "Content-Type: application/json\r\n";
|
|
|
|
// Use CP_UTF8 for proper Unicode handling
|
|
// CRITICAL: Use StringLen() to avoid null terminator in body
|
|
StringToCharArray(jsonBody, data, 0, StringLen(jsonBody), CP_UTF8);
|
|
|
|
ResetLastError();
|
|
int statusCode = WebRequest("POST", url, headers, timeout, data, result, resultHeaders);
|
|
|
|
if(statusCode == -1)
|
|
{
|
|
int error = GetLastError();
|
|
PrintFormat("WebRequest failed: error %d", error);
|
|
return "";
|
|
}
|
|
|
|
// Decode response with UTF-8
|
|
string response = CharArrayToString(result, 0, WHOLE_ARRAY, CP_UTF8);
|
|
return response;
|
|
}
|
|
```
|
|
|
|
### CHttpClient Class (MQL5)
|
|
|
|
Complete reusable HTTP client class for REST API communication:
|
|
|
|
```mql5
|
|
//+------------------------------------------------------------------+
|
|
//| CHttpClient - Reusable HTTP client for REST APIs |
|
|
//+------------------------------------------------------------------+
|
|
class CHttpClient
|
|
{
|
|
private:
|
|
string m_baseUrl;
|
|
int m_timeout;
|
|
string m_authHeader; // Optional auth header
|
|
|
|
string DoRequest(string method, string endpoint, string body = "")
|
|
{
|
|
char data[];
|
|
char result[];
|
|
string resultHeaders;
|
|
|
|
string url = m_baseUrl + endpoint;
|
|
string headers = "Content-Type: application/json\r\n";
|
|
if(m_authHeader != "")
|
|
headers += m_authHeader + "\r\n";
|
|
|
|
if(body != "")
|
|
{
|
|
// CRITICAL: Use StringLen() to avoid null terminator byte in request body
|
|
StringToCharArray(body, data, 0, StringLen(body), CP_UTF8);
|
|
}
|
|
|
|
ResetLastError();
|
|
int statusCode = WebRequest(method, url, headers, m_timeout, data, result, resultHeaders);
|
|
|
|
if(statusCode == -1)
|
|
{
|
|
int error = GetLastError();
|
|
if(error == 4014)
|
|
PrintFormat("ERROR: URL not whitelisted: %s", url);
|
|
else if(error == 4060)
|
|
PrintFormat("ERROR: WebRequest not allowed in this context");
|
|
else
|
|
PrintFormat("ERROR: WebRequest failed, error: %d", error);
|
|
return "";
|
|
}
|
|
|
|
string response = CharArrayToString(result, 0, WHOLE_ARRAY, CP_UTF8);
|
|
|
|
if(statusCode < 200 || statusCode >= 300)
|
|
{
|
|
PrintFormat("HTTP %d: %s %s -> %s", statusCode, method, endpoint, response);
|
|
return "";
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
public:
|
|
void Init(string baseUrl, int timeoutMs = 5000)
|
|
{
|
|
m_baseUrl = baseUrl;
|
|
m_timeout = timeoutMs;
|
|
m_authHeader = "";
|
|
}
|
|
|
|
void SetAuth(string token)
|
|
{
|
|
m_authHeader = "Authorization: Bearer " + token;
|
|
}
|
|
|
|
string Get(string endpoint)
|
|
{
|
|
return DoRequest("GET", endpoint);
|
|
}
|
|
|
|
string Post(string endpoint, string jsonBody)
|
|
{
|
|
return DoRequest("POST", endpoint, jsonBody);
|
|
}
|
|
|
|
string Put(string endpoint, string jsonBody)
|
|
{
|
|
return DoRequest("PUT", endpoint, jsonBody);
|
|
}
|
|
|
|
string Delete(string endpoint)
|
|
{
|
|
return DoRequest("DELETE", endpoint);
|
|
}
|
|
};
|
|
```
|
|
|
|
**Usage:**
|
|
|
|
```mql5
|
|
CHttpClient httpClient;
|
|
|
|
int OnInit()
|
|
{
|
|
httpClient.Init("https://api.example.com", 5000);
|
|
httpClient.SetAuth(InpApiToken);
|
|
return INIT_SUCCEEDED;
|
|
}
|
|
|
|
void OnTick()
|
|
{
|
|
string response = httpClient.Get("/api/signals?symbol=" + Symbol());
|
|
if(response != "")
|
|
{
|
|
// Process response
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## JSON Handling
|
|
|
|
MQL has no native JSON parser. For simple payloads, manual string building and parsing works well. For complex nested structures, consider the `CJAVal` library from MQL5 CodeBase.
|
|
|
|
### Building JSON Manually
|
|
|
|
```mql5
|
|
string BuildTradeJSON(ulong ticket, string symbol, string action,
|
|
double lots, double price, double sl, double tp)
|
|
{
|
|
string json = "{";
|
|
json += "\"ticket\":" + IntegerToString(ticket) + ",";
|
|
json += "\"symbol\":\"" + symbol + "\",";
|
|
json += "\"action\":\"" + action + "\",";
|
|
json += "\"lots\":" + DoubleToString(lots, 2) + ",";
|
|
json += "\"price\":" + DoubleToString(price, (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS)) + ",";
|
|
json += "\"sl\":" + DoubleToString(sl, (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS)) + ",";
|
|
json += "\"tp\":" + DoubleToString(tp, (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS)) + ",";
|
|
json += "\"account\":" + IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN)) + ",";
|
|
json += "\"broker\":\"" + AccountInfoString(ACCOUNT_COMPANY) + "\",";
|
|
json += "\"balance\":" + DoubleToString(AccountInfoDouble(ACCOUNT_BALANCE), 2) + ",";
|
|
json += "\"equity\":" + DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY), 2) + ",";
|
|
json += "\"timestamp\":\"" + TimeToString(TimeCurrent(), TIME_DATE | TIME_SECONDS) + "\"";
|
|
json += "}";
|
|
return json;
|
|
}
|
|
```
|
|
|
|
**MQL4 variant** (uses `AccountBalance()`, `AccountCompany()`, etc. instead of `AccountInfoXxx()`):
|
|
|
|
```mql4
|
|
string BuildTradeJSON_MQL4(int ticket, string symbol, string action,
|
|
double lots, double price, double sl, double tp)
|
|
{
|
|
string json = "{";
|
|
json += "\"ticket\":" + IntegerToString(ticket) + ",";
|
|
json += "\"symbol\":\"" + symbol + "\",";
|
|
json += "\"action\":\"" + action + "\",";
|
|
json += "\"lots\":" + DoubleToString(lots, 2) + ",";
|
|
json += "\"price\":" + DoubleToString(price, Digits) + ",";
|
|
json += "\"sl\":" + DoubleToString(sl, Digits) + ",";
|
|
json += "\"tp\":" + DoubleToString(tp, Digits) + ",";
|
|
json += "\"account\":" + IntegerToString(AccountNumber()) + ",";
|
|
json += "\"broker\":\"" + AccountCompany() + "\",";
|
|
json += "\"balance\":" + DoubleToString(AccountBalance(), 2) + ",";
|
|
json += "\"equity\":" + DoubleToString(AccountEquity(), 2) + ",";
|
|
json += "\"timestamp\":\"" + TimeToString(TimeCurrent(), TIME_DATE | TIME_SECONDS) + "\"";
|
|
json += "}";
|
|
return json;
|
|
}
|
|
```
|
|
|
|
### Parsing JSON Manually
|
|
|
|
Helper functions for extracting values from flat JSON strings:
|
|
|
|
```mql5
|
|
//+------------------------------------------------------------------+
|
|
//| Extract string value for a given key from JSON |
|
|
//+------------------------------------------------------------------+
|
|
string JsonGetString(const string json, const string key)
|
|
{
|
|
string searchKey = "\"" + key + "\"";
|
|
int keyPos = StringFind(json, searchKey);
|
|
if(keyPos == -1) return "";
|
|
|
|
// Find the colon after the key
|
|
int colonPos = StringFind(json, ":", keyPos + StringLen(searchKey));
|
|
if(colonPos == -1) return "";
|
|
|
|
// Find opening quote of value
|
|
int startQuote = StringFind(json, "\"", colonPos + 1);
|
|
if(startQuote == -1) return "";
|
|
|
|
// Find closing quote of value
|
|
int endQuote = StringFind(json, "\"", startQuote + 1);
|
|
if(endQuote == -1) return "";
|
|
|
|
return StringSubstr(json, startQuote + 1, endQuote - startQuote - 1);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Extract double value for a given key from JSON |
|
|
//+------------------------------------------------------------------+
|
|
double JsonGetDouble(const string json, const string key)
|
|
{
|
|
string searchKey = "\"" + key + "\"";
|
|
int keyPos = StringFind(json, searchKey);
|
|
if(keyPos == -1) return 0.0;
|
|
|
|
int colonPos = StringFind(json, ":", keyPos + StringLen(searchKey));
|
|
if(colonPos == -1) return 0.0;
|
|
|
|
// Skip whitespace after colon
|
|
int valueStart = colonPos + 1;
|
|
while(valueStart < StringLen(json) &&
|
|
(StringGetCharacter(json, valueStart) == ' ' ||
|
|
StringGetCharacter(json, valueStart) == '\t'))
|
|
valueStart++;
|
|
|
|
// Find end of number (comma, closing brace, closing bracket, or end of string)
|
|
int valueEnd = valueStart;
|
|
while(valueEnd < StringLen(json))
|
|
{
|
|
ushort ch = StringGetCharacter(json, valueEnd);
|
|
if(ch == ',' || ch == '}' || ch == ']' || ch == ' ' || ch == '\n' || ch == '\r')
|
|
break;
|
|
valueEnd++;
|
|
}
|
|
|
|
string valueStr = StringSubstr(json, valueStart, valueEnd - valueStart);
|
|
return StringToDouble(valueStr);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Extract boolean value for a given key from JSON |
|
|
//+------------------------------------------------------------------+
|
|
bool JsonGetBool(const string json, const string key)
|
|
{
|
|
string searchKey = "\"" + key + "\"";
|
|
int keyPos = StringFind(json, searchKey);
|
|
if(keyPos == -1) return false;
|
|
|
|
int colonPos = StringFind(json, ":", keyPos + StringLen(searchKey));
|
|
if(colonPos == -1) return false;
|
|
|
|
// Check if "true" appears after colon (before next comma/brace)
|
|
int truePos = StringFind(json, "true", colonPos);
|
|
int commaPos = StringFind(json, ",", colonPos);
|
|
int bracePos = StringFind(json, "}", colonPos);
|
|
|
|
if(truePos == -1) return false;
|
|
if(commaPos != -1 && truePos > commaPos) return false;
|
|
if(bracePos != -1 && truePos > bracePos) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Extract integer value for a given key from JSON |
|
|
//+------------------------------------------------------------------+
|
|
int JsonGetInt(const string json, const string key)
|
|
{
|
|
return (int)JsonGetDouble(json, key);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Extract long value for a given key from JSON |
|
|
//+------------------------------------------------------------------+
|
|
long JsonGetLong(const string json, const string key)
|
|
{
|
|
string searchKey = "\"" + key + "\"";
|
|
int keyPos = StringFind(json, searchKey);
|
|
if(keyPos == -1) return 0;
|
|
|
|
int colonPos = StringFind(json, ":", keyPos + StringLen(searchKey));
|
|
if(colonPos == -1) return 0;
|
|
|
|
int valueStart = colonPos + 1;
|
|
while(valueStart < StringLen(json) &&
|
|
(StringGetCharacter(json, valueStart) == ' ' ||
|
|
StringGetCharacter(json, valueStart) == '\t'))
|
|
valueStart++;
|
|
|
|
int valueEnd = valueStart;
|
|
while(valueEnd < StringLen(json))
|
|
{
|
|
ushort ch = StringGetCharacter(json, valueEnd);
|
|
if(ch == ',' || ch == '}' || ch == ']' || ch == ' ')
|
|
break;
|
|
valueEnd++;
|
|
}
|
|
|
|
string valueStr = StringSubstr(json, valueStart, valueEnd - valueStart);
|
|
return StringToInteger(valueStr);
|
|
}
|
|
```
|
|
|
|
**Note:** These helpers work for flat (non-nested) JSON. For production use with nested objects or arrays, consider the **CJAVal library** from MQL5 CodeBase, which provides proper recursive JSON parsing with object/array traversal.
|
|
|
|
---
|
|
|
|
## Node.js Integration Patterns
|
|
|
|
### EA -> Node.js: Send Trade Data
|
|
|
|
Report executed trades to a Node.js backend:
|
|
|
|
```mql5
|
|
CHttpClient httpClient;
|
|
|
|
void NotifyServer(string action, string symbol, double lots, double price, ulong ticket)
|
|
{
|
|
string json = BuildTradeJSON(ticket, symbol, action, lots, price, 0, 0);
|
|
string response = httpClient.Post("/api/trades", json);
|
|
if(response == "")
|
|
PrintFormat("WARNING: Failed to notify server about %s %s", action, symbol);
|
|
}
|
|
|
|
// Call after trade execution:
|
|
void OnTradeTransaction(const MqlTradeTransaction &trans,
|
|
const MqlTradeRequest &request,
|
|
const MqlTradeResult &result)
|
|
{
|
|
if(trans.type == TRADE_TRANSACTION_DEAL_ADD)
|
|
{
|
|
// New deal executed
|
|
NotifyServer("BUY", trans.symbol, trans.volume, trans.price, trans.deal);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Node.js -> EA: Receive Commands (Polling)
|
|
|
|
Timer-based polling pattern to receive trading signals from a Node.js server:
|
|
|
|
```mql5
|
|
CHttpClient httpClient;
|
|
|
|
int OnInit()
|
|
{
|
|
httpClient.Init("https://api.example.com", 5000);
|
|
httpClient.SetAuth(InpApiToken);
|
|
EventSetTimer(5); // Poll every 5 seconds
|
|
return INIT_SUCCEEDED;
|
|
}
|
|
|
|
void OnTimer()
|
|
{
|
|
string endpoint = "/api/signals?account=" +
|
|
IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN)) +
|
|
"&symbol=" + Symbol();
|
|
string response = httpClient.Get(endpoint);
|
|
if(response == "") return;
|
|
|
|
string action = JsonGetString(response, "action");
|
|
if(action == "") return;
|
|
|
|
double lots = JsonGetDouble(response, "lots");
|
|
double sl = JsonGetDouble(response, "sl");
|
|
double tp = JsonGetDouble(response, "tp");
|
|
string symbol = JsonGetString(response, "symbol");
|
|
if(symbol == "") symbol = Symbol();
|
|
|
|
if(action == "BUY")
|
|
ExecuteBuy(symbol, lots, sl, tp);
|
|
else if(action == "SELL")
|
|
ExecuteSell(symbol, lots, sl, tp);
|
|
else if(action == "CLOSE")
|
|
ClosePosition(symbol);
|
|
}
|
|
|
|
void OnDeinit(const int reason)
|
|
{
|
|
EventKillTimer();
|
|
}
|
|
```
|
|
|
|
### Send Account Status Updates
|
|
|
|
Periodically report account state to the server:
|
|
|
|
```mql5
|
|
void SendAccountStatus()
|
|
{
|
|
string json = "{";
|
|
json += "\"account\":" + IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN)) + ",";
|
|
json += "\"balance\":" + DoubleToString(AccountInfoDouble(ACCOUNT_BALANCE), 2) + ",";
|
|
json += "\"equity\":" + DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY), 2) + ",";
|
|
json += "\"margin\":" + DoubleToString(AccountInfoDouble(ACCOUNT_MARGIN), 2) + ",";
|
|
json += "\"freeMargin\":" + DoubleToString(AccountInfoDouble(ACCOUNT_MARGIN_FREE), 2) + ",";
|
|
json += "\"openPositions\":" + IntegerToString(PositionsTotal()) + ",";
|
|
json += "\"server\":\"" + AccountInfoString(ACCOUNT_SERVER) + "\",";
|
|
json += "\"timestamp\":\"" + TimeToString(TimeCurrent(), TIME_DATE | TIME_SECONDS) + "\"";
|
|
json += "}";
|
|
|
|
httpClient.Post("/api/status", json);
|
|
}
|
|
```
|
|
|
|
### Common API Endpoints Pattern
|
|
|
|
| Method | Endpoint | Purpose |
|
|
|--------|----------|---------|
|
|
| POST | `/api/trades` | Report executed trades |
|
|
| GET | `/api/signals` | Poll for trading signals |
|
|
| POST | `/api/status` | Send account status (balance, equity, positions) |
|
|
| GET | `/api/config` | Fetch EA configuration parameters |
|
|
| POST | `/api/errors` | Report errors and alerts |
|
|
| POST | `/api/logs` | Send EA log entries |
|
|
|
|
### Authentication
|
|
|
|
Include an authentication token in every request via headers:
|
|
|
|
```mql5
|
|
input string InpApiToken = ""; // API Bearer Token
|
|
|
|
// In CHttpClient or raw WebRequest:
|
|
string headers = "Content-Type: application/json\r\n"
|
|
+ "Authorization: Bearer " + InpApiToken + "\r\n";
|
|
```
|
|
|
|
For the `CHttpClient` class, use `httpClient.SetAuth(InpApiToken)` after `Init()`.
|
|
|
|
---
|
|
|
|
## Network Error Handling
|
|
|
|
### Retry Pattern with Progressive Backoff
|
|
|
|
```mql5
|
|
string RequestWithRetry(string method, string url, string headers,
|
|
string body, int maxRetries = 3)
|
|
{
|
|
char data[];
|
|
char result[];
|
|
string resultHeaders;
|
|
|
|
if(body != "")
|
|
StringToCharArray(body, data, 0, StringLen(body), CP_UTF8);
|
|
|
|
for(int attempt = 0; attempt < maxRetries; attempt++)
|
|
{
|
|
if(attempt > 0)
|
|
{
|
|
int delayMs = 1000 * (attempt + 1); // 2s, 3s progressive backoff
|
|
PrintFormat("Retry %d/%d after %d ms delay...", attempt + 1, maxRetries, delayMs);
|
|
Sleep(delayMs);
|
|
}
|
|
|
|
ResetLastError();
|
|
int statusCode = WebRequest(method, url, headers, 5000, data, result, resultHeaders);
|
|
|
|
if(statusCode == -1)
|
|
{
|
|
int error = GetLastError();
|
|
PrintFormat("WebRequest attempt %d failed: error %d", attempt + 1, error);
|
|
// Don't retry non-recoverable errors
|
|
if(error == 4014 || error == 4060) return "";
|
|
continue;
|
|
}
|
|
|
|
if(statusCode >= 500)
|
|
{
|
|
PrintFormat("Server error %d, attempt %d", statusCode, attempt + 1);
|
|
continue; // Retry on server errors
|
|
}
|
|
|
|
// Return response for any non-server-error status
|
|
return CharArrayToString(result, 0, WHOLE_ARRAY, CP_UTF8);
|
|
}
|
|
|
|
PrintFormat("All %d attempts failed for %s %s", maxRetries, method, url);
|
|
return "";
|
|
}
|
|
```
|
|
|
|
### Common Error Codes
|
|
|
|
| Status Code | GetLastError() | Meaning |
|
|
|-------------|---------------|---------|
|
|
| -1 | 4014 | URL not in allowed list (whitelist in terminal settings) |
|
|
| -1 | 4060 | Function not allowed (called from indicator or Strategy Tester) |
|
|
| -1 | 5203 | No connection to server / network error |
|
|
| -1 | 5200-5299 | Various network/internet errors |
|
|
| HTTP 0 | - | Timeout / no response from server |
|
|
| HTTP 400 | - | Bad request (check JSON format) |
|
|
| HTTP 401 | - | Authentication failed (check token) |
|
|
| HTTP 403 | - | Forbidden (check permissions) |
|
|
| HTTP 404 | - | Endpoint not found (check URL) |
|
|
| HTTP 429 | - | Rate limited (add delays between requests) |
|
|
| HTTP 500 | - | Internal server error (server-side issue) |
|
|
| HTTP 502/503 | - | Server unavailable (retry later) |
|
|
|
|
### Error Reporting to Server
|
|
|
|
```mql5
|
|
void ReportErrorToServer(string source, int errorCode, string details)
|
|
{
|
|
string json = "{";
|
|
json += "\"source\":\"" + source + "\",";
|
|
json += "\"errorCode\":" + IntegerToString(errorCode) + ",";
|
|
json += "\"details\":\"" + details + "\",";
|
|
json += "\"account\":" + IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN)) + ",";
|
|
json += "\"timestamp\":\"" + TimeToString(TimeCurrent(), TIME_DATE | TIME_SECONDS) + "\"";
|
|
json += "}";
|
|
|
|
// Don't retry error reporting itself to avoid infinite loops
|
|
httpClient.Post("/api/errors", json);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Internal Communication (Between MQL Programs)
|
|
|
|
### Global Variables of the Terminal
|
|
|
|
Global variables are shared across all MQL programs running in a single terminal instance. They store `double` values and persist across program restarts (saved to disk by the terminal).
|
|
|
|
```mql5
|
|
// Write a value
|
|
GlobalVariableSet("EA_Signal_EURUSD", 1.0);
|
|
|
|
// Read a value
|
|
double signal = GlobalVariableGet("EA_Signal_EURUSD");
|
|
|
|
// Check existence
|
|
bool exists = GlobalVariableCheck("EA_Signal_EURUSD");
|
|
|
|
// Delete
|
|
GlobalVariableDel("EA_Signal_EURUSD");
|
|
|
|
// Create temporary (auto-deleted when terminal closes, not saved to disk)
|
|
GlobalVariableTemp("EA_Temp_Signal");
|
|
|
|
// Set only if doesn't exist (atomic check-and-set, useful for locking)
|
|
bool wasCreated = GlobalVariableSetOnCondition("EA_Lock", 1.0, 0.0);
|
|
|
|
// Get value and set time simultaneously
|
|
datetime lastAccess;
|
|
double value = GlobalVariableGet("EA_Signal", lastAccess);
|
|
|
|
// List and iterate all global variables
|
|
int total = GlobalVariablesTotal();
|
|
for(int i = 0; i < total; i++)
|
|
{
|
|
string name = GlobalVariableName(i);
|
|
double val = GlobalVariableGet(name);
|
|
PrintFormat("GVar: %s = %f", name, val);
|
|
}
|
|
```
|
|
|
|
**Constraints:**
|
|
- Name maximum: **63 characters**
|
|
- Value type: always **double** (encode other types as double or use naming conventions)
|
|
- Shared across **all programs in the same terminal**
|
|
- Persisted to disk on terminal shutdown (except `GlobalVariableTemp`)
|
|
|
|
**Use cases:**
|
|
- Signal passing between EAs and indicators on different charts
|
|
- Simple locking mechanism with `GlobalVariableSetOnCondition()`
|
|
- State persistence across EA restarts
|
|
- Coordination between multiple EAs (e.g., portfolio-level risk)
|
|
|
|
### Custom Events (MQL5 Only)
|
|
|
|
Custom events allow one MQL5 program to send events to a chart's `OnChartEvent()` handler:
|
|
|
|
```mql5
|
|
// --- Define custom event IDs ---
|
|
#define EVENT_SIGNAL_BUY (CHARTEVENT_CUSTOM + 1)
|
|
#define EVENT_SIGNAL_SELL (CHARTEVENT_CUSTOM + 2)
|
|
#define EVENT_UPDATE_PANEL (CHARTEVENT_CUSTOM + 3)
|
|
#define EVENT_CLOSE_ALL (CHARTEVENT_CUSTOM + 4)
|
|
|
|
// --- Sender (indicator, script, or another EA on the same chart): ---
|
|
// Send to current chart
|
|
EventChartCustom(ChartID(), EVENT_SIGNAL_BUY, 0, 1.23456, "EURUSD");
|
|
|
|
// Send to a specific chart by chart ID
|
|
long targetChart = ChartFirst();
|
|
EventChartCustom(targetChart, EVENT_SIGNAL_BUY, 12345, 1.5, "EURUSD");
|
|
|
|
// --- Receiver (EA with OnChartEvent handler): ---
|
|
void OnChartEvent(const int id, const long &lparam,
|
|
const double &dparam, const string &sparam)
|
|
{
|
|
if(id == EVENT_SIGNAL_BUY)
|
|
{
|
|
long ticket = lparam; // Custom long parameter
|
|
double price = dparam; // Custom double parameter
|
|
string symbol = sparam; // Custom string parameter
|
|
PrintFormat("BUY signal received: %s at %f", symbol, price);
|
|
}
|
|
else if(id == EVENT_SIGNAL_SELL)
|
|
{
|
|
PrintFormat("SELL signal received: %s at %f", sparam, dparam);
|
|
}
|
|
else if(id == EVENT_CLOSE_ALL)
|
|
{
|
|
// Close all positions
|
|
}
|
|
}
|
|
```
|
|
|
|
**Parameters per event:**
|
|
- `lparam` (long): one integer/long value
|
|
- `dparam` (double): one double value
|
|
- `sparam` (string): one string value (can encode JSON for more data)
|
|
|
|
**Constraints:**
|
|
- Can only send to charts in the same terminal
|
|
- The receiving chart must have an EA or indicator with `OnChartEvent()`
|
|
- Custom event IDs range: `CHARTEVENT_CUSTOM` to `CHARTEVENT_CUSTOM + 65535`
|
|
|
|
### File-Based Communication
|
|
|
|
Programs can communicate by reading and writing files in the terminal data folder (`MQL4/Files` or `MQL5/Files`):
|
|
|
|
```mql5
|
|
// --- Writer (EA or Script) ---
|
|
void WriteSignalFile(string signal, string symbol, double price)
|
|
{
|
|
string filename = "signals_" + symbol + ".csv";
|
|
int handle = FileOpen(filename, FILE_WRITE | FILE_CSV | FILE_ANSI, ',');
|
|
if(handle == INVALID_HANDLE)
|
|
{
|
|
PrintFormat("Failed to open file: %s, error: %d", filename, GetLastError());
|
|
return;
|
|
}
|
|
FileWrite(handle, signal, symbol, DoubleToString(price, 5),
|
|
TimeToString(TimeCurrent()));
|
|
FileClose(handle);
|
|
}
|
|
|
|
// --- Reader (another EA or Indicator) ---
|
|
string ReadSignalFile(string symbol)
|
|
{
|
|
string filename = "signals_" + symbol + ".csv";
|
|
if(!FileIsExist(filename)) return "";
|
|
|
|
int handle = FileOpen(filename, FILE_READ | FILE_CSV | FILE_ANSI, ',');
|
|
if(handle == INVALID_HANDLE) return "";
|
|
|
|
string signal = FileReadString(handle);
|
|
FileClose(handle);
|
|
|
|
// Delete after reading (one-time signal)
|
|
FileDelete(filename);
|
|
return signal;
|
|
}
|
|
```
|
|
|
|
**Cross-terminal file sharing** using `FILE_COMMON` flag:
|
|
|
|
```mql5
|
|
// Write to common folder (shared across all terminal instances)
|
|
int handle = FileOpen("shared_signal.txt", FILE_WRITE | FILE_TXT | FILE_COMMON);
|
|
FileWriteString(handle, "BUY EURUSD 1.12345");
|
|
FileClose(handle);
|
|
|
|
// Read from common folder in another terminal
|
|
int handle2 = FileOpen("shared_signal.txt", FILE_READ | FILE_TXT | FILE_COMMON);
|
|
string data = FileReadString(handle2);
|
|
FileClose(handle2);
|
|
```
|
|
|
|
**File locking pattern** (prevent concurrent read/write corruption):
|
|
|
|
```mql5
|
|
bool AcquireFileLock(string lockName, int timeoutMs = 5000)
|
|
{
|
|
string lockFile = lockName + ".lock";
|
|
datetime start = TimeLocal();
|
|
while(FileIsExist(lockFile))
|
|
{
|
|
if((TimeLocal() - start) * 1000 > timeoutMs)
|
|
return false; // Timeout
|
|
Sleep(50);
|
|
}
|
|
// Create lock file
|
|
int h = FileOpen(lockFile, FILE_WRITE | FILE_TXT);
|
|
if(h == INVALID_HANDLE) return false;
|
|
FileClose(h);
|
|
return true;
|
|
}
|
|
|
|
void ReleaseFileLock(string lockName)
|
|
{
|
|
FileDelete(lockName + ".lock");
|
|
}
|
|
```
|
|
|
|
### Named Pipes (MQL4, Windows Only)
|
|
|
|
Named pipes provide fast IPC for Windows-based communication (e.g., between MetaTrader and a C#/Python application):
|
|
|
|
```mql4
|
|
#import "kernel32.dll"
|
|
int CreateFileW(string name, uint access, uint share, int security,
|
|
uint creation, uint flags, int template);
|
|
int WriteFile(int handle, const uchar &buffer[], int bytes,
|
|
int &written[], int overlapped);
|
|
int ReadFile(int handle, uchar &buffer[], int bytes,
|
|
int &read[], int overlapped);
|
|
int CloseHandle(int handle);
|
|
int FlushFileBuffers(int handle);
|
|
#import
|
|
|
|
#define GENERIC_READ 0x80000000
|
|
#define GENERIC_WRITE 0x40000000
|
|
#define OPEN_EXISTING 3
|
|
#define INVALID_HANDLE_VALUE -1
|
|
|
|
// Connect to a named pipe server
|
|
int ConnectToPipe(string pipeName)
|
|
{
|
|
string fullName = "\\\\.\\pipe\\" + pipeName;
|
|
int pipe = CreateFileW(fullName,
|
|
GENERIC_READ | GENERIC_WRITE,
|
|
0, 0, OPEN_EXISTING, 0, 0);
|
|
if(pipe == INVALID_HANDLE_VALUE)
|
|
{
|
|
Print("Failed to connect to pipe: ", pipeName);
|
|
return INVALID_HANDLE_VALUE;
|
|
}
|
|
return pipe;
|
|
}
|
|
|
|
// Send message through pipe
|
|
bool SendPipeMessage(int pipe, string message)
|
|
{
|
|
uchar data[];
|
|
StringToCharArray(message, data);
|
|
int written[];
|
|
ArrayResize(written, 1);
|
|
return WriteFile(pipe, data, ArraySize(data), written, 0) != 0;
|
|
}
|
|
|
|
// Read message from pipe
|
|
string ReadPipeMessage(int pipe, int bufferSize = 4096)
|
|
{
|
|
uchar buffer[];
|
|
ArrayResize(buffer, bufferSize);
|
|
int bytesRead[];
|
|
ArrayResize(bytesRead, 1);
|
|
if(ReadFile(pipe, buffer, bufferSize, bytesRead, 0))
|
|
return CharArrayToString(buffer, 0, bytesRead[0]);
|
|
return "";
|
|
}
|
|
|
|
// Clean up
|
|
void ClosePipe(int pipe)
|
|
{
|
|
FlushFileBuffers(pipe);
|
|
CloseHandle(pipe);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## MQL5 Sockets (Advanced)
|
|
|
|
MQL5 provides built-in TCP socket support for real-time bidirectional communication. This is more efficient than WebRequest polling for scenarios requiring low-latency or persistent connections.
|
|
|
|
### Plain TCP Socket
|
|
|
|
```mql5
|
|
int g_socket = INVALID_HANDLE;
|
|
|
|
bool SocketConnectToServer(string host, int port, int timeoutMs = 5000)
|
|
{
|
|
g_socket = SocketCreate();
|
|
if(g_socket == INVALID_HANDLE)
|
|
{
|
|
PrintFormat("SocketCreate failed: %d", GetLastError());
|
|
return false;
|
|
}
|
|
|
|
if(!SocketConnect(g_socket, host, port, timeoutMs))
|
|
{
|
|
PrintFormat("SocketConnect failed: %d", GetLastError());
|
|
SocketClose(g_socket);
|
|
g_socket = INVALID_HANDLE;
|
|
return false;
|
|
}
|
|
|
|
PrintFormat("Connected to %s:%d", host, port);
|
|
return true;
|
|
}
|
|
|
|
bool SocketSendMessage(string message)
|
|
{
|
|
if(g_socket == INVALID_HANDLE) return false;
|
|
|
|
uchar data[];
|
|
int len = StringToCharArray(message, data, 0, StringLen(message), CP_UTF8);
|
|
|
|
int sent = SocketSend(g_socket, data, len);
|
|
if(sent == -1)
|
|
{
|
|
PrintFormat("SocketSend failed: %d", GetLastError());
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
string SocketReceiveMessage(int timeoutMs = 1000)
|
|
{
|
|
if(g_socket == INVALID_HANDLE) return "";
|
|
|
|
uchar response[];
|
|
int received = SocketRead(g_socket, response, 4096, timeoutMs);
|
|
if(received <= 0) return "";
|
|
|
|
return CharArrayToString(response, 0, received, CP_UTF8);
|
|
}
|
|
|
|
void SocketDisconnect()
|
|
{
|
|
if(g_socket != INVALID_HANDLE)
|
|
{
|
|
SocketClose(g_socket);
|
|
g_socket = INVALID_HANDLE;
|
|
}
|
|
}
|
|
```
|
|
|
|
### TLS/SSL Encrypted Socket
|
|
|
|
For secure communication (HTTPS servers, encrypted APIs):
|
|
|
|
```mql5
|
|
bool SocketConnectTLS(string host, int port, int timeoutMs = 5000)
|
|
{
|
|
g_socket = SocketCreate();
|
|
if(g_socket == INVALID_HANDLE) return false;
|
|
|
|
if(!SocketConnect(g_socket, host, port, timeoutMs))
|
|
{
|
|
SocketClose(g_socket);
|
|
g_socket = INVALID_HANDLE;
|
|
return false;
|
|
}
|
|
|
|
// Perform TLS handshake
|
|
if(!SocketTlsHandshake(g_socket, host))
|
|
{
|
|
PrintFormat("TLS handshake failed: %d", GetLastError());
|
|
SocketClose(g_socket);
|
|
g_socket = INVALID_HANDLE;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// For TLS, use SocketTlsSend / SocketTlsRead instead:
|
|
bool SocketSendTLS(string message)
|
|
{
|
|
if(g_socket == INVALID_HANDLE) return false;
|
|
|
|
uchar data[];
|
|
int len = StringToCharArray(message, data, 0, StringLen(message), CP_UTF8);
|
|
int sent = SocketTlsSend(g_socket, data, len);
|
|
return (sent > 0);
|
|
}
|
|
|
|
string SocketReceiveTLS(int timeoutMs = 1000)
|
|
{
|
|
if(g_socket == INVALID_HANDLE) return "";
|
|
|
|
uchar response[];
|
|
int received = SocketTlsRead(g_socket, response, 4096, timeoutMs);
|
|
if(received <= 0) return "";
|
|
|
|
return CharArrayToString(response, 0, received, CP_UTF8);
|
|
}
|
|
```
|
|
|
|
### Socket Constraints
|
|
|
|
- Only available in **EAs and Scripts** (NOT indicators)
|
|
- Maximum **128 sockets per program**
|
|
- Server URLs/IPs must be **whitelisted** in terminal settings (same as WebRequest)
|
|
- Non-blocking reads with timeout; blocking sends
|
|
- MQL5 does not support acting as a socket server (client only)
|
|
- For WebSocket protocol, you must implement the handshake and framing manually or use a library
|
|
|
|
---
|
|
|
|
## Communication Method Comparison
|
|
|
|
| Method | Direction | Latency | Complexity | MQL4 | MQL5 | Notes |
|
|
|--------|-----------|---------|------------|------|------|-------|
|
|
| WebRequest | EA -> Server | High (blocking) | Low | Yes | Yes | Simple REST, synchronous |
|
|
| Sockets | Bidirectional | Low | Medium | No | Yes | Persistent connection |
|
|
| Global Variables | Internal | Very low | Very low | Yes | Yes | Double values only |
|
|
| Custom Events | Internal | Very low | Low | No | Yes | Same terminal only |
|
|
| Files | Both | Medium | Low | Yes | Yes | FILE_COMMON for cross-terminal |
|
|
| Named Pipes | Bidirectional | Low | High | Yes (DLL) | Yes (DLL) | Windows only, requires DLL import |
|