//+------------------------------------------------------------------+ //| Ten12_GuardX.mq4 | //| Ten12 GuardX: risk manager for MetaTrader 4 (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 live trading", | //| "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). | //| © Ten12 Mentors. Educational software, not investment advice. | //+------------------------------------------------------------------+ #property copyright "Ten12 Mentors" #property link "https://tentwelvementors.com/app/#/guardx" #property version "1.01" #property strict #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." 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 pendingNewest = 0; datetime lockUntilUtc = 0; datetime lockAtUtc = 0; int ruleSlGrace = -1; double ruleMaxLot = 0, ruleMaxRisk = 0; int ruleMaxOpen = 0; datetime lastBeat = 0; int lastCount = -1; string status = "Ten12 GuardX: starting…"; 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); } bool IsMarket(int type) { return type == OP_BUY || type == OP_SELL; } double RiskOf(string sym, double openPx, double sl, double vol) { if(sl <= 0) return 0; double tv = MarketInfo(sym, MODE_TICKVALUE), ts = MarketInfo(sym, MODE_TICKSIZE); 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(AccountNumber()); 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"); EventSetTimer(MathMax(5, MathMin(60, EverySeconds))); Beat(); return(INIT_SUCCEEDED); } void OnDeinit(const int reason) { EventKillTimer(); Comment(""); } void OnTimer() { Beat(); } // a new order since the last tick: check it straight away void OnTick() { int c = OrdersTotal(); if(c != lastCount) { lastCount = c; 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 (AutoTrading button green, "Allow live 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: AUTOTRADING IS OFF, so GuardX can't close trades. Click the AutoTrading button so it turns green" + (MQLInfoInteger(MQL_TRADE_ALLOWED) != 0 ? "." : ", and tick Allow live trading in this EA's settings (F7)."); } bool Locked() { return lockUntilUtc > UtcNow(); } void CloseTicket(int ticket, string why) { if(!OrderSelect(ticket, SELECT_BY_TICKET, MODE_TRADES) || OrderCloseTime() != 0) return; string sym = OrderSymbol(); bool ok; if(IsMarket(OrderType())) { RefreshRates(); double px = OrderType() == OP_BUY ? MarketInfo(sym, MODE_BID) : MarketInfo(sym, MODE_ASK); ok = OrderClose(ticket, OrderLots(), px, Slippage, clrNONE); } else ok = OrderDelete(ticket); if(ok) { closeErr = ""; Print("Ten12 GuardX closed ", ticket, ": ", why); } else { int e = GetLastError(); closeErr = "error " + IntegerToString(e); Print("Ten12 GuardX could not close ", ticket, " (error ", e, "): ", why); } } void CloseAll(string why) { for(int i = OrdersTotal() - 1; i >= 0; i--) if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES) && IsMarket(OrderType())) CloseTicket(OrderTicket(), why); } void DeletePending() { for(int i = OrdersTotal() - 1; i >= 0; i--) if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES) && !IsMarket(OrderType())) CloseTicket(OrderTicket(), "GuardX lock"); } // rules the EA applies by itself, so protection continues if the internet drops void LocalChecks() { if(Locked()) DeletePending(); datetime nowUtc = UtcNow(); long offset = (long)(TimeCurrent() - nowUtc); int nOpen = 0; for(int i = OrdersTotal() - 1; i >= 0; i--) { if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES) || !IsMarket(OrderType())) continue; int t = OrderTicket(); datetime openedUtc = (datetime)((long)OrderOpenTime() - offset); double vol = OrderLots(), sl = OrderStopLoss(); if(Locked() && openedUtc >= lockAtUtc - 5) { CloseTicket(t, "New trade during the GuardX cooldown"); continue; } if(ruleMaxLot > 0 && vol > ruleMaxLot + 1e-9) { CloseTicket(t, "Above the max lot size"); continue; } if(ruleSlGrace >= 0 && sl <= 0 && (long)nowUtc - (long)openedUtc > ruleSlGrace) { CloseTicket(t, "No stop loss"); continue; } if(ruleMaxRisk > 0 && RiskOf(OrderSymbol(), OrderOpenPrice(), sl, vol) > ruleMaxRisk * 1.02) { CloseTicket(t, "Risk to the stop above the limit"); continue; } nOpen++; } while(ruleMaxOpen > 0 && nOpen > ruleMaxOpen) { int newest = -1; datetime nt = 0; for(int i = OrdersTotal() - 1; i >= 0; i--) if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES) && IsMarket(OrderType()) && OrderOpenTime() >= nt) { nt = OrderOpenTime(); newest = OrderTicket(); } if(newest < 0) break; CloseTicket(newest, "Above the max open positions"); nOpen--; } } string BuildReport() { datetime now = TimeCurrent(); long offset = (long)(now - UtcNow()); datetime from48 = now - 48 * 3600; string openJs = "", recent = "", opens = "", cash = "", trades = ""; int n = 0, nr = 0, no = 0, nc = 0, nt = 0, pend = 0; for(int i = 0; i < OrdersTotal(); i++) { if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue; if(!IsMarket(OrderType())) { pend++; continue; } string sym = OrderSymbol(); int dg = (int)MarketInfo(sym, MODE_DIGITS); openJs += (n ? "," : "") + "{\"ticket\":\"" + IntegerToString(OrderTicket()) + "\",\"symbol\":\"" + Esc(sym) + "\",\"side\":\"" + (OrderType() == OP_SELL ? "sell" : "buy") + "\",\"volume\":" + Num(OrderLots(), 2) + ",\"open_time\":\"" + Tm(OrderOpenTime()) + "\",\"open_price\":" + Num(OrderOpenPrice(), dg) + ",\"sl\":" + Num(OrderStopLoss(), dg) + ",\"tp\":" + Num(OrderTakeProfit(), dg) + ",\"profit\":" + Num(OrderProfit() + OrderSwap() + OrderCommission(), 2) + ",\"risk_usd\":" + Num(RiskOf(sym, OrderOpenPrice(), OrderStopLoss(), OrderLots()), 2) + "}"; n++; if(OrderOpenTime() >= from48 && no < 400) { opens += (no ? "," : "") + "\"" + Tm(OrderOpenTime()) + "\""; no++; } } datetime newest = lastSent; for(int i = OrdersHistoryTotal() - 1; i >= 0; i--) { if(!OrderSelect(i, SELECT_BY_POS, MODE_HISTORY)) continue; int type = OrderType(); datetime ct = OrderCloseTime(); if(type == 6) // balance operation: deposit or withdrawal { if(nc < 300) { cash += (nc ? "," : "") + "{\"ticket\":\"" + IntegerToString(OrderTicket()) + "\",\"time\":\"" + Tm(OrderOpenTime()) + "\",\"amount\":" + Num(OrderProfit(), 2) + ",\"comment\":\"" + Esc(OrderComment()) + "\"}"; nc++; } continue; } if(!IsMarket(type)) continue; string sym = OrderSymbol(); double net = OrderProfit() + OrderSwap() + OrderCommission(); if(OrderOpenTime() >= from48 && no < 400) { opens += (no ? "," : "") + "\"" + Tm(OrderOpenTime()) + "\""; no++; } if(ct >= from48 && nr < 400) { recent += (nr ? "," : "") + "{\"ticket\":\"" + IntegerToString(OrderTicket()) + "\",\"open_time\":\"" + Tm(OrderOpenTime()) + "\",\"close_time\":\"" + Tm(ct) + "\",\"net\":" + Num(net, 2) + "}"; nr++; } if(ct > lastSent && nt < 300) { int dg = (int)MarketInfo(sym, MODE_DIGITS); trades += (nt ? "," : "") + "{\"ticket\":\"" + IntegerToString(OrderTicket()) + "\",\"symbol\":\"" + Esc(sym) + "\",\"side\":\"" + (type == OP_SELL ? "sell" : "buy") + "\",\"volume\":" + Num(OrderLots(), 2) + ",\"open_time\":\"" + Tm(OrderOpenTime()) + "\",\"open_price\":" + Num(OrderOpenPrice(), dg) + ",\"close_time\":\"" + Tm(ct) + "\",\"close_price\":" + Num(OrderClosePrice(), dg) + ",\"sl\":" + Num(OrderStopLoss(), dg) + ",\"tp\":" + Num(OrderTakeProfit(), dg) + ",\"profit\":" + Num(OrderProfit(), 2) + ",\"commission\":" + Num(OrderCommission(), 2) + ",\"swap\":" + Num(OrderSwap(), 2) + ",\"risk_usd\":" + Num(RiskOf(sym, OrderOpenPrice(), OrderStopLoss(), OrderLots()), 2) + ",\"comment\":\"" + Esc(OrderComment()) + "\"}"; nt++; if(ct > newest) newest = ct; } } pendingNewest = newest; return "{\"platform\":\"MT4\",\"ea\":\"1.01\",\"account\":\"" + IntegerToString(AccountNumber()) + "\",\"company\":\"" + Esc(AccountCompany()) + "\",\"server\":\"" + Esc(AccountServer()) + "\",\"currency\":\"" + Esc(AccountCurrency()) + "\",\"leverage\":" + IntegerToString(AccountLeverage()) + ",\"balance\":" + Num(AccountBalance(), 2) + ",\"equity\":" + Num(AccountEquity(), 2) + ",\"server_offset\":" + IntegerToString(offset) + ",\"trade_ok\":" + (CanTrade() ? "true" : "false") + ",\"close_err\":\"" + Esc(closeErr) + "\",\"pending\":" + IntegerToString(pend) + ",\"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) { int err = GetLastError(); if(err == 4060 || err == 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) { long fix = (long)StringToInteger(p[1]) - (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) CloseTicket((int)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); }