//+------------------------------------------------------------------+ //| wstradecopier.mq5 | //| Copyright 2022, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #include "..\..\..\Include\ws\wsclient.mqh" #include "..\..\..\Include\TradeUtils.mqh" #include "..\..\..\Include\DealMonitor.mqh" #include "..\..\..\Include\MqlTradeSync.mqh" #include "..\..\..\Include\PositionFilter.mqh" #include "..\..\..\Include\Tuples.mqh" #include "..\..\..\Include\toyjson.mqh" enum TRADE_ROLE { TRADE_PUBLISHER, // Trade Publisher TRADE_SUBSCRIBER // Trade Subscriber }; //+------------------------------------------------------------------+ //| I N P U T S | //+------------------------------------------------------------------+ input string Server = "ws://localhost:9000/"; input TRADE_ROLE Role = TRADE_PUBLISHER; input bool VerboseJson = true; // VerboseJson (user-friendly/machine-efficient) input group "Publisher"; input string PublisherID = "PUB_ID_001"; input string PublisherPrivateKey = "PUB_KEY_FFF"; input string SymbolFilter = ""; // SymbolFilter (empty - current, '*' - any) input ulong MagicFilter = 0; // MagicFilter (0 - any) input group "Subscriber"; input string SubscriberID = "SUB_ID_100"; input string SubscribeToPublisherID = "PUB_ID_001"; input string SubscriberAccessKey = "fd3f7a105eae8c2d9afce0a7a4e11bf267a40f04b7c216dd01cf78c7165a2a5a"; input string SymbolSubstitute = "EURUSD=GBPUSD"; // SymbolSubstitute (=,...) input ulong SubscriberMagic = 0; string Substitutes[][2]; //+------------------------------------------------------------------+ //| Initialization of symbol substitution list, which is also used | //| to accept actions on selected symbols only, | //| for example, EURUSD=EURUSD to allow trading EURUSD on receiving | //+------------------------------------------------------------------+ void FillSubstitutes() { string list[]; const int n = StringSplit(SymbolSubstitute, ',', list); ArrayResize(Substitutes, n); for(int i = 0; i < n; ++i) { string pair[]; if(StringSplit(list[i], '=', pair) == 2) { Substitutes[i][0] = pair[0]; Substitutes[i][1] = pair[1]; } else { Print("Wrong substitute: ", list[i]); } } } //+------------------------------------------------------------------+ //| Symbol substitution on subscriber | //+------------------------------------------------------------------+ string FindSubstitute(const string s) { for(int i = 0; i < ArrayRange(Substitutes, 0); ++i) { if(Substitutes[i][0] == s) return Substitutes[i][1]; } return NULL; } //+------------------------------------------------------------------+ //| Class for JSON representation of a trade request result | //+------------------------------------------------------------------+ struct MqlTradeResultWeb: public MqlTradeResult { MqlTradeResultWeb(const MqlTradeResult &r) { ZeroMemory(this); retcode = r.retcode; deal = r.deal; order = r.order; volume = r.volume; price = r.price; bid = r.bid; ask = r.ask; // not used in json comment = r.comment; request_id = r.request_id; retcode_external = r.retcode_external; } JsValue *asJsValue() const { JsValue *result = new JsValue(); result.put("code", retcode); if(deal) result.put("d", deal); if(order) result.put("o", order); if(volume) result.put("v", TU::StringOf(volume)); if(price) result.put("p", TU::StringOf(price)); if(bid) result.put("b", TU::StringOf(bid)); if(ask) result.put("a", TU::StringOf(ask)); return result; } }; //+------------------------------------------------------------------+ //| Class for JSON representation of a trade request | //+------------------------------------------------------------------+ struct MqlTradeRequestWeb: public MqlTradeRequest { MqlTradeRequestWeb(const MqlTradeRequest &r) { ZeroMemory(this); action = r.action; magic = r.magic; order = r.order; symbol = r.symbol; volume = r.volume; price = r.price; stoplimit = r.stoplimit; sl = r.sl; tp = r.tp; type = r.type; type_filling = r.type_filling; type_time = r.type_time; expiration = r.expiration; comment = r.comment; position = r.position; position_by = r.position_by; } JsValue *asJsValue() const { JsValue *req = new JsValue(); // main block: action, symbol, type req.put("a", VerboseJson ? EnumToString(action) : (string)action); // number or enum_xyz if(StringLen(symbol) != 0) req.put("s", symbol); req.put("t", VerboseJson ? EnumToString(type) : (string)type); // number or enum_xyz // volume block if(volume != 0) req.put("v", TU::StringOf(volume)); req.put("f", VerboseJson ? EnumToString(type_filling) : (string)type_filling); // number or enum_xyz // all prices block if(price != 0) req.put("p", TU::StringOf(price)); if(stoplimit != 0) req.put("x", TU::StringOf(stoplimit)); if(sl != 0) req.put("sl", TU::StringOf(sl)); if(tp != 0) req.put("tp", TU::StringOf(tp)); // pending block if(TU::IsPendingType(type)) { req.put("t", VerboseJson ? EnumToString(type_time) : (string)type_time); // number or enum_xyz if(expiration != 0) req.put("d", TimeToString(expiration)); } // modification block if(order != 0) req.put("o", order); if(position != 0) req.put("q", position); if(position_by != 0) req.put("b", position_by); // auxiliary block if(magic != 0) req.put("m", magic); if(StringLen(comment)) req.put("c", comment); return req; } }; //+------------------------------------------------------------------+ //| Class for JSON representation of a deal | //+------------------------------------------------------------------+ class DealMonitorWeb: public DealMonitor { public: DealMonitorWeb(const ulong t): DealMonitor(t) { } JsValue *asJsValue() const { JsValue *deal = new JsValue(); deal.put("d", get(DEAL_TICKET)); deal.put("o", get(DEAL_ORDER)); deal.put("t", TimeToString((datetime)get(DEAL_TIME), TIME_DATE | TIME_SECONDS)); deal.put("tmsc", get(DEAL_TIME_MSC)); deal.put("type", VerboseJson ? stringify(DEAL_TYPE) : (string)get(DEAL_TYPE)); deal.put("entry", VerboseJson ? stringify(DEAL_ENTRY) : (string)get(DEAL_ENTRY)); deal.put("pid", get(DEAL_POSITION_ID)); deal.put("r", VerboseJson ? stringify(DEAL_REASON) : (string)get(DEAL_REASON)); deal.put("v", TU::StringOf(get(DEAL_VOLUME))); deal.put("p", TU::StringOf(get(DEAL_PRICE))); if(get(DEAL_COMMISSION)) deal.put("com", TU::StringOf(get(DEAL_COMMISSION))); if(get(DEAL_SWAP)) deal.put("swap", TU::StringOf(get(DEAL_SWAP))); if(get(DEAL_PROFIT)) deal.put("m", TU::StringOf(get(DEAL_PROFIT))); if(get(DEAL_SL)) deal.put("sl", TU::StringOf(get(DEAL_SL))); if(get(DEAL_TP)) deal.put("tp", TU::StringOf(get(DEAL_TP))); deal.put("s", get(DEAL_SYMBOL)); if(StringLen(get(DEAL_COMMENT))) deal.put("c", get(DEAL_COMMENT)); if(StringLen(get(DEAL_EXTERNAL_ID))) deal.put("ext", get(DEAL_EXTERNAL_ID)); return deal; } }; //+------------------------------------------------------------------+ //| Custom client class to handle WebSocket events | //+------------------------------------------------------------------+ class MyWebSocket: public WebSocketClient { public: MyWebSocket(const string address, const bool compress = false): WebSocketClient(address, compress) { } /* void onConnected() override { } */ void onDisconnect() override { // ... // can do something more and call (or not) inherited code WebSocketClient::onDisconnect(); } void onMessage(IWebSocketMessage *msg) override { Alert(msg.getString()); JsValue *obj = JsParser::jsonify(msg.getString()); if(obj && obj["msg"]) { obj["msg"].print(); if(!RemoteTrade(obj["msg"])) { /* handle errors */ } delete obj; } delete msg; } }; MyWebSocket wss(Server); bool RemoteTrade(JsValue *obj) { bool success = false; if(obj["req"]["a"] == TRADE_ACTION_DEAL || obj["req"]["a"] == "TRADE_ACTION_DEAL") // enum values/strings are supported all the way { const string symbol = FindSubstitute(obj["req"]["s"].s); if(symbol == NULL) { Print("Suitable symbol not found for ", obj["req"]["s"].s); return false; // not allowed/not found } // NB: price, stop levels, lot limitations, etc are not analized here, // copy-trade is performed at current local price JsValue *pType = obj["req"]["t"]; if(pType == ORDER_TYPE_BUY || pType == ORDER_TYPE_SELL || pType == "ORDER_TYPE_BUY" || pType == "ORDER_TYPE_SELL") { ENUM_ORDER_TYPE type; if(pType.detect() >= JS_STRING) { if(pType == "ORDER_TYPE_BUY") type = ORDER_TYPE_BUY; else type = ORDER_TYPE_SELL; } else { type = obj["req"]["t"].get(); } MqlTradeRequestSync request; request.deviation = 10; request.magic = SubscriberMagic; request.type = type; const double lot = obj["req"]["v"].get(); JsValue *pDir = obj["deal"]["entry"]; if(pDir == DEAL_ENTRY_IN || pDir == "DEAL_ENTRY_IN") { success = request._market(symbol, lot) && request.completed(); Alert(StringFormat("Trade by subscription: market entry %s %s %s - %s", EnumToString(type), TU::StringOf(lot), symbol, success ? "Successful" : "Failed")); } else if(pDir == DEAL_ENTRY_OUT || pDir == "DEAL_ENTRY_OUT") { // this action assumes an existing position to close PositionFilter filter; int props[] = {POSITION_TICKET, POSITION_TYPE, POSITION_VOLUME}; Tuple3 values[]; filter.let(POSITION_SYMBOL, symbol).let(POSITION_MAGIC, SubscriberMagic).select(props, values); for(int i = 0; i < ArraySize(values) && !success; ++i) { if(!TU::IsSameType((ENUM_ORDER_TYPE)values[i]._2, type)) // opposite direction found { if(TU::Equal(values[i]._3, lot)) // sufficient volume found { success = request.close(values[i]._1, lot) && request.completed(); Alert(StringFormat("Trade by subscription: market exit %s %s %s - %s", EnumToString(type), TU::StringOf(lot), symbol, success ? "Successful" : "Failed")); } else { Print("Not enough volume in existing position ", values[i]._1); } } else { Print("Not required direction in existing position ", values[i]._1); } } if(!success) { Print("No suitable position to close"); } } else { Print("Unsupported trade direction ", pDir.stringify()); } } else { Print("Unsupported trade type ", pType.stringify()); } } else { Print("Not a trade command"); } return success; } PositionFilter Positions; //+------------------------------------------------------------------+ //| Initialization handler | //+------------------------------------------------------------------+ int OnInit() { Print("\n"); if(MagicFilter) Positions.let(POSITION_MAGIC, MagicFilter); if(SymbolFilter == "") Positions.let(POSITION_SYMBOL, _Symbol); else if(SymbolFilter != "*") Positions.let(POSITION_SYMBOL, SymbolFilter); FillSubstitutes(); EventSetTimer(1); wss.setTimeOut(1000); Print("Opening..."); string custom; if(Role == TRADE_PUBLISHER) { custom = "Sec-Websocket-Protocol: X-MQL5-publisher-" + PublisherID + "-" + PublisherPrivateKey + "\r\n"; } else { custom = "Sec-Websocket-Protocol: X-MQL5-subscriber-" + SubscriberID + "-" + SubscribeToPublisherID + "-" + SubscriberAccessKey + "\r\n"; } return wss.open(custom) ? INIT_SUCCEEDED : INIT_FAILED; } //+------------------------------------------------------------------+ //| Trade filter for publisher | //+------------------------------------------------------------------+ bool FilterMatched(const string s, const ulong m) { if(MagicFilter != 0 && MagicFilter != m) { return false; } if(StringLen(SymbolFilter) == 0) { if(s != _Symbol) { return false; } } else if(SymbolFilter != s && SymbolFilter != "*") { return false; } return true; } //+------------------------------------------------------------------+ //| Trade transactions handler | //+------------------------------------------------------------------+ void OnTradeTransaction(const MqlTradeTransaction &transaction, const MqlTradeRequest &request, const MqlTradeResult &result) { // debug // static ulong count = 0; // Print("(" + (string)count++ + ")"); // Print(TU::StringOf(transaction)); if(transaction.type == TRADE_TRANSACTION_REQUEST) { Print(TU::StringOf(request)); Print(TU::StringOf(result)); if(result.retcode == TRADE_RETCODE_PLACED // successful action || result.retcode == TRADE_RETCODE_DONE || result.retcode == TRADE_RETCODE_DONE_PARTIAL) { if(FilterMatched(request.symbol, request.magic)) { // container object for message, // can contain more sub-objects (status, advice, etc.), // it will be delivered to subscribers // as "msg" property of json in websocket-messages: // {"origin" : "this_publisher_id", "msg" : { your data goes here }} JsValue msg; MqlTradeRequestWeb req(request); msg.put("req", req.asJsValue()); MqlTradeResultWeb res(result); msg.put("res", res.asJsValue()); if(result.deal != 0) { DealMonitorWeb deal(result.deal); msg.put("deal", deal.asJsValue()); } ulong tickets[]; Positions.select(tickets); JsValue pos; pos.put("n", ArraySize(tickets)); msg.put("pos", &pos); // TODO: collect account status: // - positions and their props, // - pending orders and their props, // - margin level, drawdown, etc. string buffer; msg.stringify(buffer); Print(buffer); wss.send(buffer); } } } } //+------------------------------------------------------------------+ //| Timer events handler | //+------------------------------------------------------------------+ void OnTimer() { // if new messages arrive, call MyWebSocket::OnMessage() for each wss.checkMessages(false); // in timer use non-blocking check /* // Alternative way of processing messages // (if implemented - we may not define MyWebSocket and use // WebSocketClient wss(Server);) IWebSocketMessage *m; while((m = wss.readMessage(false)) != NULL) { Alert(m.getString()); // ... // other stuff which is currently in MyWebSocket::OnMessage() delete m; } */ } //+------------------------------------------------------------------+ //| Finalization handler | //+------------------------------------------------------------------+ void OnDeinit(const int) { if(wss.isConnected()) { Print("Closing..."); wss.close(); } } //+------------------------------------------------------------------+