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

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