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 23KB

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