algorembrant commited on
Commit
59da845
·
verified ·
1 Parent(s): f11fdb0

Upload 25 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ SUM3API[[:space:]](local)/MQL5/Libraries/libsodium.dll filter=lfs diff=lfs merge=lfs -text
37
+ SUM3API[[:space:]](local)/MQL5/Libraries/libzmq.dll filter=lfs diff=lfs merge=lfs -text
SUM3API (local)/MQL5/Experts/ZmqPublisher.mq5 ADDED
@@ -0,0 +1,450 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //+------------------------------------------------------------------+
2
+ //| ZmqPublisher.mq5 |
3
+ //| Copyright 2024, Antigravity |
4
+ //| |
5
+ //+------------------------------------------------------------------+
6
+ #property copyright "Antigravity"
7
+ #property link ""
8
+ #property version "2.00"
9
+
10
+ // Include our ZMQ wrapper
11
+ #include <Zmq/Zmq.mqh>
12
+
13
+ // Include trading functions
14
+ #include <Trade/Trade.mqh>
15
+
16
+ // Input parameters
17
+ input string InpPubAddress = "tcp://0.0.0.0:5555"; // Tick Publisher Address
18
+ input string InpRepAddress = "tcp://0.0.0.0:5556"; // Order Handler Address
19
+ input double InpDefaultSlippage = 10; // Default Slippage (points)
20
+
21
+ CZmq *g_publisher; // PUB socket for tick data
22
+ CZmq *g_responder; // REP socket for order handling
23
+ CTrade g_trade; // Trading helper
24
+
25
+ //+------------------------------------------------------------------+
26
+ //| Expert initialization function |
27
+ //+------------------------------------------------------------------+
28
+ int OnInit()
29
+ {
30
+ Print("Initializing ZmqPublisher v2.0 with Order Support...");
31
+
32
+ // Initialize tick publisher (PUB socket)
33
+ g_publisher = new CZmq();
34
+ if(!g_publisher.Init(ZMQ_PUB)) {
35
+ Print("Failed to initialize ZMQ Publisher");
36
+ return(INIT_FAILED);
37
+ }
38
+ if(!g_publisher.Bind(InpPubAddress)) {
39
+ Print("Failed to bind publisher to ", InpPubAddress);
40
+ return(INIT_FAILED);
41
+ }
42
+ Print("Tick Publisher bound to ", InpPubAddress);
43
+
44
+ // Initialize order responder (REP socket)
45
+ g_responder = new CZmq();
46
+ if(!g_responder.Init(ZMQ_REP)) {
47
+ Print("Failed to initialize ZMQ Responder");
48
+ return(INIT_FAILED);
49
+ }
50
+ if(!g_responder.Bind(InpRepAddress)) {
51
+ Print("Failed to bind responder to ", InpRepAddress);
52
+ return(INIT_FAILED);
53
+ }
54
+ Print("Order Responder bound to ", InpRepAddress);
55
+
56
+ // Configure trade settings
57
+ g_trade.SetDeviationInPoints((ulong)InpDefaultSlippage);
58
+ g_trade.SetTypeFilling(ORDER_FILLING_IOC);
59
+
60
+ return(INIT_SUCCEEDED);
61
+ }
62
+
63
+ //+------------------------------------------------------------------+
64
+ //| Expert deinitialization function |
65
+ //+------------------------------------------------------------------+
66
+ void OnDeinit(const int reason)
67
+ {
68
+ Print("Deinitializing ZmqPublisher...");
69
+ if(g_publisher != NULL) {
70
+ g_publisher.Shutdown();
71
+ delete g_publisher;
72
+ g_publisher = NULL;
73
+ }
74
+ if(g_responder != NULL) {
75
+ g_responder.Shutdown();
76
+ delete g_responder;
77
+ g_responder = NULL;
78
+ }
79
+ }
80
+
81
+ //+------------------------------------------------------------------+
82
+ //| Process incoming order request |
83
+ //+------------------------------------------------------------------+
84
+ //+------------------------------------------------------------------+
85
+ //| Process incoming order request |
86
+ //+------------------------------------------------------------------+
87
+ string ProcessOrderRequest(string request)
88
+ {
89
+ // Expected JSON format:
90
+ // {"type":"market_buy"|"close_position"|"cancel_order"|...,
91
+ // "symbol":"XAUUSDc", "volume":0.01, "price":2000.0, "ticket":12345}
92
+
93
+ string orderType = ExtractJsonString(request, "type");
94
+ string symbol = ExtractJsonString(request, "symbol");
95
+ double volume = ExtractJsonDouble(request, "volume");
96
+ double price = ExtractJsonDouble(request, "price");
97
+ ulong ticket = (ulong)ExtractJsonDouble(request, "ticket"); // Simple extraction
98
+
99
+ if(symbol == "") symbol = _Symbol;
100
+ if(volume <= 0) volume = 0.01;
101
+
102
+ Print("Order request: type=", orderType, " symbol=", symbol, " vol=", volume, " price=", price, " ticket=", ticket);
103
+
104
+ bool success = false;
105
+ ulong resultTicket = 0;
106
+ string errorMsg = "";
107
+
108
+ // Execute order based on type
109
+ if(orderType == "market_buy") {
110
+ double askPrice = SymbolInfoDouble(symbol, SYMBOL_ASK);
111
+ success = g_trade.Buy(volume, symbol, askPrice, 0, 0, "Rust GUI Order");
112
+ if(success) resultTicket = g_trade.ResultOrder();
113
+ else errorMsg = GetLastErrorDescription();
114
+ }
115
+ else if(orderType == "market_sell") {
116
+ double bidPrice = SymbolInfoDouble(symbol, SYMBOL_BID);
117
+ success = g_trade.Sell(volume, symbol, bidPrice, 0, 0, "Rust GUI Order");
118
+ if(success) resultTicket = g_trade.ResultOrder();
119
+ else errorMsg = GetLastErrorDescription();
120
+ }
121
+ else if(orderType == "limit_buy") {
122
+ success = g_trade.BuyLimit(volume, price, symbol, 0, 0, ORDER_TIME_GTC, 0, "Rust GUI Limit");
123
+ if(success) resultTicket = g_trade.ResultOrder();
124
+ else errorMsg = GetLastErrorDescription();
125
+ }
126
+ else if(orderType == "limit_sell") {
127
+ success = g_trade.SellLimit(volume, price, symbol, 0, 0, ORDER_TIME_GTC, 0, "Rust GUI Limit");
128
+ if(success) resultTicket = g_trade.ResultOrder();
129
+ else errorMsg = GetLastErrorDescription();
130
+ }
131
+ else if(orderType == "stop_buy") {
132
+ success = g_trade.BuyStop(volume, price, symbol, 0, 0, ORDER_TIME_GTC, 0, "Rust GUI Stop");
133
+ if(success) resultTicket = g_trade.ResultOrder();
134
+ else errorMsg = GetLastErrorDescription();
135
+ }
136
+ else if(orderType == "stop_sell") {
137
+ success = g_trade.SellStop(volume, price, symbol, 0, 0, ORDER_TIME_GTC, 0, "Rust GUI Stop");
138
+ if(success) resultTicket = g_trade.ResultOrder();
139
+ else errorMsg = GetLastErrorDescription();
140
+ }
141
+ else if(orderType == "close_position") {
142
+ if(ticket > 0) {
143
+ success = g_trade.PositionClose(ticket);
144
+ if(success) errorMsg = "Position closed";
145
+ else errorMsg = GetLastErrorDescription();
146
+ } else {
147
+ errorMsg = "Invalid ticket for close_position";
148
+ }
149
+ }
150
+ else if(orderType == "cancel_order") {
151
+ if(ticket > 0) {
152
+ success = g_trade.OrderDelete(ticket);
153
+ if(success) errorMsg = "Order deleted";
154
+ else errorMsg = GetLastErrorDescription();
155
+ } else {
156
+ errorMsg = "Invalid ticket for cancel_order";
157
+ }
158
+ }
159
+ else if(orderType == "download_history") {
160
+ // Format: {type: "download_history", symbol: "XAUUSD", timeframe: "M1", start: "2024.01.01", end: "2024.01.02", mode: "OHLC"|"TICKS"}
161
+ string tfStr = ExtractJsonString(request, "timeframe");
162
+ string startStr = ExtractJsonString(request, "start");
163
+ string endStr = ExtractJsonString(request, "end");
164
+ string mode = ExtractJsonString(request, "mode");
165
+
166
+ if(mode == "") mode = "OHLC";
167
+
168
+ success = DownloadHistory(symbol, tfStr, startStr, endStr, mode, errorMsg);
169
+ }
170
+ else {
171
+ errorMsg = "Unknown order type: " + orderType;
172
+ }
173
+
174
+ // Build response JSON
175
+ string response;
176
+ if(success) {
177
+ if(orderType == "download_history") {
178
+ // ensure errorMsg contains the filename if success
179
+ StringConcatenate(response, "{\"success\":true,\"message\":\"", errorMsg, "\"}");
180
+ } else {
181
+ StringConcatenate(response, "{\"success\":true,\"ticket\":", IntegerToString(resultTicket), "}");
182
+ }
183
+ } else {
184
+ StringConcatenate(response, "{\"success\":false,\"error\":\"", errorMsg, "\"}");
185
+ }
186
+
187
+ return response;
188
+ }
189
+
190
+ //+------------------------------------------------------------------+
191
+ //| Download History - Returns CSV content via ZMQ |
192
+ //+------------------------------------------------------------------+
193
+ bool DownloadHistory(string symbol, string tfStr, string startStr, string endStr, string mode, string &resultMsg)
194
+ {
195
+ datetime start = StringToTime(startStr);
196
+ datetime end = StringToTime(endStr);
197
+ if(start == 0) start = D'2024.01.01 00:00'; // Default fallback
198
+ if(end == 0) end = TimeCurrent();
199
+
200
+ ENUM_TIMEFRAMES tf = PERIOD_M1;
201
+ if(tfStr == "M5") tf = PERIOD_M5;
202
+ else if(tfStr == "M15") tf = PERIOD_M15;
203
+ else if(tfStr == "H1") tf = PERIOD_H1;
204
+ else if(tfStr == "H4") tf = PERIOD_H4;
205
+ else if(tfStr == "D1") tf = PERIOD_D1;
206
+
207
+ string csvContent = "";
208
+ int count = 0;
209
+
210
+ // Use |NL| as line separator (JSON-safe, Rust will convert to real newlines)
211
+ string NL = "|NL|";
212
+
213
+ if(mode == "TICKS") {
214
+ MqlTick ticks[];
215
+ int received = CopyTicksRange(symbol, ticks, COPY_TICKS_ALL, start * 1000, end * 1000);
216
+
217
+ if(received > 0) {
218
+ csvContent = "Time,Bid,Ask,Volume" + NL;
219
+ for(int i=0; i<received && i<50000; i++) { // Limit to 50k rows
220
+ csvContent += TimeToString(ticks[i].time, TIME_DATE|TIME_SECONDS) + "," +
221
+ DoubleToString(ticks[i].bid, _Digits) + "," +
222
+ DoubleToString(ticks[i].ask, _Digits) + "," +
223
+ IntegerToString(ticks[i].volume) + NL;
224
+ }
225
+ count = MathMin(received, 50000);
226
+ }
227
+ }
228
+ else {
229
+ // OHLC
230
+ MqlRates rates[];
231
+ ArraySetAsSeries(rates, false);
232
+ int received = CopyRates(symbol, tf, start, end, rates);
233
+
234
+ if(received > 0) {
235
+ csvContent = "Time,Open,High,Low,Close,TickVol,Spread" + NL;
236
+ for(int i=0; i<received && i<100000; i++) { // Limit to 100k rows
237
+ csvContent += TimeToString(rates[i].time, TIME_DATE|TIME_MINUTES) + "," +
238
+ DoubleToString(rates[i].open, _Digits) + "," +
239
+ DoubleToString(rates[i].high, _Digits) + "," +
240
+ DoubleToString(rates[i].low, _Digits) + "," +
241
+ DoubleToString(rates[i].close, _Digits) + "," +
242
+ IntegerToString(rates[i].tick_volume) + "," +
243
+ IntegerToString(rates[i].spread) + NL;
244
+ }
245
+ count = MathMin(received, 100000);
246
+ }
247
+ }
248
+
249
+ if(count > 0) {
250
+ // Return CSV content in a special format that Rust can parse
251
+ // We use ||CSV_DATA|| as delimiter to separate count info from actual data
252
+ resultMsg = IntegerToString(count) + " records||CSV_DATA||" + csvContent;
253
+ return true;
254
+ } else {
255
+ resultMsg = "No data found for period";
256
+ return false;
257
+ }
258
+ }
259
+
260
+ //+------------------------------------------------------------------+
261
+ //| Extract string value from JSON |
262
+ //+------------------------------------------------------------------+
263
+ string ExtractJsonString(string json, string key)
264
+ {
265
+ string searchKey = "\"" + key + "\":\"";
266
+ int startPos = StringFind(json, searchKey);
267
+ if(startPos < 0) return "";
268
+
269
+ startPos += StringLen(searchKey);
270
+ int endPos = StringFind(json, "\"", startPos);
271
+ if(endPos < 0) return "";
272
+
273
+ return StringSubstr(json, startPos, endPos - startPos);
274
+ }
275
+
276
+ //+------------------------------------------------------------------+
277
+ //| Extract double value from JSON |
278
+ //+------------------------------------------------------------------+
279
+ double ExtractJsonDouble(string json, string key)
280
+ {
281
+ string searchKey = "\"" + key + "\":";
282
+ int startPos = StringFind(json, searchKey);
283
+ if(startPos < 0) return 0.0;
284
+
285
+ startPos += StringLen(searchKey);
286
+
287
+ // Find end of number (comma, }, or end of string)
288
+ int endPos = startPos;
289
+ int len = StringLen(json);
290
+ while(endPos < len) {
291
+ ushort ch = StringGetCharacter(json, endPos);
292
+ if(ch == ',' || ch == '}' || ch == ' ') break;
293
+ endPos++;
294
+ }
295
+
296
+ string valueStr = StringSubstr(json, startPos, endPos - startPos);
297
+ return StringToDouble(valueStr);
298
+ }
299
+
300
+ //+------------------------------------------------------------------+
301
+ //| Get human-readable error description |
302
+ //+------------------------------------------------------------------+
303
+ string GetLastErrorDescription()
304
+ {
305
+ int err = GetLastError();
306
+ return "Error " + IntegerToString(err) + ": " + ErrorDescription(err);
307
+ }
308
+
309
+ //+------------------------------------------------------------------+
310
+ //| Error description helper |
311
+ //+------------------------------------------------------------------+
312
+ string ErrorDescription(int error)
313
+ {
314
+ switch(error) {
315
+ case 0: return "No error";
316
+ case 10004: return "Requote";
317
+ case 10006: return "Request rejected";
318
+ case 10007: return "Request canceled by trader";
319
+ case 10010: return "Request rejected - only part of the request was fulfilled";
320
+ case 10011: return "Request error";
321
+ case 10012: return "Request canceled due to timeout";
322
+ case 10013: return "Invalid request";
323
+ case 10014: return "Invalid volume";
324
+ case 10015: return "Invalid price";
325
+ case 10016: return "Invalid stops";
326
+ case 10017: return "Trade disabled";
327
+ case 10018: return "Market is closed";
328
+ case 10019: return "Not enough money";
329
+ case 10020: return "Prices changed";
330
+ case 10021: return "No quotes to process request";
331
+ case 10022: return "Invalid order expiration date";
332
+ case 10023: return "Order state changed";
333
+ case 10024: return "Too many requests";
334
+ case 10025: return "No changes in request";
335
+ case 10026: return "Autotrading disabled by server";
336
+ case 10027: return "Autotrading disabled by client terminal";
337
+ case 10028: return "Request locked for processing";
338
+ case 10029: return "Long positions only allowed";
339
+ case 10030: return "Maximum position volume exceeded";
340
+ default: return "Unknown error";
341
+ }
342
+ }
343
+
344
+ //+------------------------------------------------------------------+
345
+ //| Expert tick function |
346
+ //+------------------------------------------------------------------+
347
+ void OnTick()
348
+ {
349
+ // Handle order requests (non-blocking)
350
+ if(g_responder != NULL) {
351
+ string request = g_responder.Receive(true);
352
+ if(request != "") {
353
+ Print("Received order request: ", request);
354
+ string response = ProcessOrderRequest(request);
355
+ g_responder.Send(response, false); // Blocking send for REP pattern
356
+ Print("Sent response: ", response);
357
+ }
358
+ }
359
+
360
+ // Publish tick data with account info
361
+ if(g_publisher == NULL) return;
362
+
363
+ MqlTick tick;
364
+ if(SymbolInfoTick(_Symbol, tick)) {
365
+ // Get account info
366
+ double balance = AccountInfoDouble(ACCOUNT_BALANCE);
367
+ double equity = AccountInfoDouble(ACCOUNT_EQUITY);
368
+ double margin = AccountInfoDouble(ACCOUNT_MARGIN);
369
+ double freeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
370
+
371
+ // Get symbol trading constraints
372
+ double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
373
+ double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
374
+ double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
375
+
376
+ // Get Active Positions (Only for current symbol to simplify)
377
+ string positionsJson = "[";
378
+ int posCount = PositionsTotal();
379
+ bool firstPos = true;
380
+ for(int i = 0; i < posCount; i++) {
381
+ ulong ticket = PositionGetTicket(i);
382
+ if(PositionSelectByTicket(ticket)) {
383
+ if(PositionGetString(POSITION_SYMBOL) == _Symbol) {
384
+ if(!firstPos) StringAdd(positionsJson, ",");
385
+
386
+ string posType = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) ? "BUY" : "SELL";
387
+ StringAdd(positionsJson, "{\"ticket\":" + IntegerToString(ticket) +
388
+ ",\"type\":\"" + posType + "\"" +
389
+ ",\"volume\":" + DoubleToString(PositionGetDouble(POSITION_VOLUME), 2) +
390
+ ",\"price\":" + DoubleToString(PositionGetDouble(POSITION_PRICE_OPEN), _Digits) +
391
+ ",\"profit\":" + DoubleToString(PositionGetDouble(POSITION_PROFIT), 2) +
392
+ "}");
393
+ firstPos = false;
394
+ }
395
+ }
396
+ }
397
+ StringAdd(positionsJson, "]");
398
+
399
+ // Get Pending Orders (Only for current symbol)
400
+ string ordersJson = "[";
401
+ int orderCount = OrdersTotal();
402
+ bool firstOrder = true;
403
+ for(int i = 0; i < orderCount; i++) {
404
+ ulong ticket = OrderGetTicket(i);
405
+ if(OrderSelect(ticket)) {
406
+ if(OrderGetString(ORDER_SYMBOL) == _Symbol) {
407
+ if(!firstOrder) StringAdd(ordersJson, ",");
408
+
409
+ ENUM_ORDER_TYPE type = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE);
410
+ string orderTypeStr = "UNKNOWN";
411
+ if(type == ORDER_TYPE_BUY_LIMIT) orderTypeStr = "BUY LIMIT";
412
+ else if(type == ORDER_TYPE_SELL_LIMIT) orderTypeStr = "SELL LIMIT";
413
+ else if(type == ORDER_TYPE_BUY_STOP) orderTypeStr = "BUY STOP";
414
+ else if(type == ORDER_TYPE_SELL_STOP) orderTypeStr = "SELL STOP";
415
+
416
+ StringAdd(ordersJson, "{\"ticket\":" + IntegerToString(ticket) +
417
+ ",\"type\":\"" + orderTypeStr + "\"" +
418
+ ",\"volume\":" + DoubleToString(OrderGetDouble(ORDER_VOLUME_INITIAL), 2) +
419
+ ",\"price\":" + DoubleToString(OrderGetDouble(ORDER_PRICE_OPEN), _Digits) +
420
+ "}");
421
+ firstOrder = false;
422
+ }
423
+ }
424
+ }
425
+ StringAdd(ordersJson, "]");
426
+
427
+ // Create JSON with tick data + account info + positions + orders
428
+ string json;
429
+ StringConcatenate(json, "{\"symbol\":\"", _Symbol,
430
+ "\",\"bid\":", DoubleToString(tick.bid, _Digits),
431
+ ",\"ask\":", DoubleToString(tick.ask, _Digits),
432
+ ",\"time\":", IntegerToString(tick.time),
433
+ ",\"volume\":", IntegerToString(tick.volume),
434
+ ",\"balance\":", DoubleToString(balance, 2),
435
+ ",\"equity\":", DoubleToString(equity, 2),
436
+ ",\"margin\":", DoubleToString(margin, 2),
437
+ ",\"free_margin\":", DoubleToString(freeMargin, 2),
438
+ ",\"min_lot\":", DoubleToString(minLot, 2),
439
+ ",\"max_lot\":", DoubleToString(maxLot, 2),
440
+ ",\"lot_step\":", DoubleToString(lotStep, 2),
441
+ ",\"positions\":", positionsJson,
442
+ ",\"orders\":", ordersJson,
443
+ "}");
444
+
445
+ g_publisher.Send(json);
446
+ // Print("Published: ", json); // Uncomment for debugging (spammy)
447
+ }
448
+ }
449
+
450
+ //+------------------------------------------------------------------+
SUM3API (local)/MQL5/Include/Zmq/Zmq.mqh ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //+------------------------------------------------------------------+
2
+ //| Zmq.mqh |
3
+ //| Copyright 2026, Algorembrant |
4
+ //| |
5
+ //+------------------------------------------------------------------+
6
+ #property copyright "Algorembrant"
7
+ #property link "https://github.com/ContinualQuasars/SUM3API"
8
+ #property version "2.00"
9
+ #property strict
10
+
11
+ // Define ZMQ constants
12
+ #define ZMQ_PUB 1
13
+ #define ZMQ_SUB 2
14
+ #define ZMQ_REQ 3
15
+ #define ZMQ_REP 4
16
+
17
+ #define ZMQ_NOBLOCK 1
18
+
19
+ // Import functions from libzmq.dll
20
+ // NOTE: Make sure libzmq.dll is in your MQL5/Libraries folder
21
+ // Handles are void* (64-bit on x64), so using 'long' works for both 32 (too big but safe) and 64 bit
22
+ #import "libzmq.dll"
23
+ long zmq_ctx_new();
24
+ int zmq_ctx_term(long context);
25
+ long zmq_socket(long context, int type);
26
+ int zmq_close(long socket);
27
+ int zmq_bind(long socket, uchar &endpoint[]);
28
+ int zmq_connect(long socket, uchar &endpoint[]);
29
+ int zmq_send(long socket, uchar &buf[], int len, int flags);
30
+ int zmq_recv(long socket, uchar &buf[], int len, int flags);
31
+ int zmq_errno();
32
+ #import
33
+
34
+ class CZmq {
35
+ private:
36
+ long m_context;
37
+ long m_socket;
38
+ bool m_initialized;
39
+
40
+ public:
41
+ CZmq() {
42
+ m_context = 0;
43
+ m_socket = 0;
44
+ m_initialized = false;
45
+ }
46
+
47
+ ~CZmq() {
48
+ Shutdown();
49
+ }
50
+
51
+ bool Init(int type) {
52
+ if(m_initialized) return true;
53
+
54
+ m_context = zmq_ctx_new();
55
+ if(m_context == 0) {
56
+ Print("ZMQ Init failed: Context creation error");
57
+ return false;
58
+ }
59
+
60
+ m_socket = zmq_socket(m_context, type);
61
+ if(m_socket == 0) {
62
+ Print("ZMQ Init failed: Socket creation error");
63
+ return false;
64
+ }
65
+
66
+ m_initialized = true;
67
+ return true;
68
+ }
69
+
70
+ bool Bind(string endpoint) {
71
+ if(!m_initialized) return false;
72
+
73
+ uchar data[];
74
+ StringToCharArray(endpoint, data, 0, WHOLE_ARRAY, CP_UTF8);
75
+
76
+ int rc = zmq_bind(m_socket, data);
77
+ if(rc != 0) {
78
+ Print("ZMQ Bind failed. Error: ", zmq_errno());
79
+ return false;
80
+ }
81
+ return true;
82
+ }
83
+
84
+ bool Connect(string endpoint) {
85
+ if(!m_initialized) return false;
86
+
87
+ uchar data[];
88
+ StringToCharArray(endpoint, data, 0, WHOLE_ARRAY, CP_UTF8);
89
+
90
+ int rc = zmq_connect(m_socket, data);
91
+ if(rc != 0) {
92
+ Print("ZMQ Connect failed. Error: ", zmq_errno());
93
+ return false;
94
+ }
95
+ return true;
96
+ }
97
+
98
+ int Send(string message, bool nonBlocking = true) {
99
+ if(!m_initialized) return -1;
100
+
101
+ uchar data[];
102
+ StringToCharArray(message, data, 0, WHOLE_ARRAY, CP_UTF8);
103
+ // StringToCharArray includes null terminator, we might not want to send it
104
+ // ZMQ messages are just bytes.
105
+ // -1 because array size includes null char, usually we check ArraySize(data)
106
+ int len = ArraySize(data) - 1;
107
+ if (len < 0) len = 0;
108
+
109
+ int flags = 0;
110
+ if(nonBlocking) flags = ZMQ_NOBLOCK;
111
+
112
+ int bytesSent = zmq_send(m_socket, data, len, flags);
113
+ return bytesSent;
114
+ }
115
+
116
+ // Non-blocking receive - returns empty string if no message available
117
+ string Receive(bool nonBlocking = true) {
118
+ if(!m_initialized) return "";
119
+
120
+ uchar buffer[4096];
121
+ ArrayInitialize(buffer, 0);
122
+
123
+ int flags = 0;
124
+ if(nonBlocking) flags = ZMQ_NOBLOCK;
125
+
126
+ int bytesReceived = zmq_recv(m_socket, buffer, ArraySize(buffer) - 1, flags);
127
+
128
+ if(bytesReceived <= 0) return "";
129
+
130
+ return CharArrayToString(buffer, 0, bytesReceived, CP_UTF8);
131
+ }
132
+
133
+ void Shutdown() {
134
+ if(m_socket != 0) {
135
+ zmq_close(m_socket);
136
+ m_socket = 0;
137
+ }
138
+ if(m_context != 0) {
139
+ zmq_ctx_term(m_context);
140
+ m_context = 0;
141
+ }
142
+ m_initialized = false;
143
+ }
144
+ };
SUM3API (local)/MQL5/Libraries/libsodium.dll ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7745aad20b9578c70b0fff48b3ee5f982c840e0f17b3dc7239a23d4fb36b0717
3
+ size 302592
SUM3API (local)/MQL5/Libraries/libzmq.dll ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:19567709cb7ef4664249d46a796f23c06abbe6ba91db3c650aeaa372b4fbf989
3
+ size 451072
SUM3API (local)/Rustmt5-chart/Cargo.lock ADDED
The diff for this file is too large to render. See raw diff
 
SUM3API (local)/Rustmt5-chart/Cargo.toml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [package]
2
+ name = "Rustmt5-chart"
3
+ version = "0.1.0"
4
+ edition = "2026"
5
+
6
+ [dependencies]
7
+ eframe = "0.27.1"
8
+ egui = "0.27.1"
9
+ egui_plot = "0.27.1"
10
+ zeromq = "0.5.0-pre"
11
+ serde = { version = "1.0.197", features = ["derive"] }
12
+ serde_json = "1.0.114"
13
+ tokio = { version = "1.36.0", features = ["full"] }
14
+ futures = "0.3.30"
15
+ chrono = "0.4.43"
SUM3API (local)/Rustmt5-chart/src/main.rs ADDED
@@ -0,0 +1,852 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //+------------------------------------------------------------------+
2
+ //| main.rs |
3
+ //| Copyright 2026, Algorembrant |
4
+ //| |
5
+ //+------------------------------------------------------------------+
6
+ //property copyright "Algorembrant"
7
+ //property link "https://github.com/ContinualQuasars/SUM3API"
8
+ //property version "2.00"
9
+ //property strict
10
+
11
+ use eframe::egui;
12
+ use egui_plot::{Line, Plot, PlotPoints};
13
+ use serde::{Deserialize, Serialize};
14
+ use tokio::sync::mpsc;
15
+ use zeromq::{Socket, SocketRecv, SocketSend};
16
+ use std::fs::{self, OpenOptions};
17
+ use std::io::Write;
18
+ use std::path::PathBuf;
19
+
20
+ // ============================================================================
21
+ // Data Structures
22
+ // ============================================================================
23
+
24
+ #[derive(Clone, Debug, Deserialize)]
25
+ #[allow(dead_code)]
26
+ struct PositionData {
27
+ ticket: u64,
28
+ #[serde(rename = "type")]
29
+ pos_type: String, // "BUY" or "SELL"
30
+ volume: f64,
31
+ price: f64,
32
+ profit: f64,
33
+ }
34
+
35
+ #[derive(Clone, Debug, Deserialize)]
36
+ #[allow(dead_code)]
37
+ struct PendingOrderData {
38
+ ticket: u64,
39
+ #[serde(rename = "type")]
40
+ order_type: String, // "BUY LIMIT", "SELL STOP", etc.
41
+ volume: f64,
42
+ price: f64,
43
+ }
44
+
45
+ #[derive(Clone, Debug, Deserialize)]
46
+ struct TickData {
47
+ symbol: String,
48
+ bid: f64,
49
+ ask: f64,
50
+ time: i64,
51
+ #[serde(default)]
52
+ volume: u64,
53
+ // Account info
54
+ #[serde(default)]
55
+ balance: f64,
56
+ #[serde(default)]
57
+ equity: f64,
58
+ #[serde(default)]
59
+ margin: f64,
60
+ #[serde(default)]
61
+ free_margin: f64,
62
+ // Trading constraints
63
+ #[serde(default)]
64
+ min_lot: f64,
65
+ #[serde(default)]
66
+ max_lot: f64,
67
+ #[serde(default)]
68
+ lot_step: f64,
69
+
70
+ // Active trades
71
+ #[serde(default)]
72
+ positions: Vec<PositionData>,
73
+ #[serde(default)]
74
+ orders: Vec<PendingOrderData>,
75
+ }
76
+
77
+ #[derive(Clone, Debug, Serialize)]
78
+ struct OrderRequest {
79
+ #[serde(rename = "type")]
80
+ order_type: String,
81
+ symbol: String,
82
+ volume: f64,
83
+ price: f64,
84
+ #[serde(default)]
85
+ ticket: u64, // For close/cancel
86
+ // History params
87
+ #[serde(skip_serializing_if = "Option::is_none")]
88
+ timeframe: Option<String>,
89
+ #[serde(skip_serializing_if = "Option::is_none")]
90
+ start: Option<String>,
91
+ #[serde(skip_serializing_if = "Option::is_none")]
92
+ end: Option<String>,
93
+ #[serde(skip_serializing_if = "Option::is_none")]
94
+ mode: Option<String>,
95
+ #[serde(skip_serializing_if = "Option::is_none")]
96
+ request_id: Option<u64>, // Unique ID for history downloads
97
+ }
98
+
99
+ #[derive(Clone, Debug, Deserialize)]
100
+ struct OrderResponse {
101
+ success: bool,
102
+ ticket: Option<i64>,
103
+ error: Option<String>,
104
+ message: Option<String>,
105
+ }
106
+
107
+ // Struct for tracking order execution breaklines on chart
108
+ #[derive(Clone, Debug)]
109
+ struct OrderBreakline {
110
+ index: usize, // Data index where order was executed
111
+ order_type: String, // "BUY" or "SELL" variant
112
+ ticket: i64, // Order ticket number
113
+ }
114
+
115
+ // ============================================================================
116
+ // Application State
117
+ // ============================================================================
118
+
119
+ struct Mt5ChartApp {
120
+ // Tick data
121
+ tick_receiver: mpsc::Receiver<TickData>,
122
+ data: Vec<TickData>,
123
+ symbol: String,
124
+
125
+ // Latest account info
126
+ balance: f64,
127
+ equity: f64,
128
+ margin: f64,
129
+ free_margin: f64,
130
+ min_lot: f64,
131
+ max_lot: f64,
132
+ lot_step: f64,
133
+
134
+ // Order handling
135
+ order_sender: mpsc::Sender<OrderRequest>,
136
+ response_receiver: mpsc::Receiver<OrderResponse>,
137
+
138
+ // UI state for order panel
139
+ lot_size: f64,
140
+ lot_size_str: String,
141
+ limit_price: String,
142
+ #[allow(dead_code)]
143
+ stop_price: String,
144
+ last_order_result: Option<String>,
145
+
146
+ // History Download UI
147
+ history_start_date: String,
148
+ history_end_date: String,
149
+ history_tf: String,
150
+ history_mode: String,
151
+
152
+ // Live Recording
153
+ is_recording: bool,
154
+ live_record_file: Option<std::fs::File>,
155
+
156
+ // Live Trade Data
157
+ positions: Vec<PositionData>,
158
+ pending_orders: Vec<PendingOrderData>,
159
+
160
+ // CSV Output Management
161
+ output_dir: PathBuf,
162
+ request_counter: u64,
163
+
164
+ // Order Breaklines for Chart
165
+ order_breaklines: Vec<OrderBreakline>,
166
+ pending_order_type: Option<String>, // Track what type of order is pending
167
+
168
+ // Pending history request info for CSV naming
169
+ pending_history_request: Option<(u64, String, String, String)>, // (id, symbol, tf, mode)
170
+ }
171
+
172
+ impl Mt5ChartApp {
173
+ fn new(
174
+ tick_receiver: mpsc::Receiver<TickData>,
175
+ order_sender: mpsc::Sender<OrderRequest>,
176
+ response_receiver: mpsc::Receiver<OrderResponse>,
177
+ ) -> Self {
178
+ // Defaults dates to "yyyy.mm.dd"
179
+ let now = chrono::Local::now();
180
+ let today_str = now.format("%Y.%m.%d").to_string();
181
+
182
+ // Ensure output directory exists
183
+ let output_dir = PathBuf::from("output");
184
+ fs::create_dir_all(&output_dir).ok();
185
+
186
+ Self {
187
+ tick_receiver,
188
+ data: Vec::new(),
189
+ symbol: "Waiting for data...".to_string(),
190
+ balance: 0.0,
191
+ equity: 0.0,
192
+ margin: 0.0,
193
+ free_margin: 0.0,
194
+ min_lot: 0.01,
195
+ max_lot: 100.0,
196
+ lot_step: 0.01,
197
+ order_sender,
198
+ response_receiver,
199
+ lot_size: 0.01,
200
+ lot_size_str: "0.01".to_string(),
201
+ limit_price: "0.0".to_string(),
202
+ stop_price: "0.0".to_string(),
203
+ last_order_result: None,
204
+
205
+ history_start_date: today_str.clone(),
206
+ history_end_date: today_str,
207
+ history_tf: "M1".to_string(),
208
+ history_mode: "OHLC".to_string(),
209
+
210
+ is_recording: false,
211
+ live_record_file: None,
212
+
213
+ positions: Vec::new(),
214
+ pending_orders: Vec::new(),
215
+
216
+ // Initialize new fields
217
+ output_dir,
218
+ request_counter: 0,
219
+ order_breaklines: Vec::new(),
220
+ pending_order_type: None,
221
+ pending_history_request: None,
222
+ }
223
+ }
224
+
225
+ fn send_order(&mut self, order_type: &str, price: Option<f64>, ticket: Option<u64>) {
226
+ let price_val = price.unwrap_or(0.0);
227
+ let ticket_val = ticket.unwrap_or(0);
228
+
229
+ // Track order type for breakline visualization (only for market orders)
230
+ if order_type.contains("market") {
231
+ self.pending_order_type = Some(order_type.to_string());
232
+ }
233
+
234
+ let request = OrderRequest {
235
+ order_type: order_type.to_string(),
236
+ symbol: self.symbol.clone(),
237
+ volume: self.lot_size,
238
+ price: price_val,
239
+ ticket: ticket_val,
240
+ timeframe: None,
241
+ start: None,
242
+ end: None,
243
+ mode: None,
244
+ request_id: None,
245
+ };
246
+
247
+ self.send_request_impl(request);
248
+ }
249
+
250
+ fn send_download_request(&mut self) {
251
+ // Increment counter for unique history download ID
252
+ self.request_counter += 1;
253
+
254
+ // Store request info for CSV filename generation when response arrives
255
+ self.pending_history_request = Some((
256
+ self.request_counter,
257
+ self.symbol.replace("/", "-"),
258
+ self.history_tf.clone(),
259
+ self.history_mode.clone(),
260
+ ));
261
+
262
+ let request = OrderRequest {
263
+ order_type: "download_history".to_string(),
264
+ symbol: self.symbol.clone(),
265
+ volume: 0.0,
266
+ price: 0.0,
267
+ ticket: 0,
268
+ timeframe: Some(self.history_tf.clone()),
269
+ start: Some(self.history_start_date.clone()),
270
+ end: Some(self.history_end_date.clone()),
271
+ mode: Some(self.history_mode.clone()),
272
+ request_id: Some(self.request_counter),
273
+ };
274
+
275
+ self.send_request_impl(request);
276
+ }
277
+
278
+ fn send_request_impl(&mut self, request: OrderRequest) {
279
+ if let Err(e) = self.order_sender.try_send(request) {
280
+ self.last_order_result = Some(format!("Failed to send: {}", e));
281
+ } else {
282
+ self.last_order_result = Some("Request sent...".to_string());
283
+ }
284
+ }
285
+
286
+ fn adjust_lot_size(&mut self, delta: f64) {
287
+ let new_lot = self.lot_size + delta;
288
+ // Round to lot_step
289
+ let steps = (new_lot / self.lot_step).round();
290
+ self.lot_size = (steps * self.lot_step).max(self.min_lot).min(self.max_lot);
291
+ self.lot_size_str = format!("{:.2}", self.lot_size);
292
+ }
293
+
294
+ fn toggle_recording(&mut self) {
295
+ self.is_recording = !self.is_recording;
296
+ if self.is_recording {
297
+ // Increment counter for unique ID
298
+ self.request_counter += 1;
299
+ let filename = format!(
300
+ "{}/Live_{}_ID{:04}_{}.csv",
301
+ self.output_dir.display(),
302
+ self.symbol.replace("/", "-"),
303
+ self.request_counter,
304
+ chrono::Local::now().format("%Y%m%d_%H%M%S")
305
+ );
306
+ match OpenOptions::new().create(true).append(true).open(&filename) {
307
+ Ok(mut file) => {
308
+ let _ = writeln!(file, "Time,Bid,Ask,Volume");
309
+ self.live_record_file = Some(file);
310
+ self.last_order_result = Some(format!("Recording to {}", filename));
311
+ }
312
+ Err(e) => {
313
+ self.is_recording = false;
314
+ self.last_order_result = Some(format!("Rec Error: {}", e));
315
+ }
316
+ }
317
+ } else {
318
+ self.live_record_file = None;
319
+ self.last_order_result = Some("Recording Stopped".to_string());
320
+ }
321
+ }
322
+ }
323
+
324
+ impl eframe::App for Mt5ChartApp {
325
+ fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
326
+ // Receive all available tick data from the channel without blocking
327
+ while let Ok(tick) = self.tick_receiver.try_recv() {
328
+ self.symbol = tick.symbol.clone();
329
+
330
+ // Record if active
331
+ if self.is_recording {
332
+ if let Some(mut file) = self.live_record_file.as_ref() {
333
+ let _ = writeln!(file, "{},{},{},{}", tick.time, tick.bid, tick.ask, tick.volume);
334
+ }
335
+ }
336
+
337
+ // Update account info from latest tick
338
+ if tick.balance > 0.0 {
339
+ self.balance = tick.balance;
340
+ self.equity = tick.equity;
341
+ self.margin = tick.margin;
342
+ self.free_margin = tick.free_margin;
343
+ self.min_lot = tick.min_lot;
344
+ self.max_lot = tick.max_lot;
345
+ if tick.lot_step > 0.0 {
346
+ self.lot_step = tick.lot_step;
347
+ }
348
+ }
349
+
350
+ // Update active trades
351
+ self.positions = tick.positions.clone();
352
+ self.pending_orders = tick.orders.clone();
353
+
354
+ self.data.push(tick);
355
+ // Keep only last 2000 points
356
+ if self.data.len() > 2000 {
357
+ self.data.remove(0);
358
+ }
359
+ }
360
+
361
+ // Check for order responses
362
+ while let Ok(response) = self.response_receiver.try_recv() {
363
+ if response.success {
364
+ // Check if this is a history download with CSV data
365
+ if let Some(ref msg) = response.message {
366
+ if msg.contains("||CSV_DATA||") {
367
+ // Parse CSV data from response
368
+ let parts: Vec<&str> = msg.splitn(2, "||CSV_DATA||").collect();
369
+ if parts.len() == 2 {
370
+ let info_part = parts[0];
371
+ let csv_content = parts[1];
372
+
373
+ // Generate filename using pending request info
374
+ if let Some((id, symbol, tf, mode)) = self.pending_history_request.take() {
375
+ let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
376
+ let filename = format!(
377
+ "{}/History_{}_{}_{}_ID{:04}_{}.csv",
378
+ self.output_dir.display(),
379
+ symbol, tf, mode, id, timestamp
380
+ );
381
+
382
+ // Convert |NL| placeholders back to real newlines
383
+ let csv_with_newlines = csv_content.replace("|NL|", "\n");
384
+
385
+ // Save CSV to output folder
386
+ match std::fs::write(&filename, csv_with_newlines) {
387
+ Ok(_) => {
388
+ self.last_order_result = Some(format!(
389
+ "✓ {} → Saved to {}",
390
+ info_part, filename
391
+ ));
392
+ }
393
+ Err(e) => {
394
+ self.last_order_result = Some(format!(
395
+ "✗ Failed to save CSV: {}",
396
+ e
397
+ ));
398
+ }
399
+ }
400
+ } else {
401
+ self.last_order_result = Some(format!("✓ {}", info_part));
402
+ }
403
+ } else {
404
+ self.last_order_result = Some(format!("✓ {}", msg));
405
+ }
406
+ } else {
407
+ self.last_order_result = Some(format!("✓ {}", msg));
408
+ }
409
+ } else {
410
+ // Add breakline for successful market orders
411
+ if let Some(ref order_type) = self.pending_order_type.take() {
412
+ let breakline = OrderBreakline {
413
+ index: self.data.len().saturating_sub(1),
414
+ order_type: order_type.clone(),
415
+ ticket: response.ticket.unwrap_or(0),
416
+ };
417
+ self.order_breaklines.push(breakline);
418
+ // Keep only last 50 breaklines
419
+ if self.order_breaklines.len() > 50 {
420
+ self.order_breaklines.remove(0);
421
+ }
422
+ }
423
+
424
+ self.last_order_result = Some(format!(
425
+ "✓ Order executed! Ticket: {}",
426
+ response.ticket.unwrap_or(0)
427
+ ));
428
+ }
429
+ } else {
430
+ self.pending_order_type = None; // Clear pending on failure
431
+ self.pending_history_request = None; // Clear pending history request
432
+ self.last_order_result = Some(format!(
433
+ "✗ Failed: {}",
434
+ response.error.unwrap_or_else(|| "Unknown error".to_string())
435
+ ));
436
+ }
437
+ }
438
+
439
+ // ====================================================================
440
+ // Side Panel - Trading Controls
441
+ // ====================================================================
442
+ egui::SidePanel::left("trading_panel")
443
+ .min_width(280.0) // Widen slightly
444
+ .show(ctx, |ui| {
445
+ ui.heading("📊 Trading Panel");
446
+ ui.separator();
447
+
448
+ // Account Info
449
+ ui.collapsing("💰 Account Info", |ui| {
450
+ egui::Grid::new("account_grid")
451
+ .num_columns(2)
452
+ .spacing([10.0, 4.0])
453
+ .show(ui, |ui| {
454
+ ui.label("Balance:");
455
+ ui.colored_label(egui::Color32::from_rgb(100, 200, 100), format!("${:.2}", self.balance));
456
+ ui.end_row();
457
+ ui.label("Equity:");
458
+ ui.colored_label(egui::Color32::from_rgb(100, 180, 255), format!("${:.2}", self.equity));
459
+ ui.end_row();
460
+ ui.label("Margin Used:");
461
+ ui.colored_label(egui::Color32::from_rgb(255, 200, 100), format!("${:.2}", self.margin));
462
+ ui.end_row();
463
+ ui.label("Free Margin:");
464
+ ui.colored_label(egui::Color32::from_rgb(100, 255, 200), format!("${:.2}", self.free_margin));
465
+ ui.end_row();
466
+ });
467
+ });
468
+
469
+ ui.separator();
470
+
471
+ // Historical Data Section
472
+ ui.heading("📂 Historical Data");
473
+ ui.add_space(5.0);
474
+
475
+ egui::Grid::new("history_grid").num_columns(2).spacing([10.0, 5.0]).show(ui, |ui| {
476
+ ui.label("Start (yyyy.mm.dd):");
477
+ ui.add(egui::TextEdit::singleline(&mut self.history_start_date).desired_width(100.0));
478
+ ui.end_row();
479
+
480
+ ui.label("End (yyyy.mm.dd):");
481
+ ui.add(egui::TextEdit::singleline(&mut self.history_end_date).desired_width(100.0));
482
+ ui.end_row();
483
+
484
+ ui.label("Timeframe:");
485
+ egui::ComboBox::from_id_source("tf_combo")
486
+ .selected_text(&self.history_tf)
487
+ .show_ui(ui, |ui| {
488
+ ui.selectable_value(&mut self.history_tf, "M1".to_string(), "M1");
489
+ ui.selectable_value(&mut self.history_tf, "M5".to_string(), "M5");
490
+ ui.selectable_value(&mut self.history_tf, "M15".to_string(), "M15");
491
+ ui.selectable_value(&mut self.history_tf, "H1".to_string(), "H1");
492
+ ui.selectable_value(&mut self.history_tf, "D1".to_string(), "D1");
493
+ });
494
+ ui.end_row();
495
+
496
+ ui.label("Mode:");
497
+ egui::ComboBox::from_id_source("mode_combo")
498
+ .selected_text(&self.history_mode)
499
+ .show_ui(ui, |ui| {
500
+ ui.selectable_value(&mut self.history_mode, "OHLC".to_string(), "OHLC");
501
+ ui.selectable_value(&mut self.history_mode, "TICKS".to_string(), "TICKS");
502
+ });
503
+ ui.end_row();
504
+ });
505
+
506
+ ui.add_space(5.0);
507
+ if ui.button("⬇ Download History (CSV)").clicked() {
508
+ self.send_download_request();
509
+ }
510
+
511
+ ui.separator();
512
+
513
+ // Live Recording
514
+ ui.heading("🔴 Live Recording");
515
+ ui.horizontal(|ui| {
516
+ ui.label(if self.is_recording { "Recording..." } else { "Idle" });
517
+ if ui.button(if self.is_recording { "Stop" } else { "Start Recording" }).clicked() {
518
+ self.toggle_recording();
519
+ }
520
+ });
521
+
522
+ ui.separator();
523
+
524
+ // Order Controls
525
+ ui.heading("📦 Trade Controls");
526
+
527
+ // Lot Size
528
+ ui.horizontal(|ui| {
529
+ if ui.button("−").clicked() { self.adjust_lot_size(-self.lot_step); }
530
+ let response = ui.add(egui::TextEdit::singleline(&mut self.lot_size_str).desired_width(60.0));
531
+ if response.lost_focus() {
532
+ if let Ok(parsed) = self.lot_size_str.parse::<f64>() {
533
+ self.lot_size = parsed.max(self.min_lot).min(self.max_lot);
534
+ self.lot_size_str = format!("{:.2}", self.lot_size);
535
+ }
536
+ }
537
+ if ui.button("+").clicked() { self.adjust_lot_size(self.lot_step); }
538
+
539
+ ui.label(format!("Lots (Max: {:.1})", self.max_lot));
540
+ });
541
+
542
+ ui.add_space(5.0);
543
+ ui.label("Market Orders:");
544
+ ui.horizontal(|ui| {
545
+ if ui.button("BUY").clicked() { self.send_order("market_buy", None, None); }
546
+ if ui.button("SELL").clicked() { self.send_order("market_sell", None, None); }
547
+ });
548
+
549
+ ui.add_space(5.0);
550
+ ui.label("Pending Orders:");
551
+ ui.horizontal(|ui| {
552
+ ui.label("@ Price:");
553
+ ui.add(egui::TextEdit::singleline(&mut self.limit_price).desired_width(70.0));
554
+ });
555
+ ui.horizontal(|ui| {
556
+ let p = self.limit_price.parse().unwrap_or(0.0);
557
+ if ui.small_button("Buy Limit").clicked() { self.send_order("limit_buy", Some(p), None); }
558
+ if ui.small_button("Sell Limit").clicked() { self.send_order("limit_sell", Some(p), None); }
559
+ if ui.small_button("Buy Stop").clicked() { self.send_order("stop_buy", Some(p), None); }
560
+ if ui.small_button("Sell Stop").clicked() { self.send_order("stop_sell", Some(p), None); }
561
+ });
562
+
563
+ ui.separator();
564
+
565
+ // Order result feedback
566
+ if let Some(ref result) = self.last_order_result {
567
+ ui.heading("📨 Last Message");
568
+ ui.label(result); // Allow wrapping
569
+ }
570
+
571
+ ui.separator();
572
+
573
+ // Active Positions - Close Management
574
+ ui.collapsing("💼 Active Positions", |ui| {
575
+ if self.positions.is_empty() {
576
+ ui.label("No active positions");
577
+ } else {
578
+ let positions_clone = self.positions.clone();
579
+ for pos in positions_clone {
580
+ ui.horizontal(|ui| {
581
+ let color = if pos.pos_type == "BUY" {
582
+ egui::Color32::from_rgb(100, 200, 100)
583
+ } else {
584
+ egui::Color32::from_rgb(255, 100, 100)
585
+ };
586
+ ui.colored_label(color, format!(
587
+ "#{} {} {:.2}@{:.5} P:{:.2}",
588
+ pos.ticket, pos.pos_type, pos.volume, pos.price, pos.profit
589
+ ));
590
+ if ui.small_button("Close").clicked() {
591
+ self.send_order("close_position", Some(pos.price), Some(pos.ticket));
592
+ }
593
+ });
594
+ }
595
+ }
596
+ });
597
+
598
+ // Pending Orders - Cancel Management
599
+ ui.collapsing("⏳ Pending Orders", |ui| {
600
+ if self.pending_orders.is_empty() {
601
+ ui.label("No pending orders");
602
+ } else {
603
+ let orders_clone = self.pending_orders.clone();
604
+ for order in orders_clone {
605
+ ui.horizontal(|ui| {
606
+ let color = if order.order_type.contains("BUY") {
607
+ egui::Color32::from_rgb(100, 150, 255)
608
+ } else {
609
+ egui::Color32::from_rgb(255, 150, 100)
610
+ };
611
+ ui.colored_label(color, format!(
612
+ "#{} {} {:.2}@{:.5}",
613
+ order.ticket, order.order_type, order.volume, order.price
614
+ ));
615
+ if ui.small_button("Cancel").clicked() {
616
+ self.send_order("cancel_order", Some(order.price), Some(order.ticket));
617
+ }
618
+ });
619
+ }
620
+ }
621
+ });
622
+ });
623
+
624
+ // ====================================================================
625
+ // Central Panel - Chart
626
+ // ====================================================================
627
+ egui::CentralPanel::default().show(ctx, |ui| {
628
+ ui.heading(format!("📈 {}", self.symbol));
629
+
630
+ // Header Info
631
+ if let Some(last_tick) = self.data.last() {
632
+ ui.horizontal(|ui| {
633
+ ui.label(format!("{:.5} / {:.5}", last_tick.bid, last_tick.ask));
634
+ });
635
+ }
636
+
637
+ ui.separator();
638
+
639
+ // Price chart - Index-based X Axis
640
+ let time_map: Vec<i64> = self.data.iter().map(|t| t.time).collect();
641
+
642
+ let plot = Plot::new("mt5_price_plot")
643
+ .legend(egui_plot::Legend::default())
644
+ .allow_boxed_zoom(true)
645
+ .allow_drag(true)
646
+ .allow_scroll(true)
647
+ .allow_zoom(true)
648
+ .x_axis_formatter(move |x, _range, _width| {
649
+ let idx = x.value.round() as isize;
650
+ if idx >= 0 && (idx as usize) < time_map.len() {
651
+ let timestamp = time_map[idx as usize];
652
+ let seconds = timestamp % 60;
653
+ let minutes = (timestamp / 60) % 60;
654
+ let hours = (timestamp / 3600) % 24;
655
+ return format!("{:02}:{:02}:{:02}", hours, minutes, seconds);
656
+ }
657
+ "".to_string()
658
+ });
659
+
660
+ plot.show(ui, |plot_ui| {
661
+ let bid_points: PlotPoints = self.data
662
+ .iter()
663
+ .enumerate()
664
+ .map(|(i, t)| [i as f64, t.bid])
665
+ .collect();
666
+
667
+ let ask_points: PlotPoints = self.data
668
+ .iter()
669
+ .enumerate()
670
+ .map(|(i, t)| [i as f64, t.ask])
671
+ .collect();
672
+
673
+ plot_ui.line(Line::new(bid_points).name("Bid").color(egui::Color32::from_rgb(100, 200, 100)));
674
+ plot_ui.line(Line::new(ask_points).name("Ask").color(egui::Color32::from_rgb(200, 100, 100)));
675
+
676
+ // Draw Active Positions (horizontal lines)
677
+ for pos in &self.positions {
678
+ let color = if pos.pos_type == "BUY" {
679
+ egui::Color32::from_rgb(50, 100, 255)
680
+ } else {
681
+ egui::Color32::from_rgb(255, 50, 50)
682
+ };
683
+
684
+ plot_ui.hline(
685
+ egui_plot::HLine::new(pos.price)
686
+ .color(color)
687
+ .name(format!("{} #{}", pos.pos_type, pos.ticket))
688
+ .style(egui_plot::LineStyle::Dashed { length: 10.0 })
689
+ );
690
+ }
691
+
692
+ // Draw Order Breaklines (vertical lines at execution points)
693
+ for breakline in &self.order_breaklines {
694
+ let color = if breakline.order_type.contains("buy") {
695
+ egui::Color32::from_rgb(0, 200, 100) // Bright green for BUY
696
+ } else {
697
+ egui::Color32::from_rgb(255, 80, 80) // Bright red for SELL
698
+ };
699
+
700
+ plot_ui.vline(
701
+ egui_plot::VLine::new(breakline.index as f64)
702
+ .color(color)
703
+ .name(format!("Order #{}", breakline.ticket))
704
+ .width(2.0)
705
+ );
706
+ }
707
+ });
708
+ });
709
+
710
+ // Request a repaint to update the chart continuously
711
+ ctx.request_repaint();
712
+ }
713
+ }
714
+
715
+ // ============================================================================
716
+ // Main Entry Point
717
+ // ============================================================================
718
+
719
+ #[tokio::main]
720
+ async fn main() -> Result<(), Box<dyn std::error::Error>> {
721
+ // Channels for tick data
722
+ let (tick_tx, tick_rx) = mpsc::channel(100);
723
+
724
+ // Channels for order requests and responses
725
+ let (order_tx, mut order_rx) = mpsc::channel::<OrderRequest>(10);
726
+ let (response_tx, response_rx) = mpsc::channel::<OrderResponse>(10);
727
+
728
+ // ========================================================================
729
+ // Spawn ZMQ Tick Subscriber task
730
+ // ========================================================================
731
+ tokio::spawn(async move {
732
+ let mut socket = zeromq::SubSocket::new();
733
+ match socket.connect("tcp://127.0.0.1:5555").await {
734
+ Ok(_) => println!("Connected to ZMQ Tick Publisher on port 5555"),
735
+ Err(e) => eprintln!("Failed to connect to ZMQ tick publisher: {}", e),
736
+ }
737
+
738
+ let _ = socket.subscribe("").await;
739
+
740
+ loop {
741
+ match socket.recv().await {
742
+ Ok(msg) => {
743
+ if let Some(payload_bytes) = msg.get(0) {
744
+ if let Ok(json_str) = std::str::from_utf8(payload_bytes) {
745
+ match serde_json::from_str::<TickData>(json_str) {
746
+ Ok(tick) => {
747
+ if let Err(e) = tick_tx.send(tick).await {
748
+ eprintln!("Tick channel error: {}", e);
749
+ break;
750
+ }
751
+ }
752
+ Err(e) => eprintln!("JSON Parse Error: {}. Msg: {}", e, json_str),
753
+ }
754
+ }
755
+ }
756
+ }
757
+ Err(e) => {
758
+ eprintln!("ZMQ Tick Recv Error: {}", e);
759
+ tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
760
+ }
761
+ }
762
+ }
763
+ });
764
+
765
+ // ========================================================================
766
+ // Spawn ZMQ Order Request/Response task
767
+ // ========================================================================
768
+ tokio::spawn(async move {
769
+ let mut socket = zeromq::ReqSocket::new();
770
+ match socket.connect("tcp://127.0.0.1:5556").await {
771
+ Ok(_) => println!("Connected to ZMQ Order Handler on port 5556"),
772
+ Err(e) => {
773
+ eprintln!("Failed to connect to ZMQ order handler: {}", e);
774
+ return;
775
+ }
776
+ }
777
+
778
+ while let Some(order_request) = order_rx.recv().await {
779
+ // Serialize order request to JSON
780
+ let json_request = match serde_json::to_string(&order_request) {
781
+ Ok(json) => json,
782
+ Err(e) => {
783
+ eprintln!("Failed to serialize order request: {}", e);
784
+ continue;
785
+ }
786
+ };
787
+
788
+ println!("Sending request: {}", json_request);
789
+
790
+ // Send request
791
+ if let Err(e) = socket.send(json_request.into()).await {
792
+ eprintln!("Failed to send: {}", e);
793
+ let _ = response_tx.send(OrderResponse {
794
+ success: false,
795
+ ticket: None,
796
+ error: Some(format!("Send failed: {}", e)),
797
+ message: None,
798
+ }).await;
799
+ continue;
800
+ }
801
+
802
+ // Wait for response
803
+ match socket.recv().await {
804
+ Ok(msg) => {
805
+ if let Some(payload_bytes) = msg.get(0) {
806
+ if let Ok(json_str) = std::str::from_utf8(payload_bytes) {
807
+ println!("Received response: {}", json_str);
808
+ match serde_json::from_str::<OrderResponse>(json_str) {
809
+ Ok(response) => {
810
+ let _ = response_tx.send(response).await;
811
+ }
812
+ Err(e) => {
813
+ let _ = response_tx.send(OrderResponse {
814
+ success: false,
815
+ ticket: None,
816
+ error: Some(format!("Parse error: {}", e)),
817
+ message: None,
818
+ }).await;
819
+ }
820
+ }
821
+ }
822
+ }
823
+ }
824
+ Err(e) => {
825
+ eprintln!("Response recv error: {}", e);
826
+ let _ = response_tx.send(OrderResponse {
827
+ success: false,
828
+ ticket: None,
829
+ error: Some(format!("Recv failed: {}", e)),
830
+ message: None,
831
+ }).await;
832
+ }
833
+ }
834
+ }
835
+ });
836
+
837
+ // ========================================================================
838
+ // Run the egui application
839
+ // ========================================================================
840
+ let options = eframe::NativeOptions {
841
+ viewport: egui::ViewportBuilder::default()
842
+ .with_inner_size([1200.0, 800.0])
843
+ .with_title("Rust + ZMQ + MT5 Trading Chart"),
844
+ ..Default::default()
845
+ };
846
+
847
+ eframe::run_native(
848
+ "Rust + ZMQ + MT5 Trading Chart",
849
+ options,
850
+ Box::new(|_cc| Box::new(Mt5ChartApp::new(tick_rx, order_tx, response_rx))),
851
+ ).map_err(|e| e.into())
852
+ }
mql-rust/.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ /target
mql-rust/Cargo.lock ADDED
The diff for this file is too large to render. See raw diff
 
mql-rust/Cargo.toml ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [package]
2
+ name = "mql-rust"
3
+ version = "0.1.0"
4
+ edition = "2024"
5
+
6
+ [dependencies]
7
+ chrono = "0.4.44"
8
+ crossbeam = "0.8.4"
9
+ eframe = "0.33.3"
10
+ egui = "0.33.3"
11
+ egui_plot = "0.34.1"
12
+ futures = "0.3.32"
13
+ ndarray = "0.17.2"
14
+ nom = "8.0.0"
15
+ rayon = "1.11.0"
16
+ rfd = "0.17.2"
17
+ serde = "1.0.228"
18
+ serde_json = "1.0.149"
19
+ tokio = "1.50.0"
20
+ zeromq = "0.5.0"
mql-rust/src/compiler/ast.rs ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use crate::compiler::lexer::Token;
2
+
3
+ #[derive(Debug, Clone, PartialEq)]
4
+ pub enum AstNode {
5
+ Program(Vec<AstNode>),
6
+
7
+ // Declarations
8
+ VarDecl {
9
+ var_type: Token,
10
+ is_input: bool,
11
+ name: String,
12
+ init_value: Option<Box<AstNode>>,
13
+ },
14
+
15
+ FunctionDecl {
16
+ return_type: Token,
17
+ name: String,
18
+ params: Vec<AstNode>, // VarDecl
19
+ body: Box<AstNode>, // Block
20
+ },
21
+
22
+ Block(Vec<AstNode>),
23
+
24
+ // Statements
25
+ IfStatement {
26
+ condition: Box<AstNode>,
27
+ then_branch: Box<AstNode>,
28
+ else_branch: Option<Box<AstNode>>,
29
+ },
30
+
31
+ ReturnStatement(Option<Box<AstNode>>),
32
+ ExpressionStatement(Box<AstNode>),
33
+
34
+ // Expressions
35
+ BinaryOp {
36
+ left: Box<AstNode>,
37
+ op: Token,
38
+ right: Box<AstNode>,
39
+ },
40
+
41
+ UnaryOp {
42
+ op: Token,
43
+ expr: Box<AstNode>,
44
+ },
45
+
46
+ FunctionCall {
47
+ name: String,
48
+ args: Vec<AstNode>,
49
+ },
50
+
51
+ // Built-in MQL5 logic
52
+ OrderSendCall {
53
+ symbol: String,
54
+ cmd: Token, // OP_BUY, OP_SELL (represented as tokens or mapped strings)
55
+ volume: f64,
56
+ price: f64,
57
+ sl: f64,
58
+ tp: f64,
59
+ },
60
+
61
+ Identifier(String),
62
+ Literal(Token),
63
+ }
mql-rust/src/compiler/lexer.rs ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use std::fmt;
2
+
3
+ #[derive(Debug, Clone, PartialEq)]
4
+ pub enum Token {
5
+ // Keywords
6
+ Int,
7
+ Double,
8
+ String,
9
+ Bool,
10
+ Void,
11
+ If,
12
+ Else,
13
+ For,
14
+ While,
15
+ Return,
16
+ Input,
17
+
18
+ // Built-in MQL5 Types
19
+ MqlTick,
20
+ MqlRates,
21
+
22
+ // Identifiers & Literals
23
+ Identifier(String),
24
+ IntLiteral(i64),
25
+ DoubleLiteral(f64),
26
+ StringLiteral(String),
27
+ BoolLiteral(bool),
28
+
29
+ // Operators
30
+ Plus, // +
31
+ Minus, // -
32
+ Star, // *
33
+ Slash, // /
34
+ Assign, // =
35
+ Equals, // ==
36
+ NotEquals, // !=
37
+ Greater, // >
38
+ Less, // <
39
+ GreaterEq, // >=
40
+ LessEq, // <=
41
+ And, // &&
42
+ Or, // ||
43
+ Not, // !
44
+
45
+ // Punctuation
46
+ OpenParen, // (
47
+ CloseParen, // )
48
+ OpenBrace, // {
49
+ CloseBrace, // }
50
+ OpenBracket,// [
51
+ CloseBracket,//]
52
+ Comma, // ,
53
+ Semicolon, // ;
54
+ Dot, // .
55
+
56
+ // EOF
57
+ Eof,
58
+ }
59
+
60
+ impl fmt::Display for Token {
61
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62
+ write!(f, "{:?}", self)
63
+ }
64
+ }
mql-rust/src/compiler/mod.rs ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ pub mod ast;
2
+ pub mod lexer;
3
+ pub mod parser;
4
+ pub mod transpiler;
mql-rust/src/compiler/parser.rs ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use nom::{
2
+ branch::alt,
3
+ bytes::complete::{tag, take_while1, take_until, take_while},
4
+ character::complete::{alpha1, alphanumeric1, char, digit1, multispace0, multispace1, anychar},
5
+ combinator::{map, map_res, recognize, value, eof, opt},
6
+ multi::many0,
7
+ sequence::{delimited, pair, preceded, tuple},
8
+ IResult,
9
+ };
10
+ use crate::compiler::lexer::Token;
11
+ use std::str::FromStr;
12
+
13
+ pub struct Lexer;
14
+
15
+ impl Lexer {
16
+ pub fn tokenize(input: &str) -> IResult<&str, Vec<Token>> {
17
+ many0(preceded(
18
+ // Skip whitespace and comments
19
+ alt((
20
+ multispace1,
21
+ // Line comment //
22
+ recognize(pair(tag("//"), take_while(|c| c != '\n' && c != '\r'))),
23
+ // Block comment /* */
24
+ recognize(delimited(tag("/*"), take_until("*/"), tag("*/"))),
25
+ )),
26
+ // Parse tokens
27
+ alt((
28
+ // Keywords & Types
29
+ Self::parse_keyword,
30
+
31
+ // Literals
32
+ Self::parse_double_literal,
33
+ Self::parse_int_literal,
34
+ Self::parse_string_literal,
35
+ Self::parse_bool_literal,
36
+
37
+ // Identifiers
38
+ Self::parse_identifier,
39
+
40
+ // Operators & Punctuation (Order matters for prefix matching, e.g. == before =)
41
+ Self::parse_operator,
42
+ Self::parse_punctuation,
43
+ ))
44
+ ))(input)
45
+ }
46
+
47
+ fn parse_keyword(input: &str) -> IResult<&str, Token> {
48
+ alt((
49
+ value(Token::Int, tag("int")),
50
+ value(Token::Double, tag("double")),
51
+ value(Token::String, tag("string")),
52
+ value(Token::Bool, tag("bool")),
53
+ value(Token::Void, tag("void")),
54
+ value(Token::Input, tag("input")),
55
+ value(Token::If, tag("if")),
56
+ value(Token::Else, tag("else")),
57
+ value(Token::For, tag("for")),
58
+ value(Token::While, tag("while")),
59
+ value(Token::Return, tag("return")),
60
+ value(Token::MqlTick, tag("MqlTick")),
61
+ value(Token::MqlRates, tag("MqlRates")),
62
+ ))(input)
63
+ }
64
+
65
+ fn parse_bool_literal(input: &str) -> IResult<&str, Token> {
66
+ alt((
67
+ value(Token::BoolLiteral(true), tag("true")),
68
+ value(Token::BoolLiteral(false), tag("false")),
69
+ ))(input)
70
+ }
71
+
72
+ fn parse_identifier(input: &str) -> IResult<&str, Token> {
73
+ map(
74
+ recognize(pair(
75
+ alt((alpha1, tag("_"))),
76
+ take_while(|c: char| c.is_alphanumeric() || c == '_')
77
+ )),
78
+ |s: &str| Token::Identifier(s.to_string())
79
+ )(input)
80
+ }
81
+
82
+ fn parse_int_literal(input: &str) -> IResult<&str, Token> {
83
+ map_res(digit1, |s: &str| s.parse::<i64>().map(Token::IntLiteral))(input)
84
+ }
85
+
86
+ fn parse_double_literal(input: &str) -> IResult<&str, Token> {
87
+ map_res(
88
+ recognize(tuple((
89
+ digit1,
90
+ char('.'),
91
+ digit1
92
+ ))),
93
+ |s: &str| s.parse::<f64>().map(Token::DoubleLiteral)
94
+ )(input)
95
+ }
96
+
97
+ fn parse_string_literal(input: &str) -> IResult<&str, Token> {
98
+ map(
99
+ delimited(char('"'), take_while(|c| c != '"'), char('"')),
100
+ |s: &str| Token::StringLiteral(s.to_string())
101
+ )(input)
102
+ }
103
+
104
+ fn parse_operator(input: &str) -> IResult<&str, Token> {
105
+ alt((
106
+ value(Token::Equals, tag("==")),
107
+ value(Token::NotEquals, tag("!=")),
108
+ value(Token::GreaterEq, tag(">=")),
109
+ value(Token::LessEq, tag("<=")),
110
+ value(Token::And, tag("&&")),
111
+ value(Token::Or, tag("||")),
112
+ value(Token::Assign, tag("=")),
113
+ value(Token::Greater, tag(">")),
114
+ value(Token::Less, tag("<")),
115
+ value(Token::Plus, tag("+")),
116
+ value(Token::Minus, tag("-")),
117
+ value(Token::Star, tag("*")),
118
+ value(Token::Slash, tag("/")),
119
+ value(Token::Not, tag("!")),
120
+ ))(input)
121
+ }
122
+
123
+ fn parse_punctuation(input: &str) -> IResult<&str, Token> {
124
+ alt((
125
+ value(Token::OpenParen, tag("(")),
126
+ value(Token::CloseParen, tag(")")),
127
+ value(Token::OpenBrace, tag("{")),
128
+ value(Token::CloseBrace, tag("}")),
129
+ value(Token::OpenBracket, tag("[")),
130
+ value(Token::CloseBracket, tag("]")),
131
+ value(Token::Comma, tag(",")),
132
+ value(Token::Semicolon, tag(";")),
133
+ value(Token::Dot, tag(".")),
134
+ ))(input)
135
+ }
136
+ }
mql-rust/src/compiler/transpiler.rs ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use crate::compiler::ast::AstNode;
2
+ use crate::compiler::lexer::Token;
3
+
4
+ pub struct Transpiler;
5
+
6
+ impl Transpiler {
7
+ pub fn new() -> Self {
8
+ Self
9
+ }
10
+
11
+ pub fn transpile(&self, ast: &AstNode) -> Result<String, String> {
12
+ let mut output = String::new();
13
+
14
+ output.push_str("// Auto-generated by MQL-Rust Transpiler\n");
15
+ output.push_str("use crate::engine::backtester::{Backtester, OrderType};\n\n");
16
+
17
+ self.visit_node(ast, &mut output)?;
18
+
19
+ Ok(output)
20
+ }
21
+
22
+ fn visit_node(&self, node: &AstNode, out: &mut String) -> Result<(), String> {
23
+ match node {
24
+ AstNode::Program(nodes) => {
25
+ for n in nodes {
26
+ self.visit_node(n, out)?;
27
+ out.push('\n');
28
+ }
29
+ }
30
+ AstNode::VarDecl { var_type, is_input, name, init_value } => {
31
+ // Convert input variables to public struct fields (for UI configuration) or global consts
32
+ let type_str = self.mql_to_rust_type(var_type)?;
33
+ if *is_input {
34
+ // For now, hardcode inputs as consts to simplify the transpiler proof-of-concept
35
+ out.push_str(&format!("pub const {}: {} = ", name.to_uppercase(), type_str));
36
+ } else {
37
+ out.push_str(&format!("let mut {}: {} = ", name, type_str));
38
+ }
39
+
40
+ if let Some(val) = init_value {
41
+ self.visit_node(val, out)?;
42
+ } else {
43
+ out.push_str("Default::default()");
44
+ }
45
+ out.push_str(";\n");
46
+ }
47
+ AstNode::FunctionDecl { return_type, name, params, body } => {
48
+ let ret_str = self.mql_to_rust_type(return_type)?;
49
+
50
+ // Special handling for MQL5 entry points
51
+ if name == "OnInit" || name == "OnDeinit" || name == "OnTick" || name == "OnTimer" {
52
+ // Passing the backtester context
53
+ out.push_str(&format!("pub fn mql_{}(bt_ctx: &mut Backtester", name));
54
+ } else {
55
+ out.push_str(&format!("fn {}(", name));
56
+ }
57
+
58
+ // Add params explicitly handling
59
+ if !params.is_empty() {
60
+ if name.starts_with("On") { out.push_str(", "); } // Add comma if context was added
61
+ for (i, p) in params.iter().enumerate() {
62
+ if let AstNode::VarDecl { var_type, name: p_name, .. } = p {
63
+ let p_type = self.mql_to_rust_type(var_type)?;
64
+ out.push_str(&format!("{}: &{}", p_name, p_type));
65
+ if i < params.len() - 1 { out.push_str(", "); }
66
+ }
67
+ }
68
+ }
69
+
70
+ out.push_str(")");
71
+ if *return_type != Token::Void {
72
+ out.push_str(&format!(" -> {}", ret_str));
73
+ }
74
+ out.push_str(" ");
75
+ self.visit_node(body, out)?;
76
+ }
77
+ AstNode::Block(nodes) => {
78
+ out.push_str("{\n");
79
+ for n in nodes {
80
+ self.visit_node(n, out)?;
81
+ out.push('\n');
82
+ }
83
+ out.push_str("}\n");
84
+ }
85
+ AstNode::ExpressionStatement(expr) => {
86
+ self.visit_node(expr, out)?;
87
+ out.push_str(";");
88
+ }
89
+ AstNode::ReturnStatement(opt_expr) => {
90
+ out.push_str("return ");
91
+ if let Some(expr) = opt_expr {
92
+ self.visit_node(expr, out)?;
93
+ }
94
+ out.push_str(";");
95
+ }
96
+ AstNode::IfStatement { condition, then_branch, else_branch } => {
97
+ out.push_str("if ");
98
+ self.visit_node(condition, out)?;
99
+ out.push_str(" ");
100
+ self.visit_node(then_branch, out)?;
101
+ if let Some(e) = else_branch {
102
+ out.push_str(" else ");
103
+ self.visit_node(e, out)?;
104
+ }
105
+ }
106
+ AstNode::BinaryOp { left, op, right } => {
107
+ self.visit_node(left, out)?;
108
+ let op_str = match op {
109
+ Token::Plus => " + ",
110
+ Token::Minus => " - ",
111
+ Token::Star => " * ",
112
+ Token::Slash => " / ",
113
+ Token::Assign => " = ",
114
+ Token::Equals => " == ",
115
+ Token::NotEquals => " != ",
116
+ Token::GreaterEq => " >= ",
117
+ Token::LessEq => " <= ",
118
+ Token::Greater => " > ",
119
+ Token::Less => " < ",
120
+ Token::And => " && ",
121
+ Token::Or => " || ",
122
+ _ => return Err(format!("Unsupported binary operator {:?}", op)),
123
+ };
124
+ out.push_str(op_str);
125
+ self.visit_node(right, out)?;
126
+ }
127
+ AstNode::UnaryOp { op, expr } => {
128
+ let op_str = match op {
129
+ Token::Not => "!",
130
+ Token::Minus => "-",
131
+ _ => return Err(format!("Unsupported unary operator {:?}", op)),
132
+ };
133
+ out.push_str(op_str);
134
+ self.visit_node(expr, out)?;
135
+ }
136
+ AstNode::FunctionCall { name, args } => {
137
+ // If it's a built in order function, route to the context
138
+ if name == "OrderSend" { // Simulating simpler logic
139
+ out.push_str("bt_ctx.market_order(");
140
+ } else {
141
+ out.push_str(&format!("{}(", name));
142
+ }
143
+
144
+ for (i, arg) in args.iter().enumerate() {
145
+ self.visit_node(arg, out)?;
146
+ if i < args.len() - 1 { out.push_str(", "); }
147
+ }
148
+ out.push_str(")");
149
+ }
150
+ AstNode::OrderSendCall { symbol, cmd, volume, price, sl, tp } => {
151
+ // Specialized mapping for trading commands
152
+ let cmd_str = if *cmd == Token::Identifier("ORDER_TYPE_BUY".to_string()) {
153
+ "OrderType::Buy"
154
+ } else {
155
+ "OrderType::Sell"
156
+ };
157
+ out.push_str(&format!(
158
+ "bt_ctx.market_order(\"{}\", {}, {}, Some({}), Some({}), \"Time_Sim\".to_string())",
159
+ symbol, cmd_str, volume, sl, tp
160
+ ));
161
+ }
162
+ AstNode::Identifier(id) => {
163
+ out.push_str(id);
164
+ }
165
+ AstNode::Literal(tok) => {
166
+ match tok {
167
+ Token::IntLiteral(i) => out.push_str(&i.to_string()),
168
+ Token::DoubleLiteral(d) => out.push_str(&format!("{:.5}", d)),
169
+ Token::BoolLiteral(b) => out.push_str(if *b { "true" } else { "false" }),
170
+ Token::StringLiteral(s) => out.push_str(&format!("\"{}\"", s)),
171
+ _ => return Err(format!("Unexpected literal token {:?}", tok)),
172
+ }
173
+ }
174
+ _ => return Err("Unsupported AST Node".to_string()),
175
+ }
176
+ Ok(())
177
+ }
178
+
179
+ fn mql_to_rust_type(&self, mql_type: &Token) -> Result<&'static str, String> {
180
+ match mql_type {
181
+ Token::Int => Ok("i64"),
182
+ Token::Double => Ok("f64"),
183
+ Token::Bool => Ok("bool"),
184
+ Token::String => Ok("String"),
185
+ Token::Void => Ok("()"),
186
+ Token::MqlTick => Ok("crate::data::parser::Tick"),
187
+ Token::MqlRates => Ok("crate::data::parser::Ohlcv"),
188
+ _ => Err(format!("Unknown or unmapped MQL5 type: {:?}", mql_type)),
189
+ }
190
+ }
191
+ }
mql-rust/src/data/mod.rs ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ pub mod mt5_client;
2
+ pub mod parser;
mql-rust/src/data/mt5_client.rs ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use serde::{Deserialize, Serialize};
2
+ use tokio::sync::mpsc;
3
+ use zeromq::{Socket, SocketRecv, SocketSend};
4
+
5
+ #[derive(Clone, Debug, Serialize)]
6
+ pub struct HistoryRequest {
7
+ #[serde(rename = "type")]
8
+ pub req_type: String, // "download_history"
9
+ pub symbol: String,
10
+ pub timeframe: String,
11
+ pub start: String,
12
+ pub end: String,
13
+ pub mode: String, // "OHLC" or "TICKS"
14
+ }
15
+
16
+ #[derive(Clone, Debug, Deserialize)]
17
+ pub struct HistoryResponse {
18
+ pub success: bool,
19
+ pub error: Option<String>,
20
+ pub message: Option<String>,
21
+ }
22
+
23
+ pub struct Mt5Client {
24
+ pub rep_address: String,
25
+ }
26
+
27
+ impl Mt5Client {
28
+ pub fn new(rep_address: &str) -> Self {
29
+ Self {
30
+ rep_address: rep_address.to_string(),
31
+ }
32
+ }
33
+
34
+ pub async fn download_history(&self, request: HistoryRequest) -> Result<String, String> {
35
+ let mut socket = zeromq::ReqSocket::new();
36
+ socket.connect(&self.rep_address).await.map_err(|e| format!("Failed to connect: {}", e))?;
37
+
38
+ let json_request = serde_json::to_string(&request).map_err(|e| format!("Serialization error: {}", e))?;
39
+
40
+ socket.send(json_request.into()).await.map_err(|e| format!("Send error: {}", e))?;
41
+
42
+ let msg = socket.recv().await.map_err(|e| format!("Receive error: {}", e))?;
43
+ let payload = msg.get(0).ok_or("Empty response")?;
44
+ let json_str = std::str::from_utf8(payload).map_err(|e| format!("UTF8 error: {}", e))?;
45
+
46
+ let response: HistoryResponse = serde_json::from_str(json_str).map_err(|e| format!("JSON Parse error: {}", e))?;
47
+
48
+ if response.success {
49
+ if let Some(msg) = response.message {
50
+ if msg.contains("||CSV_DATA||") {
51
+ let parts: Vec<&str> = msg.splitn(2, "||CSV_DATA||").collect();
52
+ if parts.len() == 2 {
53
+ let csv_content = parts[1].replace("|NL|", "\n");
54
+ return Ok(csv_content);
55
+ }
56
+ }
57
+ return Ok(msg);
58
+ }
59
+ Ok("Success".to_string())
60
+ } else {
61
+ Err(response.error.unwrap_or_else(|| "Unknown error".to_string()))
62
+ }
63
+ }
64
+ }
mql-rust/src/data/parser.rs ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use std::str::FromStr;
2
+
3
+ #[derive(Debug, Clone)]
4
+ pub struct Ohlcv {
5
+ pub time: String,
6
+ pub open: f64,
7
+ pub high: f64,
8
+ pub low: f64,
9
+ pub close: f64,
10
+ pub tick_volume: u64,
11
+ pub spread: u32,
12
+ }
13
+
14
+ #[derive(Debug, Clone)]
15
+ pub struct Tick {
16
+ pub time: String,
17
+ pub bid: f64,
18
+ pub ask: f64,
19
+ pub volume: u64,
20
+ }
21
+
22
+ pub fn parse_ohlcv_csv(csv_data: &str) -> Vec<Ohlcv> {
23
+ let mut results = Vec::new();
24
+ let mut lines = csv_data.lines();
25
+
26
+ // Skip header
27
+ lines.next();
28
+
29
+ for line in lines {
30
+ if line.trim().is_empty() {
31
+ continue;
32
+ }
33
+ let parts: Vec<&str> = line.split(',').collect();
34
+ if parts.len() >= 7 {
35
+ if let (Ok(o), Ok(h), Ok(l), Ok(c), Ok(tv), Ok(s)) = (
36
+ f64::from_str(parts[1]),
37
+ f64::from_str(parts[2]),
38
+ f64::from_str(parts[3]),
39
+ f64::from_str(parts[4]),
40
+ u64::from_str(parts[5]),
41
+ u32::from_str(parts[6]),
42
+ ) {
43
+ results.push(Ohlcv {
44
+ time: parts[0].to_string(),
45
+ open: o,
46
+ high: h,
47
+ low: l,
48
+ close: c,
49
+ tick_volume: tv,
50
+ spread: s,
51
+ });
52
+ }
53
+ }
54
+ }
55
+ results
56
+ }
57
+
58
+ pub fn parse_tick_csv(csv_data: &str) -> Vec<Tick> {
59
+ let mut results = Vec::new();
60
+ let mut lines = csv_data.lines();
61
+
62
+ // Skip header
63
+ lines.next();
64
+
65
+ for line in lines {
66
+ if line.trim().is_empty() {
67
+ continue;
68
+ }
69
+ let parts: Vec<&str> = line.split(',').collect();
70
+ if parts.len() >= 4 {
71
+ if let (Ok(b), Ok(a), Ok(v)) = (
72
+ f64::from_str(parts[1]),
73
+ f64::from_str(parts[2]),
74
+ u64::from_str(parts[3]),
75
+ ) {
76
+ results.push(Tick {
77
+ time: parts[0].to_string(),
78
+ bid: b,
79
+ ask: a,
80
+ volume: v,
81
+ });
82
+ }
83
+ }
84
+ }
85
+ results
86
+ }
mql-rust/src/engine/backtester.rs ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use crate::data::parser::{Ohlcv, Tick};
2
+ use std::collections::HashMap;
3
+
4
+ #[derive(Debug, Clone, PartialEq)]
5
+ pub enum OrderType {
6
+ Buy,
7
+ Sell,
8
+ // Add pending later
9
+ }
10
+
11
+ #[derive(Debug, Clone)]
12
+ pub struct Position {
13
+ pub ticket: u64,
14
+ pub symbol: String,
15
+ pub order_type: OrderType,
16
+ pub volume: f64,
17
+ pub open_price: f64,
18
+ pub sl: Option<f64>,
19
+ pub tp: Option<f64>,
20
+ pub open_time: String,
21
+ }
22
+
23
+ #[derive(Debug, Clone)]
24
+ pub struct ClosedTrade {
25
+ pub ticket: u64,
26
+ pub symbol: String,
27
+ pub order_type: OrderType,
28
+ pub volume: f64,
29
+ pub open_price: f64,
30
+ pub close_price: f64,
31
+ pub profit: f64,
32
+ pub open_time: String,
33
+ pub close_time: String,
34
+ }
35
+
36
+ #[derive(Debug, Clone)]
37
+ pub struct BacktestConfig {
38
+ pub initial_deposit: f64,
39
+ pub leverage: f64,
40
+ pub spread_modifier: u32,
41
+ }
42
+
43
+ pub struct Backtester {
44
+ pub config: BacktestConfig,
45
+ pub balance: f64,
46
+ pub equity: f64,
47
+ pub free_margin: f64,
48
+ pub margin: f64,
49
+ pub open_positions: Vec<Position>,
50
+ pub history: Vec<ClosedTrade>,
51
+ ticket_counter: u64,
52
+
53
+ // Internal state cache for symbols
54
+ pub current_prices: HashMap<String, (f64, f64)>, // Bid, Ask
55
+ }
56
+
57
+ impl Backtester {
58
+ pub fn new(config: BacktestConfig) -> Self {
59
+ Self {
60
+ balance: config.initial_deposit,
61
+ equity: config.initial_deposit,
62
+ free_margin: config.initial_deposit,
63
+ margin: 0.0,
64
+ config,
65
+ open_positions: Vec::new(),
66
+ history: Vec::new(),
67
+ ticket_counter: 1,
68
+ current_prices: HashMap::new(),
69
+ }
70
+ }
71
+
72
+ pub fn update_tick(&mut self, symbol: &str, bid: f64, ask: f64) {
73
+ self.current_prices.insert(symbol.to_string(), (bid, ask));
74
+ self.recalculate_equity();
75
+ }
76
+
77
+ pub fn update_ohlcv(&mut self, symbol: &str, bar: &Ohlcv) {
78
+ // Simple approximation for bar execution (using Close price for equity calculation)
79
+ let simulated_bid = bar.close;
80
+ let simulated_ask = bar.close + (bar.spread as f64 * 0.00001); // Assumes generic 5-digit broker formatting
81
+ self.update_tick(symbol, simulated_bid, simulated_ask);
82
+ }
83
+
84
+ fn recalculate_equity(&mut self) {
85
+ let mut floating_profit = 0.0;
86
+ let contract_size = 100000.0; // Assume standard Forex lots for MVP
87
+
88
+ for pos in &self.open_positions {
89
+ if let Some(&(bid, ask)) = self.current_prices.get(&pos.symbol) {
90
+ if pos.order_type == OrderType::Buy {
91
+ floating_profit += (bid - pos.open_price) * contract_size * pos.volume;
92
+ } else if pos.order_type == OrderType::Sell {
93
+ floating_profit += (pos.open_price - ask) * contract_size * pos.volume;
94
+ }
95
+ }
96
+ }
97
+
98
+ self.equity = self.balance + floating_profit;
99
+ self.free_margin = self.equity - self.margin;
100
+ }
101
+
102
+ pub fn market_order(&mut self, symbol: &str, order_type: OrderType, volume: f64, sl: Option<f64>, tp: Option<f64>, time: String) -> Result<u64, String> {
103
+ let &(bid, ask) = self.current_prices.get(symbol).ok_or("No price data for symbol")?;
104
+
105
+ // Ensure Margin
106
+ let required_margin = (volume * 100000.0) / self.config.leverage;
107
+ if self.free_margin < required_margin {
108
+ return Err("Not enough free margin".to_string());
109
+ }
110
+
111
+ let price = match order_type {
112
+ OrderType::Buy => ask,
113
+ OrderType::Sell => bid,
114
+ };
115
+
116
+ let ticket = self.ticket_counter;
117
+ self.ticket_counter += 1;
118
+
119
+ let pos = Position {
120
+ ticket,
121
+ symbol: symbol.to_string(),
122
+ order_type,
123
+ volume,
124
+ open_price: price,
125
+ sl,
126
+ tp,
127
+ open_time: time,
128
+ };
129
+
130
+ self.margin += required_margin;
131
+ self.open_positions.push(pos);
132
+ self.recalculate_equity();
133
+
134
+ Ok(ticket)
135
+ }
136
+
137
+ pub fn close_position(&mut self, ticket: u64, time: String) -> Result<(), String> {
138
+ let pos_index = self.open_positions.iter().position(|p| p.ticket == ticket)
139
+ .ok_or("Position ticket not found")?;
140
+
141
+ let pos = self.open_positions.remove(pos_index);
142
+ let &(bid, ask) = self.current_prices.get(&pos.symbol).ok_or("No price data for symbol")?;
143
+
144
+ let contract_size = 100000.0;
145
+
146
+ let (close_price, profit) = match pos.order_type {
147
+ OrderType::Buy => (bid, (bid - pos.open_price) * contract_size * pos.volume),
148
+ OrderType::Sell => (ask, (pos.open_price - ask) * contract_size * pos.volume),
149
+ };
150
+
151
+ self.balance += profit;
152
+
153
+ // Free margin back
154
+ let required_margin = (pos.volume * 100000.0) / self.config.leverage;
155
+ self.margin -= required_margin;
156
+
157
+ self.history.push(ClosedTrade {
158
+ ticket: pos.ticket,
159
+ symbol: pos.symbol.clone(),
160
+ order_type: pos.order_type,
161
+ volume: pos.volume,
162
+ open_price: pos.open_price,
163
+ close_price,
164
+ profit,
165
+ open_time: pos.open_time,
166
+ close_time: time,
167
+ });
168
+
169
+ self.recalculate_equity();
170
+ Ok(())
171
+ }
172
+ }
mql-rust/src/engine/data.rs ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use crate::data::parser::{Ohlcv, Tick};
2
+
3
+ pub enum DataRow {
4
+ Ohlc(Ohlcv),
5
+ TickData(Tick),
6
+ }
7
+
8
+ pub struct DataFeed {
9
+ pub symbol: String,
10
+ pub data: Vec<DataRow>,
11
+ cursor: usize,
12
+ }
13
+
14
+ impl DataFeed {
15
+ pub fn new(symbol: &str, data: Vec<DataRow>) -> Self {
16
+ Self {
17
+ symbol: symbol.to_string(),
18
+ data,
19
+ cursor: 0,
20
+ }
21
+ }
22
+
23
+ pub fn reset(&mut self) {
24
+ self.cursor = 0;
25
+ }
26
+
27
+ pub fn next(&mut self) -> Option<&DataRow> {
28
+ if self.cursor < self.data.len() {
29
+ let row = &self.data[self.cursor];
30
+ self.cursor += 1;
31
+ Some(row)
32
+ } else {
33
+ None
34
+ }
35
+ }
36
+
37
+ pub fn len(&self) -> usize {
38
+ self.data.len()
39
+ }
40
+
41
+ pub fn is_empty(&self) -> bool {
42
+ self.data.is_empty()
43
+ }
44
+ }
mql-rust/src/engine/mod.rs ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ pub mod backtester;
2
+ pub mod data;
3
+ pub mod tests;
mql-rust/src/engine/tests.rs ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #[cfg(test)]
2
+ mod tests {
3
+ use crate::compiler::lexer::Lexer;
4
+ use crate::compiler::lexer::Token;
5
+ use crate::engine::backtester::{Backtester, BacktestConfig, OrderType};
6
+
7
+ #[test]
8
+ fn test_mql5_lexer() {
9
+ let code = r#"
10
+ double input lotSize = 0.1;
11
+ void OnTick() {
12
+ OrderSend("EURUSD", OP_BUY, lotSize, Ask, 10, 0, 0);
13
+ }
14
+ "#;
15
+
16
+ // This is just a structural test that the lexer doesn't panic and returns tokens
17
+ let result = Lexer::tokenize(code);
18
+ assert!(result.is_ok());
19
+
20
+ let (_, tokens) = result.unwrap();
21
+ assert!(!tokens.is_empty());
22
+ assert!(tokens.contains(&Token::Double));
23
+ assert!(tokens.contains(&Token::Input));
24
+ assert!(tokens.contains(&Token::Identifier("lotSize".to_string())));
25
+ }
26
+
27
+ #[test]
28
+ fn test_backtest_engine_equity() {
29
+ let config = BacktestConfig {
30
+ initial_deposit: 10000.0,
31
+ leverage: 100.0,
32
+ spread_modifier: 0,
33
+ };
34
+
35
+ let mut bt = Backtester::new(config);
36
+
37
+ // Simulate feed: Bid=1.00000, Ask=1.00010
38
+ bt.update_tick("EURUSD", 1.00000, 1.00010);
39
+
40
+ let ticket = bt.market_order("EURUSD", OrderType::Buy, 1.0, None, None, "10:00".to_string()).unwrap();
41
+
42
+ // Price goes up 10 pips: Bid=1.00100, Ask=1.00110
43
+ bt.update_tick("EURUSD", 1.00100, 1.00110);
44
+
45
+ // We bought at Ask (1.00010), current Bid is 1.00100. Diff = +0.00090
46
+ // Profit = 0.00090 * 100000 * 1.0 = $90.00
47
+ assert!(bt.equity > 10080.0 && bt.equity < 10100.0);
48
+
49
+ // Close position
50
+ bt.close_position(ticket, "10:05".to_string()).unwrap();
51
+ assert!(bt.balance > 10080.0 && bt.balance < 10100.0);
52
+ assert_eq!(bt.open_positions.len(), 0);
53
+ assert_eq!(bt.history.len(), 1);
54
+ }
55
+ }
mql-rust/src/main.rs ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ pub mod compiler;
2
+ pub mod data;
3
+ pub mod engine;
4
+ pub mod ui;
5
+
6
+ use eframe::egui;
7
+ use ui::backtest_panel::BacktestPanel;
8
+
9
+ struct MqlRustApp {
10
+ backtest_panel: BacktestPanel,
11
+ }
12
+
13
+ impl MqlRustApp {
14
+ fn new(_cc: &eframe::CreationContext<'_>) -> Self {
15
+ Self {
16
+ backtest_panel: BacktestPanel::new(),
17
+ }
18
+ }
19
+ }
20
+
21
+ impl eframe::App for MqlRustApp {
22
+ fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
23
+ egui::CentralPanel::default().show(ctx, |ui| {
24
+ ui.heading("MQL-Rust Compiler & Backtester");
25
+ ui.separator();
26
+
27
+ // Draw configuration panel
28
+ if let Some(config) = self.backtest_panel.draw(ui) {
29
+ // Return config triggered a backtest run request
30
+ self.backtest_panel.compiler_log.push_str(&format!(
31
+ "\n> Starting backtest with Deposit: ${}, Leverage: 1:{}, Spread Modifier: {}",
32
+ config.initial_deposit, config.leverage, config.spread_modifier
33
+ ));
34
+ // TODO: Wire parsing and backtesting execution threads
35
+ }
36
+ });
37
+ }
38
+ }
39
+
40
+ fn main() -> eframe::Result<()> {
41
+ let options = eframe::NativeOptions {
42
+ viewport: egui::ViewportBuilder::default()
43
+ .with_inner_size([600.0, 700.0])
44
+ .with_title("MQL-Rust"),
45
+ ..Default::default()
46
+ };
47
+
48
+ eframe::run_native(
49
+ "MQL-Rust Console",
50
+ options,
51
+ Box::new(|cc| Ok(Box::new(MqlRustApp::new(cc)))),
52
+ )
53
+ }
mql-rust/src/ui/backtest_panel.rs ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use eframe::egui;
2
+ use rfd::FileDialog;
3
+ use std::path::PathBuf;
4
+ use crate::engine::backtester::BacktestConfig;
5
+
6
+ pub struct BacktestPanel {
7
+ pub initial_deposit_str: String,
8
+ pub leverage_str: String,
9
+ pub spread_modifier_str: String,
10
+ pub selected_symbols: Vec<String>,
11
+ pub new_symbol_input: String,
12
+ pub loaded_mql5_path: Option<PathBuf>,
13
+ pub compiler_log: String,
14
+ pub is_running: bool,
15
+ }
16
+
17
+ impl BacktestPanel {
18
+ pub fn new() -> Self {
19
+ Self {
20
+ initial_deposit_str: "10000.0".to_string(),
21
+ leverage_str: "100.0".to_string(),
22
+ spread_modifier_str: "0".to_string(),
23
+ selected_symbols: vec!["EURUSD".to_string()],
24
+ new_symbol_input: "".to_string(),
25
+ loaded_mql5_path: None,
26
+ compiler_log: "Ready.".to_string(),
27
+ is_running: false,
28
+ }
29
+ }
30
+
31
+ pub fn draw(&mut self, ui: &mut egui::Ui) -> Option<BacktestConfig> {
32
+ ui.heading("⚙️ Backtest Configuration");
33
+ ui.separator();
34
+
35
+ // Account Settings
36
+ ui.group(|ui| {
37
+ ui.label("Account Settings");
38
+ egui::Grid::new("config_grid").num_columns(2).spacing([10.0, 5.0]).show(ui, |ui| {
39
+ ui.label("Initial Deposit ($):");
40
+ ui.text_edit_singleline(&mut self.initial_deposit_str);
41
+ ui.end_row();
42
+
43
+ ui.label("Leverage (1:X):");
44
+ ui.text_edit_singleline(&mut self.leverage_str);
45
+ ui.end_row();
46
+
47
+ ui.label("Spread Modifier (points):");
48
+ ui.text_edit_singleline(&mut self.spread_modifier_str);
49
+ ui.end_row();
50
+ });
51
+ });
52
+
53
+ ui.add_space(10.0);
54
+
55
+ // Symbol Selection
56
+ ui.group(|ui| {
57
+ ui.label("Symbols for Backtest");
58
+ ui.horizontal(|ui| {
59
+ ui.text_edit_singleline(&mut self.new_symbol_input);
60
+ if ui.button("Add").clicked() {
61
+ if !self.new_symbol_input.is_empty() && !self.selected_symbols.contains(&self.new_symbol_input) {
62
+ self.selected_symbols.push(self.new_symbol_input.clone());
63
+ self.new_symbol_input.clear();
64
+ }
65
+ }
66
+ });
67
+
68
+ ui.horizontal_wrapped(|ui| {
69
+ let mut to_remove = None;
70
+ for (i, sym) in self.selected_symbols.iter().enumerate() {
71
+ ui.group(|ui| {
72
+ ui.horizontal(|ui| {
73
+ ui.label(sym);
74
+ if ui.small_button("x").clicked() {
75
+ to_remove = Some(i);
76
+ }
77
+ });
78
+ });
79
+ }
80
+ if let Some(i) = to_remove {
81
+ self.selected_symbols.remove(i);
82
+ }
83
+ });
84
+ });
85
+
86
+ ui.add_space(10.0);
87
+
88
+ // MQL5 File Loading
89
+ ui.group(|ui| {
90
+ ui.label("MQL5 Strategy Code");
91
+ ui.horizontal(|ui| {
92
+ if ui.button("📂 Load .mq5 File").clicked() {
93
+ if let Some(path) = FileDialog::new()
94
+ .add_filter("MQL5 Source", &["mq5"])
95
+ .pick_file()
96
+ {
97
+ self.loaded_mql5_path = Some(path);
98
+ self.compiler_log = "File loaded. Ready to compile.".to_string();
99
+ }
100
+ }
101
+ if let Some(p) = &self.loaded_mql5_path {
102
+ ui.label(p.display().to_string());
103
+ } else {
104
+ ui.label("No file selected.");
105
+ }
106
+ });
107
+ });
108
+
109
+ ui.add_space(10.0);
110
+
111
+ // Start button
112
+ ui.horizontal(|ui| {
113
+ if self.is_running {
114
+ ui.add_enabled(false, egui::Button::new("Running Backtest..."));
115
+ } else {
116
+ if ui.button("🚀 Compile & Run Backtest").clicked() {
117
+ if self.loaded_mql5_path.is_none() {
118
+ self.compiler_log = "Error: Please load an .mq5 file first.".to_string();
119
+ return None;
120
+ }
121
+ if self.selected_symbols.is_empty() {
122
+ self.compiler_log = "Error: Please add at least one symbol.".to_string();
123
+ return None;
124
+ }
125
+
126
+ if let (Ok(dep), Ok(lev), Ok(spr)) = (
127
+ self.initial_deposit_str.parse::<f64>(),
128
+ self.leverage_str.parse::<f64>(),
129
+ self.spread_modifier_str.parse::<u32>(),
130
+ ) {
131
+ self.compiler_log = "Starting compilation...".to_string();
132
+ self.is_running = true;
133
+
134
+ return Some(BacktestConfig {
135
+ initial_deposit: dep,
136
+ leverage: lev,
137
+ spread_modifier: spr,
138
+ });
139
+ } else {
140
+ self.compiler_log = "Error: Invalid numeric configuration fields.".to_string();
141
+ }
142
+ }
143
+ }
144
+ });
145
+
146
+ ui.add_space(5.0);
147
+
148
+ // Logs
149
+ ui.group(|ui| {
150
+ ui.set_min_height(60.0);
151
+ ui.label(&self.compiler_log);
152
+ });
153
+
154
+ None
155
+ }
156
+ }
mql-rust/src/ui/mod.rs ADDED
@@ -0,0 +1 @@
 
 
1
+ pub mod backtest_panel;