|
|
@@ -1,313 +1,410 @@
|
|
1
|
1
|
//+------------------------------------------------------------------+
|
|
2
|
|
-//| MarketDataSender.mq5 |
|
|
3
|
|
-//| Copyright 2025, MetaQuotes Software Corp. |
|
|
4
|
|
-//| https://www.mql5.com |
|
|
|
2
|
+//| MarketDataSender.mq5 (Final Fixed Version) |
|
|
5
|
3
|
//+------------------------------------------------------------------+
|
|
6
|
|
-#property copyright "Copyright 2025, MetaQuotes Software Corp."
|
|
7
|
|
-#property link "https://www.mql5.com"
|
|
8
|
|
-#property version "1.00"
|
|
9
|
|
-#property description "Sends historical candles and live prices to REST API"
|
|
10
|
|
-#property script_show_inputs
|
|
|
4
|
+#property strict
|
|
|
5
|
+#property description "Fetches all symbols' candles and live prices, sends to API."
|
|
11
|
6
|
|
|
12
|
7
|
#include <Trade\SymbolInfo.mqh>
|
|
13
|
|
-#include <JAson.mqh> // Include JSON library
|
|
14
|
8
|
|
|
15
|
|
-//--- REST API Configuration
|
|
16
|
|
-input string ApiBaseUrl = "http://localhost:3000"; // Base URL of your API
|
|
17
|
|
-input string ApiKey = ""; // Optional API key if required
|
|
|
9
|
+input string ApiBaseUrl = "http://market-price.insightbull.io";
|
|
|
10
|
+input int HistoricalCandleCount = 1000;
|
|
|
11
|
+input ENUM_TIMEFRAMES HistoricalTimeframe = PERIOD_H1;
|
|
|
12
|
+input int LivePriceIntervalSeconds = 5;
|
|
18
|
13
|
|
|
19
|
|
-//--- Historical Data Configuration
|
|
20
|
|
-input int HistoricalCandleCount = 1000; // Number of historical candles to send
|
|
21
|
|
-input ENUM_TIMEFRAMES HistoricalTimeframe = PERIOD_H1; // Timeframe for historical data
|
|
22
|
|
-
|
|
23
|
|
-//--- Global variables
|
|
24
|
|
-CJAson json;
|
|
25
|
|
-CSymbolInfo symbolInfo;
|
|
|
14
|
+// Globals
|
|
26
|
15
|
string symbols[];
|
|
27
|
|
-datetime lastSentTime = 0;
|
|
|
16
|
+int symbolIds[];
|
|
|
17
|
+datetime lastSend = 0;
|
|
28
|
18
|
|
|
29
|
|
-//+------------------------------------------------------------------+
|
|
30
|
|
-//| Expert initialization function |
|
|
31
|
19
|
//+------------------------------------------------------------------+
|
|
32
|
20
|
int OnInit()
|
|
33
|
21
|
{
|
|
34
|
|
- // Get all symbols
|
|
35
|
|
- int count = SymbolsTotal(true);
|
|
36
|
|
- ArrayResize(symbols, count);
|
|
37
|
|
-
|
|
38
|
|
- for(int i = 0; i < count; i++)
|
|
|
22
|
+ Print("Initializing MarketDataSender EA...");
|
|
|
23
|
+ if(!InitializeSymbols())
|
|
39
|
24
|
{
|
|
40
|
|
- symbols[i] = SymbolName(i, true);
|
|
|
25
|
+ Print("❌ Failed to initialize symbols.");
|
|
|
26
|
+ return(INIT_FAILED);
|
|
41
|
27
|
}
|
|
42
|
|
-
|
|
43
|
|
- // Send initial historical data
|
|
44
|
|
- SendHistoricalData();
|
|
45
|
|
-
|
|
|
28
|
+
|
|
|
29
|
+ Print("✅ Symbols initialized: ", ArraySize(symbols));
|
|
|
30
|
+ SendAllHistoricalCandles();
|
|
46
|
31
|
return(INIT_SUCCEEDED);
|
|
47
|
32
|
}
|
|
48
|
33
|
|
|
49
|
|
-//+------------------------------------------------------------------+
|
|
50
|
|
-//| Expert tick function |
|
|
51
|
34
|
//+------------------------------------------------------------------+
|
|
52
|
35
|
void OnTick()
|
|
53
|
36
|
{
|
|
54
|
|
- // Send live prices every second
|
|
55
|
|
- if(TimeCurrent() - lastSentTime >= 1)
|
|
|
37
|
+ if(TimeCurrent() - lastSend >= LivePriceIntervalSeconds)
|
|
56
|
38
|
{
|
|
57
|
39
|
SendLivePrices();
|
|
58
|
|
- lastSentTime = TimeCurrent();
|
|
|
40
|
+ lastSend = TimeCurrent();
|
|
59
|
41
|
}
|
|
60
|
42
|
}
|
|
61
|
43
|
|
|
62
|
44
|
//+------------------------------------------------------------------+
|
|
63
|
|
-//| Send historical candle data |
|
|
64
|
|
-//+------------------------------------------------------------------+
|
|
65
|
|
-void SendHistoricalData()
|
|
|
45
|
+bool InitializeSymbols()
|
|
66
|
46
|
{
|
|
67
|
|
- for(int s = 0; s < ArraySize(symbols); s++)
|
|
|
47
|
+ int total = SymbolsTotal(true);
|
|
|
48
|
+ if(total <= 0)
|
|
68
|
49
|
{
|
|
69
|
|
- string symbol = symbols[s];
|
|
70
|
|
-
|
|
71
|
|
- // Get historical candles
|
|
72
|
|
- MqlRates rates[];
|
|
73
|
|
- int copied = CopyRates(symbol, HistoricalTimeframe, 0, HistoricalCandleCount, rates);
|
|
74
|
|
-
|
|
75
|
|
- if(copied <= 0) continue;
|
|
76
|
|
-
|
|
77
|
|
- // Prepare JSON payload
|
|
78
|
|
- CJAsonArray candlesArray;
|
|
79
|
|
-
|
|
80
|
|
- for(int i = 0; i < copied; i++)
|
|
81
|
|
- {
|
|
82
|
|
- CJAson candleObj;
|
|
83
|
|
- candleObj.Add("symbolId", GetSymbolId(symbol)); // You need to implement GetSymbolId()
|
|
84
|
|
- candleObj.Add("openTime", TimeToString(rates[i].time, TIME_DATE|TIME_MINUTES|TIME_SECONDS));
|
|
85
|
|
- candleObj.Add("closeTime", TimeToString(rates[i].time + PeriodSeconds(HistoricalTimeframe), TIME_DATE|TIME_MINUTES|TIME_SECONDS));
|
|
86
|
|
- candleObj.Add("open", rates[i].open);
|
|
87
|
|
- candleObj.Add("high", rates[i].high);
|
|
88
|
|
- candleObj.Add("low", rates[i].low);
|
|
89
|
|
- candleObj.Add("close", rates[i].close);
|
|
90
|
|
- candleObj.Add("volume", rates[i].tick_volume);
|
|
91
|
|
-
|
|
92
|
|
- candlesArray.Add(candleObj);
|
|
93
|
|
- }
|
|
94
|
|
-
|
|
95
|
|
- // Create final payload
|
|
96
|
|
- CJAson payload;
|
|
97
|
|
- payload.Add("candles", candlesArray);
|
|
98
|
|
-
|
|
99
|
|
- // Send to API
|
|
100
|
|
- string url = ApiBaseUrl + "/api/candles/bulk";
|
|
101
|
|
- string result;
|
|
102
|
|
- string headers = "Content-Type: application/json";
|
|
103
|
|
- if(StringLen(ApiKey) > 0) headers += "\r\nAuthorization: Bearer " + ApiKey;
|
|
104
|
|
-
|
|
105
|
|
- // Implement retry logic with exponential backoff
|
|
106
|
|
- int retries = 3;
|
|
107
|
|
- int delayMs = 1000;
|
|
108
|
|
- int res = -1;
|
|
109
|
|
-
|
|
110
|
|
- for(int attempt = 0; attempt < retries; attempt++) {
|
|
111
|
|
- res = WebRequest("POST", url, headers, 5000, payload.GetJson(), result);
|
|
112
|
|
-
|
|
113
|
|
- if(res == 200) break;
|
|
114
|
|
-
|
|
115
|
|
- Print("Attempt ", attempt+1, " failed (", res, "). Retrying in ", delayMs, "ms");
|
|
116
|
|
- Sleep(delayMs);
|
|
117
|
|
- delayMs *= 2; // Exponential backoff
|
|
118
|
|
- }
|
|
119
|
|
-
|
|
120
|
|
- if(res == 200) {
|
|
121
|
|
- Print("Successfully sent historical data for ", symbol);
|
|
122
|
|
- } else {
|
|
123
|
|
- Print("Permanent failure sending historical data for ", symbol, ": ", res, " - ", result);
|
|
124
|
|
- // TODO: Implement dead letter queue storage
|
|
125
|
|
- }
|
|
|
50
|
+ Print("❌ No symbols found!");
|
|
|
51
|
+ return false;
|
|
126
|
52
|
}
|
|
127
|
|
-}
|
|
128
|
53
|
|
|
129
|
|
-//+------------------------------------------------------------------+
|
|
130
|
|
-//| Send live prices |
|
|
131
|
|
-//+------------------------------------------------------------------+
|
|
132
|
|
-void SendLivePrices()
|
|
133
|
|
-{
|
|
134
|
|
- CJAsonArray pricesArray;
|
|
135
|
|
-
|
|
136
|
|
- for(int s = 0; s < ArraySize(symbols); s++)
|
|
|
54
|
+ ArrayResize(symbols, total);
|
|
|
55
|
+ ArrayResize(symbolIds, total);
|
|
|
56
|
+
|
|
|
57
|
+ for(int i = 0; i < total; i++)
|
|
137
|
58
|
{
|
|
138
|
|
- string symbol = symbols[s];
|
|
139
|
|
-
|
|
140
|
|
- if(!symbolInfo.Name(symbol)) continue;
|
|
141
|
|
- symbolInfo.RefreshRates();
|
|
142
|
|
-
|
|
143
|
|
- CJAson priceObj;
|
|
144
|
|
- priceObj.Add("symbolId", GetSymbolId(symbol));
|
|
145
|
|
- priceObj.Add("price", symbolInfo.Last());
|
|
146
|
|
- priceObj.Add("bid", symbolInfo.Bid());
|
|
147
|
|
- priceObj.Add("ask", symbolInfo.Ask());
|
|
148
|
|
- priceObj.Add("bidSize", symbolInfo.VolumeBid());
|
|
149
|
|
- priceObj.Add("askSize", symbolInfo.VolumeAsk());
|
|
150
|
|
-
|
|
151
|
|
- pricesArray.Add(priceObj);
|
|
|
59
|
+ symbols[i] = SymbolName(i, true);
|
|
|
60
|
+ symbolIds[i] = -1;
|
|
152
|
61
|
}
|
|
153
|
|
-
|
|
154
|
|
- // Create final payload
|
|
155
|
|
- CJAson payload;
|
|
156
|
|
- payload.Add("prices", pricesArray);
|
|
157
|
|
-
|
|
158
|
|
- // Send to API
|
|
159
|
|
- string url = ApiBaseUrl + "/api/live-prices/bulk";
|
|
160
|
|
- string result;
|
|
161
|
|
- string headers = "Content-Type: application/json";
|
|
162
|
|
- if(StringLen(ApiKey) > 0) headers += "\r\nAuthorization: Bearer " + ApiKey;
|
|
163
|
|
-
|
|
164
|
|
- // Implement retry logic with exponential backoff
|
|
165
|
|
- int retries = 3;
|
|
166
|
|
- int delayMs = 1000;
|
|
167
|
|
- int res = -1;
|
|
168
|
|
-
|
|
169
|
|
- for(int attempt = 0; attempt < retries; attempt++) {
|
|
170
|
|
- res = WebRequest("POST", url, headers, 5000, payload.GetJson(), result);
|
|
171
|
|
-
|
|
172
|
|
- if(res == 200) break;
|
|
173
|
|
-
|
|
174
|
|
- Print("Attempt ", attempt+1, " failed (", res, "). Retrying in ", delayMs, "ms");
|
|
175
|
|
- Sleep(delayMs);
|
|
176
|
|
- delayMs *= 2; // Exponential backoff
|
|
177
|
|
- }
|
|
178
|
|
-
|
|
179
|
|
- if(res == 200) {
|
|
180
|
|
- Print("Successfully sent live prices");
|
|
181
|
|
- } else {
|
|
182
|
|
- Print("Permanent failure sending live prices: ", res, " - ", result);
|
|
183
|
|
- // TODO: Implement dead letter queue storage
|
|
184
|
|
- }
|
|
|
62
|
+
|
|
|
63
|
+ if(!SyncSymbolsWithDatabase())
|
|
|
64
|
+ {
|
|
|
65
|
+ Print("❌ Failed to sync symbols with database");
|
|
|
66
|
+ return false;
|
|
|
67
|
+ }
|
|
|
68
|
+
|
|
|
69
|
+ return true;
|
|
185
|
70
|
}
|
|
186
|
71
|
|
|
187
|
72
|
//+------------------------------------------------------------------+
|
|
188
|
|
-//| Initialize symbol map from database |
|
|
189
|
|
-//+------------------------------------------------------------------+
|
|
190
|
|
-bool InitializeSymbolMap()
|
|
|
73
|
+bool SyncSymbolsWithDatabase()
|
|
191
|
74
|
{
|
|
192
|
|
- // Fetch existing symbols from API
|
|
193
|
|
- string url = ApiBaseUrl + "/api/symbols";
|
|
194
|
|
- string result;
|
|
195
|
|
- string headers = "Content-Type: application/json";
|
|
196
|
|
- if(StringLen(ApiKey) > 0) headers += "\r\nAuthorization: Bearer " + ApiKey;
|
|
197
|
|
-
|
|
198
|
|
- int res = WebRequest("GET", url, headers, 5000, "", result);
|
|
199
|
|
-
|
|
200
|
|
- if(res != 200)
|
|
|
75
|
+ Print("Syncing symbols with database...");
|
|
|
76
|
+
|
|
|
77
|
+ string url = ApiBaseUrl + "/api/symbols";
|
|
|
78
|
+ string headers = "Content-Type: application/json\r\n";
|
|
|
79
|
+ string resultHeaders = "";
|
|
|
80
|
+ char result[];
|
|
|
81
|
+ char emptyData[]; // ✅ required placeholder for GET request
|
|
|
82
|
+
|
|
|
83
|
+ ResetLastError();
|
|
|
84
|
+
|
|
|
85
|
+ // ✅ Correct GET request signature: includes empty data[]
|
|
|
86
|
+ int res = WebRequest("GET", url, headers, 5000, emptyData, result, resultHeaders);
|
|
|
87
|
+
|
|
|
88
|
+ if(res == -1)
|
|
201
|
89
|
{
|
|
202
|
|
- Print("Failed to fetch symbols: ", res, " - ", result);
|
|
|
90
|
+ int err = GetLastError();
|
|
|
91
|
+ Print("❌ WebRequest connection error: ", err, " URL=", url);
|
|
203
|
92
|
return false;
|
|
204
|
93
|
}
|
|
205
|
|
-
|
|
206
|
|
- // Parse response
|
|
207
|
|
- CJAson parser;
|
|
208
|
|
- if(!parser.Parse(result))
|
|
|
94
|
+
|
|
|
95
|
+ if(res != 200)
|
|
209
|
96
|
{
|
|
210
|
|
- Print("Failed to parse symbols response");
|
|
|
97
|
+ Print("❌ Failed to fetch symbols from API: HTTP ", res, " Response: ", CharArrayToString(result));
|
|
211
|
98
|
return false;
|
|
212
|
99
|
}
|
|
213
|
|
-
|
|
214
|
|
- // Create lookup table
|
|
215
|
|
- CJAsonArray symbolsArray = parser.GetArray("data");
|
|
216
|
|
- int dbSymbolCount = symbolsArray.Size();
|
|
217
|
|
-
|
|
218
|
|
- for(int s = 0; s < ArraySize(symbols); s++)
|
|
|
100
|
+
|
|
|
101
|
+ string symbolsResponse = CharArrayToString(result);
|
|
|
102
|
+
|
|
|
103
|
+ if(StringFind(symbolsResponse, "\"data\"") < 0)
|
|
219
|
104
|
{
|
|
220
|
|
- string mt5Symbol = symbols[s];
|
|
221
|
|
- bool found = false;
|
|
222
|
|
-
|
|
223
|
|
- // Search for matching symbol in database
|
|
224
|
|
- for(int i = 0; i < dbSymbolCount; i++)
|
|
|
105
|
+ Print("⚠️ Unexpected response format from symbols API: ", symbolsResponse);
|
|
|
106
|
+ }
|
|
|
107
|
+
|
|
|
108
|
+ for(int i = 0; i < ArraySize(symbols); i++)
|
|
|
109
|
+ {
|
|
|
110
|
+ string symbolName = symbols[i];
|
|
|
111
|
+ int symbolId = FindSymbolId(symbolsResponse, symbolName);
|
|
|
112
|
+
|
|
|
113
|
+ if(symbolId > 0)
|
|
225
|
114
|
{
|
|
226
|
|
- CJAson dbSymbol = symbolsArray.GetObject(i);
|
|
227
|
|
- string dbSymbolName = dbSymbol.GetString("symbol");
|
|
228
|
|
-
|
|
229
|
|
- if(dbSymbolName == mt5Symbol)
|
|
230
|
|
- {
|
|
231
|
|
- symbolIdMap[s] = (int)dbSymbol.GetInt("id");
|
|
232
|
|
- found = true;
|
|
233
|
|
- break;
|
|
234
|
|
- }
|
|
|
115
|
+ symbolIds[i] = symbolId;
|
|
|
116
|
+ Print("✅ Found existing symbol: ", symbolName, " (ID: ", symbolId, ")");
|
|
235
|
117
|
}
|
|
236
|
|
-
|
|
237
|
|
- // Create symbol if not found
|
|
238
|
|
- if(!found)
|
|
|
118
|
+ else
|
|
239
|
119
|
{
|
|
240
|
|
- int newId = CreateSymbol(mt5Symbol);
|
|
241
|
|
- if(newId > 0)
|
|
|
120
|
+ Sleep(300); // prevent overload (0.3 second delay)
|
|
|
121
|
+ symbolId = CreateSymbolInDatabase(symbolName);
|
|
|
122
|
+ if(symbolId > 0)
|
|
242
|
123
|
{
|
|
243
|
|
- symbolIdMap[s] = newId;
|
|
244
|
|
- Print("Created new symbol: ", mt5Symbol, " (ID: ", newId, ")");
|
|
|
124
|
+ symbolIds[i] = symbolId;
|
|
|
125
|
+ Print("✅ Created new symbol: ", symbolName, " (ID: ", symbolId, ")");
|
|
245
|
126
|
}
|
|
246
|
127
|
else
|
|
247
|
128
|
{
|
|
248
|
|
- Print("Failed to create symbol: ", mt5Symbol);
|
|
249
|
|
- return false;
|
|
|
129
|
+ Print("❌ Failed to create symbol: ", symbolName," (ID: ", symbolId, ")");
|
|
|
130
|
+ symbolIds[i] = -1;
|
|
250
|
131
|
}
|
|
251
|
132
|
}
|
|
252
|
133
|
}
|
|
253
|
|
-
|
|
|
134
|
+
|
|
254
|
135
|
return true;
|
|
255
|
136
|
}
|
|
256
|
137
|
|
|
257
|
138
|
//+------------------------------------------------------------------+
|
|
258
|
|
-//| Create new symbol in database |
|
|
|
139
|
+int FindSymbolId(string response, string symbolName)
|
|
|
140
|
+{
|
|
|
141
|
+ string searchPattern = StringFormat("\"symbol\":\"%s\"", symbolName);
|
|
|
142
|
+ int symbolPos = StringFind(response, searchPattern);
|
|
|
143
|
+ if(symbolPos < 0) return -1;
|
|
|
144
|
+
|
|
|
145
|
+ int idPos = StringFind(response, "\"id\":", symbolPos);
|
|
|
146
|
+ if(idPos < 0) return -1;
|
|
|
147
|
+
|
|
|
148
|
+ int startPos = idPos + 5;
|
|
|
149
|
+ int endPos = StringFind(response, ",", startPos);
|
|
|
150
|
+ if(endPos < 0) endPos = StringFind(response, "}", startPos);
|
|
|
151
|
+
|
|
|
152
|
+ string idStr = StringSubstr(response, startPos, endPos - startPos);
|
|
|
153
|
+ return (int)StringToInteger(idStr);
|
|
|
154
|
+}
|
|
|
155
|
+
|
|
259
|
156
|
//+------------------------------------------------------------------+
|
|
260
|
|
-int CreateSymbol(string symbol)
|
|
|
157
|
+int CreateSymbolInDatabase(string symbolName)
|
|
261
|
158
|
{
|
|
262
|
|
- string url = ApiBaseUrl + "/api/symbols";
|
|
263
|
|
- string result;
|
|
264
|
|
- string headers = "Content-Type: application/json";
|
|
265
|
|
- if(StringLen(ApiKey) > 0) headers += "\r\nAuthorization: Bearer " + ApiKey;
|
|
266
|
|
-
|
|
267
|
|
- // Extract exchange and instrument type from symbol name
|
|
268
|
|
- string parts[];
|
|
269
|
|
- StringSplit(symbol, '_', parts);
|
|
270
|
|
- string exchange = (ArraySize(parts) > 1) ? parts[0] : "MT5";
|
|
271
|
|
- string instrumentType = "forex"; // Default, can be improved
|
|
272
|
|
-
|
|
273
|
|
- // Prepare payload
|
|
274
|
|
- CJAson payload;
|
|
275
|
|
- payload.Add("symbol", symbol);
|
|
276
|
|
- payload.Add("exchange", exchange);
|
|
277
|
|
- payload.Add("instrumentType", instrumentType);
|
|
278
|
|
- payload.Add("isActive", true);
|
|
279
|
|
-
|
|
280
|
|
- int res = WebRequest("POST", url, headers, 5000, payload.GetJson(), result);
|
|
281
|
|
-
|
|
282
|
|
- if(res != 201)
|
|
|
159
|
+ string baseAsset = "";
|
|
|
160
|
+ string quoteAsset = "";
|
|
|
161
|
+ string exchange = "MT5";
|
|
|
162
|
+ string instrumentType = "forex";
|
|
|
163
|
+
|
|
|
164
|
+ if(StringLen(symbolName) >= 6)
|
|
283
|
165
|
{
|
|
284
|
|
- Print("Error creating symbol: ", res, " - ", result);
|
|
285
|
|
- return -1;
|
|
|
166
|
+ baseAsset = StringSubstr(symbolName, 0, 3);
|
|
|
167
|
+ quoteAsset = StringSubstr(symbolName, 3, 3);
|
|
286
|
168
|
}
|
|
287
|
|
-
|
|
288
|
|
- // Parse response to get new ID
|
|
289
|
|
- CJAson parser;
|
|
290
|
|
- if(!parser.Parse(result))
|
|
|
169
|
+
|
|
|
170
|
+ string json = StringFormat(
|
|
|
171
|
+ "{\"symbol\":\"%s\",\"baseAsset\":\"%s\",\"quoteAsset\":\"%s\",\"exchange\":\"%s\",\"instrumentType\":\"%s\",\"isActive\":true}",
|
|
|
172
|
+ symbolName, baseAsset, quoteAsset, exchange, instrumentType
|
|
|
173
|
+ );
|
|
|
174
|
+
|
|
|
175
|
+ string url = ApiBaseUrl + "/api/symbols";
|
|
|
176
|
+ string headers = "Content-Type: application/json\r\n";
|
|
|
177
|
+ string resultHeaders = "";
|
|
|
178
|
+
|
|
|
179
|
+ char postData[];
|
|
|
180
|
+ StringToCharArray(json, postData, 0, CP_UTF8);
|
|
|
181
|
+
|
|
|
182
|
+ // ✅ FIX: Remove trailing null terminator from JSON
|
|
|
183
|
+ if(ArraySize(postData) > 0 && postData[ArraySize(postData) - 1] == 0)
|
|
|
184
|
+ ArrayResize(postData, ArraySize(postData) - 1);
|
|
|
185
|
+
|
|
|
186
|
+ char result[];
|
|
|
187
|
+
|
|
|
188
|
+ int res = WebRequest("POST", url, headers, 5000, postData, result, resultHeaders);
|
|
|
189
|
+
|
|
|
190
|
+ if(res != 201 && res != 200)
|
|
291
|
191
|
{
|
|
292
|
|
- Print("Failed to parse create symbol response");
|
|
|
192
|
+ Print("❌ Failed to create symbol: ", res, " Response: ", CharArrayToString(result));
|
|
293
|
193
|
return -1;
|
|
294
|
194
|
}
|
|
295
|
|
-
|
|
296
|
|
- return (int)parser.GetObject("data").GetInt("id");
|
|
|
195
|
+
|
|
|
196
|
+ string createResponse = CharArrayToString(result);
|
|
|
197
|
+ int idPos = StringFind(createResponse, "\"id\":");
|
|
|
198
|
+ if(idPos < 0) return -1;
|
|
|
199
|
+
|
|
|
200
|
+ int startPos = idPos + 5;
|
|
|
201
|
+ int endPos = StringFind(createResponse, ",", startPos);
|
|
|
202
|
+ if(endPos < 0) endPos = StringFind(createResponse, "}", startPos);
|
|
|
203
|
+
|
|
|
204
|
+ string idStr = StringSubstr(createResponse, startPos, endPos - startPos);
|
|
|
205
|
+ return (int)StringToInteger(idStr);
|
|
297
|
206
|
}
|
|
298
|
207
|
|
|
299
|
208
|
//+------------------------------------------------------------------+
|
|
300
|
|
-//| Get symbol ID from map |
|
|
301
|
209
|
//+------------------------------------------------------------------+
|
|
302
|
|
-int GetSymbolId(string symbol)
|
|
|
210
|
+//| Send all historical candles to the API (Fixed Version) |
|
|
|
211
|
+//+------------------------------------------------------------------+
|
|
|
212
|
+void SendAllHistoricalCandles()
|
|
303
|
213
|
{
|
|
|
214
|
+ Print("Starting historical upload for ", ArraySize(symbols), " symbols...");
|
|
|
215
|
+
|
|
304
|
216
|
for(int i = 0; i < ArraySize(symbols); i++)
|
|
305
|
217
|
{
|
|
306
|
|
- if(symbols[i] == symbol)
|
|
|
218
|
+ string sym = symbols[i];
|
|
|
219
|
+
|
|
|
220
|
+ // --- Ensure data is ready ---
|
|
|
221
|
+ Sleep(300);
|
|
|
222
|
+ int tries = 0;
|
|
|
223
|
+ while(!SeriesInfoInteger(sym, HistoricalTimeframe, SERIES_SYNCHRONIZED) && tries < 10)
|
|
|
224
|
+ {
|
|
|
225
|
+ Print("⏳ Waiting for ", sym, " history to load...");
|
|
|
226
|
+ Sleep(500);
|
|
|
227
|
+ tries++;
|
|
|
228
|
+ }
|
|
|
229
|
+
|
|
|
230
|
+ // --- Now copy candles ---
|
|
|
231
|
+ MqlRates rates[];
|
|
|
232
|
+ ResetLastError();
|
|
|
233
|
+ int copied = CopyRates(sym, HistoricalTimeframe, 0, HistoricalCandleCount, rates);
|
|
|
234
|
+ int err = GetLastError();
|
|
|
235
|
+
|
|
|
236
|
+ if(copied <= 0)
|
|
|
237
|
+ {
|
|
|
238
|
+ Print("⚠️ Failed to copy candles for ", sym, " (copied=", copied, ", err=", err, ")");
|
|
|
239
|
+ continue;
|
|
|
240
|
+ }
|
|
|
241
|
+
|
|
|
242
|
+ Print("✅ Copied ", copied, " candles for ", sym);
|
|
|
243
|
+
|
|
|
244
|
+ // --- Print a few sample candles ---
|
|
|
245
|
+ int sampleCount = MathMin(5, copied); // show up to 5 examples
|
|
|
246
|
+ for(int j = 0; j < sampleCount; j++)
|
|
307
|
247
|
{
|
|
308
|
|
- return symbolIdMap[i];
|
|
|
248
|
+ MqlRates r = rates[j];
|
|
|
249
|
+ string openTime = TimeToString(r.time, TIME_DATE|TIME_SECONDS);
|
|
|
250
|
+ string closeTime = TimeToString(r.time + PeriodSeconds(HistoricalTimeframe), TIME_DATE|TIME_SECONDS);
|
|
|
251
|
+ PrintFormat(
|
|
|
252
|
+ "🕒 [%s] %s → %s | O=%.5f H=%.5f L=%.5f C=%.5f | Vol=%.2f",
|
|
|
253
|
+ sym, openTime, closeTime, r.open, r.high, r.low, r.close, r.tick_volume
|
|
|
254
|
+ );
|
|
309
|
255
|
}
|
|
|
256
|
+
|
|
|
257
|
+ // --- Send candles in batches to API ---
|
|
|
258
|
+ int sentTotal = 0;
|
|
|
259
|
+ int batchSize = 200;
|
|
|
260
|
+ for(int start = 0; start < copied; start += batchSize)
|
|
|
261
|
+ {
|
|
|
262
|
+ int size = MathMin(batchSize, copied - start);
|
|
|
263
|
+ int symbolId = symbolIds[i];
|
|
|
264
|
+ if(symbolId <= 0) continue;
|
|
|
265
|
+
|
|
|
266
|
+ string json = BuildCandleJSONFromRates(symbolId, rates, start, size);
|
|
|
267
|
+ string url = ApiBaseUrl + "/api/candles/bulk";
|
|
|
268
|
+ string response;
|
|
|
269
|
+ bool ok = SendJSON(url, json, response);
|
|
|
270
|
+ if(!ok)
|
|
|
271
|
+ {
|
|
|
272
|
+ Print("❌ Failed to send candle batch for ", sym, " start=", start);
|
|
|
273
|
+ break;
|
|
|
274
|
+ }
|
|
|
275
|
+ sentTotal += size;
|
|
|
276
|
+ Print("📤 Sent candles for ", sym, ": ", sentTotal, "/", copied);
|
|
|
277
|
+ }
|
|
|
278
|
+ }
|
|
|
279
|
+ Print("✅ Historical upload finished.");
|
|
|
280
|
+}
|
|
|
281
|
+
|
|
|
282
|
+
|
|
|
283
|
+//+------------------------------------------------------------------+
|
|
|
284
|
+void SendLivePrices()
|
|
|
285
|
+{
|
|
|
286
|
+ bool firstItem = true;
|
|
|
287
|
+ string json = "{\"prices\":[";
|
|
|
288
|
+
|
|
|
289
|
+ int sentCount = 0;
|
|
|
290
|
+ for(int i = 0; i < ArraySize(symbols); i++)
|
|
|
291
|
+ {
|
|
|
292
|
+ string sym = symbols[i];
|
|
|
293
|
+ double bid = SymbolInfoDouble(sym, SYMBOL_BID);
|
|
|
294
|
+ double ask = SymbolInfoDouble(sym, SYMBOL_ASK);
|
|
|
295
|
+ double last = SymbolInfoDouble(sym, SYMBOL_LAST);
|
|
|
296
|
+ if(bid <= 0 || ask <= 0 || last <= 0) continue;
|
|
|
297
|
+
|
|
|
298
|
+ int symId = symbolIds[i];
|
|
|
299
|
+ if(symId <= 0) continue;
|
|
|
300
|
+
|
|
|
301
|
+ string item = StringFormat("{\"symbolId\":%d,\"price\":%.8f,\"bid\":%.8f,\"ask\":%.8f,\"bidSize\":%.8f,\"askSize\":%.8f}",
|
|
|
302
|
+ symId, last, bid, ask, 0.0, 0.0);
|
|
|
303
|
+ if(!firstItem) json += ",";
|
|
|
304
|
+ json += item;
|
|
|
305
|
+ firstItem = false;
|
|
|
306
|
+ sentCount++;
|
|
|
307
|
+ }
|
|
|
308
|
+
|
|
|
309
|
+ json += "]}";
|
|
|
310
|
+
|
|
|
311
|
+ if(sentCount == 0)
|
|
|
312
|
+ {
|
|
|
313
|
+ Print("No valid live prices to send right now.");
|
|
|
314
|
+ return;
|
|
|
315
|
+ }
|
|
|
316
|
+
|
|
|
317
|
+ string url = ApiBaseUrl + "/api/live-prices/bulk";
|
|
|
318
|
+ string response;
|
|
|
319
|
+ bool ok = SendJSON(url, json, response);
|
|
|
320
|
+ if(ok)
|
|
|
321
|
+ Print("✅ Sent ", sentCount, " live prices.");
|
|
|
322
|
+ else
|
|
|
323
|
+ Print("❌ Failed to send live prices (sent ", sentCount, " items). Response: ", response);
|
|
|
324
|
+}
|
|
|
325
|
+
|
|
|
326
|
+//+------------------------------------------------------------------+
|
|
|
327
|
+string ToISO8601(datetime t)
|
|
|
328
|
+{
|
|
|
329
|
+ MqlDateTime st;
|
|
|
330
|
+ TimeToStruct(t, st);
|
|
|
331
|
+ return StringFormat("%04d-%02d-%02dT%02d:%02d:%02d.000Z", st.year, st.mon, st.day, st.hour, st.min, st.sec);
|
|
|
332
|
+}
|
|
|
333
|
+
|
|
|
334
|
+string BuildCandleJSONFromRates(int symbolId, MqlRates &rates[], int startIndex, int count)
|
|
|
335
|
+{
|
|
|
336
|
+ string json = "{\"candles\":[";
|
|
|
337
|
+ bool first = true;
|
|
|
338
|
+ int ratesSize = ArraySize(rates);
|
|
|
339
|
+
|
|
|
340
|
+ for(int i = startIndex; i < startIndex + count && i < ratesSize; i++)
|
|
|
341
|
+ {
|
|
|
342
|
+ MqlRates r = rates[i];
|
|
|
343
|
+ if(r.time <= 0) continue;
|
|
|
344
|
+
|
|
|
345
|
+ datetime open_dt = (datetime)r.time;
|
|
|
346
|
+ datetime close_dt = (datetime)(r.time + (datetime)PeriodSeconds(HistoricalTimeframe));
|
|
|
347
|
+
|
|
|
348
|
+ string openTime = ToISO8601(open_dt);
|
|
|
349
|
+ string closeTime = ToISO8601(close_dt);
|
|
|
350
|
+
|
|
|
351
|
+ double volume = (r.tick_volume > 0 ? r.tick_volume : 1);
|
|
|
352
|
+ double quoteVolume = (r.real_volume > 0 ? r.real_volume : volume);
|
|
|
353
|
+
|
|
|
354
|
+ string one = StringFormat(
|
|
|
355
|
+ "{\"symbolId\":%d,\"openTime\":\"%s\",\"closeTime\":\"%s\",\"open\":%.5f,\"high\":%.5f,\"low\":%.5f,\"close\":%.5f,\"volume\":%.5f,\"tradesCount\":%d,\"quoteVolume\":%.5f}",
|
|
|
356
|
+ symbolId, openTime, closeTime,
|
|
|
357
|
+ r.open, r.high, r.low, r.close,
|
|
|
358
|
+ volume, (int)volume, quoteVolume
|
|
|
359
|
+ );
|
|
|
360
|
+
|
|
|
361
|
+ if(!first) json += ",";
|
|
|
362
|
+ json += one;
|
|
|
363
|
+ first = false;
|
|
310
|
364
|
}
|
|
311
|
|
- return -1;
|
|
|
365
|
+
|
|
|
366
|
+ json += "]}";
|
|
|
367
|
+ return json;
|
|
312
|
368
|
}
|
|
|
369
|
+
|
|
|
370
|
+//+------------------------------------------------------------------+
|
|
|
371
|
+bool SendJSON(string url, string json, string &response)
|
|
|
372
|
+{
|
|
|
373
|
+ ResetLastError();
|
|
|
374
|
+
|
|
|
375
|
+ char postData[];
|
|
|
376
|
+ StringToCharArray(json, postData, 0, CP_UTF8);
|
|
|
377
|
+
|
|
|
378
|
+ // ✅ Remove trailing null terminator
|
|
|
379
|
+ if(ArraySize(postData) > 0 && postData[ArraySize(postData) - 1] == 0)
|
|
|
380
|
+ ArrayResize(postData, ArraySize(postData) - 1);
|
|
|
381
|
+
|
|
|
382
|
+ if(ArraySize(postData) <= 0)
|
|
|
383
|
+ {
|
|
|
384
|
+ Print("❌ Empty postData for URL: ", url);
|
|
|
385
|
+ return false;
|
|
|
386
|
+ }
|
|
|
387
|
+
|
|
|
388
|
+ char result[];
|
|
|
389
|
+ string headers = "Content-Type: application/json\r\n";
|
|
|
390
|
+ string resultHeaders = "";
|
|
|
391
|
+ int timeout = 15000;
|
|
|
392
|
+
|
|
|
393
|
+ int res = WebRequest("POST", url, headers, timeout, postData, result, resultHeaders);
|
|
|
394
|
+
|
|
|
395
|
+ if(res == -1)
|
|
|
396
|
+ {
|
|
|
397
|
+ int err = GetLastError();
|
|
|
398
|
+ Print("WebRequest error: ", err, " url=", url);
|
|
|
399
|
+ return false;
|
|
|
400
|
+ }
|
|
|
401
|
+
|
|
|
402
|
+ response = CharArrayToString(result);
|
|
|
403
|
+ if(res == 200 || res == 201)
|
|
|
404
|
+ return true;
|
|
|
405
|
+
|
|
|
406
|
+ Print("HTTP status ", res, " response: ", response);
|
|
|
407
|
+ return false;
|
|
|
408
|
+}
|
|
|
409
|
+
|
|
313
|
410
|
//+------------------------------------------------------------------+
|