|
|
@@ -16,6 +16,10 @@ string symbols[];
|
|
|
int symbolIds[];
|
|
|
datetime lastSend = 0;
|
|
|
datetime lastCandleSync = 0;
|
|
|
+
|
|
|
+// --- Supported timeframes ---
|
|
|
+ENUM_TIMEFRAMES Timeframes[] = { PERIOD_M15, PERIOD_M30, PERIOD_H1, PERIOD_D1, PERIOD_W1, PERIOD_MN1 };
|
|
|
+string TimeframeStrings[] = { "15m", "30m", "1h", "1D", "1W", "1M" };
|
|
|
//+------------------------------------------------------------------+
|
|
|
int OnInit()
|
|
|
{
|
|
|
@@ -290,9 +294,9 @@ int CreateSymbolInDatabase(string symbolName)
|
|
|
//+------------------------------------------------------------------+
|
|
|
//| Fetch latest stored candle openTime from API |
|
|
|
//+------------------------------------------------------------------+
|
|
|
-datetime GetLatestCandleTime(int symbolId)
|
|
|
+datetime GetLatestCandleTime(int symbolId, string timeframe)
|
|
|
{
|
|
|
- string url = ApiBaseUrl + "/api/candles/" + IntegerToString(symbolId) + "/latest";
|
|
|
+ string url = ApiBaseUrl + "/api/candles/" + IntegerToString(symbolId) + "/latest?timeframe=" + timeframe;
|
|
|
string headers = "Content-Type: application/json\r\n";
|
|
|
string resultHeaders = "";
|
|
|
char result[];
|
|
|
@@ -303,7 +307,7 @@ datetime GetLatestCandleTime(int symbolId)
|
|
|
|
|
|
if(res != 200)
|
|
|
{
|
|
|
- Print("⚠️ Could not fetch latest candle for symbolId=", symbolId, " (HTTP ", res, ")");
|
|
|
+ Print("⚠️ Could not fetch latest candle for symbolId=", symbolId, " timeframe=", timeframe, " (HTTP ", res, ")");
|
|
|
return 0;
|
|
|
}
|
|
|
|
|
|
@@ -311,7 +315,7 @@ datetime GetLatestCandleTime(int symbolId)
|
|
|
int pos = StringFind(response, "\"openTime\":\"");
|
|
|
if(pos < 0)
|
|
|
{
|
|
|
- Print("⚠️ No openTime found in response for symbolId=", symbolId);
|
|
|
+ Print("⚠️ No openTime found in response for symbolId=", symbolId, " timeframe=", timeframe);
|
|
|
return 0;
|
|
|
}
|
|
|
|
|
|
@@ -319,7 +323,6 @@ datetime GetLatestCandleTime(int symbolId)
|
|
|
int end = StringFind(response, "\"", pos);
|
|
|
string openTimeStr = StringSubstr(response, pos, end - pos);
|
|
|
|
|
|
- // --- Parse ISO8601 to datetime ---
|
|
|
int year = (int)StringToInteger(StringSubstr(openTimeStr, 0, 4));
|
|
|
int month = (int)StringToInteger(StringSubstr(openTimeStr, 5, 2));
|
|
|
int day = (int)StringToInteger(StringSubstr(openTimeStr, 8, 2));
|
|
|
@@ -332,7 +335,7 @@ datetime GetLatestCandleTime(int symbolId)
|
|
|
t.hour = hour; t.min = min; t.sec = sec;
|
|
|
|
|
|
datetime dt = StructToTime(t);
|
|
|
- Print("🕓 Latest stored candle openTime for symbolId=", symbolId, " → ", TimeToString(dt, TIME_DATE|TIME_SECONDS));
|
|
|
+ PrintFormat("🕓 Latest stored candle for %s (symbolId=%d) = %s", timeframe, symbolId, TimeToString(dt, TIME_DATE|TIME_SECONDS));
|
|
|
return dt;
|
|
|
}
|
|
|
|
|
|
@@ -345,7 +348,7 @@ datetime GetLatestCandleTime(int symbolId)
|
|
|
//+------------------------------------------------------------------+
|
|
|
void SendAllHistoricalCandles()
|
|
|
{
|
|
|
- Print("Starting historical upload for ", ArraySize(symbols), " symbols...");
|
|
|
+ Print("Starting multi-timeframe historical upload for ", ArraySize(symbols), " symbols...");
|
|
|
|
|
|
for(int i = 0; i < ArraySize(symbols); i++)
|
|
|
{
|
|
|
@@ -353,90 +356,91 @@ void SendAllHistoricalCandles()
|
|
|
int symbolId = symbolIds[i];
|
|
|
if(symbolId <= 0) continue;
|
|
|
|
|
|
- // --- Get last stored candle time ---
|
|
|
- datetime latestApiTime = GetLatestCandleTime(symbolId);
|
|
|
+ // --- Loop through all timeframes ---
|
|
|
+ for(int tfIndex = 0; tfIndex < ArraySize(Timeframes); tfIndex++)
|
|
|
+ {
|
|
|
+ ENUM_TIMEFRAMES tf = Timeframes[tfIndex];
|
|
|
+ string tfStr = TimeframeStrings[tfIndex];
|
|
|
+ PrintFormat("📊 Processing %s timeframe for %s", tfStr, sym);
|
|
|
+
|
|
|
+ datetime latestApiTime = GetLatestCandleTime(symbolId, tfStr);
|
|
|
|
|
|
- // --- Ensure history data is available ---
|
|
|
- Sleep(300);
|
|
|
- int tries = 0;
|
|
|
- bool historyReady = false;
|
|
|
+ Sleep(300);
|
|
|
+ int tries = 0;
|
|
|
+ bool historyReady = false;
|
|
|
|
|
|
- while(tries < 10)
|
|
|
- {
|
|
|
- if(SeriesInfoInteger(sym, HistoricalTimeframe, SERIES_SYNCHRONIZED))
|
|
|
+ while(tries < 10)
|
|
|
{
|
|
|
- historyReady = true;
|
|
|
- break;
|
|
|
+ if(SeriesInfoInteger(sym, tf, SERIES_SYNCHRONIZED))
|
|
|
+ {
|
|
|
+ historyReady = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ PrintFormat("⏳ Waiting for %s (%s) history to load... (try %d/10)", sym, tfStr, tries + 1);
|
|
|
+ Sleep(500);
|
|
|
+ tries++;
|
|
|
}
|
|
|
- PrintFormat("⏳ Waiting for %s history to load... (try %d/10)", sym, tries + 1);
|
|
|
- Sleep(500);
|
|
|
- tries++;
|
|
|
- }
|
|
|
|
|
|
- if(!historyReady)
|
|
|
- {
|
|
|
- PrintFormat("⚠️ Skipping %s — history not loaded after 10 tries (~5s timeout).", sym);
|
|
|
- continue;
|
|
|
- }
|
|
|
+ if(!historyReady)
|
|
|
+ {
|
|
|
+ PrintFormat("⚠️ Skipping %s (%s) — history not loaded.", sym, tfStr);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
|
|
|
- // --- Copy rates ---
|
|
|
- MqlRates rates[];
|
|
|
- ResetLastError();
|
|
|
- int copied = CopyRates(sym, HistoricalTimeframe, 0, HistoricalCandleCount, rates);
|
|
|
+ MqlRates rates[];
|
|
|
+ ResetLastError();
|
|
|
+ int copied = CopyRates(sym, tf, 0, HistoricalCandleCount, rates);
|
|
|
|
|
|
- if(copied <= 0)
|
|
|
- {
|
|
|
- int err = GetLastError();
|
|
|
- PrintFormat("⚠️ Failed to copy candles for %s (error %d)", sym, err);
|
|
|
- continue;
|
|
|
- }
|
|
|
+ if(copied <= 0)
|
|
|
+ {
|
|
|
+ int err = GetLastError();
|
|
|
+ PrintFormat("⚠️ Failed to copy %s candles (%s) (error %d)", sym, tfStr, err);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
|
|
|
- PrintFormat("✅ Copied %d candles for %s", copied, sym);
|
|
|
+ int startIndex = 0;
|
|
|
+ for(int j = 0; j < copied; j++)
|
|
|
+ {
|
|
|
+ if(rates[j].time > latestApiTime)
|
|
|
+ {
|
|
|
+ startIndex = j;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- // --- Filter new candles ---
|
|
|
- int startIndex = 0;
|
|
|
- for(int j = 0; j < copied; j++)
|
|
|
- {
|
|
|
- if(rates[j].time > latestApiTime)
|
|
|
+ int newCount = copied - startIndex;
|
|
|
+ if(newCount <= 0)
|
|
|
{
|
|
|
- startIndex = j;
|
|
|
- break;
|
|
|
+ PrintFormat("ℹ️ No new %s candles for %s", tfStr, sym);
|
|
|
+ continue;
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- int newCount = copied - startIndex;
|
|
|
- if(newCount <= 0)
|
|
|
- {
|
|
|
- PrintFormat("ℹ️ No new candles to send for %s", sym);
|
|
|
- continue;
|
|
|
- }
|
|
|
+ PrintFormat("🆕 Sending %d new %s candles for %s", newCount, tfStr, sym);
|
|
|
|
|
|
- PrintFormat("🆕 Sending %d new candles for %s after %s", newCount, sym, TimeToString(latestApiTime, TIME_DATE|TIME_SECONDS));
|
|
|
+ int batchSize = 200;
|
|
|
+ int sentTotal = 0;
|
|
|
|
|
|
- // --- Send new candles in batches ---
|
|
|
- int batchSize = 200;
|
|
|
- int sentTotal = 0;
|
|
|
+ for(int start = startIndex; start < copied; start += batchSize)
|
|
|
+ {
|
|
|
+ int size = MathMin(batchSize, copied - start);
|
|
|
+ string json = BuildCandleJSONFromRates(symbolId, rates, start, size, tfStr, tf);
|
|
|
+ string url = ApiBaseUrl + "/api/candles/bulk";
|
|
|
+ string response;
|
|
|
|
|
|
- for(int start = startIndex; start < copied; start += batchSize)
|
|
|
- {
|
|
|
- int size = MathMin(batchSize, copied - start);
|
|
|
- string json = BuildCandleJSONFromRates(symbolId, rates, start, size);
|
|
|
- string url = ApiBaseUrl + "/api/candles/bulk";
|
|
|
- string response;
|
|
|
+ bool ok = SendJSON(url, json, response);
|
|
|
+ if(!ok)
|
|
|
+ {
|
|
|
+ PrintFormat("❌ Failed to send %s batch for %s (start=%d)", tfStr, sym, start);
|
|
|
+ break;
|
|
|
+ }
|
|
|
|
|
|
- bool ok = SendJSON(url, json, response);
|
|
|
- if(!ok)
|
|
|
- {
|
|
|
- PrintFormat("❌ Failed to send candle batch for %s (start=%d)", sym, start);
|
|
|
- break;
|
|
|
+ sentTotal += size;
|
|
|
+ PrintFormat("📤 Sent %d/%d %s candles for %s", sentTotal, newCount, tfStr, sym);
|
|
|
}
|
|
|
-
|
|
|
- sentTotal += size;
|
|
|
- PrintFormat("📤 Sent %d/%d new candles for %s", sentTotal, newCount, sym);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- Print("✅ Incremental candle upload finished.");
|
|
|
+ Print("✅ Multi-timeframe candle upload finished.");
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -583,7 +587,7 @@ string ToISO8601(datetime t)
|
|
|
return StringFormat("%04d-%02d-%02dT%02d:%02d:%02d.000Z", st.year, st.mon, st.day, st.hour, st.min, st.sec);
|
|
|
}
|
|
|
|
|
|
-string BuildCandleJSONFromRates(int symbolId, MqlRates &rates[], int startIndex, int count)
|
|
|
+string BuildCandleJSONFromRates(int symbolId, MqlRates &rates[], int startIndex, int count, string timeframe, ENUM_TIMEFRAMES tf)
|
|
|
{
|
|
|
string json = "{\"candles\":[";
|
|
|
bool first = true;
|
|
|
@@ -595,7 +599,7 @@ string BuildCandleJSONFromRates(int symbolId, MqlRates &rates[], int startIndex,
|
|
|
if(r.time <= 0) continue;
|
|
|
|
|
|
datetime open_dt = (datetime)r.time;
|
|
|
- datetime close_dt = (datetime)(r.time + (datetime)PeriodSeconds(HistoricalTimeframe));
|
|
|
+ datetime close_dt = (datetime)(r.time + (datetime)PeriodSeconds(tf));
|
|
|
|
|
|
string openTime = ToISO8601(open_dt);
|
|
|
string closeTime = ToISO8601(close_dt);
|
|
|
@@ -604,8 +608,8 @@ string BuildCandleJSONFromRates(int symbolId, MqlRates &rates[], int startIndex,
|
|
|
double quoteVolume = (r.real_volume > 0 ? r.real_volume : volume);
|
|
|
|
|
|
string one = StringFormat(
|
|
|
- "{\"symbolId\":%d,\"openTime\":\"%s\",\"closeTime\":\"%s\",\"open\":%.5f,\"high\":%.5f,\"low\":%.5f,\"close\":%.5f,\"volume\":%.5f,\"tradesCount\":%d,\"quoteVolume\":%.5f}",
|
|
|
- symbolId, openTime, closeTime,
|
|
|
+ "{\"symbolId\":%d,\"timeframe\":\"%s\",\"openTime\":\"%s\",\"closeTime\":\"%s\",\"open\":%.5f,\"high\":%.5f,\"low\":%.5f,\"close\":%.5f,\"volume\":%.5f,\"tradesCount\":%d,\"quoteVolume\":%.5f}",
|
|
|
+ symbolId, timeframe, openTime, closeTime,
|
|
|
r.open, r.high, r.low, r.close,
|
|
|
volume, (int)volume, quoteVolume
|
|
|
);
|