Просмотр исходного кода

Enhance MT5 MarketDataSender with robust symbol matching and incremental candle uploads

- Rewrote FindSymbolId function for exact symbol matching in JSON responses with improved parsing and debug logging
- Added GetLatestCandleTime function to fetch latest stored candle time from API
- Modified SendAllHistoricalCandles to perform incremental uploads, sending only new candles after the latest stored time
- Enhanced SendLivePrices with better price validation, fallback mechanisms, and market depth integration for bid/ask sizes
- Improved error handling and logging throughout for better debugging
uzairrizwan1 месяцев назад: 3
Родитель
Сommit
98f74bfbcd
1 измененных файлов с 229 добавлено и 43 удалено
  1. 229 43
      MT5/Experts/MarketDataSender.mq5

+ 229 - 43
MT5/Experts/MarketDataSender.mq5

@@ -135,22 +135,63 @@ bool SyncSymbolsWithDatabase()
135 135
    return true;
136 136
 }
137 137
 
138
+//+------------------------------------------------------------------+
139
+//+------------------------------------------------------------------+
140
+//| Find exact symbolId from JSON response                           |
141
+//+------------------------------------------------------------------+
142
+//+------------------------------------------------------------------+
143
+//| Robust JSON search: matches exact symbol only                    |
138 144
 //+------------------------------------------------------------------+
139 145
 int FindSymbolId(string response, string symbolName)
140 146
 {
141
-   string searchPattern = StringFormat("\"symbol\":\"%s\"", symbolName);
142
-   int symbolPos = StringFind(response, searchPattern);
143
-   if(symbolPos < 0) return -1;
147
+   int pos = 0;
148
+   string patternSymbol = "\"symbol\":\"";
149
+   string patternId = "\"id\":";
144 150
 
145
-   int idPos = StringFind(response, "\"id\":", symbolPos);
146
-   if(idPos < 0) return -1;
151
+   while(true)
152
+   {
153
+      // find each symbol occurrence
154
+      int symPos = StringFind(response, patternSymbol, pos);
155
+      if(symPos < 0)
156
+         break;
147 157
 
148
-   int startPos = idPos + 5;
149
-   int endPos = StringFind(response, ",", startPos);
150
-   if(endPos < 0) endPos = StringFind(response, "}", startPos);
158
+      // extract actual symbol value
159
+      int symStart = symPos + StringLen(patternSymbol);
160
+      int symEnd = StringFind(response, "\"", symStart);
161
+      if(symEnd < 0) break;
151 162
 
152
-   string idStr = StringSubstr(response, startPos, endPos - startPos);
153
-   return (int)StringToInteger(idStr);
163
+      string foundSymbol = StringSubstr(response, symStart, symEnd - symStart);
164
+
165
+      // 🟩 DEBUG LOG: show all symbols found
166
+      // ✅ exact match check (case-sensitive)
167
+      if(foundSymbol == symbolName)
168
+      {
169
+         // find id that comes *before* this symbol entry
170
+         int blockStart = StringFind(response, patternId, symPos - 100);
171
+         if(blockStart < 0)
172
+            blockStart = StringFind(response, patternId, symPos);
173
+
174
+         if(blockStart >= 0)
175
+         {
176
+            int idStart = blockStart + StringLen(patternId);
177
+            int idEnd = StringFind(response, ",", idStart);
178
+            if(idEnd < 0) idEnd = StringFind(response, "}", idStart);
179
+
180
+            string idStr = StringSubstr(response, idStart, idEnd - idStart);
181
+            int id = (int)StringToInteger(idStr);
182
+
183
+            // 🟩 Success log
184
+            Print("✅ Exact match found → symbol='", symbolName, "' | ID=", id);
185
+            return id;
186
+         }
187
+      }
188
+
189
+      // move to next
190
+      pos = symEnd + 1;
191
+   }
192
+
193
+   Print("⚠️ No exact match for symbol ", symbolName);
194
+   return -1;
154 195
 }
155 196
 
156 197
 //+------------------------------------------------------------------+
@@ -204,6 +245,54 @@ int CreateSymbolInDatabase(string symbolName)
204 245
    string idStr = StringSubstr(createResponse, startPos, endPos - startPos);
205 246
    return (int)StringToInteger(idStr);
206 247
 }
248
+//+------------------------------------------------------------------+
249
+//| Fetch latest stored candle openTime from API                     |
250
+//+------------------------------------------------------------------+
251
+datetime GetLatestCandleTime(int symbolId)
252
+{
253
+   string url = ApiBaseUrl + "/api/candles/" + IntegerToString(symbolId) + "/latest";
254
+   string headers = "Content-Type: application/json\r\n";
255
+   string resultHeaders = "";
256
+   char result[];
257
+   char emptyData[];
258
+
259
+   ResetLastError();
260
+   int res = WebRequest("GET", url, headers, 10000, emptyData, result, resultHeaders);
261
+
262
+   if(res != 200)
263
+   {
264
+      Print("⚠️ Could not fetch latest candle for symbolId=", symbolId, " (HTTP ", res, ")");
265
+      return 0;
266
+   }
267
+
268
+   string response = CharArrayToString(result);
269
+   int pos = StringFind(response, "\"openTime\":\"");
270
+   if(pos < 0)
271
+   {
272
+      Print("⚠️ No openTime found in response for symbolId=", symbolId);
273
+      return 0;
274
+   }
275
+
276
+   pos += StringLen("\"openTime\":\"");
277
+   int end = StringFind(response, "\"", pos);
278
+   string openTimeStr = StringSubstr(response, pos, end - pos);
279
+
280
+   // --- Parse ISO8601 to datetime ---
281
+   int year  = (int)StringToInteger(StringSubstr(openTimeStr, 0, 4));
282
+   int month = (int)StringToInteger(StringSubstr(openTimeStr, 5, 2));
283
+   int day   = (int)StringToInteger(StringSubstr(openTimeStr, 8, 2));
284
+   int hour  = (int)StringToInteger(StringSubstr(openTimeStr, 11, 2));
285
+   int min   = (int)StringToInteger(StringSubstr(openTimeStr, 14, 2));
286
+   int sec   = (int)StringToInteger(StringSubstr(openTimeStr, 17, 2));
287
+
288
+   MqlDateTime t;
289
+   t.year = year; t.mon = month; t.day = day;
290
+   t.hour = hour; t.min = min; t.sec = sec;
291
+
292
+   datetime dt = StructToTime(t);
293
+   Print("🕓 Latest stored candle openTime for symbolId=", symbolId, " → ", TimeToString(dt, TIME_DATE|TIME_SECONDS));
294
+   return dt;
295
+}
207 296
 
208 297
 //+------------------------------------------------------------------+
209 298
 //+------------------------------------------------------------------+
@@ -216,6 +305,11 @@ void SendAllHistoricalCandles()
216 305
    for(int i = 0; i < ArraySize(symbols); i++)
217 306
    {
218 307
       string sym = symbols[i];
308
+      int symbolId = symbolIds[i];
309
+      if(symbolId <= 0) continue;
310
+
311
+      // --- Get last stored candle time ---
312
+      datetime latestApiTime = GetLatestCandleTime(symbolId);
219 313
 
220 314
       // --- Ensure data is ready ---
221 315
       Sleep(300);
@@ -227,42 +321,43 @@ void SendAllHistoricalCandles()
227 321
          tries++;
228 322
       }
229 323
 
230
-      // --- Now copy candles ---
231 324
       MqlRates rates[];
232 325
       ResetLastError();
233 326
       int copied = CopyRates(sym, HistoricalTimeframe, 0, HistoricalCandleCount, rates);
234
-      int err = GetLastError();
235
-
236 327
       if(copied <= 0)
237 328
       {
238
-         Print("⚠️ Failed to copy candles for ", sym, " (copied=", copied, ", err=", err, ")");
329
+         Print("⚠️ Failed to copy candles for ", sym);
239 330
          continue;
240 331
       }
241 332
 
242 333
       Print("✅ Copied ", copied, " candles for ", sym);
243 334
 
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++)
335
+      // --- Filter new candles ---
336
+      int startIndex = 0;
337
+      for(int j = 0; j < copied; j++)
247 338
       {
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
-         );
339
+         if(rates[j].time > latestApiTime)
340
+         {
341
+            startIndex = j;
342
+            break;
343
+         }
255 344
       }
256 345
 
257
-      // --- Send candles in batches to API ---
258
-      int sentTotal = 0;
346
+      int newCount = copied - startIndex;
347
+      if(newCount <= 0)
348
+      {
349
+         Print("ℹ️ No new candles to send for ", sym);
350
+         continue;
351
+      }
352
+
353
+      Print("🆕 Sending ", newCount, " new candles for ", sym, " after ", TimeToString(latestApiTime, TIME_DATE|TIME_SECONDS));
354
+
355
+      // --- Send new candles in batches ---
259 356
       int batchSize = 200;
260
-      for(int start = 0; start < copied; start += batchSize)
357
+      int sentTotal = 0;
358
+      for(int start = startIndex; start < copied; start += batchSize)
261 359
       {
262 360
          int size = MathMin(batchSize, copied - start);
263
-         int symbolId = symbolIds[i];
264
-         if(symbolId <= 0) continue;
265
-
266 361
          string json = BuildCandleJSONFromRates(symbolId, rates, start, size);
267 362
          string url = ApiBaseUrl + "/api/candles/bulk";
268 363
          string response;
@@ -273,33 +368,116 @@ void SendAllHistoricalCandles()
273 368
             break;
274 369
          }
275 370
          sentTotal += size;
276
-         Print("📤 Sent candles for ", sym, ": ", sentTotal, "/", copied);
371
+         Print("📤 Sent ", sentTotal, "/", newCount, " new candles for ", sym);
277 372
       }
278 373
    }
279
-   Print("✅ Historical upload finished.");
374
+   Print("✅ Incremental candle upload finished.");
280 375
 }
281 376
 
282
-
377
+//+------------------------------------------------------------------+
378
+//| Send live prices of all active symbols                           |
283 379
 //+------------------------------------------------------------------+
284 380
 void SendLivePrices()
285 381
 {
286 382
    bool firstItem = true;
287 383
    string json = "{\"prices\":[";
288
-
289 384
    int sentCount = 0;
385
+
290 386
    for(int i = 0; i < ArraySize(symbols); i++)
291 387
    {
292 388
       string sym = symbols[i];
389
+      int symId = symbolIds[i];
390
+      if(symId <= 0) continue;
391
+
392
+      // Ensure symbol is visible in Market Watch
393
+      if(!SymbolSelect(sym, true))
394
+      {
395
+         Print("⚠️ Failed to select symbol: ", sym);
396
+         continue;
397
+      }
398
+
399
+      // Read primary prices
293 400
       double bid = SymbolInfoDouble(sym, SYMBOL_BID);
294 401
       double ask = SymbolInfoDouble(sym, SYMBOL_ASK);
295 402
       double last = SymbolInfoDouble(sym, SYMBOL_LAST);
296
-      if(bid <= 0 || ask <= 0 || last <= 0) continue;
297 403
 
298
-      int symId = symbolIds[i];
299
-      if(symId <= 0) continue;
404
+      // If last = 0 (some providers), use midprice as fallback
405
+      if(last <= 0 && bid > 0 && ask > 0)
406
+         last = (bid + ask) / 2.0;
300 407
 
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);
408
+      // Skip if prices are still invalid
409
+      if(bid <= 0 || ask <= 0 || last <= 0)
410
+      {
411
+         Print("⚠️ Skipping symbol ", sym, " — invalid bid/ask/last (", DoubleToString(bid,8), "/", DoubleToString(ask,8), "/", DoubleToString(last,8), ")");
412
+         continue;
413
+      }
414
+
415
+      // Initialize sizes
416
+      double bidSize = 0.0;
417
+      double askSize = 0.0;
418
+
419
+      // Try to fetch market depth (book) and classify volumes by price vs bid/ask
420
+      MqlBookInfo book[];
421
+      if(MarketBookGet(sym, book) && ArraySize(book) > 0)
422
+      {
423
+         for(int j = 0; j < ArraySize(book); j++)
424
+         {
425
+            double p = book[j].price;
426
+            double v = book[j].volume;
427
+
428
+            // If price is >= ask => ask side
429
+            if(p >= ask) askSize += v;
430
+            // If price is <= bid => bid side
431
+            else if(p <= bid) bidSize += v;
432
+            else
433
+            {
434
+               // price in-between -> assign to nearer side
435
+               double distToBid = MathAbs(p - bid);
436
+               double distToAsk = MathAbs(ask - p);
437
+               if(distToBid <= distToAsk) bidSize += v; else askSize += v;
438
+            }
439
+         }
440
+         PrintFormat("ℹ️ MarketBook for %s → bid=%.8f ask=%.8f bidSize=%.2f askSize=%.2f (book entries=%d)", sym, bid, ask, bidSize, askSize, ArraySize(book));
441
+      }
442
+      else
443
+      {
444
+         // MarketBook not available or empty
445
+         // Try SymbolInfoTick as fallback
446
+         MqlTick tick;
447
+         if(SymbolInfoTick(sym, tick))
448
+         {
449
+            // tick.volume is aggregated tick volume — not exact bid/ask sizes but better than zero
450
+            double tickVol = (double)tick.volume;
451
+            if(tickVol > 0.0)
452
+            {
453
+               // assign tick volume to both sides conservatively
454
+               if(bidSize <= 0) bidSize = tickVol;
455
+               if(askSize <= 0) askSize = tickVol;
456
+               PrintFormat("ℹ️ tick fallback for %s → tick.volume=%.2f", sym, tickVol);
457
+            }
458
+            else
459
+            {
460
+               Print("ℹ️ tick available but volume zero for ", sym);
461
+            }
462
+         }
463
+         else
464
+         {
465
+            Print("ℹ️ MarketBook and tick not available for ", sym);
466
+         }
467
+      }
468
+
469
+      // Final safety: ensure API-required positive numbers
470
+      // If a side is zero or negative, set to minimal positive 1.0
471
+      if(bidSize <= 0.0) bidSize = 1.0;
472
+      if(askSize <= 0.0) askSize = 1.0;
473
+
474
+      // Build JSON item for this symbol
475
+      string item = StringFormat(
476
+         "{\"symbolId\":%d,\"price\":%.8f,\"bid\":%.8f,\"ask\":%.8f,\"bidSize\":%.8f,\"askSize\":%.8f}",
477
+         symId, last, bid, ask, bidSize, askSize
478
+      );
479
+
480
+      // Add to aggregate payload
303 481
       if(!firstItem) json += ",";
304 482
       json += item;
305 483
       firstItem = false;
@@ -310,17 +488,25 @@ void SendLivePrices()
310 488
 
311 489
    if(sentCount == 0)
312 490
    {
313
-      Print("No valid live prices to send right now.");
491
+      Print("⚠️ No valid live prices to send right now. Check if market is open and symbols have tick data.");
314 492
       return;
315 493
    }
316 494
 
495
+   // Log URL and truncated payload for debugging
317 496
    string url = ApiBaseUrl + "/api/live-prices/bulk";
497
+   int maxShow = 1000;
498
+   string payloadLog = (StringLen(json) > maxShow) ? StringSubstr(json, 0, maxShow) + "...(truncated)" : json;
499
+   Print("📤 Calling API: ", url);
500
+   Print("📦 Payload (truncated): ", payloadLog);
501
+
502
+   // Send and report
318 503
    string response;
319 504
    bool ok = SendJSON(url, json, response);
505
+
320 506
    if(ok)
321
-      Print("✅ Sent ", sentCount, " live prices.");
507
+      Print("✅ Successfully sent ", sentCount, " live prices to API.");
322 508
    else
323
-      Print("❌ Failed to send live prices (sent ", sentCount, " items). Response: ", response);
509
+      Print("❌ Failed to send live prices (", sentCount, " items). API response: ", response);
324 510
 }
325 511
 
326 512
 //+------------------------------------------------------------------+
@@ -407,4 +593,4 @@ bool SendJSON(string url, string json, string &response)
407 593
    return false;
408 594
 }
409 595
 
410
-//+------------------------------------------------------------------+
596
+//+------------------------------------------------------------------+