|
|
@@ -16,6 +16,10 @@ string symbols[];
|
|
16
|
16
|
int symbolIds[];
|
|
17
|
17
|
datetime lastSend = 0;
|
|
18
|
18
|
datetime lastCandleSync = 0;
|
|
|
19
|
+
|
|
|
20
|
+// --- Supported timeframes ---
|
|
|
21
|
+ENUM_TIMEFRAMES Timeframes[] = { PERIOD_M15, PERIOD_M30, PERIOD_H1, PERIOD_D1, PERIOD_W1, PERIOD_MN1 };
|
|
|
22
|
+string TimeframeStrings[] = { "15m", "30m", "1h", "1D", "1W", "1M" };
|
|
19
|
23
|
//+------------------------------------------------------------------+
|
|
20
|
24
|
int OnInit()
|
|
21
|
25
|
{
|
|
|
@@ -290,9 +294,9 @@ int CreateSymbolInDatabase(string symbolName)
|
|
290
|
294
|
//+------------------------------------------------------------------+
|
|
291
|
295
|
//| Fetch latest stored candle openTime from API |
|
|
292
|
296
|
//+------------------------------------------------------------------+
|
|
293
|
|
-datetime GetLatestCandleTime(int symbolId)
|
|
|
297
|
+datetime GetLatestCandleTime(int symbolId, string timeframe)
|
|
294
|
298
|
{
|
|
295
|
|
- string url = ApiBaseUrl + "/api/candles/" + IntegerToString(symbolId) + "/latest";
|
|
|
299
|
+ string url = ApiBaseUrl + "/api/candles/" + IntegerToString(symbolId) + "/latest?timeframe=" + timeframe;
|
|
296
|
300
|
string headers = "Content-Type: application/json\r\n";
|
|
297
|
301
|
string resultHeaders = "";
|
|
298
|
302
|
char result[];
|
|
|
@@ -303,7 +307,7 @@ datetime GetLatestCandleTime(int symbolId)
|
|
303
|
307
|
|
|
304
|
308
|
if(res != 200)
|
|
305
|
309
|
{
|
|
306
|
|
- Print("⚠️ Could not fetch latest candle for symbolId=", symbolId, " (HTTP ", res, ")");
|
|
|
310
|
+ Print("⚠️ Could not fetch latest candle for symbolId=", symbolId, " timeframe=", timeframe, " (HTTP ", res, ")");
|
|
307
|
311
|
return 0;
|
|
308
|
312
|
}
|
|
309
|
313
|
|
|
|
@@ -311,7 +315,7 @@ datetime GetLatestCandleTime(int symbolId)
|
|
311
|
315
|
int pos = StringFind(response, "\"openTime\":\"");
|
|
312
|
316
|
if(pos < 0)
|
|
313
|
317
|
{
|
|
314
|
|
- Print("⚠️ No openTime found in response for symbolId=", symbolId);
|
|
|
318
|
+ Print("⚠️ No openTime found in response for symbolId=", symbolId, " timeframe=", timeframe);
|
|
315
|
319
|
return 0;
|
|
316
|
320
|
}
|
|
317
|
321
|
|
|
|
@@ -319,7 +323,6 @@ datetime GetLatestCandleTime(int symbolId)
|
|
319
|
323
|
int end = StringFind(response, "\"", pos);
|
|
320
|
324
|
string openTimeStr = StringSubstr(response, pos, end - pos);
|
|
321
|
325
|
|
|
322
|
|
- // --- Parse ISO8601 to datetime ---
|
|
323
|
326
|
int year = (int)StringToInteger(StringSubstr(openTimeStr, 0, 4));
|
|
324
|
327
|
int month = (int)StringToInteger(StringSubstr(openTimeStr, 5, 2));
|
|
325
|
328
|
int day = (int)StringToInteger(StringSubstr(openTimeStr, 8, 2));
|
|
|
@@ -332,7 +335,7 @@ datetime GetLatestCandleTime(int symbolId)
|
|
332
|
335
|
t.hour = hour; t.min = min; t.sec = sec;
|
|
333
|
336
|
|
|
334
|
337
|
datetime dt = StructToTime(t);
|
|
335
|
|
- Print("🕓 Latest stored candle openTime for symbolId=", symbolId, " → ", TimeToString(dt, TIME_DATE|TIME_SECONDS));
|
|
|
338
|
+ PrintFormat("🕓 Latest stored candle for %s (symbolId=%d) = %s", timeframe, symbolId, TimeToString(dt, TIME_DATE|TIME_SECONDS));
|
|
336
|
339
|
return dt;
|
|
337
|
340
|
}
|
|
338
|
341
|
|
|
|
@@ -345,7 +348,7 @@ datetime GetLatestCandleTime(int symbolId)
|
|
345
|
348
|
//+------------------------------------------------------------------+
|
|
346
|
349
|
void SendAllHistoricalCandles()
|
|
347
|
350
|
{
|
|
348
|
|
- Print("Starting historical upload for ", ArraySize(symbols), " symbols...");
|
|
|
351
|
+ Print("Starting multi-timeframe historical upload for ", ArraySize(symbols), " symbols...");
|
|
349
|
352
|
|
|
350
|
353
|
for(int i = 0; i < ArraySize(symbols); i++)
|
|
351
|
354
|
{
|
|
|
@@ -353,90 +356,91 @@ void SendAllHistoricalCandles()
|
|
353
|
356
|
int symbolId = symbolIds[i];
|
|
354
|
357
|
if(symbolId <= 0) continue;
|
|
355
|
358
|
|
|
356
|
|
- // --- Get last stored candle time ---
|
|
357
|
|
- datetime latestApiTime = GetLatestCandleTime(symbolId);
|
|
|
359
|
+ // --- Loop through all timeframes ---
|
|
|
360
|
+ for(int tfIndex = 0; tfIndex < ArraySize(Timeframes); tfIndex++)
|
|
|
361
|
+ {
|
|
|
362
|
+ ENUM_TIMEFRAMES tf = Timeframes[tfIndex];
|
|
|
363
|
+ string tfStr = TimeframeStrings[tfIndex];
|
|
|
364
|
+ PrintFormat("📊 Processing %s timeframe for %s", tfStr, sym);
|
|
|
365
|
+
|
|
|
366
|
+ datetime latestApiTime = GetLatestCandleTime(symbolId, tfStr);
|
|
358
|
367
|
|
|
359
|
|
- // --- Ensure history data is available ---
|
|
360
|
|
- Sleep(300);
|
|
361
|
|
- int tries = 0;
|
|
362
|
|
- bool historyReady = false;
|
|
|
368
|
+ Sleep(300);
|
|
|
369
|
+ int tries = 0;
|
|
|
370
|
+ bool historyReady = false;
|
|
363
|
371
|
|
|
364
|
|
- while(tries < 10)
|
|
365
|
|
- {
|
|
366
|
|
- if(SeriesInfoInteger(sym, HistoricalTimeframe, SERIES_SYNCHRONIZED))
|
|
|
372
|
+ while(tries < 10)
|
|
367
|
373
|
{
|
|
368
|
|
- historyReady = true;
|
|
369
|
|
- break;
|
|
|
374
|
+ if(SeriesInfoInteger(sym, tf, SERIES_SYNCHRONIZED))
|
|
|
375
|
+ {
|
|
|
376
|
+ historyReady = true;
|
|
|
377
|
+ break;
|
|
|
378
|
+ }
|
|
|
379
|
+ PrintFormat("⏳ Waiting for %s (%s) history to load... (try %d/10)", sym, tfStr, tries + 1);
|
|
|
380
|
+ Sleep(500);
|
|
|
381
|
+ tries++;
|
|
370
|
382
|
}
|
|
371
|
|
- PrintFormat("⏳ Waiting for %s history to load... (try %d/10)", sym, tries + 1);
|
|
372
|
|
- Sleep(500);
|
|
373
|
|
- tries++;
|
|
374
|
|
- }
|
|
375
|
383
|
|
|
376
|
|
- if(!historyReady)
|
|
377
|
|
- {
|
|
378
|
|
- PrintFormat("⚠️ Skipping %s — history not loaded after 10 tries (~5s timeout).", sym);
|
|
379
|
|
- continue;
|
|
380
|
|
- }
|
|
|
384
|
+ if(!historyReady)
|
|
|
385
|
+ {
|
|
|
386
|
+ PrintFormat("⚠️ Skipping %s (%s) — history not loaded.", sym, tfStr);
|
|
|
387
|
+ continue;
|
|
|
388
|
+ }
|
|
381
|
389
|
|
|
382
|
|
- // --- Copy rates ---
|
|
383
|
|
- MqlRates rates[];
|
|
384
|
|
- ResetLastError();
|
|
385
|
|
- int copied = CopyRates(sym, HistoricalTimeframe, 0, HistoricalCandleCount, rates);
|
|
|
390
|
+ MqlRates rates[];
|
|
|
391
|
+ ResetLastError();
|
|
|
392
|
+ int copied = CopyRates(sym, tf, 0, HistoricalCandleCount, rates);
|
|
386
|
393
|
|
|
387
|
|
- if(copied <= 0)
|
|
388
|
|
- {
|
|
389
|
|
- int err = GetLastError();
|
|
390
|
|
- PrintFormat("⚠️ Failed to copy candles for %s (error %d)", sym, err);
|
|
391
|
|
- continue;
|
|
392
|
|
- }
|
|
|
394
|
+ if(copied <= 0)
|
|
|
395
|
+ {
|
|
|
396
|
+ int err = GetLastError();
|
|
|
397
|
+ PrintFormat("⚠️ Failed to copy %s candles (%s) (error %d)", sym, tfStr, err);
|
|
|
398
|
+ continue;
|
|
|
399
|
+ }
|
|
393
|
400
|
|
|
394
|
|
- PrintFormat("✅ Copied %d candles for %s", copied, sym);
|
|
|
401
|
+ int startIndex = 0;
|
|
|
402
|
+ for(int j = 0; j < copied; j++)
|
|
|
403
|
+ {
|
|
|
404
|
+ if(rates[j].time > latestApiTime)
|
|
|
405
|
+ {
|
|
|
406
|
+ startIndex = j;
|
|
|
407
|
+ break;
|
|
|
408
|
+ }
|
|
|
409
|
+ }
|
|
395
|
410
|
|
|
396
|
|
- // --- Filter new candles ---
|
|
397
|
|
- int startIndex = 0;
|
|
398
|
|
- for(int j = 0; j < copied; j++)
|
|
399
|
|
- {
|
|
400
|
|
- if(rates[j].time > latestApiTime)
|
|
|
411
|
+ int newCount = copied - startIndex;
|
|
|
412
|
+ if(newCount <= 0)
|
|
401
|
413
|
{
|
|
402
|
|
- startIndex = j;
|
|
403
|
|
- break;
|
|
|
414
|
+ PrintFormat("ℹ️ No new %s candles for %s", tfStr, sym);
|
|
|
415
|
+ continue;
|
|
404
|
416
|
}
|
|
405
|
|
- }
|
|
406
|
417
|
|
|
407
|
|
- int newCount = copied - startIndex;
|
|
408
|
|
- if(newCount <= 0)
|
|
409
|
|
- {
|
|
410
|
|
- PrintFormat("ℹ️ No new candles to send for %s", sym);
|
|
411
|
|
- continue;
|
|
412
|
|
- }
|
|
|
418
|
+ PrintFormat("🆕 Sending %d new %s candles for %s", newCount, tfStr, sym);
|
|
413
|
419
|
|
|
414
|
|
- PrintFormat("🆕 Sending %d new candles for %s after %s", newCount, sym, TimeToString(latestApiTime, TIME_DATE|TIME_SECONDS));
|
|
|
420
|
+ int batchSize = 200;
|
|
|
421
|
+ int sentTotal = 0;
|
|
415
|
422
|
|
|
416
|
|
- // --- Send new candles in batches ---
|
|
417
|
|
- int batchSize = 200;
|
|
418
|
|
- int sentTotal = 0;
|
|
|
423
|
+ for(int start = startIndex; start < copied; start += batchSize)
|
|
|
424
|
+ {
|
|
|
425
|
+ int size = MathMin(batchSize, copied - start);
|
|
|
426
|
+ string json = BuildCandleJSONFromRates(symbolId, rates, start, size, tfStr, tf);
|
|
|
427
|
+ string url = ApiBaseUrl + "/api/candles/bulk";
|
|
|
428
|
+ string response;
|
|
419
|
429
|
|
|
420
|
|
- for(int start = startIndex; start < copied; start += batchSize)
|
|
421
|
|
- {
|
|
422
|
|
- int size = MathMin(batchSize, copied - start);
|
|
423
|
|
- string json = BuildCandleJSONFromRates(symbolId, rates, start, size);
|
|
424
|
|
- string url = ApiBaseUrl + "/api/candles/bulk";
|
|
425
|
|
- string response;
|
|
|
430
|
+ bool ok = SendJSON(url, json, response);
|
|
|
431
|
+ if(!ok)
|
|
|
432
|
+ {
|
|
|
433
|
+ PrintFormat("❌ Failed to send %s batch for %s (start=%d)", tfStr, sym, start);
|
|
|
434
|
+ break;
|
|
|
435
|
+ }
|
|
426
|
436
|
|
|
427
|
|
- bool ok = SendJSON(url, json, response);
|
|
428
|
|
- if(!ok)
|
|
429
|
|
- {
|
|
430
|
|
- PrintFormat("❌ Failed to send candle batch for %s (start=%d)", sym, start);
|
|
431
|
|
- break;
|
|
|
437
|
+ sentTotal += size;
|
|
|
438
|
+ PrintFormat("📤 Sent %d/%d %s candles for %s", sentTotal, newCount, tfStr, sym);
|
|
432
|
439
|
}
|
|
433
|
|
-
|
|
434
|
|
- sentTotal += size;
|
|
435
|
|
- PrintFormat("📤 Sent %d/%d new candles for %s", sentTotal, newCount, sym);
|
|
436
|
440
|
}
|
|
437
|
441
|
}
|
|
438
|
442
|
|
|
439
|
|
- Print("✅ Incremental candle upload finished.");
|
|
|
443
|
+ Print("✅ Multi-timeframe candle upload finished.");
|
|
440
|
444
|
}
|
|
441
|
445
|
|
|
442
|
446
|
|
|
|
@@ -583,7 +587,7 @@ string ToISO8601(datetime t)
|
|
583
|
587
|
return StringFormat("%04d-%02d-%02dT%02d:%02d:%02d.000Z", st.year, st.mon, st.day, st.hour, st.min, st.sec);
|
|
584
|
588
|
}
|
|
585
|
589
|
|
|
586
|
|
-string BuildCandleJSONFromRates(int symbolId, MqlRates &rates[], int startIndex, int count)
|
|
|
590
|
+string BuildCandleJSONFromRates(int symbolId, MqlRates &rates[], int startIndex, int count, string timeframe, ENUM_TIMEFRAMES tf)
|
|
587
|
591
|
{
|
|
588
|
592
|
string json = "{\"candles\":[";
|
|
589
|
593
|
bool first = true;
|
|
|
@@ -595,7 +599,7 @@ string BuildCandleJSONFromRates(int symbolId, MqlRates &rates[], int startIndex,
|
|
595
|
599
|
if(r.time <= 0) continue;
|
|
596
|
600
|
|
|
597
|
601
|
datetime open_dt = (datetime)r.time;
|
|
598
|
|
- datetime close_dt = (datetime)(r.time + (datetime)PeriodSeconds(HistoricalTimeframe));
|
|
|
602
|
+ datetime close_dt = (datetime)(r.time + (datetime)PeriodSeconds(tf));
|
|
599
|
603
|
|
|
600
|
604
|
string openTime = ToISO8601(open_dt);
|
|
601
|
605
|
string closeTime = ToISO8601(close_dt);
|
|
|
@@ -604,8 +608,8 @@ string BuildCandleJSONFromRates(int symbolId, MqlRates &rates[], int startIndex,
|
|
604
|
608
|
double quoteVolume = (r.real_volume > 0 ? r.real_volume : volume);
|
|
605
|
609
|
|
|
606
|
610
|
string one = StringFormat(
|
|
607
|
|
- "{\"symbolId\":%d,\"openTime\":\"%s\",\"closeTime\":\"%s\",\"open\":%.5f,\"high\":%.5f,\"low\":%.5f,\"close\":%.5f,\"volume\":%.5f,\"tradesCount\":%d,\"quoteVolume\":%.5f}",
|
|
608
|
|
- symbolId, openTime, closeTime,
|
|
|
611
|
+ "{\"symbolId\":%d,\"timeframe\":\"%s\",\"openTime\":\"%s\",\"closeTime\":\"%s\",\"open\":%.5f,\"high\":%.5f,\"low\":%.5f,\"close\":%.5f,\"volume\":%.5f,\"tradesCount\":%d,\"quoteVolume\":%.5f}",
|
|
|
612
|
+ symbolId, timeframe, openTime, closeTime,
|
|
609
|
613
|
r.open, r.high, r.low, r.close,
|
|
610
|
614
|
volume, (int)volume, quoteVolume
|
|
611
|
615
|
);
|