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

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