//+------------------------------------------------------------------+ //| CyberSoftLabConnector.mq5 | //| Drop on a chart in the VPS terminal. | //| Tools → Options → Expert Advisors → Allow WebRequest for the | //| site origin. URL and secret come from the cabinet account card. | //+------------------------------------------------------------------+ #property copyright "CyberSoftLab" #property link "https://t.me/leonovdev" #property version "1.10" #property description "Pushes balance, equity, positions and deals. Pulls cabinet signals." input string InpUrl = "https://example.com/api/mt5/ACCOUNT_ID"; input string InpSecret = ""; input int InpTimerSec = 5; input int InpSlippagePoints = 30; // MT5 server clock offset from UTC, e.g. 2 or 3. Adjust when broker changes DST. input int InpServerUtcOffsetHours = 0; input string InpHistorySymbol = "EURUSD"; input ENUM_TIMEFRAMES InpHistoryTimeframe = PERIOD_H1; const long CSL_MAGIC = 20261027; string g_ack = ""; int OnInit() { EventSetTimer((uint)MathMax(2, InpTimerSec)); return INIT_SUCCEEDED; } void OnDeinit(const int reason) { EventKillTimer(); } void OnTimer() { if(StringLen(InpSecret) < 8 || StringFind(InpUrl, "/api/mt5/") < 0) { Print("CSL: set the cabinet URL and secret"); return; } string body = BuildBody(); uchar data[]; uchar result[]; int written = StringToCharArray(body, data, 0, WHOLE_ARRAY, CP_UTF8); if(written > 0) ArrayResize(data, written - 1); string headers = "Content-Type: application/json\r\nAuthorization: Bearer " + InpSecret + "\r\n"; string resHeaders; ResetLastError(); int code = WebRequest("POST", InpUrl, headers, 8000, data, result, resHeaders); if(code == -1) { Print("CSL WebRequest failed ", GetLastError(), " — allow this URL in terminal options"); return; } if(code != 200) { Print("CSL HTTP ", code, " ", CharArrayToString(result, 0, WHOLE_ARRAY, CP_UTF8)); return; } g_ack = ""; string json = CharArrayToString(result, 0, WHOLE_ARRAY, CP_UTF8); ApplySignals(json); } string BuildBody() { string market = "null"; string tf = InpHistoryTimeframe == PERIOD_M15 ? "M15" : InpHistoryTimeframe == PERIOD_H4 ? "H4" : "H1"; if(InpHistoryTimeframe == PERIOD_M15 || InpHistoryTimeframe == PERIOD_H1 || InpHistoryTimeframe == PERIOD_H4) { MqlRates rates[]; int copied = CopyRates(InpHistorySymbol, InpHistoryTimeframe, 1, 500, rates); if(copied >= 80) { string candles = "["; for(int b = 0; b < copied; b++) { if(b > 0) candles += ","; long utc = ((long)rates[b].time - InpServerUtcOffsetHours * 3600) * 1000; candles += StringFormat("{\"t\":%I64d,\"o\":%.8f,\"h\":%.8f,\"l\":%.8f,\"c\":%.8f}", utc, rates[b].open, rates[b].high, rates[b].low, rates[b].close); } candles += "]"; market = StringFormat("{\"symbol\":\"%s\",\"timeframe\":\"%s\",\"bars\":%s}",InpHistorySymbol,tf,candles); } } string positions = "["; int n = PositionsTotal(); bool firstPos = true; for(int i = 0; i < n; i++) { ulong ticket = PositionGetTicket(i); if(ticket == 0 || !PositionSelectByTicket(ticket)) continue; if(!firstPos) positions += ","; firstPos = false; string side = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL ? "sell" : "buy"; positions += StringFormat( "{\"ticket\":%I64u,\"symbol\":\"%s\",\"side\":\"%s\",\"volume\":%s,\"price\":%s,\"profit\":%s}", ticket, PositionGetString(POSITION_SYMBOL), side, DoubleToString(PositionGetDouble(POSITION_VOLUME), 2), DoubleToString(PositionGetDouble(POSITION_PRICE_OPEN), 5), DoubleToString(PositionGetDouble(POSITION_PROFIT), 2) ); } positions += "]"; string deals = "["; datetime from = TimeCurrent() - 30 * 24 * 3600; HistorySelect(from, TimeCurrent()); int total = HistoryDealsTotal(); int sent = 0; bool first = true; for(int i = total - 1; i >= 0 && sent < 200; i--) { ulong ticket = HistoryDealGetTicket(i); if(ticket == 0) continue; long entry = HistoryDealGetInteger(ticket, DEAL_ENTRY); if(entry != DEAL_ENTRY_OUT && entry != DEAL_ENTRY_INOUT && entry != DEAL_ENTRY_OUT_BY) continue; double profit = HistoryDealGetDouble(ticket, DEAL_PROFIT) + HistoryDealGetDouble(ticket, DEAL_SWAP) + HistoryDealGetDouble(ticket, DEAL_COMMISSION); long type = HistoryDealGetInteger(ticket, DEAL_TYPE); string side = (type == DEAL_TYPE_SELL) ? "sell" : "buy"; if(!first) deals += ","; first = false; deals += StringFormat( "{\"ticket\":%I64u,\"symbol\":\"%s\",\"side\":\"%s\",\"volume\":%s,\"price\":%s,\"profit\":%s,\"time\":\"%s\"}", ticket, HistoryDealGetString(ticket, DEAL_SYMBOL), side, DoubleToString(HistoryDealGetDouble(ticket, DEAL_VOLUME), 2), DoubleToString(HistoryDealGetDouble(ticket, DEAL_PRICE), 5), DoubleToString(profit, 2), Iso((datetime)HistoryDealGetInteger(ticket, DEAL_TIME)) ); sent++; } deals += "]"; string ack = "[]"; if(StringLen(g_ack) > 0) { string parts[]; int k = StringSplit(g_ack, ',', parts); ack = "["; for(int i = 0; i < k; i++) { if(i > 0) ack += ","; ack += "\"" + parts[i] + "\""; } ack += "]"; } return StringFormat( "{\"balance\":%s,\"equity\":%s,\"positions\":%s,\"deals\":%s,\"ack\":%s,\"market\":%s}", DoubleToString(AccountInfoDouble(ACCOUNT_BALANCE), 2), DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY), 2), positions, deals, ack, market ); } string Iso(datetime t) { MqlDateTime d; TimeToStruct(t, d); return StringFormat("%04d-%02d-%02dT%02d:%02d:%02dZ", d.year, d.mon, d.day, d.hour, d.min, d.sec); } void ApplySignals(const string json) { int bracket = StringFind(json, "\"signals\""); if(bracket < 0) return; int start = StringFind(json, "[", bracket); int end = StringFind(json, "]", start); if(start < 0 || end < 0) return; string list = StringSubstr(json, start + 1, end - start - 1); string objects[]; int count = SplitObjects(list, objects); for(int i = 0; i < count; i++) { string id = JsonString(objects[i], "id"); string symbol = JsonString(objects[i], "symbol"); string side = JsonString(objects[i], "side"); double volume = JsonNumber(objects[i], "volume"); double sl = JsonNumber(objects[i], "sl"); double tp = JsonNumber(objects[i], "tp"); if(StringLen(id) < 4 || StringLen(symbol) < 3) continue; if(AlreadyHandled(id)) { RememberAck(id); continue; } if(SendSignal(id, symbol, side, volume, sl, tp)) RememberAck(id); } } bool AlreadyHandled(const string id) { for(int i = 0; i < PositionsTotal(); i++) { ulong ticket = PositionGetTicket(i); if(ticket == 0 || !PositionSelectByTicket(ticket)) continue; if(PositionGetString(POSITION_COMMENT) == id) return true; } HistorySelect(TimeCurrent() - 30 * 24 * 3600, TimeCurrent()); int total = HistoryDealsTotal(); for(int i = total - 1; i >= 0 && i >= total - 300; i--) { ulong ticket = HistoryDealGetTicket(i); if(ticket == 0) continue; if(HistoryDealGetString(ticket, DEAL_COMMENT) == id) return true; } return false; } bool SendSignal(const string id, const string symbol, const string side, double volume, double sl, double tp) { if(!SymbolSelect(symbol, true)) { Print("CSL: symbol not found ", symbol); return false; } double step = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP); double vmin = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN); double vmax = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX); if(step <= 0) step = 0.01; volume = MathFloor(volume / step) * step; volume = MathMax(vmin, MathMin(vmax, volume)); int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS); bool sell = (side == "sell"); double price = sell ? SymbolInfoDouble(symbol, SYMBOL_BID) : SymbolInfoDouble(symbol, SYMBOL_ASK); MqlTradeRequest req; MqlTradeResult res; ZeroMemory(req); ZeroMemory(res); req.action = TRADE_ACTION_DEAL; req.symbol = symbol; req.volume = volume; req.type = sell ? ORDER_TYPE_SELL : ORDER_TYPE_BUY; req.price = price; req.sl = sl > 0 ? NormalizeDouble(sl, digits) : 0; req.tp = tp > 0 ? NormalizeDouble(tp, digits) : 0; req.deviation = InpSlippagePoints; req.magic = CSL_MAGIC; req.comment = id; req.type_filling = Filling(symbol); if(!OrderSend(req, res)) { Print("CSL OrderSend failed ", res.retcode, " ", symbol); return false; } return res.retcode == TRADE_RETCODE_DONE || res.retcode == TRADE_RETCODE_PLACED; } ENUM_ORDER_TYPE_FILLING Filling(const string symbol) { int mode = (int)SymbolInfoInteger(symbol, SYMBOL_FILLING_MODE); if((mode & SYMBOL_FILLING_IOC) == SYMBOL_FILLING_IOC) return ORDER_FILLING_IOC; if((mode & SYMBOL_FILLING_FOK) == SYMBOL_FILLING_FOK) return ORDER_FILLING_FOK; return ORDER_FILLING_RETURN; } void RememberAck(const string id) { if(StringFind("," + g_ack + ",", "," + id + ",") >= 0) return; if(StringLen(g_ack) > 0) g_ack += ","; g_ack += id; } int SplitObjects(const string list, string &out[]) { ArrayResize(out, 0); int n = 0; int depth = 0; int from = -1; for(int i = 0; i < StringLen(list); i++) { ushort ch = StringGetCharacter(list, i); if(ch == '{') { if(depth == 0) from = i; depth++; } else if(ch == '}') { depth--; if(depth == 0 && from >= 0) { ArrayResize(out, n + 1); out[n] = StringSubstr(list, from, i - from + 1); n++; from = -1; } } } return n; } string JsonString(const string obj, const string key) { string pat = "\"" + key + "\""; int p = StringFind(obj, pat); if(p < 0) return ""; int colon = StringFind(obj, ":", p); if(colon < 0) return ""; int a = StringFind(obj, "\"", colon); if(a < 0) return ""; int b = StringFind(obj, "\"", a + 1); if(b < 0) return ""; return StringSubstr(obj, a + 1, b - a - 1); } double JsonNumber(const string obj, const string key) { string pat = "\"" + key + "\""; int p = StringFind(obj, pat); if(p < 0) return 0; int colon = StringFind(obj, ":", p); if(colon < 0) return 0; string tail = StringSubstr(obj, colon + 1); StringTrimLeft(tail); return StringToDouble(tail); }