Explorar el Código

Add multi-timeframe support for candles

Implement support for multiple timeframes (15m, 30m, 1h, 1D, 1W, 1M) in candle data. Rename Candle1h model to Candle and add timeframe field to database schema. Update API routes, controllers, and tests to handle timeframe parameter. Modify MT5 expert to send historical and live data for all supported timeframes. Add .env.example with configuration templates.
uzairrizwan1 hace 3 meses
padre
commit
893ec7bead

+ 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,