//+------------------------------------------------------------------+ //| Ten12_GuardX.mq5 | //| Ten12 GuardX: risk manager for MetaTrader 5 (Ten12 Pro). | //| Reports the account to Ten12 every few seconds and enforces the | //| rules you set in Ten12 GuardX: it closes trades that break a | //| rule and, during a cooldown, closes any new trade at once. | //| It never opens trades and never moves your stops or targets. | //| Setup: | //| 1. Tools > Options > Expert Advisors: tick "Allow algorithmic | //| trading" and "Allow WebRequest for listed URL", and add | //| https://tentwelvementors.com | //| 2. Drag this Expert Advisor onto ONE chart of the account, paste | //| your MetaTrader key (Ten12 GuardX > Connect) and click OK. | //| 3. Keep the terminal running (a VPS is best) so the rules apply | //| around the clock. | //| © Ten12 Mentors. Educational software, not investment advice. | //+------------------------------------------------------------------+ #property copyright "Ten12 Mentors" #property link "https://tentwelvementors.com/app/#/guardx" #property version "1.01" #property description "Ten12 GuardX enforces your risk rules: daily loss limit, max trades, profit target, stop loss, risk per trade and cooldowns. It never opens trades." #include input string GuardKey = ""; // Your Ten12 MetaTrader key (t12_...) input int EverySeconds = 10; // How often to report and check (5-60 seconds) input int HistoryDays = 90; // On the first run, send trades closed in the last N days (journal and trade score) input int Slippage = 30; // Allowed slippage in points when GuardX closes a trade string URL = "https://tentwelvementors.com/app/api/guardx-ea.php?t="; string GV, GVL, GVC; long clockFix = 0; // Ten12 server clock minus this computer's UTC clock (seconds), used when the computer clock is off string closeErr = ""; // why the last close failed ("" = fine) datetime lastSent = 0; datetime lockUntilUtc = 0; // cooldown end (UTC) as told by the server datetime lockAtUtc = 0; int ruleSlGrace = -1; // local fallback limits (used when the server can't be reached) double ruleMaxLot = 0, ruleMaxRisk = 0; int ruleMaxOpen = 0; datetime lastBeat = 0, lastOk = 0; datetime pendingNewest = 0; // newest closed trade in the last report (saved once the server confirms) string status = "Ten12 GuardX: starting…"; CTrade trade; string Esc(string s) { StringReplace(s, "\\", "\\\\"); StringReplace(s, "\"", "\\\""); StringReplace(s, "\n", " "); StringReplace(s, "\r", " "); return s; } string Num(double v, int dp = 8) { return DoubleToString(v, dp); } string Tm(datetime t) { return TimeToString(t, TIME_DATE | TIME_SECONDS); } double RiskOf(string sym, double openPx, double sl, double vol) { if(sl <= 0) return 0; double tv = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_VALUE), ts = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_SIZE); if(tv <= 0 || ts <= 0) return 0; return MathAbs(openPx - sl) / ts * tv * vol; } int OnInit() { if(StringLen(GuardKey) < 30) { Alert("Ten12 GuardX: paste your MetaTrader key from Ten12 GuardX > Connect."); return(INIT_PARAMETERS_INCORRECT); } GV = "T12GX_" + IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN)); GVL = GV + "_LOCK"; GVC = GV + "_CLK"; if(GlobalVariableCheck(GVC)) clockFix = (long)GlobalVariableGet(GVC); if(GlobalVariableCheck(GV)) lastSent = (datetime)GlobalVariableGet(GV); else lastSent = TimeCurrent() - HistoryDays * 86400; if(GlobalVariableCheck(GVL)) lockUntilUtc = (datetime)GlobalVariableGet(GVL); if(GlobalVariableCheck(GVL + "A")) lockAtUtc = (datetime)GlobalVariableGet(GVL + "A"); trade.SetDeviationInPoints(Slippage); trade.SetAsyncMode(false); EventSetTimer(MathMax(5, MathMin(60, EverySeconds))); Beat(); return(INIT_SUCCEEDED); } void OnDeinit(const int reason) { EventKillTimer(); Comment(""); } void OnTimer() { Beat(); } // a new position: check it straight away void OnTradeTransaction(const MqlTradeTransaction &trans, const MqlTradeRequest &request, const MqlTradeResult &result) { if(trans.type == TRADE_TRANSACTION_ORDER_ADD && Locked()) { DeletePending(); return; } // a pending order during the cooldown: delete it before it can fill if(trans.type != TRADE_TRANSACTION_DEAL_ADD) return; LocalChecks(); if(TimeLocal() - lastBeat >= 2) Beat(); } // UTC from this computer, corrected by the Ten12 server clock when the two differ by more than 2 minutes datetime UtcNow() { return (datetime)((long)TimeGMT() + clockFix); } // MetaTrader lets this EA close trades (Algo Trading button green, "Allow Algo Trading" ticked, broker allows EAs) bool CanTrade() { return TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) != 0 && MQLInfoInteger(MQL_TRADE_ALLOWED) != 0 && AccountInfoInteger(ACCOUNT_TRADE_EXPERT) != 0; } string NoTradeMsg() { return "Ten12 GuardX: ALGO TRADING IS OFF, so GuardX can't close trades. Click the Algo Trading button so it turns green" + (MQLInfoInteger(MQL_TRADE_ALLOWED) != 0 ? "." : ", and tick Allow Algo Trading in this EA's settings (F7)."); } bool Locked() { return lockUntilUtc > UtcNow(); } void ClosePos(ulong ticket, string why) { if(!PositionSelectByTicket(ticket)) return; if(trade.PositionClose(ticket, Slippage)) { closeErr = ""; Print("Ten12 GuardX closed position ", ticket, ": ", why); } else { closeErr = trade.ResultRetcodeDescription(); Print("Ten12 GuardX could not close ", ticket, " (", closeErr, "): ", why); } } void CloseAll(string why) { for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong t = PositionGetTicket(i); if(t > 0) ClosePos(t, why); } } void DeletePending() { for(int i = OrdersTotal() - 1; i >= 0; i--) { ulong t = OrderGetTicket(i); if(t == 0) continue; long ty = OrderGetInteger(ORDER_TYPE); if(ty == ORDER_TYPE_BUY || ty == ORDER_TYPE_SELL || ty == ORDER_TYPE_CLOSE_BY) continue; trade.OrderDelete(t); } } // rules the EA applies by itself, so protection continues if the internet drops void LocalChecks() { if(Locked() && OrdersTotal() > 0) DeletePending(); datetime nowUtc = UtcNow(); long offset = (long)(TimeTradeServer() - nowUtc); int nOpen = 0; for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong t = PositionGetTicket(i); if(t == 0) continue; datetime opened = (datetime)PositionGetInteger(POSITION_TIME); // server time datetime openedUtc = (datetime)((long)opened - offset); string sym = PositionGetString(POSITION_SYMBOL); double vol = PositionGetDouble(POSITION_VOLUME), sl = PositionGetDouble(POSITION_SL), px = PositionGetDouble(POSITION_PRICE_OPEN); if(Locked() && openedUtc >= lockAtUtc - 5) { ClosePos(t, "New trade during the GuardX cooldown"); continue; } if(ruleMaxLot > 0 && vol > ruleMaxLot + 1e-9) { ClosePos(t, "Above the max lot size"); continue; } if(ruleSlGrace >= 0 && sl <= 0 && (long)nowUtc - (long)openedUtc > ruleSlGrace) { ClosePos(t, "No stop loss"); continue; } if(ruleMaxRisk > 0 && RiskOf(sym, px, sl, vol) > ruleMaxRisk * 1.02) { ClosePos(t, "Risk to the stop above the limit"); continue; } nOpen++; } if(ruleMaxOpen > 0 && nOpen > ruleMaxOpen) { // close the newest positions beyond the limit while(PositionsTotal() > ruleMaxOpen) { ulong newest = 0; datetime nt = 0; for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong t = PositionGetTicket(i); if(t == 0) continue; datetime ot = (datetime)PositionGetInteger(POSITION_TIME); if(ot >= nt) { nt = ot; newest = t; } } if(newest == 0) break; int before = PositionsTotal(); ClosePos(newest, "Above the max open positions"); if(PositionsTotal() >= before) break; } } } string BuildReport() { datetime now = TimeTradeServer(); long offset = (long)(now - UtcNow()); string openJs = ""; int n = 0; for(int i = 0; i < PositionsTotal(); i++) { ulong t = PositionGetTicket(i); if(t == 0) continue; string sym = PositionGetString(POSITION_SYMBOL); int dg = (int)SymbolInfoInteger(sym, SYMBOL_DIGITS); double vol = PositionGetDouble(POSITION_VOLUME), px = PositionGetDouble(POSITION_PRICE_OPEN), sl = PositionGetDouble(POSITION_SL), tp = PositionGetDouble(POSITION_TP); string side = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) ? "sell" : "buy"; openJs += (n ? "," : "") + "{\"ticket\":\"" + IntegerToString((long)t) + "\",\"symbol\":\"" + Esc(sym) + "\",\"side\":\"" + side + "\",\"volume\":" + Num(vol, 4) + ",\"open_time\":\"" + Tm((datetime)PositionGetInteger(POSITION_TIME)) + "\",\"open_price\":" + Num(px, dg) + ",\"sl\":" + Num(sl, dg) + ",\"tp\":" + Num(tp, dg) + ",\"profit\":" + Num(PositionGetDouble(POSITION_PROFIT) + PositionGetDouble(POSITION_SWAP), 2) + ",\"risk_usd\":" + Num(RiskOf(sym, px, sl, vol), 2) + "}"; n++; } // the last 48 hours: closed positions (net result) and every opening deal (trades per day) string recent = "", opens = "", cash = "", trades = ""; int nr = 0, no = 0, nc = 0, nt = 0; datetime from48 = now - 48 * 3600; datetime fromHist = (datetime)((from48 < lastSent ? (long)from48 : (long)lastSent) - 60 * 86400); datetime newest = lastSent; if(HistorySelect(fromHist, now + 60)) { int total = HistoryDealsTotal(); for(int i = 0; i < total; i++) { ulong d = HistoryDealGetTicket(i); if(d == 0) continue; datetime dt = (datetime)HistoryDealGetInteger(d, DEAL_TIME); long type = HistoryDealGetInteger(d, DEAL_TYPE), entry = HistoryDealGetInteger(d, DEAL_ENTRY); if(type == DEAL_TYPE_BALANCE) { if(nc < 300) { cash += (nc ? "," : "") + "{\"ticket\":\"" + IntegerToString((long)d) + "\",\"time\":\"" + Tm(dt) + "\",\"amount\":" + Num(HistoryDealGetDouble(d, DEAL_PROFIT), 2) + ",\"comment\":\"" + Esc(HistoryDealGetString(d, DEAL_COMMENT)) + "\"}"; nc++; } continue; } if(type != DEAL_TYPE_BUY && type != DEAL_TYPE_SELL) continue; if(entry == DEAL_ENTRY_IN && dt >= from48 && no < 400) { opens += (no ? "," : "") + "\"" + Tm(dt) + "\""; no++; } if(entry != DEAL_ENTRY_OUT && entry != DEAL_ENTRY_OUT_BY) continue; double profit = HistoryDealGetDouble(d, DEAL_PROFIT), swap = HistoryDealGetDouble(d, DEAL_SWAP), comm = HistoryDealGetDouble(d, DEAL_COMMISSION); long pos = HistoryDealGetInteger(d, DEAL_POSITION_ID); string sym = HistoryDealGetString(d, DEAL_SYMBOL); double vol = HistoryDealGetDouble(d, DEAL_VOLUME), closePx = HistoryDealGetDouble(d, DEAL_PRICE); // the opening deal: entry price, time, side and its commission double openPx = closePx; datetime ot = dt; string side = (type == DEAL_TYPE_SELL) ? "buy" : "sell"; for(int j = 0; j < total; j++) { ulong o = HistoryDealGetTicket(j); if(o == 0 || HistoryDealGetInteger(o, DEAL_POSITION_ID) != pos || HistoryDealGetInteger(o, DEAL_ENTRY) != DEAL_ENTRY_IN) continue; openPx = HistoryDealGetDouble(o, DEAL_PRICE); ot = (datetime)HistoryDealGetInteger(o, DEAL_TIME); side = (HistoryDealGetInteger(o, DEAL_TYPE) == DEAL_TYPE_BUY) ? "buy" : "sell"; comm += HistoryDealGetDouble(o, DEAL_COMMISSION) * (vol / MathMax(HistoryDealGetDouble(o, DEAL_VOLUME), 1e-9)); break; } if(dt >= from48 && nr < 400) { recent += (nr ? "," : "") + "{\"ticket\":\"" + IntegerToString((long)d) + "\",\"open_time\":\"" + Tm(ot) + "\",\"close_time\":\"" + Tm(dt) + "\",\"net\":" + Num(profit + swap + comm, 2) + "}"; nr++; } if(dt > lastSent && nt < 300) { int dg = (int)SymbolInfoInteger(sym, SYMBOL_DIGITS); double sl = HistoryDealGetDouble(d, DEAL_SL), tp = HistoryDealGetDouble(d, DEAL_TP); trades += (nt ? "," : "") + "{\"ticket\":\"" + IntegerToString((long)d) + "\",\"symbol\":\"" + Esc(sym) + "\",\"side\":\"" + side + "\",\"volume\":" + Num(vol, 4) + ",\"open_time\":\"" + Tm(ot) + "\",\"open_price\":" + Num(openPx, dg) + ",\"close_time\":\"" + Tm(dt) + "\",\"close_price\":" + Num(closePx, dg) + ",\"sl\":" + Num(sl, dg) + ",\"tp\":" + Num(tp, dg) + ",\"profit\":" + Num(profit, 2) + ",\"commission\":" + Num(comm, 2) + ",\"swap\":" + Num(swap, 2) + ",\"risk_usd\":" + Num(RiskOf(sym, openPx, sl, vol), 2) + ",\"comment\":\"" + Esc(HistoryDealGetString(d, DEAL_COMMENT)) + "\"}"; nt++; if(dt > newest) newest = dt; } } } pendingNewest = newest; return "{\"platform\":\"MT5\",\"ea\":\"1.01\",\"account\":\"" + IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN)) + "\",\"company\":\"" + Esc(AccountInfoString(ACCOUNT_COMPANY)) + "\",\"server\":\"" + Esc(AccountInfoString(ACCOUNT_SERVER)) + "\",\"currency\":\"" + Esc(AccountInfoString(ACCOUNT_CURRENCY)) + "\",\"leverage\":" + IntegerToString(AccountInfoInteger(ACCOUNT_LEVERAGE)) + ",\"balance\":" + Num(AccountInfoDouble(ACCOUNT_BALANCE), 2) + ",\"equity\":" + Num(AccountInfoDouble(ACCOUNT_EQUITY), 2) + ",\"server_offset\":" + IntegerToString(offset) + ",\"trade_ok\":" + (CanTrade() ? "true" : "false") + ",\"close_err\":\"" + Esc(closeErr) + "\",\"pending\":" + IntegerToString(OrdersTotal()) + ",\"open\":[" + openJs + "],\"recent\":[" + recent + "],\"opens\":[" + opens + "],\"cash\":[" + cash + "],\"trades\":[" + trades + "]}"; } void Beat() { lastBeat = TimeLocal(); LocalChecks(); string body = BuildReport(); char data[], result[]; string resHeaders; StringToCharArray(body, data, 0, StringLen(body), CP_UTF8); ResetLastError(); int code = WebRequest("POST", URL + GuardKey, "Content-Type: application/json\r\n", 15000, data, result, resHeaders); if(code == -1) { if(GetLastError() == 4014) status = "Ten12 GuardX: allow WebRequest for https://tentwelvementors.com (Tools > Options > Expert Advisors)."; else status = "Ten12 GuardX: offline, local rules active" + (Locked() ? " · cooldown running" : ""); Comment(CanTrade() ? status : NoTradeMsg() + "\n" + status); return; } string reply = CharArrayToString(result, 0, WHOLE_ARRAY, CP_UTF8); string lines[]; int k = StringSplit(reply, '\n', lines); for(int i = 0; i < k; i++) { string p[]; int m = StringSplit(lines[i], '|', p); if(m < 1) continue; string cmd = p[0]; if(cmd == "OK" && m > 1) { lastOk = (datetime)StringToInteger(p[1]); long fix = (long)lastOk - (long)TimeGMT(); clockFix = (fix > 120 || fix < -120) ? fix : 0; GlobalVariableSet(GVC, (double)clockFix); } else if(cmd == "ERR") { status = "Ten12 GuardX: " + (m > 1 ? p[1] : "error"); Print(status); } else if(cmd == "CLOSEALL") CloseAll(m > 1 ? p[1] : "GuardX lock"); else if(cmd == "DELPENDING") DeletePending(); else if(cmd == "CLOSE" && m > 1) ClosePos((ulong)StringToInteger(p[1]), m > 2 ? p[2] : "GuardX rule"); else if(cmd == "LOCK" && m > 2) { datetime u = (datetime)StringToInteger(p[1]); datetime since = (datetime)StringToInteger(p[2]); if(u > UtcNow() && u != lockUntilUtc) Alert("Ten12 GuardX: trading locked. ", m > 3 ? p[3] : ""); lockUntilUtc = u; lockAtUtc = (u > UtcNow()) ? since : 0; GlobalVariableSet(GVL, (double)lockUntilUtc); GlobalVariableSet(GVL + "A", (double)lockAtUtc); } else if(cmd == "RSL" && m > 1) ruleSlGrace = (int)StringToInteger(p[1]); else if(cmd == "RLOT" && m > 1) ruleMaxLot = StringToDouble(p[1]); else if(cmd == "ROPEN" && m > 1) ruleMaxOpen = (int)StringToInteger(p[1]); else if(cmd == "RRISK" && m > 1) ruleMaxRisk = StringToDouble(p[1]); else if(cmd == "MSG" && m > 1) status = p[1]; } if(code == 200 && pendingNewest > lastSent) { lastSent = pendingNewest; GlobalVariableSet(GV, (double)lastSent); } Comment(CanTrade() ? status : NoTradeMsg() + "\n" + status); }