497 lines
17 KiB
MQL5
497 lines
17 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| 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 (<from>=<to>,...)
|
|
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<Hybi>
|
|
{
|
|
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<Hybi>::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<ENUM_ORDER_TYPE>();
|
|
}
|
|
|
|
MqlTradeRequestSync request;
|
|
request.deviation = 10;
|
|
request.magic = SubscriberMagic;
|
|
request.type = type;
|
|
|
|
const double lot = obj["req"]["v"].get<double>();
|
|
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<long,long,double> 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<Hybi> 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();
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|