Просмотр исходного кода

feat: Add MT5 symbol synchronization and historical candle fetching

- Implement MQL5 Expert Advisor for symbol management
- Add symbol synchronization with database API
- Fetch and store last 1000 historical candles per symbol
- Add bulk candle upload functionality to API endpoints
- Include basic error handling for data operations

- Create Symbol Controller with full CRUD operations
- Add symbol search and filtering capabilities
- Implement pagination for symbol listing
- Add soft delete functionality for symbol management
uzairrizwan1 месяцев назад: 3
Родитель
Сommit
c7a4acaec3
2 измененных файлов с 340 добавлено и 241 удалено
  1. 335 238
      MT5/Experts/MarketDataSender.mq5
  2. 5 3
      src/controllers/symbolController.js

+ 335 - 238
MT5/Experts/MarketDataSender.mq5

@@ -1,313 +1,410 @@
1 1
 //+------------------------------------------------------------------+
2
-//|                                 MarketDataSender.mq5             |
3
-//|                        Copyright 2025, MetaQuotes Software Corp. |
4
-//|                                             https://www.mql5.com |
2
+//|                   MarketDataSender.mq5 (Final Fixed Version)     |
5 3
 //+------------------------------------------------------------------+
6
-#property copyright "Copyright 2025, MetaQuotes Software Corp."
7
-#property link      "https://www.mql5.com"
8
-#property version   "1.00"
9
-#property description "Sends historical candles and live prices to REST API"
10
-#property script_show_inputs
4
+#property strict
5
+#property description "Fetches all symbols' candles and live prices, sends to API."
11 6
 
12 7
 #include <Trade\SymbolInfo.mqh>
13
-#include <JAson.mqh> // Include JSON library
14 8
 
15
-//--- REST API Configuration
16
-input string ApiBaseUrl = "http://localhost:3000"; // Base URL of your API
17
-input string ApiKey = ""; // Optional API key if required
9
+input string ApiBaseUrl = "http://market-price.insightbull.io";
10
+input int HistoricalCandleCount = 1000;
11
+input ENUM_TIMEFRAMES HistoricalTimeframe = PERIOD_H1;
12
+input int LivePriceIntervalSeconds = 5;
18 13
 
19
-//--- Historical Data Configuration
20
-input int    HistoricalCandleCount = 1000; // Number of historical candles to send
21
-input ENUM_TIMEFRAMES HistoricalTimeframe = PERIOD_H1; // Timeframe for historical data
22
-
23
-//--- Global variables
24
-CJAson json;
25
-CSymbolInfo symbolInfo;
14
+// Globals
26 15
 string symbols[];
27
-datetime lastSentTime = 0;
16
+int symbolIds[];
17
+datetime lastSend = 0;
28 18
 
29
-//+------------------------------------------------------------------+
30
-//| Expert initialization function                                   |
31 19
 //+------------------------------------------------------------------+
32 20
 int OnInit()
33 21
 {
34
-   // Get all symbols
35
-   int count = SymbolsTotal(true);
36
-   ArrayResize(symbols, count);
37
-   
38
-   for(int i = 0; i < count; i++)
22
+   Print("Initializing MarketDataSender EA...");
23
+   if(!InitializeSymbols())
39 24
    {
40
-      symbols[i] = SymbolName(i, true);
25
+      Print("❌ Failed to initialize symbols.");
26
+      return(INIT_FAILED);
41 27
    }
42
-   
43
-   // Send initial historical data
44
-   SendHistoricalData();
45
-   
28
+
29
+   Print("✅ Symbols initialized: ", ArraySize(symbols));
30
+   SendAllHistoricalCandles();
46 31
    return(INIT_SUCCEEDED);
47 32
 }
48 33
 
49
-//+------------------------------------------------------------------+
50
-//| Expert tick function                                             |
51 34
 //+------------------------------------------------------------------+
52 35
 void OnTick()
53 36
 {
54
-   // Send live prices every second
55
-   if(TimeCurrent() - lastSentTime >= 1)
37
+   if(TimeCurrent() - lastSend >= LivePriceIntervalSeconds)
56 38
    {
57 39
       SendLivePrices();
58
-      lastSentTime = TimeCurrent();
40
+      lastSend = TimeCurrent();
59 41
    }
60 42
 }
61 43
 
62 44
 //+------------------------------------------------------------------+
63
-//| Send historical candle data                                      |
64
-//+------------------------------------------------------------------+
65
-void SendHistoricalData()
45
+bool InitializeSymbols()
66 46
 {
67
-   for(int s = 0; s < ArraySize(symbols); s++)
47
+   int total = SymbolsTotal(true);
48
+   if(total <= 0)
68 49
    {
69
-      string symbol = symbols[s];
70
-      
71
-      // Get historical candles
72
-      MqlRates rates[];
73
-      int copied = CopyRates(symbol, HistoricalTimeframe, 0, HistoricalCandleCount, rates);
74
-      
75
-      if(copied <= 0) continue;
76
-      
77
-      // Prepare JSON payload
78
-      CJAsonArray candlesArray;
79
-      
80
-      for(int i = 0; i < copied; i++)
81
-      {
82
-         CJAson candleObj;
83
-         candleObj.Add("symbolId", GetSymbolId(symbol)); // You need to implement GetSymbolId()
84
-         candleObj.Add("openTime", TimeToString(rates[i].time, TIME_DATE|TIME_MINUTES|TIME_SECONDS));
85
-         candleObj.Add("closeTime", TimeToString(rates[i].time + PeriodSeconds(HistoricalTimeframe), TIME_DATE|TIME_MINUTES|TIME_SECONDS));
86
-         candleObj.Add("open", rates[i].open);
87
-         candleObj.Add("high", rates[i].high);
88
-         candleObj.Add("low", rates[i].low);
89
-         candleObj.Add("close", rates[i].close);
90
-         candleObj.Add("volume", rates[i].tick_volume);
91
-         
92
-         candlesArray.Add(candleObj);
93
-      }
94
-      
95
-      // Create final payload
96
-      CJAson payload;
97
-      payload.Add("candles", candlesArray);
98
-      
99
-      // Send to API
100
-      string url = ApiBaseUrl + "/api/candles/bulk";
101
-      string result;
102
-      string headers = "Content-Type: application/json";
103
-      if(StringLen(ApiKey) > 0) headers += "\r\nAuthorization: Bearer " + ApiKey;
104
-      
105
-      // Implement retry logic with exponential backoff
106
-      int retries = 3;
107
-      int delayMs = 1000;
108
-      int res = -1;
109
-      
110
-      for(int attempt = 0; attempt < retries; attempt++) {
111
-         res = WebRequest("POST", url, headers, 5000, payload.GetJson(), result);
112
-         
113
-         if(res == 200) break;
114
-         
115
-         Print("Attempt ", attempt+1, " failed (", res, "). Retrying in ", delayMs, "ms");
116
-         Sleep(delayMs);
117
-         delayMs *= 2; // Exponential backoff
118
-      }
119
-      
120
-      if(res == 200) {
121
-         Print("Successfully sent historical data for ", symbol);
122
-      } else {
123
-         Print("Permanent failure sending historical data for ", symbol, ": ", res, " - ", result);
124
-         // TODO: Implement dead letter queue storage
125
-      }
50
+      Print("❌ No symbols found!");
51
+      return false;
126 52
    }
127
-}
128 53
 
129
-//+------------------------------------------------------------------+
130
-//| Send live prices                                                 |
131
-//+------------------------------------------------------------------+
132
-void SendLivePrices()
133
-{
134
-   CJAsonArray pricesArray;
135
-   
136
-   for(int s = 0; s < ArraySize(symbols); s++)
54
+   ArrayResize(symbols, total);
55
+   ArrayResize(symbolIds, total);
56
+
57
+   for(int i = 0; i < total; i++)
137 58
    {
138
-      string symbol = symbols[s];
139
-      
140
-      if(!symbolInfo.Name(symbol)) continue;
141
-      symbolInfo.RefreshRates();
142
-      
143
-      CJAson priceObj;
144
-      priceObj.Add("symbolId", GetSymbolId(symbol));
145
-      priceObj.Add("price", symbolInfo.Last());
146
-      priceObj.Add("bid", symbolInfo.Bid());
147
-      priceObj.Add("ask", symbolInfo.Ask());
148
-      priceObj.Add("bidSize", symbolInfo.VolumeBid());
149
-      priceObj.Add("askSize", symbolInfo.VolumeAsk());
150
-      
151
-      pricesArray.Add(priceObj);
59
+      symbols[i] = SymbolName(i, true);
60
+      symbolIds[i] = -1;
152 61
    }
153
-   
154
-   // Create final payload
155
-   CJAson payload;
156
-   payload.Add("prices", pricesArray);
157
-   
158
-   // Send to API
159
-   string url = ApiBaseUrl + "/api/live-prices/bulk";
160
-   string result;
161
-   string headers = "Content-Type: application/json";
162
-   if(StringLen(ApiKey) > 0) headers += "\r\nAuthorization: Bearer " + ApiKey;
163
-   
164
-      // Implement retry logic with exponential backoff
165
-      int retries = 3;
166
-      int delayMs = 1000;
167
-      int res = -1;
168
-      
169
-      for(int attempt = 0; attempt < retries; attempt++) {
170
-         res = WebRequest("POST", url, headers, 5000, payload.GetJson(), result);
171
-         
172
-         if(res == 200) break;
173
-         
174
-         Print("Attempt ", attempt+1, " failed (", res, "). Retrying in ", delayMs, "ms");
175
-         Sleep(delayMs);
176
-         delayMs *= 2; // Exponential backoff
177
-      }
178
-      
179
-      if(res == 200) {
180
-         Print("Successfully sent live prices");
181
-      } else {
182
-         Print("Permanent failure sending live prices: ", res, " - ", result);
183
-         // TODO: Implement dead letter queue storage
184
-      }
62
+
63
+   if(!SyncSymbolsWithDatabase())
64
+   {
65
+      Print("❌ Failed to sync symbols with database");
66
+      return false;
67
+   }
68
+
69
+   return true;
185 70
 }
186 71
 
187 72
 //+------------------------------------------------------------------+
188
-//| Initialize symbol map from database                              |
189
-//+------------------------------------------------------------------+
190
-bool InitializeSymbolMap()
73
+bool SyncSymbolsWithDatabase()
191 74
 {
192
-   // Fetch existing symbols from API
193
-   string url = ApiBaseUrl + "/api/symbols";
194
-   string result;
195
-   string headers = "Content-Type: application/json";
196
-   if(StringLen(ApiKey) > 0) headers += "\r\nAuthorization: Bearer " + ApiKey;
197
-   
198
-   int res = WebRequest("GET", url, headers, 5000, "", result);
199
-   
200
-   if(res != 200)
75
+   Print("Syncing symbols with database...");
76
+
77
+     string url = ApiBaseUrl + "/api/symbols";
78
+   string headers = "Content-Type: application/json\r\n";
79
+   string resultHeaders = "";
80
+   char result[];
81
+   char emptyData[];   // ✅ required placeholder for GET request
82
+
83
+   ResetLastError();
84
+
85
+   // ✅ Correct GET request signature: includes empty data[]
86
+   int res = WebRequest("GET", url, headers, 5000, emptyData, result, resultHeaders);
87
+
88
+   if(res == -1)
201 89
    {
202
-      Print("Failed to fetch symbols: ", res, " - ", result);
90
+      int err = GetLastError();
91
+      Print("❌ WebRequest connection error: ", err, " URL=", url);
203 92
       return false;
204 93
    }
205
-   
206
-   // Parse response
207
-   CJAson parser;
208
-   if(!parser.Parse(result))
94
+
95
+   if(res != 200)
209 96
    {
210
-      Print("Failed to parse symbols response");
97
+      Print("❌ Failed to fetch symbols from API: HTTP ", res, " Response: ", CharArrayToString(result));
211 98
       return false;
212 99
    }
213
-   
214
-   // Create lookup table
215
-   CJAsonArray symbolsArray = parser.GetArray("data");
216
-   int dbSymbolCount = symbolsArray.Size();
217
-   
218
-   for(int s = 0; s < ArraySize(symbols); s++)
100
+
101
+   string symbolsResponse = CharArrayToString(result);
102
+
103
+   if(StringFind(symbolsResponse, "\"data\"") < 0)
219 104
    {
220
-      string mt5Symbol = symbols[s];
221
-      bool found = false;
222
-      
223
-      // Search for matching symbol in database
224
-      for(int i = 0; i < dbSymbolCount; i++)
105
+      Print("⚠️ Unexpected response format from symbols API: ", symbolsResponse);
106
+   }
107
+
108
+   for(int i = 0; i < ArraySize(symbols); i++)
109
+   {
110
+      string symbolName = symbols[i];
111
+      int symbolId = FindSymbolId(symbolsResponse, symbolName);
112
+
113
+      if(symbolId > 0)
225 114
       {
226
-         CJAson dbSymbol = symbolsArray.GetObject(i);
227
-         string dbSymbolName = dbSymbol.GetString("symbol");
228
-         
229
-         if(dbSymbolName == mt5Symbol)
230
-         {
231
-            symbolIdMap[s] = (int)dbSymbol.GetInt("id");
232
-            found = true;
233
-            break;
234
-         }
115
+         symbolIds[i] = symbolId;
116
+         Print("✅ Found existing symbol: ", symbolName, " (ID: ", symbolId, ")");
235 117
       }
236
-      
237
-      // Create symbol if not found
238
-      if(!found)
118
+      else
239 119
       {
240
-         int newId = CreateSymbol(mt5Symbol);
241
-         if(newId > 0)
120
+         Sleep(300); // prevent overload (0.3 second delay)
121
+         symbolId = CreateSymbolInDatabase(symbolName);
122
+         if(symbolId > 0)
242 123
          {
243
-            symbolIdMap[s] = newId;
244
-            Print("Created new symbol: ", mt5Symbol, " (ID: ", newId, ")");
124
+            symbolIds[i] = symbolId;
125
+            Print("✅ Created new symbol: ", symbolName, " (ID: ", symbolId, ")");
245 126
          }
246 127
          else
247 128
          {
248
-            Print("Failed to create symbol: ", mt5Symbol);
249
-            return false;
129
+            Print("❌ Failed to create symbol: ", symbolName," (ID: ", symbolId, ")");
130
+            symbolIds[i] = -1;
250 131
          }
251 132
       }
252 133
    }
253
-   
134
+
254 135
    return true;
255 136
 }
256 137
 
257 138
 //+------------------------------------------------------------------+
258
-//| Create new symbol in database                                    |
139
+int FindSymbolId(string response, string symbolName)
140
+{
141
+   string searchPattern = StringFormat("\"symbol\":\"%s\"", symbolName);
142
+   int symbolPos = StringFind(response, searchPattern);
143
+   if(symbolPos < 0) return -1;
144
+
145
+   int idPos = StringFind(response, "\"id\":", symbolPos);
146
+   if(idPos < 0) return -1;
147
+
148
+   int startPos = idPos + 5;
149
+   int endPos = StringFind(response, ",", startPos);
150
+   if(endPos < 0) endPos = StringFind(response, "}", startPos);
151
+
152
+   string idStr = StringSubstr(response, startPos, endPos - startPos);
153
+   return (int)StringToInteger(idStr);
154
+}
155
+
259 156
 //+------------------------------------------------------------------+
260
-int CreateSymbol(string symbol)
157
+int CreateSymbolInDatabase(string symbolName)
261 158
 {
262
-   string url = ApiBaseUrl + "/api/symbols";
263
-   string result;
264
-   string headers = "Content-Type: application/json";
265
-   if(StringLen(ApiKey) > 0) headers += "\r\nAuthorization: Bearer " + ApiKey;
266
-   
267
-   // Extract exchange and instrument type from symbol name
268
-   string parts[];
269
-   StringSplit(symbol, '_', parts);
270
-   string exchange = (ArraySize(parts) > 1) ? parts[0] : "MT5";
271
-   string instrumentType = "forex"; // Default, can be improved
272
-   
273
-   // Prepare payload
274
-   CJAson payload;
275
-   payload.Add("symbol", symbol);
276
-   payload.Add("exchange", exchange);
277
-   payload.Add("instrumentType", instrumentType);
278
-   payload.Add("isActive", true);
279
-   
280
-   int res = WebRequest("POST", url, headers, 5000, payload.GetJson(), result);
281
-   
282
-   if(res != 201)
159
+   string baseAsset = "";
160
+   string quoteAsset = "";
161
+   string exchange = "MT5";
162
+   string instrumentType = "forex";
163
+
164
+   if(StringLen(symbolName) >= 6)
283 165
    {
284
-      Print("Error creating symbol: ", res, " - ", result);
285
-      return -1;
166
+      baseAsset = StringSubstr(symbolName, 0, 3);
167
+      quoteAsset = StringSubstr(symbolName, 3, 3);
286 168
    }
287
-   
288
-   // Parse response to get new ID
289
-   CJAson parser;
290
-   if(!parser.Parse(result))
169
+
170
+   string json = StringFormat(
171
+      "{\"symbol\":\"%s\",\"baseAsset\":\"%s\",\"quoteAsset\":\"%s\",\"exchange\":\"%s\",\"instrumentType\":\"%s\",\"isActive\":true}",
172
+      symbolName, baseAsset, quoteAsset, exchange, instrumentType
173
+   );
174
+
175
+   string url = ApiBaseUrl + "/api/symbols";
176
+   string headers = "Content-Type: application/json\r\n";
177
+   string resultHeaders = "";
178
+
179
+   char postData[];
180
+   StringToCharArray(json, postData, 0, CP_UTF8);
181
+
182
+   // ✅ FIX: Remove trailing null terminator from JSON
183
+   if(ArraySize(postData) > 0 && postData[ArraySize(postData) - 1] == 0)
184
+      ArrayResize(postData, ArraySize(postData) - 1);
185
+
186
+   char result[];
187
+
188
+   int res = WebRequest("POST", url, headers, 5000, postData, result, resultHeaders);
189
+
190
+   if(res != 201 && res != 200)
291 191
    {
292
-      Print("Failed to parse create symbol response");
192
+      Print("Failed to create symbol: ", res, " Response: ", CharArrayToString(result));
293 193
       return -1;
294 194
    }
295
-   
296
-   return (int)parser.GetObject("data").GetInt("id");
195
+
196
+   string createResponse = CharArrayToString(result);
197
+   int idPos = StringFind(createResponse, "\"id\":");
198
+   if(idPos < 0) return -1;
199
+
200
+   int startPos = idPos + 5;
201
+   int endPos = StringFind(createResponse, ",", startPos);
202
+   if(endPos < 0) endPos = StringFind(createResponse, "}", startPos);
203
+
204
+   string idStr = StringSubstr(createResponse, startPos, endPos - startPos);
205
+   return (int)StringToInteger(idStr);
297 206
 }
298 207
 
299 208
 //+------------------------------------------------------------------+
300
-//| Get symbol ID from map                                           |
301 209
 //+------------------------------------------------------------------+
302
-int GetSymbolId(string symbol)
210
+//| Send all historical candles to the API (Fixed Version)           |
211
+//+------------------------------------------------------------------+
212
+void SendAllHistoricalCandles()
303 213
 {
214
+   Print("Starting historical upload for ", ArraySize(symbols), " symbols...");
215
+
304 216
    for(int i = 0; i < ArraySize(symbols); i++)
305 217
    {
306
-      if(symbols[i] == symbol)
218
+      string sym = symbols[i];
219
+
220
+      // --- Ensure data is ready ---
221
+      Sleep(300);
222
+      int tries = 0;
223
+      while(!SeriesInfoInteger(sym, HistoricalTimeframe, SERIES_SYNCHRONIZED) && tries < 10)
224
+      {
225
+         Print("⏳ Waiting for ", sym, " history to load...");
226
+         Sleep(500);
227
+         tries++;
228
+      }
229
+
230
+      // --- Now copy candles ---
231
+      MqlRates rates[];
232
+      ResetLastError();
233
+      int copied = CopyRates(sym, HistoricalTimeframe, 0, HistoricalCandleCount, rates);
234
+      int err = GetLastError();
235
+
236
+      if(copied <= 0)
237
+      {
238
+         Print("⚠️ Failed to copy candles for ", sym, " (copied=", copied, ", err=", err, ")");
239
+         continue;
240
+      }
241
+
242
+      Print("✅ Copied ", copied, " candles for ", sym);
243
+
244
+      // --- Print a few sample candles ---
245
+      int sampleCount = MathMin(5, copied); // show up to 5 examples
246
+      for(int j = 0; j < sampleCount; j++)
307 247
       {
308
-         return symbolIdMap[i];
248
+         MqlRates r = rates[j];
249
+         string openTime = TimeToString(r.time, TIME_DATE|TIME_SECONDS);
250
+         string closeTime = TimeToString(r.time + PeriodSeconds(HistoricalTimeframe), TIME_DATE|TIME_SECONDS);
251
+         PrintFormat(
252
+            "🕒 [%s] %s → %s | O=%.5f H=%.5f L=%.5f C=%.5f | Vol=%.2f",
253
+            sym, openTime, closeTime, r.open, r.high, r.low, r.close, r.tick_volume
254
+         );
309 255
       }
256
+
257
+      // --- Send candles in batches to API ---
258
+      int sentTotal = 0;
259
+      int batchSize = 200;
260
+      for(int start = 0; start < copied; start += batchSize)
261
+      {
262
+         int size = MathMin(batchSize, copied - start);
263
+         int symbolId = symbolIds[i];
264
+         if(symbolId <= 0) continue;
265
+
266
+         string json = BuildCandleJSONFromRates(symbolId, rates, start, size);
267
+         string url = ApiBaseUrl + "/api/candles/bulk";
268
+         string response;
269
+         bool ok = SendJSON(url, json, response);
270
+         if(!ok)
271
+         {
272
+            Print("❌ Failed to send candle batch for ", sym, " start=", start);
273
+            break;
274
+         }
275
+         sentTotal += size;
276
+         Print("📤 Sent candles for ", sym, ": ", sentTotal, "/", copied);
277
+      }
278
+   }
279
+   Print("✅ Historical upload finished.");
280
+}
281
+
282
+
283
+//+------------------------------------------------------------------+
284
+void SendLivePrices()
285
+{
286
+   bool firstItem = true;
287
+   string json = "{\"prices\":[";
288
+
289
+   int sentCount = 0;
290
+   for(int i = 0; i < ArraySize(symbols); i++)
291
+   {
292
+      string sym = symbols[i];
293
+      double bid = SymbolInfoDouble(sym, SYMBOL_BID);
294
+      double ask = SymbolInfoDouble(sym, SYMBOL_ASK);
295
+      double last = SymbolInfoDouble(sym, SYMBOL_LAST);
296
+      if(bid <= 0 || ask <= 0 || last <= 0) continue;
297
+
298
+      int symId = symbolIds[i];
299
+      if(symId <= 0) continue;
300
+
301
+      string item = StringFormat("{\"symbolId\":%d,\"price\":%.8f,\"bid\":%.8f,\"ask\":%.8f,\"bidSize\":%.8f,\"askSize\":%.8f}",
302
+                                 symId, last, bid, ask, 0.0, 0.0);
303
+      if(!firstItem) json += ",";
304
+      json += item;
305
+      firstItem = false;
306
+      sentCount++;
307
+   }
308
+
309
+   json += "]}";
310
+
311
+   if(sentCount == 0)
312
+   {
313
+      Print("No valid live prices to send right now.");
314
+      return;
315
+   }
316
+
317
+   string url = ApiBaseUrl + "/api/live-prices/bulk";
318
+   string response;
319
+   bool ok = SendJSON(url, json, response);
320
+   if(ok)
321
+      Print("✅ Sent ", sentCount, " live prices.");
322
+   else
323
+      Print("❌ Failed to send live prices (sent ", sentCount, " items). Response: ", response);
324
+}
325
+
326
+//+------------------------------------------------------------------+
327
+string ToISO8601(datetime t)
328
+{
329
+   MqlDateTime st;
330
+   TimeToStruct(t, st);
331
+   return StringFormat("%04d-%02d-%02dT%02d:%02d:%02d.000Z", st.year, st.mon, st.day, st.hour, st.min, st.sec);
332
+}
333
+
334
+string BuildCandleJSONFromRates(int symbolId, MqlRates &rates[], int startIndex, int count)
335
+{
336
+   string json = "{\"candles\":[";
337
+   bool first = true;
338
+   int ratesSize = ArraySize(rates);
339
+
340
+   for(int i = startIndex; i < startIndex + count && i < ratesSize; i++)
341
+   {
342
+      MqlRates r = rates[i];
343
+      if(r.time <= 0) continue;
344
+
345
+      datetime open_dt  = (datetime)r.time;
346
+      datetime close_dt = (datetime)(r.time + (datetime)PeriodSeconds(HistoricalTimeframe));
347
+
348
+      string openTime  = ToISO8601(open_dt);
349
+      string closeTime = ToISO8601(close_dt);
350
+
351
+      double volume       = (r.tick_volume > 0 ? r.tick_volume : 1);
352
+      double quoteVolume  = (r.real_volume > 0 ? r.real_volume : volume);
353
+
354
+      string one = StringFormat(
355
+         "{\"symbolId\":%d,\"openTime\":\"%s\",\"closeTime\":\"%s\",\"open\":%.5f,\"high\":%.5f,\"low\":%.5f,\"close\":%.5f,\"volume\":%.5f,\"tradesCount\":%d,\"quoteVolume\":%.5f}",
356
+         symbolId, openTime, closeTime,
357
+         r.open, r.high, r.low, r.close,
358
+         volume, (int)volume, quoteVolume
359
+      );
360
+
361
+      if(!first) json += ",";
362
+      json += one;
363
+      first = false;
310 364
    }
311
-   return -1;
365
+
366
+   json += "]}";
367
+   return json;
312 368
 }
369
+
370
+//+------------------------------------------------------------------+
371
+bool SendJSON(string url, string json, string &response)
372
+{
373
+   ResetLastError();
374
+
375
+   char postData[];
376
+   StringToCharArray(json, postData, 0, CP_UTF8);
377
+
378
+   // ✅ Remove trailing null terminator
379
+   if(ArraySize(postData) > 0 && postData[ArraySize(postData) - 1] == 0)
380
+      ArrayResize(postData, ArraySize(postData) - 1);
381
+
382
+   if(ArraySize(postData) <= 0)
383
+   {
384
+      Print("❌ Empty postData for URL: ", url);
385
+      return false;
386
+   }
387
+
388
+   char result[];
389
+   string headers = "Content-Type: application/json\r\n";
390
+   string resultHeaders = "";
391
+   int timeout = 15000;
392
+
393
+   int res = WebRequest("POST", url, headers, timeout, postData, result, resultHeaders);
394
+
395
+   if(res == -1)
396
+   {
397
+      int err = GetLastError();
398
+      Print("WebRequest error: ", err, " url=", url);
399
+      return false;
400
+   }
401
+
402
+   response = CharArrayToString(result);
403
+   if(res == 200 || res == 201)
404
+      return true;
405
+
406
+   Print("HTTP status ", res, " response: ", response);
407
+   return false;
408
+}
409
+
313 410
 //+------------------------------------------------------------------+

+ 5 - 3
src/controllers/symbolController.js

@@ -8,15 +8,17 @@ class SymbolController {
8 8
       const {
9 9
         exchange,
10 10
         instrumentType,
11
-        isActive = true,
12
-        limit = 100,
11
+        isActive,
12
+        limit = 1000,
13 13
         offset = 0
14 14
       } = req.query;
15 15
 
16 16
       const where = {};
17 17
       if (exchange) where.exchange = exchange;
18 18
       if (instrumentType) where.instrumentType = instrumentType;
19
-      if (isActive !== undefined) where.isActive = isActive === 'true';
19
+      if (isActive !== undefined && isActive !== '') {
20
+        where.isActive = isActive === 'true' || isActive === true;
21
+      }
20 22
 
21 23
       const symbols = await Symbol.findAndCountAll({
22 24
         where,