Upload 25 files
Browse files- .gitattributes +2 -0
- SUM3API (local)/MQL5/Experts/ZmqPublisher.mq5 +450 -0
- SUM3API (local)/MQL5/Include/Zmq/Zmq.mqh +144 -0
- SUM3API (local)/MQL5/Libraries/libsodium.dll +3 -0
- SUM3API (local)/MQL5/Libraries/libzmq.dll +3 -0
- SUM3API (local)/Rustmt5-chart/Cargo.lock +0 -0
- SUM3API (local)/Rustmt5-chart/Cargo.toml +15 -0
- SUM3API (local)/Rustmt5-chart/src/main.rs +852 -0
- mql-rust/.gitignore +1 -0
- mql-rust/Cargo.lock +0 -0
- mql-rust/Cargo.toml +20 -0
- mql-rust/src/compiler/ast.rs +63 -0
- mql-rust/src/compiler/lexer.rs +64 -0
- mql-rust/src/compiler/mod.rs +4 -0
- mql-rust/src/compiler/parser.rs +136 -0
- mql-rust/src/compiler/transpiler.rs +191 -0
- mql-rust/src/data/mod.rs +2 -0
- mql-rust/src/data/mt5_client.rs +64 -0
- mql-rust/src/data/parser.rs +86 -0
- mql-rust/src/engine/backtester.rs +172 -0
- mql-rust/src/engine/data.rs +44 -0
- mql-rust/src/engine/mod.rs +3 -0
- mql-rust/src/engine/tests.rs +55 -0
- mql-rust/src/main.rs +53 -0
- mql-rust/src/ui/backtest_panel.rs +156 -0
- mql-rust/src/ui/mod.rs +1 -0
.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;
|