//+------------------------------------------------------------------+ //| RiskDesk_EA.mq5 — RiskDesk Bridge Connector v2.3 | //| Compile (F7), drag onto any chart. No WebRequest needed. | //+------------------------------------------------------------------+ #property copyright "RiskDesk" #property version "2.3" #property strict input int PollIntervalMs = 500; input int MagicNumber = 777001; input string RelayApiKey = ""; // Paste your Relay API Key from RiskDesk mobile app // --------------------------------------------------------------------------- // Active position management (trailing stop + breakeven) // --------------------------------------------------------------------------- struct ManagedPos { ulong ticket; bool trailingActive; double trailPoints; double trailActivationPoints; // 0 = activate immediately bool breakevenActive; double breakevenTriggerPoints; double breakevenBuffer; bool breakevenDone; // set to true once SL moved to entry }; ManagedPos managedPositions[]; void AddManagedPos(ulong ticket, bool trailingActive, double trailPts, double trailActPts, bool beActive, double beTriggerPts, double beBuffer) { int n = ArraySize(managedPositions); ArrayResize(managedPositions, n + 1); managedPositions[n].ticket = ticket; managedPositions[n].trailingActive = trailingActive; managedPositions[n].trailPoints = trailPts; managedPositions[n].trailActivationPoints = trailActPts; managedPositions[n].breakevenActive = beActive; managedPositions[n].breakevenTriggerPoints= beTriggerPts; managedPositions[n].breakevenBuffer = beBuffer; managedPositions[n].breakevenDone = false; } void CleanManagedPositions() { for (int i = ArraySize(managedPositions) - 1; i >= 0; i--) { if (!PositionSelectByTicket(managedPositions[i].ticket)) { for (int j = i; j < ArraySize(managedPositions) - 1; j++) managedPositions[j] = managedPositions[j + 1]; ArrayResize(managedPositions, ArraySize(managedPositions) - 1); } } } void ManagePositions() { CleanManagedPositions(); for (int i = 0; i < ArraySize(managedPositions); i++) { if (!PositionSelectByTicket(managedPositions[i].ticket)) continue; string sym = PositionGetString(POSITION_SYMBOL); double entry = PositionGetDouble(POSITION_PRICE_OPEN); double curSL = PositionGetDouble(POSITION_SL); long ptype = PositionGetInteger(POSITION_TYPE); bool isLong = (ptype == POSITION_TYPE_BUY); int digits = (int)SymbolInfoInteger(sym, SYMBOL_DIGITS); double point = SymbolInfoDouble(sym, SYMBOL_POINT); double bid = SymbolInfoDouble(sym, SYMBOL_BID); double ask = SymbolInfoDouble(sym, SYMBOL_ASK); double profit_pts = isLong ? (bid - entry) / point : (entry - ask) / point; double newSL = curSL; // Breakeven if (managedPositions[i].breakevenActive && !managedPositions[i].breakevenDone && profit_pts >= managedPositions[i].breakevenTriggerPoints) { double beSL = isLong ? NormalizeDouble(entry + managedPositions[i].breakevenBuffer * point, digits) : NormalizeDouble(entry - managedPositions[i].breakevenBuffer * point, digits); if ((isLong && (curSL < beSL || curSL == 0)) || (!isLong && (curSL > beSL || curSL == 0))) { newSL = beSL; managedPositions[i].breakevenDone = true; Print("Breakeven: ticket=", managedPositions[i].ticket, " SL moved to ", newSL); } } // Trailing stop if (managedPositions[i].trailingActive && managedPositions[i].trailPoints > 0) { bool activated = (managedPositions[i].trailActivationPoints <= 0 || profit_pts >= managedPositions[i].trailActivationPoints); if (activated) { double trailSL = isLong ? NormalizeDouble(bid - managedPositions[i].trailPoints * point, digits) : NormalizeDouble(ask + managedPositions[i].trailPoints * point, digits); if ((isLong && trailSL > newSL) || (!isLong && (newSL == 0 || trailSL < newSL))) { newSL = trailSL; } } } // Modify SL if changed if (newSL != curSL && newSL > 0) { MqlTradeRequest req = {}; MqlTradeResult res = {}; req.action = TRADE_ACTION_SLTP; req.position = managedPositions[i].ticket; req.symbol = sym; req.sl = newSL; req.tp = PositionGetDouble(POSITION_TP); if (OrderSend(req, res)) Print("Managed SL updated: ticket=", managedPositions[i].ticket, " newSL=", newSL); else Print("Managed SL failed: ", GetLastError()); } } } //+------------------------------------------------------------------+ int OnInit() { EventSetMillisecondTimer(PollIntervalMs); Print("RiskDesk EA v2.3 started. Relay: ", RelayApiKey != "" ? "ON" : "OFF"); WritePing(); return INIT_SUCCEEDED; } void OnDeinit(const int reason) { EventKillTimer(); Print("RiskDesk EA stopped."); } void OnTimer() { WritePing(); WritePositions(); WriteHistory(); PollForOrder(); if (RelayApiKey != "") PollRelay(); ManagePositions(); } //+------------------------------------------------------------------+ // Heartbeat //+------------------------------------------------------------------+ void WritePing() { string login = IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN)); string server = AccountInfoString(ACCOUNT_SERVER); string actype = AccountInfoInteger(ACCOUNT_TRADE_MODE) == ACCOUNT_TRADE_MODE_DEMO ? "demo" : "real"; double balance = AccountInfoDouble(ACCOUNT_BALANCE); double equity = AccountInfoDouble(ACCOUNT_EQUITY); double margin = AccountInfoDouble(ACCOUNT_MARGIN); double freeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE); double marginLevel = AccountInfoDouble(ACCOUNT_MARGIN_LEVEL); double profit = AccountInfoDouble(ACCOUNT_PROFIT); string currency = AccountInfoString(ACCOUNT_CURRENCY); long leverage = AccountInfoInteger(ACCOUNT_LEVERAGE); int positions = PositionsTotal(); string body = "{\"login\":\"" + login + "\"," + "\"server\":\"" + server + "\"," + "\"account_type\":\"" + actype + "\"," + "\"balance\":" + DoubleToString(balance, 2) + "," + "\"equity\":" + DoubleToString(equity, 2) + "," + "\"margin\":" + DoubleToString(margin, 2) + "," + "\"free_margin\":" + DoubleToString(freeMargin, 2) + "," + "\"margin_level\":" + DoubleToString(marginLevel, 2) + "," + "\"profit\":" + DoubleToString(profit, 2) + "," + "\"currency\":\"" + currency + "\"," + "\"leverage\":" + IntegerToString(leverage) + "," + "\"positions\":" + IntegerToString(positions) + "," + "\"time\":\"" + TimeToString(TimeCurrent()) + "\"}"; int h = FileOpen("riskdesk_ping.json", FILE_WRITE|FILE_TXT|FILE_ANSI); if (h != INVALID_HANDLE) { FileWriteString(h, body); FileClose(h); } } //+------------------------------------------------------------------+ // Write open positions //+------------------------------------------------------------------+ void WritePositions() { int total = PositionsTotal(); string json = "["; for (int i = 0; i < total; i++) { ulong ticket = PositionGetTicket(i); if (!PositionSelectByTicket(ticket)) continue; string sym = PositionGetString(POSITION_SYMBOL); double vol = PositionGetDouble(POSITION_VOLUME); double openPr = PositionGetDouble(POSITION_PRICE_OPEN); double curPr = PositionGetDouble(POSITION_PRICE_CURRENT); double sl = PositionGetDouble(POSITION_SL); double tp = PositionGetDouble(POSITION_TP); double swap = PositionGetDouble(POSITION_SWAP); double profit = PositionGetDouble(POSITION_PROFIT); long ptype = PositionGetInteger(POSITION_TYPE); long magic = PositionGetInteger(POSITION_MAGIC); long openT = PositionGetInteger(POSITION_TIME); string comment = PositionGetString(POSITION_COMMENT); StringReplace(comment, "\"", "'"); string dir = ptype == POSITION_TYPE_BUY ? "buy" : "sell"; int digs = (int)SymbolInfoInteger(sym, SYMBOL_DIGITS); if (i > 0) json += ","; json += "{\"ticket\":" + IntegerToString((int)ticket) + ",\"symbol\":\"" + sym + "\"" + ",\"direction\":\"" + dir + "\"" + ",\"volume\":" + DoubleToString(vol, 2) + ",\"open_price\":" + DoubleToString(openPr, digs) + ",\"current_price\":" + DoubleToString(curPr, digs) + ",\"sl\":" + DoubleToString(sl, digs) + ",\"tp\":" + DoubleToString(tp, digs) + ",\"swap\":" + DoubleToString(swap, 2) + ",\"profit\":" + DoubleToString(profit, 2) + ",\"magic\":" + IntegerToString((int)magic) + ",\"open_time\":" + IntegerToString((int)openT) + ",\"comment\":\"" + comment + "\"}"; } json += "]"; int h = FileOpen("riskdesk_positions.json", FILE_WRITE|FILE_TXT|FILE_ANSI); if (h != INVALID_HANDLE) { FileWriteString(h, json); FileClose(h); } } //+------------------------------------------------------------------+ // Write trade history (closed deals) — refreshed every ~10 seconds //+------------------------------------------------------------------+ datetime lastHistoryWrite = 0; void WriteHistory() { if (TimeCurrent() - lastHistoryWrite < 10) return; lastHistoryWrite = TimeCurrent(); // Select all history from account creation to now if (!HistorySelect(0, TimeCurrent())) return; string login = IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN)); string server = AccountInfoString(ACCOUNT_SERVER); string actype = AccountInfoInteger(ACCOUNT_TRADE_MODE) == ACCOUNT_TRADE_MODE_DEMO ? "demo" : "real"; string currency = AccountInfoString(ACCOUNT_CURRENCY); string json = "{\"login\":\"" + login + "\"," + "\"server\":\"" + server + "\"," + "\"account_type\":\"" + actype + "\"," + "\"currency\":\"" + currency + "\"," + "\"deals\":["; int totalDeals = HistoryDealsTotal(); bool first = true; for (int i = 0; i < totalDeals; i++) { ulong ticket = HistoryDealGetTicket(i); if (ticket == 0) continue; long dealType = HistoryDealGetInteger(ticket, DEAL_TYPE); long dealEntry = HistoryDealGetInteger(ticket, DEAL_ENTRY); // Only include buy/sell deals that are closing trades (DEAL_ENTRY_OUT or DEAL_ENTRY_INOUT) if (dealType != DEAL_TYPE_BUY && dealType != DEAL_TYPE_SELL) continue; if (dealEntry != DEAL_ENTRY_OUT && dealEntry != DEAL_ENTRY_INOUT) continue; string sym = HistoryDealGetString(ticket, DEAL_SYMBOL); double vol = HistoryDealGetDouble(ticket, DEAL_VOLUME); double price = HistoryDealGetDouble(ticket, DEAL_PRICE); double profit = HistoryDealGetDouble(ticket, DEAL_PROFIT); double commission = HistoryDealGetDouble(ticket, DEAL_COMMISSION); double swap = HistoryDealGetDouble(ticket, DEAL_SWAP); long dealTime = HistoryDealGetInteger(ticket, DEAL_TIME); long magic = HistoryDealGetInteger(ticket, DEAL_MAGIC); long posId = HistoryDealGetInteger(ticket, DEAL_POSITION_ID); string comment = HistoryDealGetString(ticket, DEAL_COMMENT); StringReplace(comment, "\"", "'"); // For closing deals: the deal type is the CLOSING side // If it closed via a sell, the original position was long string dir = (dealType == DEAL_TYPE_SELL) ? "Long" : "Short"; int digs = (int)SymbolInfoInteger(sym, SYMBOL_DIGITS); if (digs <= 0) digs = 5; if (!first) json += ","; first = false; json += "{\"ticket\":" + IntegerToString((int)ticket) + ",\"pos_id\":" + IntegerToString((int)posId) + ",\"symbol\":\"" + sym + "\"" + ",\"direction\":\"" + dir + "\"" + ",\"volume\":" + DoubleToString(vol, 2) + ",\"close_price\":" + DoubleToString(price, digs) + ",\"profit\":" + DoubleToString(profit, 2) + ",\"commission\":" + DoubleToString(commission, 2) + ",\"swap\":" + DoubleToString(swap, 2) + ",\"close_time\":" + IntegerToString((int)dealTime) + ",\"magic\":" + IntegerToString((int)magic) + ",\"comment\":\"" + comment + "\"}"; } // Now find matching entry deals to get entry prices and open times json += "],\"entries\":["; first = true; for (int i = 0; i < totalDeals; i++) { ulong ticket = HistoryDealGetTicket(i); if (ticket == 0) continue; long dealType = HistoryDealGetInteger(ticket, DEAL_TYPE); long dealEntry = HistoryDealGetInteger(ticket, DEAL_ENTRY); if (dealType != DEAL_TYPE_BUY && dealType != DEAL_TYPE_SELL) continue; if (dealEntry != DEAL_ENTRY_IN) continue; long posId = HistoryDealGetInteger(ticket, DEAL_POSITION_ID); double price = HistoryDealGetDouble(ticket, DEAL_PRICE); long openT = HistoryDealGetInteger(ticket, DEAL_TIME); string sym = HistoryDealGetString(ticket, DEAL_SYMBOL); string entryComment = HistoryDealGetString(ticket, DEAL_COMMENT); StringReplace(entryComment, "\"", "'"); int digs = (int)SymbolInfoInteger(sym, SYMBOL_DIGITS); if (digs <= 0) digs = 5; if (!first) json += ","; first = false; json += "{\"pos_id\":" + IntegerToString((int)posId) + ",\"entry_price\":" + DoubleToString(price, digs) + ",\"open_time\":" + IntegerToString((int)openT) + ",\"comment\":\"" + entryComment + "\"}"; } json += "]}"; int h = FileOpen("riskdesk_history.json", FILE_WRITE|FILE_TXT|FILE_ANSI); if (h != INVALID_HANDLE) { FileWriteString(h, json); FileClose(h); } } //+------------------------------------------------------------------+ // Webhook parameter bundle + unit conversion helpers //+------------------------------------------------------------------+ struct WebhookParams { double size_percent; string exec_mode; double tp_points, tp_distance, tp_percent, tp_money; double sl_points, sl_distance, sl_percent, sl_money; bool trailing_stop; double trail_points, trail_distance, trail_percent; double trail_act_points, trail_act_distance, trail_act_percent; bool breakeven; double be_trigger_points, be_trigger_distance, be_trigger_percent; double be_buffer; }; // Convert a monetary amount (account currency) to a price distance for the // given symbol and volume. double MoneyToPriceDist(string symbol, double volume, double money) { double tickVal = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE); double tickSize = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE); if (tickVal <= 0 || tickSize <= 0 || volume <= 0 || money <= 0) return 0; return (money / (tickVal * volume)) * tickSize; } // Resolve points/distance/percent/money variants into one price distance. double ResolvePriceDist(string symbol, double price, double volume, double pts, double dist, double pct, double money) { double point = SymbolInfoDouble(symbol, SYMBOL_POINT); if (pts > 0) return pts * point; if (dist > 0) return dist; if (pct > 0) return price * pct / 100.0; if (money > 0) return MoneyToPriceDist(symbol, volume, money); return 0; } // Resolve points/distance/percent variants into broker points (for the // trailing/breakeven manager, which works in points). double ResolvePoints(string symbol, double price, double pts, double dist, double pct) { double point = SymbolInfoDouble(symbol, SYMBOL_POINT); if (point <= 0) return 0; if (pts > 0) return pts; if (dist > 0) return dist / point; if (pct > 0) return price * pct / 100.0 / point; return 0; } // Volume from % of account equity, using the margin required for 1 lot. // Clamped and stepped to the symbol's volume constraints. double VolumeFromPercent(string symbol, ENUM_ORDER_TYPE otype, double price, double pct) { double equity = AccountInfoDouble(ACCOUNT_EQUITY); double marginPerLot = 0; if (!OrderCalcMargin(otype, symbol, 1.0, price, marginPerLot) || marginPerLot <= 0) return 0; double vol = equity * pct / 100.0 / marginPerLot; double step = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP); double vmin = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN); double vmax = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX); if (step > 0) vol = MathFloor(vol / step) * step; if (vol < vmin) vol = vmin; if (vmax > 0 && vol > vmax) vol = vmax; return NormalizeDouble(vol, 2); } // Close positions on the opposite side of an incoming order. // Returns total volume closed. double CloseOppositeSide(string symbol, bool incomingLong) { double closedVol = 0; for (int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if (!PositionSelectByTicket(ticket)) continue; if (PositionGetString(POSITION_SYMBOL) != symbol) continue; bool posLong = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY; if (posLong == incomingLong) continue; MqlTradeRequest req = {}; MqlTradeResult res = {}; req.action = TRADE_ACTION_DEAL; req.position = ticket; req.symbol = symbol; req.volume = PositionGetDouble(POSITION_VOLUME); req.type = posLong ? ORDER_TYPE_SELL : ORDER_TYPE_BUY; req.price = req.type == ORDER_TYPE_BUY ? SymbolInfoDouble(symbol, SYMBOL_ASK) : SymbolInfoDouble(symbol, SYMBOL_BID); req.deviation = 20; req.magic = MagicNumber; req.comment = "RiskDesk net"; req.type_filling = ORDER_FILLING_IOC; if (OrderSend(req, res)) closedVol += req.volume; } return closedVol; } //+------------------------------------------------------------------+ // Poll cloud relay for pending orders //+------------------------------------------------------------------+ datetime lastRelayPoll = 0; void PollRelay() { if (TimeCurrent() - lastRelayPoll < 2) return; lastRelayPoll = TimeCurrent(); string url = "https://riskdesk-webhook.riskdesk.workers.dev/poll?apiKey=" + RelayApiKey; string headers = "Content-Type: application/json\r\n"; char post[]; char result[]; string resultHeaders; int code = WebRequest("GET", url, headers, 3000, post, result, resultHeaders); if (code != 200) return; string response = CharArrayToString(result); if (response == "" || response == "null" || response == "[]" || response == "{}") return; if (StringFind(response, "\"id\"") < 0) return; Print("Relay order: ", StringSubstr(response, 0, 300)); string order_id = ExtractString(response, "id"); string symbol = ExtractString(response, "symbol"); string action = ExtractString(response, "action"); double volume = ExtractDouble(response, "volume"); double req_price= ExtractDouble(response, "price"); double sl_val = ExtractDouble(response, "sl"); double tp_val = ExtractDouble(response, "tp"); string comment = ExtractString(response, "comment"); if (comment == "") comment = ExtractString(response, "strategy"); WebhookParams p; p.size_percent = ExtractDouble(response, "size_percent"); p.exec_mode = ExtractString(response, "execution_mode"); p.tp_points = ExtractDouble(response, "tp_points"); p.tp_distance = ExtractDouble(response, "tp_distance"); p.tp_percent = ExtractDouble(response, "tp_percent"); p.tp_money = ExtractDouble(response, "tp_money"); p.sl_points = ExtractDouble(response, "sl_points"); p.sl_distance = ExtractDouble(response, "sl_distance"); p.sl_percent = ExtractDouble(response, "sl_percent"); p.sl_money = ExtractDouble(response, "sl_money"); p.trailing_stop = ExtractString(response, "trailing_stop") == "true"; p.trail_points = ExtractDouble(response, "trail_points"); p.trail_distance = ExtractDouble(response, "trail_distance"); p.trail_percent = ExtractDouble(response, "trail_percent"); p.trail_act_points = ExtractDouble(response, "trail_activation_points"); p.trail_act_distance = ExtractDouble(response, "trail_activation_distance"); p.trail_act_percent = ExtractDouble(response, "trail_activation_percent"); p.breakeven = ExtractString(response, "breakeven") == "true"; p.be_trigger_points = ExtractDouble(response, "breakeven_trigger_points"); p.be_trigger_distance= ExtractDouble(response, "breakeven_trigger_distance"); p.be_trigger_percent = ExtractDouble(response, "breakeven_trigger_percent"); p.be_buffer = ExtractDouble(response, "breakeven_buffer"); if (order_id == "" || symbol == "" || action == "") return; Print("Relay exec: ", action, " ", volume, " ", symbol); ExecuteOrder(order_id, symbol, action, volume, sl_val, tp_val, comment, req_price, p); // Acknowledge to relay RelayAck(order_id); } void RelayAck(string order_id) { string url = "https://riskdesk-webhook.riskdesk.workers.dev/ack"; string headers = "Content-Type: application/json\r\n"; string body = "{\"apiKey\":\"" + RelayApiKey + "\",\"id\":\"" + order_id + "\"}"; char postData[]; char result[]; string resultHeaders; StringToCharArray(body, postData, 0, WHOLE_ARRAY, CP_UTF8); ArrayResize(postData, ArraySize(postData) - 1); WebRequest("POST", url, headers, 3000, postData, result, resultHeaders); } //+------------------------------------------------------------------+ // Poll for a pending order file //+------------------------------------------------------------------+ void PollForOrder() { if (!FileIsExist("riskdesk_order.json")) return; int h = FileOpen("riskdesk_order.json", FILE_READ|FILE_TXT|FILE_ANSI); if (h == INVALID_HANDLE) return; string response = ""; while (!FileIsEnding(h)) response += FileReadString(h); FileClose(h); if (response == "" || StringFind(response, "\"id\"") < 0) return; Print("Order file: ", StringSubstr(response, 0, 300)); string order_id = ExtractString(response, "id"); string symbol = ExtractString(response, "symbol"); string action = ExtractString(response, "action"); double volume = ExtractDouble(response, "volume"); double req_price= ExtractDouble(response, "price"); double sl = ExtractDouble(response, "sl"); double tp = ExtractDouble(response, "tp"); string comment = ExtractString(response, "comment"); if (comment == "") comment = ExtractString(response, "strategy"); // Extra parameters — every unit variant the RiskDesk builder can emit. // Points = broker points; distance = absolute price units; percent = % of // entry price; money = account currency converted via tick value. WebhookParams p; p.size_percent = ExtractDouble(response, "size_percent"); p.exec_mode = ExtractString(response, "execution_mode"); p.tp_points = ExtractDouble(response, "tp_points"); p.tp_distance = ExtractDouble(response, "tp_distance"); p.tp_percent = ExtractDouble(response, "tp_percent"); p.tp_money = ExtractDouble(response, "tp_money"); p.sl_points = ExtractDouble(response, "sl_points"); p.sl_distance = ExtractDouble(response, "sl_distance"); p.sl_percent = ExtractDouble(response, "sl_percent"); p.sl_money = ExtractDouble(response, "sl_money"); p.trailing_stop = ExtractString(response, "trailing_stop") == "true"; p.trail_points = ExtractDouble(response, "trail_points"); p.trail_distance = ExtractDouble(response, "trail_distance"); p.trail_percent = ExtractDouble(response, "trail_percent"); p.trail_act_points = ExtractDouble(response, "trail_activation_points"); p.trail_act_distance = ExtractDouble(response, "trail_activation_distance"); p.trail_act_percent = ExtractDouble(response, "trail_activation_percent"); p.breakeven = ExtractString(response, "breakeven") == "true"; p.be_trigger_points = ExtractDouble(response, "breakeven_trigger_points"); p.be_trigger_distance= ExtractDouble(response, "breakeven_trigger_distance"); p.be_trigger_percent = ExtractDouble(response, "breakeven_trigger_percent"); p.be_buffer = ExtractDouble(response, "breakeven_buffer"); FileDelete("riskdesk_order.json"); if (order_id == "" || symbol == "" || action == "") return; Print("RiskDesk order: ", action, " ", volume, " ", symbol, " price=", req_price, " SL=", sl, " TP=", tp, " exec=", p.exec_mode, " trail=", p.trailing_stop, " be=", p.breakeven); ExecuteOrder(order_id, symbol, action, volume, sl, tp, comment, req_price, p); } //+------------------------------------------------------------------+ // Execute the order //+------------------------------------------------------------------+ void ExecuteOrder(string order_id, string symbol, string action, double volume, double sl, double tp, string comment, double req_price, WebhookParams &p) { ENUM_ORDER_TYPE otype; if (action == "buy") otype = ORDER_TYPE_BUY; else if (action == "sell") otype = ORDER_TYPE_SELL; else if (action == "buy_limit") otype = ORDER_TYPE_BUY_LIMIT; else if (action == "sell_limit") otype = ORDER_TYPE_SELL_LIMIT; else if (action == "buy_stop") otype = ORDER_TYPE_BUY_STOP; else if (action == "sell_stop") otype = ORDER_TYPE_SELL_STOP; else if (action == "close_all") { CloseAll(symbol, order_id); return; } else if (action == "close_buy") { CloseByType(symbol, ORDER_TYPE_BUY, order_id); return; } else if (action == "close_sell") { CloseByType(symbol, ORDER_TYPE_SELL, order_id); return; } else if (action == "close_pending") { ClosePending(symbol, order_id); return; } else if (action == "close_ticket") { CloseTicket((ulong)volume, order_id); return; } else { WriteAck(order_id, 0, false, "Unknown action: " + action); return; } bool isPending = (otype != ORDER_TYPE_BUY && otype != ORDER_TYPE_SELL); bool isLong = (otype == ORDER_TYPE_BUY || otype == ORDER_TYPE_BUY_LIMIT || otype == ORDER_TYPE_BUY_STOP); double price; if (isPending && req_price > 0) { price = req_price; } else { price = isLong ? SymbolInfoDouble(symbol, SYMBOL_ASK) : SymbolInfoDouble(symbol, SYMBOL_BID); } if (price <= 0) { WriteAck(order_id, 0, false, "Symbol " + symbol + " not found or no price"); return; } int digs = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS); // Volume: explicit lots, or % of account equity if (volume <= 0 && p.size_percent > 0) volume = VolumeFromPercent(symbol, otype, price, p.size_percent); if (volume <= 0) volume = 1.0; // Execution mode netting (market orders only) if (!isPending && (p.exec_mode == "close_opposite" || p.exec_mode == "flip")) { double closedVol = CloseOppositeSide(symbol, isLong); if (closedVol > 0) Print("Netting (", p.exec_mode, "): closed ", closedVol, " lots opposite"); if (p.exec_mode == "close_opposite" && closedVol >= volume) { WriteAck(order_id, 0, true, "Netted: closed " + DoubleToString(closedVol, 2) + " lots, no new position"); return; } if (p.exec_mode == "close_opposite" && closedVol > 0) { volume = NormalizeDouble(volume - closedVol, 2); } } // Resolve SL/TP from whichever unit the webhook used if (sl == 0) { double dist = ResolvePriceDist(symbol, price, volume, p.sl_points, p.sl_distance, p.sl_percent, p.sl_money); if (dist > 0) sl = NormalizeDouble(isLong ? price - dist : price + dist, digs); } if (tp == 0) { double dist = ResolvePriceDist(symbol, price, volume, p.tp_points, p.tp_distance, p.tp_percent, p.tp_money); if (dist > 0) tp = NormalizeDouble(isLong ? price + dist : price - dist, digs); } MqlTradeRequest req = {}; MqlTradeResult res = {}; req.action = isPending ? TRADE_ACTION_PENDING : TRADE_ACTION_DEAL; req.symbol = symbol; req.volume = volume; req.type = otype; req.price = price; req.sl = sl; req.tp = tp; req.deviation = 20; req.magic = MagicNumber; req.comment = comment != "" ? comment : "RiskDesk"; req.type_filling = ORDER_FILLING_IOC; bool ok = OrderSend(req, res); string msg = ok ? "Ticket " + IntegerToString(res.order) : "Error " + IntegerToString(GetLastError()) + ": " + res.comment; Print(ok ? "SUCCESS: " : "FAILED: ", msg); WriteAck(order_id, (int)res.order, ok, msg); // Register position for active management if needed if (ok && !isPending && (p.trailing_stop || p.breakeven)) { double trailPts = ResolvePoints(symbol, price, p.trail_points, p.trail_distance, p.trail_percent); double trailActPts= ResolvePoints(symbol, price, p.trail_act_points, p.trail_act_distance, p.trail_act_percent); double beTrigPts = ResolvePoints(symbol, price, p.be_trigger_points, p.be_trigger_distance, p.be_trigger_percent); AddManagedPos((ulong)res.order, p.trailing_stop, trailPts, trailActPts, p.breakeven, beTrigPts, p.be_buffer); Print("Registered managed position: ticket=", res.order, " trail=", p.trailing_stop, " trail_pts=", trailPts, " trail_act=", trailActPts, " be=", p.breakeven, " be_trig=", beTrigPts, " be_buf=", p.be_buffer); } } //+------------------------------------------------------------------+ // Delete pending orders (optionally filtered by symbol) //+------------------------------------------------------------------+ void ClosePending(string symbol, string order_id) { int removed = 0; for (int i = OrdersTotal() - 1; i >= 0; i--) { ulong ticket = OrderGetTicket(i); if (ticket == 0) continue; if (symbol != "" && OrderGetString(ORDER_SYMBOL) != symbol) continue; MqlTradeRequest req = {}; MqlTradeResult res = {}; req.action = TRADE_ACTION_REMOVE; req.order = ticket; if (OrderSend(req, res)) removed++; } WriteAck(order_id, 0, removed > 0, "Removed " + IntegerToString(removed) + " pending orders"); } //+------------------------------------------------------------------+ // Close helpers //+------------------------------------------------------------------+ void CloseAll(string symbol, string order_id) { int closed = 0; for (int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if (PositionSelectByTicket(ticket) && (symbol == "" || PositionGetString(POSITION_SYMBOL) == symbol)) { MqlTradeRequest req = {}; MqlTradeResult res = {}; req.action = TRADE_ACTION_DEAL; req.position = ticket; req.symbol = PositionGetString(POSITION_SYMBOL); req.volume = PositionGetDouble(POSITION_VOLUME); req.type = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY ? ORDER_TYPE_SELL : ORDER_TYPE_BUY; req.price = req.type == ORDER_TYPE_BUY ? SymbolInfoDouble(req.symbol, SYMBOL_ASK) : SymbolInfoDouble(req.symbol, SYMBOL_BID); req.deviation = 20; req.magic = MagicNumber; req.comment = "RiskDesk close"; req.type_filling = ORDER_FILLING_IOC; if (OrderSend(req, res)) closed++; } } WriteAck(order_id, 0, closed > 0, "Closed " + IntegerToString(closed) + " positions"); } void CloseByType(string symbol, ENUM_ORDER_TYPE close_type, string order_id) { int closed = 0; for (int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if (PositionSelectByTicket(ticket) && (symbol == "" || PositionGetString(POSITION_SYMBOL) == symbol) && (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE) == (close_type == ORDER_TYPE_BUY ? POSITION_TYPE_BUY : POSITION_TYPE_SELL)) { MqlTradeRequest req = {}; MqlTradeResult res = {}; req.action = TRADE_ACTION_DEAL; req.position = ticket; req.symbol = PositionGetString(POSITION_SYMBOL); req.volume = PositionGetDouble(POSITION_VOLUME); req.type = close_type == ORDER_TYPE_BUY ? ORDER_TYPE_SELL : ORDER_TYPE_BUY; req.price = req.type == ORDER_TYPE_BUY ? SymbolInfoDouble(req.symbol, SYMBOL_ASK) : SymbolInfoDouble(req.symbol, SYMBOL_BID); req.deviation = 20; req.magic = MagicNumber; req.type_filling = ORDER_FILLING_IOC; if (OrderSend(req, res)) closed++; } } WriteAck(order_id, 0, closed > 0, "Closed " + IntegerToString(closed) + " positions"); } void CloseTicket(ulong close_ticket, string order_id) { if (!PositionSelectByTicket(close_ticket)) { WriteAck(order_id, 0, false, "Position " + IntegerToString((int)close_ticket) + " not found"); return; } MqlTradeRequest req = {}; MqlTradeResult res = {}; req.action = TRADE_ACTION_DEAL; req.position = close_ticket; req.symbol = PositionGetString(POSITION_SYMBOL); req.volume = PositionGetDouble(POSITION_VOLUME); req.type = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY ? ORDER_TYPE_SELL : ORDER_TYPE_BUY; req.price = req.type == ORDER_TYPE_BUY ? SymbolInfoDouble(req.symbol, SYMBOL_ASK) : SymbolInfoDouble(req.symbol, SYMBOL_BID); req.deviation = 20; req.magic = MagicNumber; req.comment = "RiskDesk close"; req.type_filling = ORDER_FILLING_IOC; bool ok = OrderSend(req, res); string msg = ok ? "Closed ticket " + IntegerToString((int)close_ticket) : "Error " + IntegerToString(GetLastError()) + ": " + res.comment; WriteAck(order_id, (int)close_ticket, ok, msg); } //+------------------------------------------------------------------+ // Write ack //+------------------------------------------------------------------+ void WriteAck(string order_id, int ticket, bool success, string message) { StringReplace(message, "\"", "'"); StringReplace(message, "\\", ""); Print("Ack: id=", order_id, " success=", success, " ticket=", ticket, " ", message); string body = "{\"id\":\"" + order_id + "\"," + "\"ticket\":" + IntegerToString(ticket) + "," + "\"success\":" + (success ? "true" : "false") + "," + "\"message\":\"" + message + "\"}"; int h = FileOpen("riskdesk_ack.json", FILE_WRITE|FILE_TXT|FILE_ANSI); if (h != INVALID_HANDLE) { FileWriteString(h, body); FileClose(h); Print("Ack file written OK"); } else { Print("ERROR: Could not write ack file! err=", GetLastError()); } } //+------------------------------------------------------------------+ // JSON extractors //+------------------------------------------------------------------+ string ExtractString(string json, string key) { string search = "\"" + key + "\":"; int pos = StringFind(json, search); if (pos < 0) return ""; pos += StringLen(search); while (pos < StringLen(json) && StringGetCharacter(json, pos) == ' ') pos++; if (pos >= StringLen(json) || StringGetCharacter(json, pos) != '"') return ""; pos++; int end = StringFind(json, "\"", pos); if (end < 0) return ""; return StringSubstr(json, pos, end - pos); } double ExtractDouble(string json, string key) { string search = "\"" + key + "\":"; int pos = StringFind(json, search); if (pos < 0) return 0; pos += StringLen(search); while (pos < StringLen(json) && StringGetCharacter(json, pos) == ' ') pos++; if (StringGetCharacter(json, pos) == '"') pos++; int end = pos; while (end < StringLen(json)) { ushort c = StringGetCharacter(json, end); if (c == ',' || c == '}' || c == '"') break; end++; } return StringToDouble(StringSubstr(json, pos, end - pos)); }