|
|
@@ -15,7 +15,7 @@ input int LivePriceIntervalSeconds = 5;
|
|
15
|
15
|
string symbols[];
|
|
16
|
16
|
int symbolIds[];
|
|
17
|
17
|
datetime lastSend = 0;
|
|
18
|
|
-
|
|
|
18
|
+datetime lastCandleSync = 0;
|
|
19
|
19
|
//+------------------------------------------------------------------+
|
|
20
|
20
|
int OnInit()
|
|
21
|
21
|
{
|
|
|
@@ -28,6 +28,8 @@ int OnInit()
|
|
28
|
28
|
|
|
29
|
29
|
Print("✅ Symbols initialized: ", ArraySize(symbols));
|
|
30
|
30
|
SendAllHistoricalCandles();
|
|
|
31
|
+ EventSetTimer(60); // ⏱️ Trigger OnTimer() every 30 minutes
|
|
|
32
|
+ Print("✅ Timer set: SendAllHistoricalCandles() will run every 30 minutes.");
|
|
31
|
33
|
return(INIT_SUCCEEDED);
|
|
32
|
34
|
}
|
|
33
|
35
|
|
|
|
@@ -135,22 +137,63 @@ bool SyncSymbolsWithDatabase()
|
|
135
|
137
|
return true;
|
|
136
|
138
|
}
|
|
137
|
139
|
|
|
|
140
|
+//+------------------------------------------------------------------+
|
|
|
141
|
+//+------------------------------------------------------------------+
|
|
|
142
|
+//| Find exact symbolId from JSON response |
|
|
|
143
|
+//+------------------------------------------------------------------+
|
|
|
144
|
+//+------------------------------------------------------------------+
|
|
|
145
|
+//| Robust JSON search: matches exact symbol only |
|
|
138
|
146
|
//+------------------------------------------------------------------+
|
|
139
|
147
|
int FindSymbolId(string response, string symbolName)
|
|
140
|
148
|
{
|
|
141
|
|
- string searchPattern = StringFormat("\"symbol\":\"%s\"", symbolName);
|
|
142
|
|
- int symbolPos = StringFind(response, searchPattern);
|
|
143
|
|
- if(symbolPos < 0) return -1;
|
|
|
149
|
+ int pos = 0;
|
|
|
150
|
+ string patternSymbol = "\"symbol\":\"";
|
|
|
151
|
+ string patternId = "\"id\":";
|
|
144
|
152
|
|
|
145
|
|
- int idPos = StringFind(response, "\"id\":", symbolPos);
|
|
146
|
|
- if(idPos < 0) return -1;
|
|
|
153
|
+ while(true)
|
|
|
154
|
+ {
|
|
|
155
|
+ // find each symbol occurrence
|
|
|
156
|
+ int symPos = StringFind(response, patternSymbol, pos);
|
|
|
157
|
+ if(symPos < 0)
|
|
|
158
|
+ break;
|
|
147
|
159
|
|
|
148
|
|
- int startPos = idPos + 5;
|
|
149
|
|
- int endPos = StringFind(response, ",", startPos);
|
|
150
|
|
- if(endPos < 0) endPos = StringFind(response, "}", startPos);
|
|
|
160
|
+ // extract actual symbol value
|
|
|
161
|
+ int symStart = symPos + StringLen(patternSymbol);
|
|
|
162
|
+ int symEnd = StringFind(response, "\"", symStart);
|
|
|
163
|
+ if(symEnd < 0) break;
|
|
151
|
164
|
|
|
152
|
|
- string idStr = StringSubstr(response, startPos, endPos - startPos);
|
|
153
|
|
- return (int)StringToInteger(idStr);
|
|
|
165
|
+ string foundSymbol = StringSubstr(response, symStart, symEnd - symStart);
|
|
|
166
|
+
|
|
|
167
|
+ // 🟩 DEBUG LOG: show all symbols found
|
|
|
168
|
+ // ✅ exact match check (case-sensitive)
|
|
|
169
|
+ if(foundSymbol == symbolName)
|
|
|
170
|
+ {
|
|
|
171
|
+ // find id that comes *before* this symbol entry
|
|
|
172
|
+ int blockStart = StringFind(response, patternId, symPos - 100);
|
|
|
173
|
+ if(blockStart < 0)
|
|
|
174
|
+ blockStart = StringFind(response, patternId, symPos);
|
|
|
175
|
+
|
|
|
176
|
+ if(blockStart >= 0)
|
|
|
177
|
+ {
|
|
|
178
|
+ int idStart = blockStart + StringLen(patternId);
|
|
|
179
|
+ int idEnd = StringFind(response, ",", idStart);
|
|
|
180
|
+ if(idEnd < 0) idEnd = StringFind(response, "}", idStart);
|
|
|
181
|
+
|
|
|
182
|
+ string idStr = StringSubstr(response, idStart, idEnd - idStart);
|
|
|
183
|
+ int id = (int)StringToInteger(idStr);
|
|
|
184
|
+
|
|
|
185
|
+ // 🟩 Success log
|
|
|
186
|
+ Print("✅ Exact match found → symbol='", symbolName, "' | ID=", id);
|
|
|
187
|
+ return id;
|
|
|
188
|
+ }
|
|
|
189
|
+ }
|
|
|
190
|
+
|
|
|
191
|
+ // move to next
|
|
|
192
|
+ pos = symEnd + 1;
|
|
|
193
|
+ }
|
|
|
194
|
+
|
|
|
195
|
+ Print("⚠️ No exact match for symbol ", symbolName);
|
|
|
196
|
+ return -1;
|
|
154
|
197
|
}
|
|
155
|
198
|
|
|
156
|
199
|
//+------------------------------------------------------------------+
|
|
|
@@ -161,11 +204,53 @@ int CreateSymbolInDatabase(string symbolName)
|
|
161
|
204
|
string exchange = "MT5";
|
|
162
|
205
|
string instrumentType = "forex";
|
|
163
|
206
|
|
|
|
207
|
+ // --- Clean suffixes like ".pro", ".m", ".r", "_i" ---
|
|
|
208
|
+ int dotPos = StringFind(symbolName, ".");
|
|
|
209
|
+ if(dotPos > 0)
|
|
|
210
|
+ symbolName = StringSubstr(symbolName, 0, dotPos);
|
|
|
211
|
+
|
|
|
212
|
+ // --- Try basic split for 6-char pairs like EURUSD, GBPJPY, BTCUSD ---
|
|
164
|
213
|
if(StringLen(symbolName) >= 6)
|
|
165
|
214
|
{
|
|
166
|
215
|
baseAsset = StringSubstr(symbolName, 0, 3);
|
|
167
|
216
|
quoteAsset = StringSubstr(symbolName, 3, 3);
|
|
168
|
217
|
}
|
|
|
218
|
+ else
|
|
|
219
|
+ {
|
|
|
220
|
+ // --- Try alternate detection ---
|
|
|
221
|
+ if(StringFind(symbolName, "USD") >= 0)
|
|
|
222
|
+ {
|
|
|
223
|
+ int pos = StringFind(symbolName, "USD");
|
|
|
224
|
+ baseAsset = StringSubstr(symbolName, 0, pos);
|
|
|
225
|
+ quoteAsset = "USD";
|
|
|
226
|
+ }
|
|
|
227
|
+ else if(StringFind(symbolName, "EUR") >= 0)
|
|
|
228
|
+ {
|
|
|
229
|
+ int pos = StringFind(symbolName, "EUR");
|
|
|
230
|
+ baseAsset = StringSubstr(symbolName, 0, pos);
|
|
|
231
|
+ quoteAsset = "EUR";
|
|
|
232
|
+ }
|
|
|
233
|
+ else
|
|
|
234
|
+ {
|
|
|
235
|
+ // Fallback safe defaults
|
|
|
236
|
+ baseAsset = symbolName;
|
|
|
237
|
+ quoteAsset = "USD";
|
|
|
238
|
+ }
|
|
|
239
|
+ }
|
|
|
240
|
+
|
|
|
241
|
+ // --- Final safety: ensure no empty fields ---
|
|
|
242
|
+ if(StringLen(baseAsset) == 0) baseAsset = "UNKNOWN";
|
|
|
243
|
+ if(StringLen(quoteAsset) == 0) quoteAsset = "USD";
|
|
|
244
|
+
|
|
|
245
|
+ // --- Decide instrument type ---
|
|
|
246
|
+ if(StringFind(symbolName, "BTC") == 0 || StringFind(symbolName, "ETH") == 0)
|
|
|
247
|
+ instrumentType = "crypto";
|
|
|
248
|
+ else if(StringFind(symbolName, "XAU") == 0 || StringFind(symbolName, "XAG") == 0)
|
|
|
249
|
+ instrumentType = "metal";
|
|
|
250
|
+ else if(StringFind(symbolName, "US30") == 0 || StringFind(symbolName, "NAS") == 0)
|
|
|
251
|
+ instrumentType = "index";
|
|
|
252
|
+ else
|
|
|
253
|
+ instrumentType = "forex";
|
|
169
|
254
|
|
|
170
|
255
|
string json = StringFormat(
|
|
171
|
256
|
"{\"symbol\":\"%s\",\"baseAsset\":\"%s\",\"quoteAsset\":\"%s\",\"exchange\":\"%s\",\"instrumentType\":\"%s\",\"isActive\":true}",
|
|
|
@@ -178,18 +263,15 @@ int CreateSymbolInDatabase(string symbolName)
|
|
178
|
263
|
|
|
179
|
264
|
char postData[];
|
|
180
|
265
|
StringToCharArray(json, postData, 0, CP_UTF8);
|
|
181
|
|
-
|
|
182
|
|
- // ✅ FIX: Remove trailing null terminator from JSON
|
|
183
|
266
|
if(ArraySize(postData) > 0 && postData[ArraySize(postData) - 1] == 0)
|
|
184
|
267
|
ArrayResize(postData, ArraySize(postData) - 1);
|
|
185
|
268
|
|
|
186
|
269
|
char result[];
|
|
187
|
|
-
|
|
188
|
270
|
int res = WebRequest("POST", url, headers, 5000, postData, result, resultHeaders);
|
|
189
|
271
|
|
|
190
|
272
|
if(res != 201 && res != 200)
|
|
191
|
273
|
{
|
|
192
|
|
- Print("❌ Failed to create symbol: ", res, " Response: ", CharArrayToString(result));
|
|
|
274
|
+ PrintFormat("❌ Failed to create symbol %s | HTTP %d | Response: %s", symbolName, res, CharArrayToString(result));
|
|
193
|
275
|
return -1;
|
|
194
|
276
|
}
|
|
195
|
277
|
|
|
|
@@ -205,10 +287,62 @@ int CreateSymbolInDatabase(string symbolName)
|
|
205
|
287
|
return (int)StringToInteger(idStr);
|
|
206
|
288
|
}
|
|
207
|
289
|
|
|
|
290
|
+//+------------------------------------------------------------------+
|
|
|
291
|
+//| Fetch latest stored candle openTime from API |
|
|
|
292
|
+//+------------------------------------------------------------------+
|
|
|
293
|
+datetime GetLatestCandleTime(int symbolId)
|
|
|
294
|
+{
|
|
|
295
|
+ string url = ApiBaseUrl + "/api/candles/" + IntegerToString(symbolId) + "/latest";
|
|
|
296
|
+ string headers = "Content-Type: application/json\r\n";
|
|
|
297
|
+ string resultHeaders = "";
|
|
|
298
|
+ char result[];
|
|
|
299
|
+ char emptyData[];
|
|
|
300
|
+
|
|
|
301
|
+ ResetLastError();
|
|
|
302
|
+ int res = WebRequest("GET", url, headers, 10000, emptyData, result, resultHeaders);
|
|
|
303
|
+
|
|
|
304
|
+ if(res != 200)
|
|
|
305
|
+ {
|
|
|
306
|
+ Print("⚠️ Could not fetch latest candle for symbolId=", symbolId, " (HTTP ", res, ")");
|
|
|
307
|
+ return 0;
|
|
|
308
|
+ }
|
|
|
309
|
+
|
|
|
310
|
+ string response = CharArrayToString(result);
|
|
|
311
|
+ int pos = StringFind(response, "\"openTime\":\"");
|
|
|
312
|
+ if(pos < 0)
|
|
|
313
|
+ {
|
|
|
314
|
+ Print("⚠️ No openTime found in response for symbolId=", symbolId);
|
|
|
315
|
+ return 0;
|
|
|
316
|
+ }
|
|
|
317
|
+
|
|
|
318
|
+ pos += StringLen("\"openTime\":\"");
|
|
|
319
|
+ int end = StringFind(response, "\"", pos);
|
|
|
320
|
+ string openTimeStr = StringSubstr(response, pos, end - pos);
|
|
|
321
|
+
|
|
|
322
|
+ // --- Parse ISO8601 to datetime ---
|
|
|
323
|
+ int year = (int)StringToInteger(StringSubstr(openTimeStr, 0, 4));
|
|
|
324
|
+ int month = (int)StringToInteger(StringSubstr(openTimeStr, 5, 2));
|
|
|
325
|
+ int day = (int)StringToInteger(StringSubstr(openTimeStr, 8, 2));
|
|
|
326
|
+ int hour = (int)StringToInteger(StringSubstr(openTimeStr, 11, 2));
|
|
|
327
|
+ int min = (int)StringToInteger(StringSubstr(openTimeStr, 14, 2));
|
|
|
328
|
+ int sec = (int)StringToInteger(StringSubstr(openTimeStr, 17, 2));
|
|
|
329
|
+
|
|
|
330
|
+ MqlDateTime t;
|
|
|
331
|
+ t.year = year; t.mon = month; t.day = day;
|
|
|
332
|
+ t.hour = hour; t.min = min; t.sec = sec;
|
|
|
333
|
+
|
|
|
334
|
+ datetime dt = StructToTime(t);
|
|
|
335
|
+ Print("🕓 Latest stored candle openTime for symbolId=", symbolId, " → ", TimeToString(dt, TIME_DATE|TIME_SECONDS));
|
|
|
336
|
+ return dt;
|
|
|
337
|
+}
|
|
|
338
|
+
|
|
208
|
339
|
//+------------------------------------------------------------------+
|
|
209
|
340
|
//+------------------------------------------------------------------+
|
|
210
|
341
|
//| Send all historical candles to the API (Fixed Version) |
|
|
211
|
342
|
//+------------------------------------------------------------------+
|
|
|
343
|
+//+------------------------------------------------------------------+
|
|
|
344
|
+//| Send all historical candles to the API (Fixed + Timeout Safe) |
|
|
|
345
|
+//+------------------------------------------------------------------+
|
|
212
|
346
|
void SendAllHistoricalCandles()
|
|
213
|
347
|
{
|
|
214
|
348
|
Print("Starting historical upload for ", ArraySize(symbols), " symbols...");
|
|
|
@@ -216,90 +350,200 @@ void SendAllHistoricalCandles()
|
|
216
|
350
|
for(int i = 0; i < ArraySize(symbols); i++)
|
|
217
|
351
|
{
|
|
218
|
352
|
string sym = symbols[i];
|
|
|
353
|
+ int symbolId = symbolIds[i];
|
|
|
354
|
+ if(symbolId <= 0) continue;
|
|
|
355
|
+
|
|
|
356
|
+ // --- Get last stored candle time ---
|
|
|
357
|
+ datetime latestApiTime = GetLatestCandleTime(symbolId);
|
|
219
|
358
|
|
|
220
|
|
- // --- Ensure data is ready ---
|
|
|
359
|
+ // --- Ensure history data is available ---
|
|
221
|
360
|
Sleep(300);
|
|
222
|
361
|
int tries = 0;
|
|
223
|
|
- while(!SeriesInfoInteger(sym, HistoricalTimeframe, SERIES_SYNCHRONIZED) && tries < 10)
|
|
|
362
|
+ bool historyReady = false;
|
|
|
363
|
+
|
|
|
364
|
+ while(tries < 10)
|
|
224
|
365
|
{
|
|
225
|
|
- Print("⏳ Waiting for ", sym, " history to load...");
|
|
|
366
|
+ if(SeriesInfoInteger(sym, HistoricalTimeframe, SERIES_SYNCHRONIZED))
|
|
|
367
|
+ {
|
|
|
368
|
+ historyReady = true;
|
|
|
369
|
+ break;
|
|
|
370
|
+ }
|
|
|
371
|
+ PrintFormat("⏳ Waiting for %s history to load... (try %d/10)", sym, tries + 1);
|
|
226
|
372
|
Sleep(500);
|
|
227
|
373
|
tries++;
|
|
228
|
374
|
}
|
|
229
|
375
|
|
|
230
|
|
- // --- Now copy candles ---
|
|
|
376
|
+ if(!historyReady)
|
|
|
377
|
+ {
|
|
|
378
|
+ PrintFormat("⚠️ Skipping %s — history not loaded after 10 tries (~5s timeout).", sym);
|
|
|
379
|
+ continue;
|
|
|
380
|
+ }
|
|
|
381
|
+
|
|
|
382
|
+ // --- Copy rates ---
|
|
231
|
383
|
MqlRates rates[];
|
|
232
|
384
|
ResetLastError();
|
|
233
|
385
|
int copied = CopyRates(sym, HistoricalTimeframe, 0, HistoricalCandleCount, rates);
|
|
234
|
|
- int err = GetLastError();
|
|
235
|
386
|
|
|
236
|
387
|
if(copied <= 0)
|
|
237
|
388
|
{
|
|
238
|
|
- Print("⚠️ Failed to copy candles for ", sym, " (copied=", copied, ", err=", err, ")");
|
|
|
389
|
+ int err = GetLastError();
|
|
|
390
|
+ PrintFormat("⚠️ Failed to copy candles for %s (error %d)", sym, err);
|
|
239
|
391
|
continue;
|
|
240
|
392
|
}
|
|
241
|
393
|
|
|
242
|
|
- Print("✅ Copied ", copied, " candles for ", sym);
|
|
|
394
|
+ PrintFormat("✅ Copied %d candles for %s", copied, sym);
|
|
243
|
395
|
|
|
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++)
|
|
|
396
|
+ // --- Filter new candles ---
|
|
|
397
|
+ int startIndex = 0;
|
|
|
398
|
+ for(int j = 0; j < copied; j++)
|
|
247
|
399
|
{
|
|
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
|
|
- );
|
|
|
400
|
+ if(rates[j].time > latestApiTime)
|
|
|
401
|
+ {
|
|
|
402
|
+ startIndex = j;
|
|
|
403
|
+ break;
|
|
|
404
|
+ }
|
|
255
|
405
|
}
|
|
256
|
406
|
|
|
257
|
|
- // --- Send candles in batches to API ---
|
|
258
|
|
- int sentTotal = 0;
|
|
|
407
|
+ int newCount = copied - startIndex;
|
|
|
408
|
+ if(newCount <= 0)
|
|
|
409
|
+ {
|
|
|
410
|
+ PrintFormat("ℹ️ No new candles to send for %s", sym);
|
|
|
411
|
+ continue;
|
|
|
412
|
+ }
|
|
|
413
|
+
|
|
|
414
|
+ PrintFormat("🆕 Sending %d new candles for %s after %s", newCount, sym, TimeToString(latestApiTime, TIME_DATE|TIME_SECONDS));
|
|
|
415
|
+
|
|
|
416
|
+ // --- Send new candles in batches ---
|
|
259
|
417
|
int batchSize = 200;
|
|
260
|
|
- for(int start = 0; start < copied; start += batchSize)
|
|
|
418
|
+ int sentTotal = 0;
|
|
|
419
|
+
|
|
|
420
|
+ for(int start = startIndex; start < copied; start += batchSize)
|
|
261
|
421
|
{
|
|
262
|
422
|
int size = MathMin(batchSize, copied - start);
|
|
263
|
|
- int symbolId = symbolIds[i];
|
|
264
|
|
- if(symbolId <= 0) continue;
|
|
265
|
|
-
|
|
266
|
423
|
string json = BuildCandleJSONFromRates(symbolId, rates, start, size);
|
|
267
|
424
|
string url = ApiBaseUrl + "/api/candles/bulk";
|
|
268
|
425
|
string response;
|
|
|
426
|
+
|
|
269
|
427
|
bool ok = SendJSON(url, json, response);
|
|
270
|
428
|
if(!ok)
|
|
271
|
429
|
{
|
|
272
|
|
- Print("❌ Failed to send candle batch for ", sym, " start=", start);
|
|
|
430
|
+ PrintFormat("❌ Failed to send candle batch for %s (start=%d)", sym, start);
|
|
273
|
431
|
break;
|
|
274
|
432
|
}
|
|
|
433
|
+
|
|
275
|
434
|
sentTotal += size;
|
|
276
|
|
- Print("📤 Sent candles for ", sym, ": ", sentTotal, "/", copied);
|
|
|
435
|
+ PrintFormat("📤 Sent %d/%d new candles for %s", sentTotal, newCount, sym);
|
|
277
|
436
|
}
|
|
278
|
437
|
}
|
|
279
|
|
- Print("✅ Historical upload finished.");
|
|
|
438
|
+
|
|
|
439
|
+ Print("✅ Incremental candle upload finished.");
|
|
280
|
440
|
}
|
|
281
|
441
|
|
|
282
|
442
|
|
|
|
443
|
+//+------------------------------------------------------------------+
|
|
|
444
|
+//| Send live prices of all active symbols |
|
|
283
|
445
|
//+------------------------------------------------------------------+
|
|
284
|
446
|
void SendLivePrices()
|
|
285
|
447
|
{
|
|
286
|
448
|
bool firstItem = true;
|
|
287
|
449
|
string json = "{\"prices\":[";
|
|
288
|
|
-
|
|
289
|
450
|
int sentCount = 0;
|
|
|
451
|
+
|
|
290
|
452
|
for(int i = 0; i < ArraySize(symbols); i++)
|
|
291
|
453
|
{
|
|
292
|
454
|
string sym = symbols[i];
|
|
|
455
|
+ int symId = symbolIds[i];
|
|
|
456
|
+ if(symId <= 0) continue;
|
|
|
457
|
+
|
|
|
458
|
+ // Ensure symbol is visible in Market Watch
|
|
|
459
|
+ if(!SymbolSelect(sym, true))
|
|
|
460
|
+ {
|
|
|
461
|
+ Print("⚠️ Failed to select symbol: ", sym);
|
|
|
462
|
+ continue;
|
|
|
463
|
+ }
|
|
|
464
|
+
|
|
|
465
|
+ // Read primary prices
|
|
293
|
466
|
double bid = SymbolInfoDouble(sym, SYMBOL_BID);
|
|
294
|
467
|
double ask = SymbolInfoDouble(sym, SYMBOL_ASK);
|
|
295
|
468
|
double last = SymbolInfoDouble(sym, SYMBOL_LAST);
|
|
296
|
|
- if(bid <= 0 || ask <= 0 || last <= 0) continue;
|
|
297
|
469
|
|
|
298
|
|
- int symId = symbolIds[i];
|
|
299
|
|
- if(symId <= 0) continue;
|
|
|
470
|
+ // If last = 0 (some providers), use midprice as fallback
|
|
|
471
|
+ if(last <= 0 && bid > 0 && ask > 0)
|
|
|
472
|
+ last = (bid + ask) / 2.0;
|
|
300
|
473
|
|
|
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);
|
|
|
474
|
+ // Skip if prices are still invalid
|
|
|
475
|
+ if(bid <= 0 || ask <= 0 || last <= 0)
|
|
|
476
|
+ {
|
|
|
477
|
+ Print("⚠️ Skipping symbol ", sym, " — invalid bid/ask/last (", DoubleToString(bid,8), "/", DoubleToString(ask,8), "/", DoubleToString(last,8), ")");
|
|
|
478
|
+ continue;
|
|
|
479
|
+ }
|
|
|
480
|
+
|
|
|
481
|
+ // Initialize sizes
|
|
|
482
|
+ double bidSize = 0.0;
|
|
|
483
|
+ double askSize = 0.0;
|
|
|
484
|
+
|
|
|
485
|
+ // Try to fetch market depth (book) and classify volumes by price vs bid/ask
|
|
|
486
|
+ MqlBookInfo book[];
|
|
|
487
|
+ if(MarketBookGet(sym, book) && ArraySize(book) > 0)
|
|
|
488
|
+ {
|
|
|
489
|
+ for(int j = 0; j < ArraySize(book); j++)
|
|
|
490
|
+ {
|
|
|
491
|
+ double p = book[j].price;
|
|
|
492
|
+ double v = book[j].volume;
|
|
|
493
|
+
|
|
|
494
|
+ // If price is >= ask => ask side
|
|
|
495
|
+ if(p >= ask) askSize += v;
|
|
|
496
|
+ // If price is <= bid => bid side
|
|
|
497
|
+ else if(p <= bid) bidSize += v;
|
|
|
498
|
+ else
|
|
|
499
|
+ {
|
|
|
500
|
+ // price in-between -> assign to nearer side
|
|
|
501
|
+ double distToBid = MathAbs(p - bid);
|
|
|
502
|
+ double distToAsk = MathAbs(ask - p);
|
|
|
503
|
+ if(distToBid <= distToAsk) bidSize += v; else askSize += v;
|
|
|
504
|
+ }
|
|
|
505
|
+ }
|
|
|
506
|
+ PrintFormat("ℹ️ MarketBook for %s → bid=%.8f ask=%.8f bidSize=%.2f askSize=%.2f (book entries=%d)", sym, bid, ask, bidSize, askSize, ArraySize(book));
|
|
|
507
|
+ }
|
|
|
508
|
+ else
|
|
|
509
|
+ {
|
|
|
510
|
+ // MarketBook not available or empty
|
|
|
511
|
+ // Try SymbolInfoTick as fallback
|
|
|
512
|
+ MqlTick tick;
|
|
|
513
|
+ if(SymbolInfoTick(sym, tick))
|
|
|
514
|
+ {
|
|
|
515
|
+ // tick.volume is aggregated tick volume — not exact bid/ask sizes but better than zero
|
|
|
516
|
+ double tickVol = (double)tick.volume;
|
|
|
517
|
+ if(tickVol > 0.0)
|
|
|
518
|
+ {
|
|
|
519
|
+ // assign tick volume to both sides conservatively
|
|
|
520
|
+ if(bidSize <= 0) bidSize = tickVol;
|
|
|
521
|
+ if(askSize <= 0) askSize = tickVol;
|
|
|
522
|
+ PrintFormat("ℹ️ tick fallback for %s → tick.volume=%.2f", sym, tickVol);
|
|
|
523
|
+ }
|
|
|
524
|
+ else
|
|
|
525
|
+ {
|
|
|
526
|
+ Print("ℹ️ tick available but volume zero for ", sym);
|
|
|
527
|
+ }
|
|
|
528
|
+ }
|
|
|
529
|
+ else
|
|
|
530
|
+ {
|
|
|
531
|
+ Print("ℹ️ MarketBook and tick not available for ", sym);
|
|
|
532
|
+ }
|
|
|
533
|
+ }
|
|
|
534
|
+
|
|
|
535
|
+ // Final safety: ensure API-required positive numbers
|
|
|
536
|
+ // If a side is zero or negative, set to minimal positive 1.0
|
|
|
537
|
+ if(bidSize <= 0.0) bidSize = 1.0;
|
|
|
538
|
+ if(askSize <= 0.0) askSize = 1.0;
|
|
|
539
|
+
|
|
|
540
|
+ // Build JSON item for this symbol
|
|
|
541
|
+ string item = StringFormat(
|
|
|
542
|
+ "{\"symbolId\":%d,\"price\":%.8f,\"bid\":%.8f,\"ask\":%.8f,\"bidSize\":%.8f,\"askSize\":%.8f}",
|
|
|
543
|
+ symId, last, bid, ask, bidSize, askSize
|
|
|
544
|
+ );
|
|
|
545
|
+
|
|
|
546
|
+ // Add to aggregate payload
|
|
303
|
547
|
if(!firstItem) json += ",";
|
|
304
|
548
|
json += item;
|
|
305
|
549
|
firstItem = false;
|
|
|
@@ -310,17 +554,25 @@ void SendLivePrices()
|
|
310
|
554
|
|
|
311
|
555
|
if(sentCount == 0)
|
|
312
|
556
|
{
|
|
313
|
|
- Print("No valid live prices to send right now.");
|
|
|
557
|
+ Print("⚠️ No valid live prices to send right now. Check if market is open and symbols have tick data.");
|
|
314
|
558
|
return;
|
|
315
|
559
|
}
|
|
316
|
560
|
|
|
|
561
|
+ // Log URL and truncated payload for debugging
|
|
317
|
562
|
string url = ApiBaseUrl + "/api/live-prices/bulk";
|
|
|
563
|
+ int maxShow = 1000;
|
|
|
564
|
+ string payloadLog = (StringLen(json) > maxShow) ? StringSubstr(json, 0, maxShow) + "...(truncated)" : json;
|
|
|
565
|
+ Print("📤 Calling API: ", url);
|
|
|
566
|
+ Print("📦 Payload (truncated): ", payloadLog);
|
|
|
567
|
+
|
|
|
568
|
+ // Send and report
|
|
318
|
569
|
string response;
|
|
319
|
570
|
bool ok = SendJSON(url, json, response);
|
|
|
571
|
+
|
|
320
|
572
|
if(ok)
|
|
321
|
|
- Print("✅ Sent ", sentCount, " live prices.");
|
|
|
573
|
+ Print("✅ Successfully sent ", sentCount, " live prices to API.");
|
|
322
|
574
|
else
|
|
323
|
|
- Print("❌ Failed to send live prices (sent ", sentCount, " items). Response: ", response);
|
|
|
575
|
+ Print("❌ Failed to send live prices (", sentCount, " items). API response: ", response);
|
|
324
|
576
|
}
|
|
325
|
577
|
|
|
326
|
578
|
//+------------------------------------------------------------------+
|
|
|
@@ -408,3 +660,57 @@ bool SendJSON(string url, string json, string &response)
|
|
408
|
660
|
}
|
|
409
|
661
|
|
|
410
|
662
|
//+------------------------------------------------------------------+
|
|
|
663
|
+//+------------------------------------------------------------------+
|
|
|
664
|
+//| Cleanup old candles (keep only last 1000) |
|
|
|
665
|
+//+------------------------------------------------------------------+
|
|
|
666
|
+void CleanupOldCandles(int symbolId)
|
|
|
667
|
+{
|
|
|
668
|
+ string url = ApiBaseUrl + "/api/candles/cleanup/" + IntegerToString(symbolId) + "?keep=1000";
|
|
|
669
|
+ string headers = "Content-Type: application/json\r\n";
|
|
|
670
|
+ string resultHeaders = "";
|
|
|
671
|
+ char result[];
|
|
|
672
|
+ char emptyData[];
|
|
|
673
|
+
|
|
|
674
|
+ ResetLastError();
|
|
|
675
|
+ int res = WebRequest("DELETE", url, headers, 10000, emptyData, result, resultHeaders);
|
|
|
676
|
+
|
|
|
677
|
+ string response = CharArrayToString(result);
|
|
|
678
|
+ if(res == 200 || res == 204)
|
|
|
679
|
+ Print("🧹 Cleanup successful for symbolId=", symbolId, " → kept last 1000 candles.");
|
|
|
680
|
+ else
|
|
|
681
|
+ Print("⚠️ Cleanup failed for symbolId=", symbolId, " HTTP=", res, " Response=", response);
|
|
|
682
|
+}
|
|
|
683
|
+
|
|
|
684
|
+//+------------------------------------------------------------------+
|
|
|
685
|
+//| Timer event: runs every 60 seconds |
|
|
|
686
|
+//+------------------------------------------------------------------+
|
|
|
687
|
+void OnTimer()
|
|
|
688
|
+{
|
|
|
689
|
+ datetime now = TimeCurrent();
|
|
|
690
|
+
|
|
|
691
|
+ // ✅ Run full candle sync only once every minute
|
|
|
692
|
+ if(now - lastCandleSync >= 600)
|
|
|
693
|
+ {
|
|
|
694
|
+ Print("⏰ Running scheduled candle sync and cleanup...");
|
|
|
695
|
+ SendAllHistoricalCandles();
|
|
|
696
|
+
|
|
|
697
|
+ // ✅ After uploading candles, clean up old ones
|
|
|
698
|
+ for(int i = 0; i < ArraySize(symbols); i++)
|
|
|
699
|
+ {
|
|
|
700
|
+ int symId = symbolIds[i];
|
|
|
701
|
+ if(symId <= 0) continue;
|
|
|
702
|
+ CleanupOldCandles(symId);
|
|
|
703
|
+ Sleep(500); // small delay to avoid API overload
|
|
|
704
|
+ }
|
|
|
705
|
+
|
|
|
706
|
+ lastCandleSync = now;
|
|
|
707
|
+ Print("✅ Candle sync + cleanup cycle completed.");
|
|
|
708
|
+ }
|
|
|
709
|
+}
|
|
|
710
|
+
|
|
|
711
|
+
|
|
|
712
|
+
|
|
|
713
|
+void OnDeinit(const int reason)
|
|
|
714
|
+{
|
|
|
715
|
+ EventKillTimer();
|
|
|
716
|
+}
|