Market Data Service is a high-performance financial data API that provides comprehensive Symbol prices of different markets through both RESTful endpoints and real-time WebSocket connections.

MarketDataSender.mq5 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717
  1. //+------------------------------------------------------------------+
  2. //| MarketDataSender.mq5 (Final Fixed Version) |
  3. //+------------------------------------------------------------------+
  4. #property strict
  5. #property description "Fetches all symbols' candles and live prices, sends to API."
  6. #include <Trade\SymbolInfo.mqh>
  7. input string ApiBaseUrl = "http://market-price.insightbull.io";
  8. input int HistoricalCandleCount = 1000;
  9. input ENUM_TIMEFRAMES HistoricalTimeframe = PERIOD_H1;
  10. input int LivePriceIntervalSeconds = 5;
  11. // Globals
  12. string symbols[];
  13. int symbolIds[];
  14. datetime lastSend = 0;
  15. datetime lastCandleSync = 0;
  16. //+------------------------------------------------------------------+
  17. int OnInit()
  18. {
  19. Print("Initializing MarketDataSender EA...");
  20. if(!InitializeSymbols())
  21. {
  22. Print("❌ Failed to initialize symbols.");
  23. return(INIT_FAILED);
  24. }
  25. Print("✅ Symbols initialized: ", ArraySize(symbols));
  26. SendAllHistoricalCandles();
  27. EventSetTimer(60); // ⏱️ Trigger OnTimer() every 30 minutes
  28. Print("✅ Timer set: SendAllHistoricalCandles() will run every 30 minutes.");
  29. return(INIT_SUCCEEDED);
  30. }
  31. //+------------------------------------------------------------------+
  32. void OnTick()
  33. {
  34. if(TimeCurrent() - lastSend >= LivePriceIntervalSeconds)
  35. {
  36. SendLivePrices();
  37. lastSend = TimeCurrent();
  38. }
  39. }
  40. //+------------------------------------------------------------------+
  41. bool InitializeSymbols()
  42. {
  43. int total = SymbolsTotal(true);
  44. if(total <= 0)
  45. {
  46. Print("❌ No symbols found!");
  47. return false;
  48. }
  49. ArrayResize(symbols, total);
  50. ArrayResize(symbolIds, total);
  51. for(int i = 0; i < total; i++)
  52. {
  53. symbols[i] = SymbolName(i, true);
  54. symbolIds[i] = -1;
  55. }
  56. if(!SyncSymbolsWithDatabase())
  57. {
  58. Print("❌ Failed to sync symbols with database");
  59. return false;
  60. }
  61. return true;
  62. }
  63. //+------------------------------------------------------------------+
  64. bool SyncSymbolsWithDatabase()
  65. {
  66. Print("Syncing symbols with database...");
  67. string url = ApiBaseUrl + "/api/symbols";
  68. string headers = "Content-Type: application/json\r\n";
  69. string resultHeaders = "";
  70. char result[];
  71. char emptyData[]; // ✅ required placeholder for GET request
  72. ResetLastError();
  73. // ✅ Correct GET request signature: includes empty data[]
  74. int res = WebRequest("GET", url, headers, 5000, emptyData, result, resultHeaders);
  75. if(res == -1)
  76. {
  77. int err = GetLastError();
  78. Print("❌ WebRequest connection error: ", err, " URL=", url);
  79. return false;
  80. }
  81. if(res != 200)
  82. {
  83. Print("❌ Failed to fetch symbols from API: HTTP ", res, " Response: ", CharArrayToString(result));
  84. return false;
  85. }
  86. string symbolsResponse = CharArrayToString(result);
  87. if(StringFind(symbolsResponse, "\"data\"") < 0)
  88. {
  89. Print("⚠️ Unexpected response format from symbols API: ", symbolsResponse);
  90. }
  91. for(int i = 0; i < ArraySize(symbols); i++)
  92. {
  93. string symbolName = symbols[i];
  94. int symbolId = FindSymbolId(symbolsResponse, symbolName);
  95. if(symbolId > 0)
  96. {
  97. symbolIds[i] = symbolId;
  98. Print("✅ Found existing symbol: ", symbolName, " (ID: ", symbolId, ")");
  99. }
  100. else
  101. {
  102. Sleep(300); // prevent overload (0.3 second delay)
  103. symbolId = CreateSymbolInDatabase(symbolName);
  104. if(symbolId > 0)
  105. {
  106. symbolIds[i] = symbolId;
  107. Print("✅ Created new symbol: ", symbolName, " (ID: ", symbolId, ")");
  108. }
  109. else
  110. {
  111. Print("❌ Failed to create symbol: ", symbolName," (ID: ", symbolId, ")");
  112. symbolIds[i] = -1;
  113. }
  114. }
  115. }
  116. return true;
  117. }
  118. //+------------------------------------------------------------------+
  119. //+------------------------------------------------------------------+
  120. //| Find exact symbolId from JSON response |
  121. //+------------------------------------------------------------------+
  122. //+------------------------------------------------------------------+
  123. //| Robust JSON search: matches exact symbol only |
  124. //+------------------------------------------------------------------+
  125. int FindSymbolId(string response, string symbolName)
  126. {
  127. int pos = 0;
  128. string patternSymbol = "\"symbol\":\"";
  129. string patternId = "\"id\":";
  130. while(true)
  131. {
  132. // find each symbol occurrence
  133. int symPos = StringFind(response, patternSymbol, pos);
  134. if(symPos < 0)
  135. break;
  136. // extract actual symbol value
  137. int symStart = symPos + StringLen(patternSymbol);
  138. int symEnd = StringFind(response, "\"", symStart);
  139. if(symEnd < 0) break;
  140. string foundSymbol = StringSubstr(response, symStart, symEnd - symStart);
  141. // 🟩 DEBUG LOG: show all symbols found
  142. // ✅ exact match check (case-sensitive)
  143. if(foundSymbol == symbolName)
  144. {
  145. // find id that comes *before* this symbol entry
  146. int blockStart = StringFind(response, patternId, symPos - 100);
  147. if(blockStart < 0)
  148. blockStart = StringFind(response, patternId, symPos);
  149. if(blockStart >= 0)
  150. {
  151. int idStart = blockStart + StringLen(patternId);
  152. int idEnd = StringFind(response, ",", idStart);
  153. if(idEnd < 0) idEnd = StringFind(response, "}", idStart);
  154. string idStr = StringSubstr(response, idStart, idEnd - idStart);
  155. int id = (int)StringToInteger(idStr);
  156. // 🟩 Success log
  157. Print("✅ Exact match found → symbol='", symbolName, "' | ID=", id);
  158. return id;
  159. }
  160. }
  161. // move to next
  162. pos = symEnd + 1;
  163. }
  164. Print("⚠️ No exact match for symbol ", symbolName);
  165. return -1;
  166. }
  167. //+------------------------------------------------------------------+
  168. int CreateSymbolInDatabase(string symbolName)
  169. {
  170. string baseAsset = "";
  171. string quoteAsset = "";
  172. string exchange = "MT5";
  173. string instrumentType = "forex";
  174. // --- Clean suffixes like ".pro", ".m", ".r", "_i" ---
  175. int dotPos = StringFind(symbolName, ".");
  176. if(dotPos > 0)
  177. symbolName = StringSubstr(symbolName, 0, dotPos);
  178. // --- Try basic split for 6-char pairs like EURUSD, GBPJPY, BTCUSD ---
  179. if(StringLen(symbolName) >= 6)
  180. {
  181. baseAsset = StringSubstr(symbolName, 0, 3);
  182. quoteAsset = StringSubstr(symbolName, 3, 3);
  183. }
  184. else
  185. {
  186. // --- Try alternate detection ---
  187. if(StringFind(symbolName, "USD") >= 0)
  188. {
  189. int pos = StringFind(symbolName, "USD");
  190. baseAsset = StringSubstr(symbolName, 0, pos);
  191. quoteAsset = "USD";
  192. }
  193. else if(StringFind(symbolName, "EUR") >= 0)
  194. {
  195. int pos = StringFind(symbolName, "EUR");
  196. baseAsset = StringSubstr(symbolName, 0, pos);
  197. quoteAsset = "EUR";
  198. }
  199. else
  200. {
  201. // Fallback safe defaults
  202. baseAsset = symbolName;
  203. quoteAsset = "USD";
  204. }
  205. }
  206. // --- Final safety: ensure no empty fields ---
  207. if(StringLen(baseAsset) == 0) baseAsset = "UNKNOWN";
  208. if(StringLen(quoteAsset) == 0) quoteAsset = "USD";
  209. // --- Decide instrument type ---
  210. if(StringFind(symbolName, "BTC") == 0 || StringFind(symbolName, "ETH") == 0)
  211. instrumentType = "crypto";
  212. else if(StringFind(symbolName, "XAU") == 0 || StringFind(symbolName, "XAG") == 0)
  213. instrumentType = "metal";
  214. else if(StringFind(symbolName, "US30") == 0 || StringFind(symbolName, "NAS") == 0)
  215. instrumentType = "index";
  216. else
  217. instrumentType = "forex";
  218. string json = StringFormat(
  219. "{\"symbol\":\"%s\",\"baseAsset\":\"%s\",\"quoteAsset\":\"%s\",\"exchange\":\"%s\",\"instrumentType\":\"%s\",\"isActive\":true}",
  220. symbolName, baseAsset, quoteAsset, exchange, instrumentType
  221. );
  222. string url = ApiBaseUrl + "/api/symbols";
  223. string headers = "Content-Type: application/json\r\n";
  224. string resultHeaders = "";
  225. char postData[];
  226. StringToCharArray(json, postData, 0, CP_UTF8);
  227. if(ArraySize(postData) > 0 && postData[ArraySize(postData) - 1] == 0)
  228. ArrayResize(postData, ArraySize(postData) - 1);
  229. char result[];
  230. int res = WebRequest("POST", url, headers, 5000, postData, result, resultHeaders);
  231. if(res != 201 && res != 200)
  232. {
  233. PrintFormat("❌ Failed to create symbol %s | HTTP %d | Response: %s", symbolName, res, CharArrayToString(result));
  234. return -1;
  235. }
  236. string createResponse = CharArrayToString(result);
  237. int idPos = StringFind(createResponse, "\"id\":");
  238. if(idPos < 0) return -1;
  239. int startPos = idPos + 5;
  240. int endPos = StringFind(createResponse, ",", startPos);
  241. if(endPos < 0) endPos = StringFind(createResponse, "}", startPos);
  242. string idStr = StringSubstr(createResponse, startPos, endPos - startPos);
  243. return (int)StringToInteger(idStr);
  244. }
  245. //+------------------------------------------------------------------+
  246. //| Fetch latest stored candle openTime from API |
  247. //+------------------------------------------------------------------+
  248. datetime GetLatestCandleTime(int symbolId)
  249. {
  250. string url = ApiBaseUrl + "/api/candles/" + IntegerToString(symbolId) + "/latest";
  251. string headers = "Content-Type: application/json\r\n";
  252. string resultHeaders = "";
  253. char result[];
  254. char emptyData[];
  255. ResetLastError();
  256. int res = WebRequest("GET", url, headers, 10000, emptyData, result, resultHeaders);
  257. if(res != 200)
  258. {
  259. Print("⚠️ Could not fetch latest candle for symbolId=", symbolId, " (HTTP ", res, ")");
  260. return 0;
  261. }
  262. string response = CharArrayToString(result);
  263. int pos = StringFind(response, "\"openTime\":\"");
  264. if(pos < 0)
  265. {
  266. Print("⚠️ No openTime found in response for symbolId=", symbolId);
  267. return 0;
  268. }
  269. pos += StringLen("\"openTime\":\"");
  270. int end = StringFind(response, "\"", pos);
  271. string openTimeStr = StringSubstr(response, pos, end - pos);
  272. // --- Parse ISO8601 to datetime ---
  273. int year = (int)StringToInteger(StringSubstr(openTimeStr, 0, 4));
  274. int month = (int)StringToInteger(StringSubstr(openTimeStr, 5, 2));
  275. int day = (int)StringToInteger(StringSubstr(openTimeStr, 8, 2));
  276. int hour = (int)StringToInteger(StringSubstr(openTimeStr, 11, 2));
  277. int min = (int)StringToInteger(StringSubstr(openTimeStr, 14, 2));
  278. int sec = (int)StringToInteger(StringSubstr(openTimeStr, 17, 2));
  279. MqlDateTime t;
  280. t.year = year; t.mon = month; t.day = day;
  281. t.hour = hour; t.min = min; t.sec = sec;
  282. datetime dt = StructToTime(t);
  283. Print("🕓 Latest stored candle openTime for symbolId=", symbolId, " → ", TimeToString(dt, TIME_DATE|TIME_SECONDS));
  284. return dt;
  285. }
  286. //+------------------------------------------------------------------+
  287. //+------------------------------------------------------------------+
  288. //| Send all historical candles to the API (Fixed Version) |
  289. //+------------------------------------------------------------------+
  290. //+------------------------------------------------------------------+
  291. //| Send all historical candles to the API (Fixed + Timeout Safe) |
  292. //+------------------------------------------------------------------+
  293. void SendAllHistoricalCandles()
  294. {
  295. Print("Starting historical upload for ", ArraySize(symbols), " symbols...");
  296. for(int i = 0; i < ArraySize(symbols); i++)
  297. {
  298. string sym = symbols[i];
  299. int symbolId = symbolIds[i];
  300. if(symbolId <= 0) continue;
  301. // --- Get last stored candle time ---
  302. datetime latestApiTime = GetLatestCandleTime(symbolId);
  303. // --- Ensure history data is available ---
  304. Sleep(300);
  305. int tries = 0;
  306. bool historyReady = false;
  307. while(tries < 10)
  308. {
  309. if(SeriesInfoInteger(sym, HistoricalTimeframe, SERIES_SYNCHRONIZED))
  310. {
  311. historyReady = true;
  312. break;
  313. }
  314. PrintFormat("⏳ Waiting for %s history to load... (try %d/10)", sym, tries + 1);
  315. Sleep(500);
  316. tries++;
  317. }
  318. if(!historyReady)
  319. {
  320. PrintFormat("⚠️ Skipping %s — history not loaded after 10 tries (~5s timeout).", sym);
  321. continue;
  322. }
  323. // --- Copy rates ---
  324. MqlRates rates[];
  325. ResetLastError();
  326. int copied = CopyRates(sym, HistoricalTimeframe, 0, HistoricalCandleCount, rates);
  327. if(copied <= 0)
  328. {
  329. int err = GetLastError();
  330. PrintFormat("⚠️ Failed to copy candles for %s (error %d)", sym, err);
  331. continue;
  332. }
  333. PrintFormat("✅ Copied %d candles for %s", copied, sym);
  334. // --- Filter new candles ---
  335. int startIndex = 0;
  336. for(int j = 0; j < copied; j++)
  337. {
  338. if(rates[j].time > latestApiTime)
  339. {
  340. startIndex = j;
  341. break;
  342. }
  343. }
  344. int newCount = copied - startIndex;
  345. if(newCount <= 0)
  346. {
  347. PrintFormat("ℹ️ No new candles to send for %s", sym);
  348. continue;
  349. }
  350. PrintFormat("🆕 Sending %d new candles for %s after %s", newCount, sym, TimeToString(latestApiTime, TIME_DATE|TIME_SECONDS));
  351. // --- Send new candles in batches ---
  352. int batchSize = 200;
  353. int sentTotal = 0;
  354. for(int start = startIndex; start < copied; start += batchSize)
  355. {
  356. int size = MathMin(batchSize, copied - start);
  357. string json = BuildCandleJSONFromRates(symbolId, rates, start, size);
  358. string url = ApiBaseUrl + "/api/candles/bulk";
  359. string response;
  360. bool ok = SendJSON(url, json, response);
  361. if(!ok)
  362. {
  363. PrintFormat("❌ Failed to send candle batch for %s (start=%d)", sym, start);
  364. break;
  365. }
  366. sentTotal += size;
  367. PrintFormat("📤 Sent %d/%d new candles for %s", sentTotal, newCount, sym);
  368. }
  369. }
  370. Print("✅ Incremental candle upload finished.");
  371. }
  372. //+------------------------------------------------------------------+
  373. //| Send live prices of all active symbols |
  374. //+------------------------------------------------------------------+
  375. void SendLivePrices()
  376. {
  377. bool firstItem = true;
  378. string json = "{\"prices\":[";
  379. int sentCount = 0;
  380. for(int i = 0; i < ArraySize(symbols); i++)
  381. {
  382. string sym = symbols[i];
  383. int symId = symbolIds[i];
  384. if(symId <= 0) continue;
  385. // Ensure symbol is visible in Market Watch
  386. if(!SymbolSelect(sym, true))
  387. {
  388. Print("⚠️ Failed to select symbol: ", sym);
  389. continue;
  390. }
  391. // Read primary prices
  392. double bid = SymbolInfoDouble(sym, SYMBOL_BID);
  393. double ask = SymbolInfoDouble(sym, SYMBOL_ASK);
  394. double last = SymbolInfoDouble(sym, SYMBOL_LAST);
  395. // If last = 0 (some providers), use midprice as fallback
  396. if(last <= 0 && bid > 0 && ask > 0)
  397. last = (bid + ask) / 2.0;
  398. // Skip if prices are still invalid
  399. if(bid <= 0 || ask <= 0 || last <= 0)
  400. {
  401. Print("⚠️ Skipping symbol ", sym, " — invalid bid/ask/last (", DoubleToString(bid,8), "/", DoubleToString(ask,8), "/", DoubleToString(last,8), ")");
  402. continue;
  403. }
  404. // Initialize sizes
  405. double bidSize = 0.0;
  406. double askSize = 0.0;
  407. // Try to fetch market depth (book) and classify volumes by price vs bid/ask
  408. MqlBookInfo book[];
  409. if(MarketBookGet(sym, book) && ArraySize(book) > 0)
  410. {
  411. for(int j = 0; j < ArraySize(book); j++)
  412. {
  413. double p = book[j].price;
  414. double v = book[j].volume;
  415. // If price is >= ask => ask side
  416. if(p >= ask) askSize += v;
  417. // If price is <= bid => bid side
  418. else if(p <= bid) bidSize += v;
  419. else
  420. {
  421. // price in-between -> assign to nearer side
  422. double distToBid = MathAbs(p - bid);
  423. double distToAsk = MathAbs(ask - p);
  424. if(distToBid <= distToAsk) bidSize += v; else askSize += v;
  425. }
  426. }
  427. PrintFormat("ℹ️ MarketBook for %s → bid=%.8f ask=%.8f bidSize=%.2f askSize=%.2f (book entries=%d)", sym, bid, ask, bidSize, askSize, ArraySize(book));
  428. }
  429. else
  430. {
  431. // MarketBook not available or empty
  432. // Try SymbolInfoTick as fallback
  433. MqlTick tick;
  434. if(SymbolInfoTick(sym, tick))
  435. {
  436. // tick.volume is aggregated tick volume — not exact bid/ask sizes but better than zero
  437. double tickVol = (double)tick.volume;
  438. if(tickVol > 0.0)
  439. {
  440. // assign tick volume to both sides conservatively
  441. if(bidSize <= 0) bidSize = tickVol;
  442. if(askSize <= 0) askSize = tickVol;
  443. PrintFormat("ℹ️ tick fallback for %s → tick.volume=%.2f", sym, tickVol);
  444. }
  445. else
  446. {
  447. Print("ℹ️ tick available but volume zero for ", sym);
  448. }
  449. }
  450. else
  451. {
  452. Print("ℹ️ MarketBook and tick not available for ", sym);
  453. }
  454. }
  455. // Final safety: ensure API-required positive numbers
  456. // If a side is zero or negative, set to minimal positive 1.0
  457. if(bidSize <= 0.0) bidSize = 1.0;
  458. if(askSize <= 0.0) askSize = 1.0;
  459. // Build JSON item for this symbol
  460. string item = StringFormat(
  461. "{\"symbolId\":%d,\"price\":%.8f,\"bid\":%.8f,\"ask\":%.8f,\"bidSize\":%.8f,\"askSize\":%.8f}",
  462. symId, last, bid, ask, bidSize, askSize
  463. );
  464. // Add to aggregate payload
  465. if(!firstItem) json += ",";
  466. json += item;
  467. firstItem = false;
  468. sentCount++;
  469. }
  470. json += "]}";
  471. if(sentCount == 0)
  472. {
  473. Print("⚠️ No valid live prices to send right now. Check if market is open and symbols have tick data.");
  474. return;
  475. }
  476. // Log URL and truncated payload for debugging
  477. string url = ApiBaseUrl + "/api/live-prices/bulk";
  478. int maxShow = 1000;
  479. string payloadLog = (StringLen(json) > maxShow) ? StringSubstr(json, 0, maxShow) + "...(truncated)" : json;
  480. Print("📤 Calling API: ", url);
  481. Print("📦 Payload (truncated): ", payloadLog);
  482. // Send and report
  483. string response;
  484. bool ok = SendJSON(url, json, response);
  485. if(ok)
  486. Print("✅ Successfully sent ", sentCount, " live prices to API.");
  487. else
  488. Print("❌ Failed to send live prices (", sentCount, " items). API response: ", response);
  489. }
  490. //+------------------------------------------------------------------+
  491. string ToISO8601(datetime t)
  492. {
  493. MqlDateTime st;
  494. TimeToStruct(t, st);
  495. return StringFormat("%04d-%02d-%02dT%02d:%02d:%02d.000Z", st.year, st.mon, st.day, st.hour, st.min, st.sec);
  496. }
  497. string BuildCandleJSONFromRates(int symbolId, MqlRates &rates[], int startIndex, int count)
  498. {
  499. string json = "{\"candles\":[";
  500. bool first = true;
  501. int ratesSize = ArraySize(rates);
  502. for(int i = startIndex; i < startIndex + count && i < ratesSize; i++)
  503. {
  504. MqlRates r = rates[i];
  505. if(r.time <= 0) continue;
  506. datetime open_dt = (datetime)r.time;
  507. datetime close_dt = (datetime)(r.time + (datetime)PeriodSeconds(HistoricalTimeframe));
  508. string openTime = ToISO8601(open_dt);
  509. string closeTime = ToISO8601(close_dt);
  510. double volume = (r.tick_volume > 0 ? r.tick_volume : 1);
  511. double quoteVolume = (r.real_volume > 0 ? r.real_volume : volume);
  512. string one = StringFormat(
  513. "{\"symbolId\":%d,\"openTime\":\"%s\",\"closeTime\":\"%s\",\"open\":%.5f,\"high\":%.5f,\"low\":%.5f,\"close\":%.5f,\"volume\":%.5f,\"tradesCount\":%d,\"quoteVolume\":%.5f}",
  514. symbolId, openTime, closeTime,
  515. r.open, r.high, r.low, r.close,
  516. volume, (int)volume, quoteVolume
  517. );
  518. if(!first) json += ",";
  519. json += one;
  520. first = false;
  521. }
  522. json += "]}";
  523. return json;
  524. }
  525. //+------------------------------------------------------------------+
  526. bool SendJSON(string url, string json, string &response)
  527. {
  528. ResetLastError();
  529. char postData[];
  530. StringToCharArray(json, postData, 0, CP_UTF8);
  531. // ✅ Remove trailing null terminator
  532. if(ArraySize(postData) > 0 && postData[ArraySize(postData) - 1] == 0)
  533. ArrayResize(postData, ArraySize(postData) - 1);
  534. if(ArraySize(postData) <= 0)
  535. {
  536. Print("❌ Empty postData for URL: ", url);
  537. return false;
  538. }
  539. char result[];
  540. string headers = "Content-Type: application/json\r\n";
  541. string resultHeaders = "";
  542. int timeout = 15000;
  543. int res = WebRequest("POST", url, headers, timeout, postData, result, resultHeaders);
  544. if(res == -1)
  545. {
  546. int err = GetLastError();
  547. Print("WebRequest error: ", err, " url=", url);
  548. return false;
  549. }
  550. response = CharArrayToString(result);
  551. if(res == 200 || res == 201)
  552. return true;
  553. Print("HTTP status ", res, " response: ", response);
  554. return false;
  555. }
  556. //+------------------------------------------------------------------+
  557. //+------------------------------------------------------------------+
  558. //| Cleanup old candles (keep only last 1000) |
  559. //+------------------------------------------------------------------+
  560. void CleanupOldCandles(int symbolId)
  561. {
  562. string url = ApiBaseUrl + "/api/candles/cleanup/" + IntegerToString(symbolId) + "?keep=1000";
  563. string headers = "Content-Type: application/json\r\n";
  564. string resultHeaders = "";
  565. char result[];
  566. char emptyData[];
  567. ResetLastError();
  568. int res = WebRequest("DELETE", url, headers, 10000, emptyData, result, resultHeaders);
  569. string response = CharArrayToString(result);
  570. if(res == 200 || res == 204)
  571. Print("🧹 Cleanup successful for symbolId=", symbolId, " → kept last 1000 candles.");
  572. else
  573. Print("⚠️ Cleanup failed for symbolId=", symbolId, " HTTP=", res, " Response=", response);
  574. }
  575. //+------------------------------------------------------------------+
  576. //| Timer event: runs every 60 seconds |
  577. //+------------------------------------------------------------------+
  578. void OnTimer()
  579. {
  580. datetime now = TimeCurrent();
  581. // ✅ Run full candle sync only once every minute
  582. if(now - lastCandleSync >= 600)
  583. {
  584. Print("⏰ Running scheduled candle sync and cleanup...");
  585. SendAllHistoricalCandles();
  586. // ✅ After uploading candles, clean up old ones
  587. for(int i = 0; i < ArraySize(symbols); i++)
  588. {
  589. int symId = symbolIds[i];
  590. if(symId <= 0) continue;
  591. CleanupOldCandles(symId);
  592. Sleep(500); // small delay to avoid API overload
  593. }
  594. lastCandleSync = now;
  595. Print("✅ Candle sync + cleanup cycle completed.");
  596. }
  597. }
  598. void OnDeinit(const int reason)
  599. {
  600. EventKillTimer();
  601. }