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

Merge branch 'feature/cherry-picked-to-master' of MQL-Development/market-data-service into master

muhammad.uzair месяцев назад: 3
Родитель
Сommit
4d743413fe

+ 20 - 0
.env.example

@@ -0,0 +1,20 @@
1
+# Database Configuration
2
+DB_TYPE=postgres
3
+DB_HOST=localhost
4
+DB_PORT=5432
5
+DB_NAME=financial_data
6
+DB_USER=postgres
7
+DB_PASSWORD=your_secure_password_here
8
+
9
+# Server Configuration
10
+PORT=3001
11
+NODE_ENV=development
12
+
13
+# JWT Configuration (if needed for authentication)
14
+JWT_SECRET=your_secure_jwt_secret_key_here
15
+
16
+# CORS Configuration
17
+CORS_ORIGIN=*
18
+
19
+# Logging
20
+LOG_LEVEL=debug

+ 79 - 75
MT5/Experts/MarketDataSender.mq5

@@ -16,6 +16,10 @@ string symbols[];
16 16
 int symbolIds[];
17 17
 datetime lastSend = 0;
18 18
 datetime lastCandleSync = 0;
19
+
20
+// --- Supported timeframes ---
21
+ENUM_TIMEFRAMES Timeframes[] = { PERIOD_M15, PERIOD_M30, PERIOD_H1, PERIOD_D1, PERIOD_W1, PERIOD_MN1 };
22
+string TimeframeStrings[]    = { "15m", "30m", "1h", "1D", "1W", "1M" };
19 23
 //+------------------------------------------------------------------+
20 24
 int OnInit()
21 25
 {
@@ -290,9 +294,9 @@ int CreateSymbolInDatabase(string symbolName)
290 294
 //+------------------------------------------------------------------+
291 295
 //| Fetch latest stored candle openTime from API                     |
292 296
 //+------------------------------------------------------------------+
293
-datetime GetLatestCandleTime(int symbolId)
297
+datetime GetLatestCandleTime(int symbolId, string timeframe)
294 298
 {
295
-   string url = ApiBaseUrl + "/api/candles/" + IntegerToString(symbolId) + "/latest";
299
+   string url = ApiBaseUrl + "/api/candles/" + IntegerToString(symbolId) + "/latest?timeframe=" + timeframe;
296 300
    string headers = "Content-Type: application/json\r\n";
297 301
    string resultHeaders = "";
298 302
    char result[];
@@ -303,7 +307,7 @@ datetime GetLatestCandleTime(int symbolId)
303 307
 
304 308
    if(res != 200)
305 309
    {
306
-      Print("⚠️ Could not fetch latest candle for symbolId=", symbolId, " (HTTP ", res, ")");
310
+      Print("⚠️ Could not fetch latest candle for symbolId=", symbolId, " timeframe=", timeframe, " (HTTP ", res, ")");
307 311
       return 0;
308 312
    }
309 313
 
@@ -311,7 +315,7 @@ datetime GetLatestCandleTime(int symbolId)
311 315
    int pos = StringFind(response, "\"openTime\":\"");
312 316
    if(pos < 0)
313 317
    {
314
-      Print("⚠️ No openTime found in response for symbolId=", symbolId);
318
+      Print("⚠️ No openTime found in response for symbolId=", symbolId, " timeframe=", timeframe);
315 319
       return 0;
316 320
    }
317 321
 
@@ -319,7 +323,6 @@ datetime GetLatestCandleTime(int symbolId)
319 323
    int end = StringFind(response, "\"", pos);
320 324
    string openTimeStr = StringSubstr(response, pos, end - pos);
321 325
 
322
-   // --- Parse ISO8601 to datetime ---
323 326
    int year  = (int)StringToInteger(StringSubstr(openTimeStr, 0, 4));
324 327
    int month = (int)StringToInteger(StringSubstr(openTimeStr, 5, 2));
325 328
    int day   = (int)StringToInteger(StringSubstr(openTimeStr, 8, 2));
@@ -332,7 +335,7 @@ datetime GetLatestCandleTime(int symbolId)
332 335
    t.hour = hour; t.min = min; t.sec = sec;
333 336
 
334 337
    datetime dt = StructToTime(t);
335
-   Print("🕓 Latest stored candle openTime for symbolId=", symbolId, " → ", TimeToString(dt, TIME_DATE|TIME_SECONDS));
338
+   PrintFormat("🕓 Latest stored candle for %s (symbolId=%d) = %s", timeframe, symbolId, TimeToString(dt, TIME_DATE|TIME_SECONDS));
336 339
    return dt;
337 340
 }
338 341
 
@@ -345,7 +348,7 @@ datetime GetLatestCandleTime(int symbolId)
345 348
 //+------------------------------------------------------------------+
346 349
 void SendAllHistoricalCandles()
347 350
 {
348
-   Print("Starting historical upload for ", ArraySize(symbols), " symbols...");
351
+   Print("Starting multi-timeframe historical upload for ", ArraySize(symbols), " symbols...");
349 352
 
350 353
    for(int i = 0; i < ArraySize(symbols); i++)
351 354
    {
@@ -353,90 +356,91 @@ void SendAllHistoricalCandles()
353 356
       int symbolId = symbolIds[i];
354 357
       if(symbolId <= 0) continue;
355 358
 
356
-      // --- Get last stored candle time ---
357
-      datetime latestApiTime = GetLatestCandleTime(symbolId);
359
+      // --- Loop through all timeframes ---
360
+      for(int tfIndex = 0; tfIndex < ArraySize(Timeframes); tfIndex++)
361
+      {
362
+         ENUM_TIMEFRAMES tf = Timeframes[tfIndex];
363
+         string tfStr = TimeframeStrings[tfIndex];
364
+         PrintFormat("📊 Processing %s timeframe for %s", tfStr, sym);
365
+
366
+         datetime latestApiTime = GetLatestCandleTime(symbolId, tfStr);
358 367
 
359
-      // --- Ensure history data is available ---
360
-      Sleep(300);
361
-      int tries = 0;
362
-      bool historyReady = false;
368
+         Sleep(300);
369
+         int tries = 0;
370
+         bool historyReady = false;
363 371
 
364
-      while(tries < 10)
365
-      {
366
-         if(SeriesInfoInteger(sym, HistoricalTimeframe, SERIES_SYNCHRONIZED))
372
+         while(tries < 10)
367 373
          {
368
-            historyReady = true;
369
-            break;
374
+            if(SeriesInfoInteger(sym, tf, SERIES_SYNCHRONIZED))
375
+            {
376
+               historyReady = true;
377
+               break;
378
+            }
379
+            PrintFormat("⏳ Waiting for %s (%s) history to load... (try %d/10)", sym, tfStr, tries + 1);
380
+            Sleep(500);
381
+            tries++;
370 382
          }
371
-         PrintFormat("⏳ Waiting for %s history to load... (try %d/10)", sym, tries + 1);
372
-         Sleep(500);
373
-         tries++;
374
-      }
375 383
 
376
-      if(!historyReady)
377
-      {
378
-         PrintFormat("⚠️ Skipping %s — history not loaded after 10 tries (~5s timeout).", sym);
379
-         continue;
380
-      }
384
+         if(!historyReady)
385
+         {
386
+            PrintFormat("⚠️ Skipping %s (%s) — history not loaded.", sym, tfStr);
387
+            continue;
388
+         }
381 389
 
382
-      // --- Copy rates ---
383
-      MqlRates rates[];
384
-      ResetLastError();
385
-      int copied = CopyRates(sym, HistoricalTimeframe, 0, HistoricalCandleCount, rates);
390
+         MqlRates rates[];
391
+         ResetLastError();
392
+         int copied = CopyRates(sym, tf, 0, HistoricalCandleCount, rates);
386 393
 
387
-      if(copied <= 0)
388
-      {
389
-         int err = GetLastError();
390
-         PrintFormat("⚠️ Failed to copy candles for %s (error %d)", sym, err);
391
-         continue;
392
-      }
394
+         if(copied <= 0)
395
+         {
396
+            int err = GetLastError();
397
+            PrintFormat("⚠️ Failed to copy %s candles (%s) (error %d)", sym, tfStr, err);
398
+            continue;
399
+         }
393 400
 
394
-      PrintFormat("✅ Copied %d candles for %s", copied, sym);
401
+         int startIndex = 0;
402
+         for(int j = 0; j < copied; j++)
403
+         {
404
+            if(rates[j].time > latestApiTime)
405
+            {
406
+               startIndex = j;
407
+               break;
408
+            }
409
+         }
395 410
 
396
-      // --- Filter new candles ---
397
-      int startIndex = 0;
398
-      for(int j = 0; j < copied; j++)
399
-      {
400
-         if(rates[j].time > latestApiTime)
411
+         int newCount = copied - startIndex;
412
+         if(newCount <= 0)
401 413
          {
402
-            startIndex = j;
403
-            break;
414
+            PrintFormat("ℹ️ No new %s candles for %s", tfStr, sym);
415
+            continue;
404 416
          }
405
-      }
406 417
 
407
-      int newCount = copied - startIndex;
408
-      if(newCount <= 0)
409
-      {
410
-         PrintFormat("ℹ️ No new candles to send for %s", sym);
411
-         continue;
412
-      }
418
+         PrintFormat("🆕 Sending %d new %s candles for %s", newCount, tfStr, sym);
413 419
 
414
-      PrintFormat("🆕 Sending %d new candles for %s after %s", newCount, sym, TimeToString(latestApiTime, TIME_DATE|TIME_SECONDS));
420
+         int batchSize = 200;
421
+         int sentTotal = 0;
415 422
 
416
-      // --- Send new candles in batches ---
417
-      int batchSize = 200;
418
-      int sentTotal = 0;
423
+         for(int start = startIndex; start < copied; start += batchSize)
424
+         {
425
+            int size = MathMin(batchSize, copied - start);
426
+            string json = BuildCandleJSONFromRates(symbolId, rates, start, size, tfStr, tf);
427
+            string url = ApiBaseUrl + "/api/candles/bulk";
428
+            string response;
419 429
 
420
-      for(int start = startIndex; start < copied; start += batchSize)
421
-      {
422
-         int size = MathMin(batchSize, copied - start);
423
-         string json = BuildCandleJSONFromRates(symbolId, rates, start, size);
424
-         string url = ApiBaseUrl + "/api/candles/bulk";
425
-         string response;
430
+            bool ok = SendJSON(url, json, response);
431
+            if(!ok)
432
+            {
433
+               PrintFormat("❌ Failed to send %s batch for %s (start=%d)", tfStr, sym, start);
434
+               break;
435
+            }
426 436
 
427
-         bool ok = SendJSON(url, json, response);
428
-         if(!ok)
429
-         {
430
-            PrintFormat("❌ Failed to send candle batch for %s (start=%d)", sym, start);
431
-            break;
437
+            sentTotal += size;
438
+            PrintFormat("📤 Sent %d/%d %s candles for %s", sentTotal, newCount, tfStr, sym);
432 439
          }
433
-
434
-         sentTotal += size;
435
-         PrintFormat("📤 Sent %d/%d new candles for %s", sentTotal, newCount, sym);
436 440
       }
437 441
    }
438 442
 
439
-   Print("✅ Incremental candle upload finished.");
443
+   Print("✅ Multi-timeframe candle upload finished.");
440 444
 }
441 445
 
442 446
 
@@ -583,7 +587,7 @@ string ToISO8601(datetime t)
583 587
    return StringFormat("%04d-%02d-%02dT%02d:%02d:%02d.000Z", st.year, st.mon, st.day, st.hour, st.min, st.sec);
584 588
 }
585 589
 
586
-string BuildCandleJSONFromRates(int symbolId, MqlRates &rates[], int startIndex, int count)
590
+string BuildCandleJSONFromRates(int symbolId, MqlRates &rates[], int startIndex, int count, string timeframe, ENUM_TIMEFRAMES tf)
587 591
 {
588 592
    string json = "{\"candles\":[";
589 593
    bool first = true;
@@ -595,7 +599,7 @@ string BuildCandleJSONFromRates(int symbolId, MqlRates &rates[], int startIndex,
595 599
       if(r.time <= 0) continue;
596 600
 
597 601
       datetime open_dt  = (datetime)r.time;
598
-      datetime close_dt = (datetime)(r.time + (datetime)PeriodSeconds(HistoricalTimeframe));
602
+      datetime close_dt = (datetime)(r.time + (datetime)PeriodSeconds(tf));
599 603
 
600 604
       string openTime  = ToISO8601(open_dt);
601 605
       string closeTime = ToISO8601(close_dt);
@@ -604,8 +608,8 @@ string BuildCandleJSONFromRates(int symbolId, MqlRates &rates[], int startIndex,
604 608
       double quoteVolume  = (r.real_volume > 0 ? r.real_volume : volume);
605 609
 
606 610
       string one = StringFormat(
607
-         "{\"symbolId\":%d,\"openTime\":\"%s\",\"closeTime\":\"%s\",\"open\":%.5f,\"high\":%.5f,\"low\":%.5f,\"close\":%.5f,\"volume\":%.5f,\"tradesCount\":%d,\"quoteVolume\":%.5f}",
608
-         symbolId, openTime, closeTime,
611
+         "{\"symbolId\":%d,\"timeframe\":\"%s\",\"openTime\":\"%s\",\"closeTime\":\"%s\",\"open\":%.5f,\"high\":%.5f,\"low\":%.5f,\"close\":%.5f,\"volume\":%.5f,\"tradesCount\":%d,\"quoteVolume\":%.5f}",
612
+         symbolId, timeframe, openTime, closeTime,
609 613
          r.open, r.high, r.low, r.close,
610 614
          volume, (int)volume, quoteVolume
611 615
       );

+ 1 - 1
README.md

@@ -86,7 +86,7 @@ market-data-service/
86 86
 │   │   └── validation.js            # Request validation
87 87
 │   ├── models/
88 88
 │   │   ├── Symbol.js                # Symbol model
89
-│   │   ├── Candle1h.js              # 1-hour candle model
89
+│   │   ├── Candle.js                # Multi-timeframe candle model
90 90
 │   │   ├── LivePrice.js             # Live price model
91 91
 │   │   └── index.js                 # Model associations
92 92
 │   ├── routes/

+ 2 - 2
migrations/20251027075914-add-index-to-instrument-type.js

@@ -3,8 +3,8 @@
3 3
 /** @type {import('sequelize-cli').Migration} */
4 4
 module.exports = {
5 5
   async up (queryInterface, Sequelize) {
6
-    // Drop the existing CHECK constraint and add a new one with 'index'
7
-    await queryInterface.sequelize.query("ALTER TABLE symbols DROP CONSTRAINT symbols_instrument_type_check;");
6
+    // Drop the existing CHECK constraint if it exists and add a new one with 'index'
7
+    await queryInterface.sequelize.query("ALTER TABLE symbols DROP CONSTRAINT IF EXISTS symbols_instrument_type_check;");
8 8
     await queryInterface.sequelize.query("ALTER TABLE symbols ADD CONSTRAINT symbols_instrument_type_check CHECK (instrument_type IN ('crypto', 'stock', 'forex', 'commodity', 'index'));");
9 9
   },
10 10
 

+ 69 - 0
migrations/20251112102032-add-timeframe-to-candles.js

@@ -0,0 +1,69 @@
1
+'use strict';
2
+
3
+/** @type {import('sequelize-cli').Migration} */
4
+module.exports = {
5
+  async up (queryInterface, Sequelize) {
6
+    // Rename table from candles_1h to candles
7
+    await queryInterface.renameTable('candles_1h', 'candles');
8
+
9
+    // Add timeframe column with enum
10
+    await queryInterface.addColumn('candles', 'timeframe', {
11
+      type: Sequelize.ENUM('15m', '30m', '1h', '1D', '1W', '1M'),
12
+      allowNull: false,
13
+      defaultValue: '1h'
14
+    });
15
+
16
+    // Update existing records to have '1h' timeframe
17
+    await queryInterface.sequelize.query('UPDATE candles SET timeframe = \'1h\' WHERE timeframe IS NULL');
18
+
19
+    // Remove the default value after setting existing records
20
+    await queryInterface.changeColumn('candles', 'timeframe', {
21
+      type: Sequelize.ENUM('15m', '30m', '1h', '1D', '1W', '1M'),
22
+      allowNull: false
23
+    });
24
+
25
+    // Drop the old unique constraint
26
+    await queryInterface.removeConstraint('candles', 'unique_symbol_open_time');
27
+
28
+    // Add new unique constraint including timeframe
29
+    await queryInterface.addConstraint('candles', {
30
+      fields: ['symbol_id', 'open_time', 'timeframe'],
31
+      type: 'unique',
32
+      name: 'unique_symbol_open_time_timeframe'
33
+    });
34
+
35
+    // Update indexes to include timeframe
36
+    await queryInterface.removeIndex('candles', 'idx_candles_open_time');
37
+    await queryInterface.addIndex('candles', ['open_time', 'timeframe'], {
38
+      name: 'idx_candles_open_time_timeframe'
39
+    });
40
+  },
41
+
42
+  async down (queryInterface, Sequelize) {
43
+    // Reverse the changes
44
+
45
+    // Remove new indexes
46
+    await queryInterface.removeIndex('candles', 'idx_candles_open_time_timeframe');
47
+
48
+    // Add back old index
49
+    await queryInterface.addIndex('candles', ['open_time'], {
50
+      name: 'idx_candles_open_time'
51
+    });
52
+
53
+    // Remove new constraint
54
+    await queryInterface.removeConstraint('candles', 'unique_symbol_open_time_timeframe');
55
+
56
+    // Add back old constraint
57
+    await queryInterface.addConstraint('candles', {
58
+      fields: ['symbol_id', 'open_time'],
59
+      type: 'unique',
60
+      name: 'unique_symbol_open_time'
61
+    });
62
+
63
+    // Remove timeframe column
64
+    await queryInterface.removeColumn('candles', 'timeframe');
65
+
66
+    // Rename table back to candles_1h
67
+    await queryInterface.renameTable('candles', 'candles_1h');
68
+  }
69
+};

+ 7 - 4
schema.sql

@@ -6,7 +6,7 @@ CREATE TABLE symbols (
6 6
     base_asset VARCHAR(50),
7 7
     quote_asset VARCHAR(50),
8 8
     exchange VARCHAR(50),
9
-    instrument_type VARCHAR(20) CHECK (instrument_type IN ('crypto', 'stock', 'forex', 'commodity')),
9
+    instrument_type VARCHAR(20) CHECK (instrument_type IN ('crypto', 'stock', 'forex', 'commodity', 'index')),
10 10
     is_active BOOLEAN DEFAULT TRUE,
11 11
     created_at TIMESTAMPTZ DEFAULT NOW(),
12 12
     updated_at TIMESTAMPTZ DEFAULT NOW()
@@ -15,11 +15,12 @@ CREATE TABLE symbols (
15 15
 CREATE INDEX idx_symbols_exchange ON symbols(exchange);
16 16
 CREATE INDEX idx_symbols_type ON symbols(instrument_type);
17 17
 
18
-CREATE TABLE candles_1h (
18
+-- candles table
19
+-- Stores multi-timeframe OHLCV data for each symbol
20
+CREATE TABLE candles (
19 21
     id BIGSERIAL PRIMARY KEY,
20 22
     symbol_id INT NOT NULL REFERENCES symbols(id) ON DELETE CASCADE,
23
+    timeframe ENUM('15m', '30m', '1h', '1D', '1W', '1M') NOT NULL DEFAULT '1h',
21 24
     open_time TIMESTAMPTZ NOT NULL,
22 25
     close_time TIMESTAMPTZ NOT NULL,
23 26
     open NUMERIC(18,8) NOT NULL,
@@ -33,8 +34,8 @@ CREATE TABLE candles_1h (
33 34
     updated_at TIMESTAMPTZ DEFAULT NOW()
34 35
 );
35 36
 
36
-CREATE UNIQUE INDEX idx_candles_symbol_time ON candles_1h(symbol_id, open_time);
37
-CREATE INDEX idx_candles_open_time ON candles_1h(open_time);
37
+CREATE UNIQUE INDEX idx_candles_symbol_time_timeframe ON candles(symbol_id, open_time, timeframe);
38
+CREATE INDEX idx_candles_open_time_timeframe ON candles(open_time, timeframe);
38 39
 
39 40
 -- live_prices table
40 41
 -- Stores the latest live market prices per symbol

+ 8 - 0
src/config/database.js

@@ -1,6 +1,14 @@
1 1
 const { Sequelize } = require('sequelize');
2 2
 require('dotenv').config();
3 3
 
4
+console.log('Database connection config:', {
5
+  database: process.env.DB_NAME,
6
+  username: process.env.DB_USER,
7
+  host: process.env.DB_HOST,
8
+  port: process.env.DB_PORT,
9
+  dialect: 'postgres'
10
+});
11
+
4 12
 const sequelize = new Sequelize(
5 13
   process.env.DB_NAME,
6 14
   process.env.DB_USER,

+ 58 - 40
src/controllers/candleController.js

@@ -1,4 +1,4 @@
1
-const { Candle1h, Symbol } = require('../models');
1
+const { Candle, Symbol } = require('../models');
2 2
 const { Op } = require('sequelize');
3 3
 
4 4
 class CandleController {
@@ -7,6 +7,7 @@ class CandleController {
7 7
     try {
8 8
       const {
9 9
         symbolId,
10
+        timeframe = '1h',
10 11
         startTime,
11 12
         endTime,
12 13
         limit = 100,
@@ -21,7 +22,10 @@ class CandleController {
21 22
         return next(error);
22 23
       }
23 24
 
24
-      const where = { symbolId: parseInt(symbolId) };
25
+      const where = {
26
+        symbolId: parseInt(symbolId),
27
+        timeframe: timeframe
28
+      };
25 29
 
26 30
       if (startTime) {
27 31
         where.openTime = {
@@ -37,7 +41,7 @@ class CandleController {
37 41
         };
38 42
       }
39 43
 
40
-      const candles = await Candle1h.findAndCountAll({
44
+      const candles = await Candle.findAndCountAll({
41 45
         where,
42 46
         limit: parseInt(limit),
43 47
         offset: parseInt(offset),
@@ -52,6 +56,7 @@ class CandleController {
52 56
       res.json({
53 57
         success: true,
54 58
         data: candles.rows,
59
+        timeframe,
55 60
         pagination: {
56 61
           total: candles.count,
57 62
           limit: parseInt(limit),
@@ -68,6 +73,7 @@ class CandleController {
68 73
   async getLatestCandle(req, res, next) {
69 74
     try {
70 75
       const { symbolId } = req.params;
76
+      const { timeframe = '1h' } = req.query;
71 77
 
72 78
       // Verify symbol exists
73 79
       const symbol = await Symbol.findByPk(symbolId);
@@ -77,8 +83,11 @@ class CandleController {
77 83
         return next(error);
78 84
       }
79 85
 
80
-      const candle = await Candle1h.findOne({
81
-        where: { symbolId: parseInt(symbolId) },
86
+      const candle = await Candle.findOne({
87
+        where: {
88
+          symbolId: parseInt(symbolId),
89
+          timeframe: timeframe
90
+        },
82 91
         order: [['openTime', 'DESC']],
83 92
         include: [{
84 93
           model: Symbol,
@@ -91,13 +100,14 @@ class CandleController {
91 100
         return res.json({
92 101
           success: true,
93 102
           data: null,
94
-          message: 'No candle data found for this symbol'
103
+          message: 'No candle data found for this symbol and timeframe'
95 104
         });
96 105
       }
97 106
 
98 107
       res.json({
99 108
         success: true,
100
-        data: candle
109
+        data: candle,
110
+        timeframe
101 111
       });
102 112
     } catch (error) {
103 113
       next(error);
@@ -109,7 +119,8 @@ class CandleController {
109 119
     try {
110 120
       const candleData = {
111 121
         ...req.body,
112
-        symbolId: parseInt(req.body.symbolId)
122
+        symbolId: parseInt(req.body.symbolId),
123
+        timeframe: req.body.timeframe || '1h'
113 124
       };
114 125
 
115 126
       // Verify symbol exists
@@ -120,7 +131,7 @@ class CandleController {
120 131
         return next(error);
121 132
       }
122 133
 
123
-      const candle = await Candle1h.create(candleData);
134
+      const candle = await Candle.create(candleData);
124 135
 
125 136
       // Emit WebSocket event for real-time updates
126 137
       const io = req.app.get('io');
@@ -128,6 +139,7 @@ class CandleController {
128 139
         const eventData = {
129 140
           symbol: symbol.symbol,
130 141
           symbolId: symbol.id,
142
+          timeframe: candle.timeframe,
131 143
           openTime: candle.openTime,
132 144
           open: candle.open,
133 145
           high: candle.high,
@@ -188,24 +200,25 @@ class CandleController {
188 200
       }
189 201
 
190 202
       // Check for existing candles to identify duplicates
191
-      const existingCandles = await Candle1h.findAll({
203
+      const existingCandles = await Candle.findAll({
192 204
         where: {
193 205
           [Op.or]: processedCandles.map(candle => ({
194 206
             symbolId: candle.symbolId,
195
-            openTime: candle.openTime
207
+            openTime: candle.openTime,
208
+            timeframe: candle.timeframe || '1h'
196 209
           }))
197 210
         },
198
-        attributes: ['symbolId', 'openTime']
211
+        attributes: ['symbolId', 'openTime', 'timeframe']
199 212
       });
200 213
 
201 214
       // Create a set of existing keys for quick lookup
202 215
       const existingKeys = new Set(
203
-        existingCandles.map(c => `${c.symbolId}-${c.openTime.toISOString()}`)
216
+        existingCandles.map(c => `${c.symbolId}-${c.openTime.toISOString()}-${c.timeframe}`)
204 217
       );
205 218
 
206 219
       // Filter out duplicates
207 220
       const newCandles = processedCandles.filter(candle =>
208
-        !existingKeys.has(`${candle.symbolId}-${candle.openTime.toISOString()}`)
221
+        !existingKeys.has(`${candle.symbolId}-${candle.openTime.toISOString()}-${candle.timeframe || '1h'}`)
209 222
       );
210 223
 
211 224
       const duplicateCount = processedCandles.length - newCandles.length;
@@ -215,7 +228,7 @@ class CandleController {
215 228
         console.log(`Bulk create candles: ${duplicateCount} duplicates skipped`);
216 229
       }
217 230
 
218
-      const createdCandles = await Candle1h.bulkCreate(newCandles);
231
+      const createdCandles = await Candle.bulkCreate(newCandles);
219 232
 
220 233
       // Emit WebSocket events for real-time updates
221 234
       const io = req.app.get('io');
@@ -268,10 +281,10 @@ class CandleController {
268 281
     }
269 282
   }
270 283
 
271
-  // Get OHLC data aggregated by time period
284
+  // Get OHLC data aggregated by timeframe
272 285
   async getOHLC(req, res, next) {
273 286
     try {
274
-      const { symbolId, period = '1h', limit = 100 } = req.query;
287
+      const { symbolId, timeframe = '1h', limit = 100 } = req.query;
275 288
 
276 289
       // Verify symbol exists
277 290
       const symbol = await Symbol.findByPk(symbolId);
@@ -281,15 +294,11 @@ class CandleController {
281 294
         return next(error);
282 295
       }
283 296
 
284
-      // For now, only support 1h period since we only have candles_1h table
285
-      if (period !== '1h') {
286
-        const error = new Error('Only 1h period is currently supported');
287
-        error.statusCode = 400;
288
-        return next(error);
289
-      }
290
-
291
-      const candles = await Candle1h.findAll({
292
-        where: { symbolId: parseInt(symbolId) },
297
+      const candles = await Candle.findAll({
298
+        where: {
299
+          symbolId: parseInt(symbolId),
300
+          timeframe: timeframe
301
+        },
293 302
         limit: parseInt(limit),
294 303
         order: [['openTime', 'DESC']],
295 304
         attributes: ['openTime', 'open', 'high', 'low', 'close', 'volume']
@@ -298,7 +307,7 @@ class CandleController {
298 307
       res.json({
299 308
         success: true,
300 309
         data: candles,
301
-        period,
310
+        timeframe,
302 311
         symbol: symbol.symbol
303 312
       });
304 313
     } catch (error) {
@@ -306,11 +315,11 @@ class CandleController {
306 315
     }
307 316
   }
308 317
 
309
-  // Clean up old candles, keep latest N candles
318
+  // Clean up old candles, keep latest N candles for a specific timeframe
310 319
   async cleanupCandles(req, res, next) {
311 320
     try {
312 321
       const { symbolId } = req.params;
313
-      const { keep = 1000 } = req.query;
322
+      const { timeframe = '1h', keep = 1000 } = req.query;
314 323
 
315 324
       // Verify symbol exists
316 325
       const symbol = await Symbol.findByPk(symbolId);
@@ -320,22 +329,29 @@ class CandleController {
320 329
         return next(error);
321 330
       }
322 331
 
323
-      // Get total count of candles for this symbol
324
-      const totalCandles = await Candle1h.count({
325
-        where: { symbolId: parseInt(symbolId) }
332
+      // Get total count of candles for this symbol and timeframe
333
+      const totalCandles = await Candle.count({
334
+        where: {
335
+          symbolId: parseInt(symbolId),
336
+          timeframe: timeframe
337
+        }
326 338
       });
327 339
 
328 340
       if (totalCandles <= keep) {
329 341
         return res.json({
330 342
           success: true,
331
-          message: `No cleanup needed. Only ${totalCandles} candles exist (keep: ${keep})`,
332
-          deletedCount: 0
343
+          message: `No cleanup needed. Only ${totalCandles} candles exist for timeframe ${timeframe} (keep: ${keep})`,
344
+          deletedCount: 0,
345
+          timeframe
333 346
         });
334 347
       }
335 348
 
336
-      // Get the IDs of candles to keep (latest N candles)
337
-      const candlesToKeep = await Candle1h.findAll({
338
-        where: { symbolId: parseInt(symbolId) },
349
+      // Get the IDs of candles to keep (latest N candles for this timeframe)
350
+      const candlesToKeep = await Candle.findAll({
351
+        where: {
352
+          symbolId: parseInt(symbolId),
353
+          timeframe: timeframe
354
+        },
339 355
         order: [['openTime', 'DESC']],
340 356
         limit: parseInt(keep),
341 357
         attributes: ['id']
@@ -344,9 +360,10 @@ class CandleController {
344 360
       const keepIds = candlesToKeep.map(candle => candle.id);
345 361
 
346 362
       // Delete older candles (those not in keepIds)
347
-      const deletedCount = await Candle1h.destroy({
363
+      const deletedCount = await Candle.destroy({
348 364
         where: {
349 365
           symbolId: parseInt(symbolId),
366
+          timeframe: timeframe,
350 367
           id: {
351 368
             [Op.notIn]: keepIds
352 369
           }
@@ -355,10 +372,11 @@ class CandleController {
355 372
 
356 373
       res.json({
357 374
         success: true,
358
-        message: `Cleanup completed. Deleted ${deletedCount} old candles, kept ${keepIds.length} latest candles`,
375
+        message: `Cleanup completed. Deleted ${deletedCount} old candles for timeframe ${timeframe}, kept ${keepIds.length} latest candles`,
359 376
         deletedCount,
360 377
         keptCount: keepIds.length,
361
-        symbol: symbol.symbol
378
+        symbol: symbol.symbol,
379
+        timeframe
362 380
       });
363 381
     } catch (error) {
364 382
       next(error);

+ 11 - 0
src/controllers/livePriceController.js

@@ -17,6 +17,16 @@ class LivePriceController {
17 17
         }]
18 18
       });
19 19
 
20
+      console.log(`getAllLivePrices: Returning ${livePrices.rows.length} records out of ${livePrices.count} total`);
21
+      if (livePrices.rows.length > 0) {
22
+        console.log('First record sample:', {
23
+          symbolId: livePrices.rows[0].symbolId,
24
+          price: livePrices.rows[0].price,
25
+          lastUpdated: livePrices.rows[0].lastUpdated,
26
+          symbol: livePrices.rows[0].livePriceSymbol?.symbol
27
+        });
28
+      }
29
+
20 30
       res.json({
21 31
         success: true,
22 32
         data: livePrices.rows,
@@ -28,6 +38,7 @@ class LivePriceController {
28 38
         }
29 39
       });
30 40
     } catch (error) {
41
+      console.error('getAllLivePrices error:', error);
31 42
       next(error);
32 43
     }
33 44
   }

+ 20 - 5
src/models/Candle1h.js

@@ -2,7 +2,7 @@ const { DataTypes } = require('sequelize');
2 2
 const { sequelize } = require('../config/database');
3 3
 const Symbol = require('./Symbol');
4 4
 
5
-const Candle1h = sequelize.define('Candle1h', {
5
+const Candle = sequelize.define('Candle', {
6 6
   id: {
7 7
     type: DataTypes.BIGINT,
8 8
     primaryKey: true,
@@ -17,6 +17,11 @@ const Candle1h = sequelize.define('Candle1h', {
17 17
       key: 'id'
18 18
     }
19 19
   },
20
+  timeframe: {
21
+    type: DataTypes.ENUM('15m', '30m', '1h', '1D', '1W', '1M'),
22
+    allowNull: false,
23
+    defaultValue: '1h'
24
+  },
20 25
   openTime: {
21 26
     type: DataTypes.DATE,
22 27
     field: 'open_time',
@@ -59,12 +64,22 @@ const Candle1h = sequelize.define('Candle1h', {
59 64
     field: 'created_at'
60 65
   }
61 66
 }, {
62
-  tableName: 'candles_1h',
67
+  tableName: 'candles',
63 68
   indexes: [
64
-    { unique: true, fields: ['symbol_id', 'open_time'] },
65
-    { fields: ['open_time'] }
69
+    { unique: true, fields: ['symbol_id', 'open_time', 'timeframe'] },
70
+    { fields: ['open_time', 'timeframe'] }
66 71
   ]
67 72
 });
68 73
 
74
+// Define associations
75
+Candle.belongsTo(Symbol, {
76
+  foreignKey: 'symbolId',
77
+  as: 'symbol'
78
+});
79
+
80
+Symbol.hasMany(Candle, {
81
+  foreignKey: 'symbolId',
82
+  as: 'candles'
83
+});
69 84
 
70
-module.exports = Candle1h;
85
+module.exports = Candle;

+ 2 - 5
src/models/index.js

@@ -1,12 +1,9 @@
1 1
 const { sequelize } = require('../config/database');
2 2
 const Symbol = require('./Symbol');
3
-const Candle1h = require('./Candle1h');
3
+const Candle = require('./Candle');
4 4
 const LivePrice = require('./LivePrice');
5 5
 
6 6
 // Define associations
7
-Symbol.hasMany(Candle1h, { foreignKey: 'symbolId', as: 'candles1h' });
8
-Candle1h.belongsTo(Symbol, { foreignKey: 'symbolId', as: 'symbol' });
9
-
10 7
 Symbol.hasOne(LivePrice, { foreignKey: 'symbolId', as: 'livePrice' });
11 8
 LivePrice.belongsTo(Symbol, { foreignKey: 'symbolId', as: 'livePriceSymbol' });
12 9
 
@@ -24,6 +21,6 @@ if (process.env.NODE_ENV === 'development') {
24 21
 module.exports = {
25 22
   sequelize,
26 23
   Symbol,
27
-  Candle1h,
24
+  Candle,
28 25
   LivePrice
29 26
 };

+ 7 - 1
src/routes/candles.js

@@ -7,6 +7,7 @@ const Joi = require('joi');
7 7
 // GET /api/candles - Get candles with filtering
8 8
 router.get('/', validateQuery(Joi.object({
9 9
   symbolId: Joi.number().integer().positive().required(),
10
+  timeframe: Joi.string().valid('15m', '30m', '1h', '1D', '1W', '1M').default('1h'),
10 11
   startTime: Joi.date().iso(),
11 12
   endTime: Joi.date().iso().when('startTime', {
12 13
     is: Joi.exist(),
@@ -19,18 +20,21 @@ router.get('/', validateQuery(Joi.object({
19 20
 // GET /api/candles/ohlc - Get OHLC data
20 21
 router.get('/ohlc', validateQuery(Joi.object({
21 22
   symbolId: Joi.number().integer().positive().required(),
22
-  period: Joi.string().valid('1h').default('1h'),
23
+  timeframe: Joi.string().valid('15m', '30m', '1h', '1D', '1W', '1M').default('1h'),
23 24
   limit: Joi.number().integer().min(1).max(1000).default(100)
24 25
 })), candleController.getOHLC);
25 26
 
26 27
 // GET /api/candles/:symbolId/latest - Get latest candle for a symbol
27 28
 router.get('/:symbolId/latest', validateParams(Joi.object({
28 29
   symbolId: Joi.number().integer().positive().required()
30
+})), validateQuery(Joi.object({
31
+  timeframe: Joi.string().valid('15m', '30m', '1h', '1D', '1W', '1M').default('1h')
29 32
 })), candleController.getLatestCandle);
30 33
 
31 34
 // POST /api/candles - Create new candle
32 35
 router.post('/', validate(Joi.object({
33 36
   symbolId: Joi.number().integer().positive().required(),
37
+  timeframe: Joi.string().valid('15m', '30m', '1h', '1D', '1W', '1M').default('1h'),
34 38
   openTime: Joi.date().iso().required(),
35 39
   closeTime: Joi.date().iso().required(),
36 40
   open: Joi.number().precision(8).positive().required(),
@@ -46,6 +50,7 @@ router.post('/', validate(Joi.object({
46 50
 router.post('/bulk', validate(Joi.object({
47 51
   candles: Joi.array().items(Joi.object({
48 52
     symbolId: Joi.number().integer().positive().required(),
53
+    timeframe: Joi.string().valid('15m', '30m', '1h', '1D', '1W', '1M').default('1h'),
49 54
     openTime: Joi.date().iso().required(),
50 55
     closeTime: Joi.date().iso().required(),
51 56
     open: Joi.number().precision(8).positive().required(),
@@ -62,6 +67,7 @@ router.post('/bulk', validate(Joi.object({
62 67
 router.delete('/cleanup/:symbolId', validateParams(Joi.object({
63 68
   symbolId: Joi.number().integer().positive().required()
64 69
 })), validateQuery(Joi.object({
70
+  timeframe: Joi.string().valid('15m', '30m', '1h', '1D', '1W', '1M').default('1h'),
65 71
   keep: Joi.number().integer().min(1).default(1000)
66 72
 })), candleController.cleanupCandles);
67 73
 

+ 13 - 10
tests/candleController.test.js

@@ -1,6 +1,6 @@
1 1
 const request = require('supertest');
2 2
 const app = require('../src/app');
3
-const { Candle1h, Symbol, sequelize } = require('../src/models');
3
+const { Candle, Symbol, sequelize } = require('../src/models');
4 4
 
5 5
 describe('Candle Controller Integration Tests', () => {
6 6
   let testSymbol;
@@ -19,7 +19,7 @@ describe('Candle Controller Integration Tests', () => {
19 19
 
20 20
   afterAll(async () => {
21 21
     // Cleanup test data
22
-    await Candle1h.destroy({ where: {} });
22
+    await Candle.destroy({ where: {} });
23 23
     await Symbol.destroy({ where: {} });
24 24
     await sequelize.close();
25 25
   });
@@ -29,8 +29,9 @@ describe('Candle Controller Integration Tests', () => {
29 29
       const mockCandles = [
30 30
         {
31 31
           symbolId: testSymbol.id,
32
-          openTime: '2025-10-17 00:00:00',
33
-          closeTime: '2025-10-17 01:00:00',
32
+          timeframe: '1h',
33
+          openTime: '2025-10-17T00:00:00.000Z',
34
+          closeTime: '2025-10-17T01:00:00.000Z',
34 35
           open: 1.1000,
35 36
           high: 1.1050,
36 37
           low: 1.0990,
@@ -39,8 +40,9 @@ describe('Candle Controller Integration Tests', () => {
39 40
         },
40 41
         {
41 42
           symbolId: testSymbol.id,
42
-          openTime: '2025-10-17 01:00:00',
43
-          closeTime: '2025-10-17 02:00:00',
43
+          timeframe: '1h',
44
+          openTime: '2025-10-17T01:00:00.000Z',
45
+          closeTime: '2025-10-17T02:00:00.000Z',
44 46
           open: 1.1025,
45 47
           high: 1.1075,
46 48
           low: 1.1005,
@@ -59,8 +61,8 @@ describe('Candle Controller Integration Tests', () => {
59 61
       expect(response.body.data.length).toBe(2);
60 62
 
61 63
       // Verify database persistence
62
-      const dbCandles = await Candle1h.findAll({
63
-        where: { symbolId: testSymbol.id },
64
+      const dbCandles = await Candle.findAll({
65
+        where: { symbolId: testSymbol.id, timeframe: '1h' },
64 66
         order: [['openTime', 'ASC']]
65 67
       });
66 68
 
@@ -82,8 +84,9 @@ describe('Candle Controller Integration Tests', () => {
82 84
     it('should handle invalid symbol IDs', async () => {
83 85
       const invalidCandles = [{
84 86
         symbolId: 999,
85
-        openTime: '2025-10-17 00:00:00',
86
-        closeTime: '2025-10-17 01:00:00',
87
+        timeframe: '1h',
88
+        openTime: '2025-10-17T00:00:00.000Z',
89
+        closeTime: '2025-10-17T01:00:00.000Z',
87 90
         open: 1.1000,
88 91
         high: 1.1050,
89 92
         low: 1.0990,