#6 Add multi-timeframe support for candles

Спојено
muhammad.uzair споји(ла) 1 комит(е) из MQL-Development/feature/cherry-picked-to-master у MQL-Development/master пре 3 месеци

+ 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
 int symbolIds[];
16
 int symbolIds[];
17
 datetime lastSend = 0;
17
 datetime lastSend = 0;
18
 datetime lastCandleSync = 0;
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
 int OnInit()
24
 int OnInit()
21
 {
25
 {
@@ -290,9 +294,9 @@ int CreateSymbolInDatabase(string symbolName)
290
 //+------------------------------------------------------------------+
294
 //+------------------------------------------------------------------+
291
 //| Fetch latest stored candle openTime from API                     |
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
    string headers = "Content-Type: application/json\r\n";
300
    string headers = "Content-Type: application/json\r\n";
297
    string resultHeaders = "";
301
    string resultHeaders = "";
298
    char result[];
302
    char result[];
@@ -303,7 +307,7 @@ datetime GetLatestCandleTime(int symbolId)
303
 
307
 
304
    if(res != 200)
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
       return 0;
311
       return 0;
308
    }
312
    }
309
 
313
 
@@ -311,7 +315,7 @@ datetime GetLatestCandleTime(int symbolId)
311
    int pos = StringFind(response, "\"openTime\":\"");
315
    int pos = StringFind(response, "\"openTime\":\"");
312
    if(pos < 0)
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
       return 0;
319
       return 0;
316
    }
320
    }
317
 
321
 
@@ -319,7 +323,6 @@ datetime GetLatestCandleTime(int symbolId)
319
    int end = StringFind(response, "\"", pos);
323
    int end = StringFind(response, "\"", pos);
320
    string openTimeStr = StringSubstr(response, pos, end - pos);
324
    string openTimeStr = StringSubstr(response, pos, end - pos);
321
 
325
 
322
-   // --- Parse ISO8601 to datetime ---
323
    int year  = (int)StringToInteger(StringSubstr(openTimeStr, 0, 4));
326
    int year  = (int)StringToInteger(StringSubstr(openTimeStr, 0, 4));
324
    int month = (int)StringToInteger(StringSubstr(openTimeStr, 5, 2));
327
    int month = (int)StringToInteger(StringSubstr(openTimeStr, 5, 2));
325
    int day   = (int)StringToInteger(StringSubstr(openTimeStr, 8, 2));
328
    int day   = (int)StringToInteger(StringSubstr(openTimeStr, 8, 2));
@@ -332,7 +335,7 @@ datetime GetLatestCandleTime(int symbolId)
332
    t.hour = hour; t.min = min; t.sec = sec;
335
    t.hour = hour; t.min = min; t.sec = sec;
333
 
336
 
334
    datetime dt = StructToTime(t);
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
    return dt;
339
    return dt;
337
 }
340
 }
338
 
341
 
@@ -345,7 +348,7 @@ datetime GetLatestCandleTime(int symbolId)
345
 //+------------------------------------------------------------------+
348
 //+------------------------------------------------------------------+
346
 void SendAllHistoricalCandles()
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
    for(int i = 0; i < ArraySize(symbols); i++)
353
    for(int i = 0; i < ArraySize(symbols); i++)
351
    {
354
    {
@@ -353,90 +356,91 @@ void SendAllHistoricalCandles()
353
       int symbolId = symbolIds[i];
356
       int symbolId = symbolIds[i];
354
       if(symbolId <= 0) continue;
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
    return StringFormat("%04d-%02d-%02dT%02d:%02d:%02d.000Z", st.year, st.mon, st.day, st.hour, st.min, st.sec);
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
    string json = "{\"candles\":[";
592
    string json = "{\"candles\":[";
589
    bool first = true;
593
    bool first = true;
@@ -595,7 +599,7 @@ string BuildCandleJSONFromRates(int symbolId, MqlRates &rates[], int startIndex,
595
       if(r.time <= 0) continue;
599
       if(r.time <= 0) continue;
596
 
600
 
597
       datetime open_dt  = (datetime)r.time;
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
       string openTime  = ToISO8601(open_dt);
604
       string openTime  = ToISO8601(open_dt);
601
       string closeTime = ToISO8601(close_dt);
605
       string closeTime = ToISO8601(close_dt);
@@ -604,8 +608,8 @@ string BuildCandleJSONFromRates(int symbolId, MqlRates &rates[], int startIndex,
604
       double quoteVolume  = (r.real_volume > 0 ? r.real_volume : volume);
608
       double quoteVolume  = (r.real_volume > 0 ? r.real_volume : volume);
605
 
609
 
606
       string one = StringFormat(
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
          r.open, r.high, r.low, r.close,
613
          r.open, r.high, r.low, r.close,
610
          volume, (int)volume, quoteVolume
614
          volume, (int)volume, quoteVolume
611
       );
615
       );

+ 1 - 1
README.md

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

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

@@ -3,8 +3,8 @@
3
 /** @type {import('sequelize-cli').Migration} */
3
 /** @type {import('sequelize-cli').Migration} */
4
 module.exports = {
4
 module.exports = {
5
   async up (queryInterface, Sequelize) {
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
     await queryInterface.sequelize.query("ALTER TABLE symbols ADD CONSTRAINT symbols_instrument_type_check CHECK (instrument_type IN ('crypto', 'stock', 'forex', 'commodity', 'index'));");
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
     base_asset VARCHAR(50),
6
     base_asset VARCHAR(50),
7
     quote_asset VARCHAR(50),
7
     quote_asset VARCHAR(50),
8
     exchange VARCHAR(50),
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
     is_active BOOLEAN DEFAULT TRUE,
10
     is_active BOOLEAN DEFAULT TRUE,
11
     created_at TIMESTAMPTZ DEFAULT NOW(),
11
     created_at TIMESTAMPTZ DEFAULT NOW(),
12
     updated_at TIMESTAMPTZ DEFAULT NOW()
12
     updated_at TIMESTAMPTZ DEFAULT NOW()
@@ -15,11 +15,12 @@ CREATE TABLE symbols (
15
 CREATE INDEX idx_symbols_exchange ON symbols(exchange);
15
 CREATE INDEX idx_symbols_exchange ON symbols(exchange);
16
 CREATE INDEX idx_symbols_type ON symbols(instrument_type);
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
     id BIGSERIAL PRIMARY KEY,
21
     id BIGSERIAL PRIMARY KEY,
20
     symbol_id INT NOT NULL REFERENCES symbols(id) ON DELETE CASCADE,
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
     open_time TIMESTAMPTZ NOT NULL,
24
     open_time TIMESTAMPTZ NOT NULL,
22
     close_time TIMESTAMPTZ NOT NULL,
25
     close_time TIMESTAMPTZ NOT NULL,
23
     open NUMERIC(18,8) NOT NULL,
26
     open NUMERIC(18,8) NOT NULL,
@@ -33,8 +34,8 @@ CREATE TABLE candles_1h (
33
     updated_at TIMESTAMPTZ DEFAULT NOW()
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
 -- live_prices table
40
 -- live_prices table
40
 -- Stores the latest live market prices per symbol
41
 -- Stores the latest live market prices per symbol

+ 8 - 0
src/config/database.js

@@ -1,6 +1,14 @@
1
 const { Sequelize } = require('sequelize');
1
 const { Sequelize } = require('sequelize');
2
 require('dotenv').config();
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
 const sequelize = new Sequelize(
12
 const sequelize = new Sequelize(
5
   process.env.DB_NAME,
13
   process.env.DB_NAME,
6
   process.env.DB_USER,
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
 const { Op } = require('sequelize');
2
 const { Op } = require('sequelize');
3
 
3
 
4
 class CandleController {
4
 class CandleController {
@@ -7,6 +7,7 @@ class CandleController {
7
     try {
7
     try {
8
       const {
8
       const {
9
         symbolId,
9
         symbolId,
10
+        timeframe = '1h',
10
         startTime,
11
         startTime,
11
         endTime,
12
         endTime,
12
         limit = 100,
13
         limit = 100,
@@ -21,7 +22,10 @@ class CandleController {
21
         return next(error);
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
       if (startTime) {
30
       if (startTime) {
27
         where.openTime = {
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
         where,
45
         where,
42
         limit: parseInt(limit),
46
         limit: parseInt(limit),
43
         offset: parseInt(offset),
47
         offset: parseInt(offset),
@@ -52,6 +56,7 @@ class CandleController {
52
       res.json({
56
       res.json({
53
         success: true,
57
         success: true,
54
         data: candles.rows,
58
         data: candles.rows,
59
+        timeframe,
55
         pagination: {
60
         pagination: {
56
           total: candles.count,
61
           total: candles.count,
57
           limit: parseInt(limit),
62
           limit: parseInt(limit),
@@ -68,6 +73,7 @@ class CandleController {
68
   async getLatestCandle(req, res, next) {
73
   async getLatestCandle(req, res, next) {
69
     try {
74
     try {
70
       const { symbolId } = req.params;
75
       const { symbolId } = req.params;
76
+      const { timeframe = '1h' } = req.query;
71
 
77
 
72
       // Verify symbol exists
78
       // Verify symbol exists
73
       const symbol = await Symbol.findByPk(symbolId);
79
       const symbol = await Symbol.findByPk(symbolId);
@@ -77,8 +83,11 @@ class CandleController {
77
         return next(error);
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
         order: [['openTime', 'DESC']],
91
         order: [['openTime', 'DESC']],
83
         include: [{
92
         include: [{
84
           model: Symbol,
93
           model: Symbol,
@@ -91,13 +100,14 @@ class CandleController {
91
         return res.json({
100
         return res.json({
92
           success: true,
101
           success: true,
93
           data: null,
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
       res.json({
107
       res.json({
99
         success: true,
108
         success: true,
100
-        data: candle
109
+        data: candle,
110
+        timeframe
101
       });
111
       });
102
     } catch (error) {
112
     } catch (error) {
103
       next(error);
113
       next(error);
@@ -109,7 +119,8 @@ class CandleController {
109
     try {
119
     try {
110
       const candleData = {
120
       const candleData = {
111
         ...req.body,
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
       // Verify symbol exists
126
       // Verify symbol exists
@@ -120,7 +131,7 @@ class CandleController {
120
         return next(error);
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
       // Emit WebSocket event for real-time updates
136
       // Emit WebSocket event for real-time updates
126
       const io = req.app.get('io');
137
       const io = req.app.get('io');
@@ -128,6 +139,7 @@ class CandleController {
128
         const eventData = {
139
         const eventData = {
129
           symbol: symbol.symbol,
140
           symbol: symbol.symbol,
130
           symbolId: symbol.id,
141
           symbolId: symbol.id,
142
+          timeframe: candle.timeframe,
131
           openTime: candle.openTime,
143
           openTime: candle.openTime,
132
           open: candle.open,
144
           open: candle.open,
133
           high: candle.high,
145
           high: candle.high,
@@ -188,24 +200,25 @@ class CandleController {
188
       }
200
       }
189
 
201
 
190
       // Check for existing candles to identify duplicates
202
       // Check for existing candles to identify duplicates
191
-      const existingCandles = await Candle1h.findAll({
203
+      const existingCandles = await Candle.findAll({
192
         where: {
204
         where: {
193
           [Op.or]: processedCandles.map(candle => ({
205
           [Op.or]: processedCandles.map(candle => ({
194
             symbolId: candle.symbolId,
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
       // Create a set of existing keys for quick lookup
214
       // Create a set of existing keys for quick lookup
202
       const existingKeys = new Set(
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
       // Filter out duplicates
219
       // Filter out duplicates
207
       const newCandles = processedCandles.filter(candle =>
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
       const duplicateCount = processedCandles.length - newCandles.length;
224
       const duplicateCount = processedCandles.length - newCandles.length;
@@ -215,7 +228,7 @@ class CandleController {
215
         console.log(`Bulk create candles: ${duplicateCount} duplicates skipped`);
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
       // Emit WebSocket events for real-time updates
233
       // Emit WebSocket events for real-time updates
221
       const io = req.app.get('io');
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
   async getOHLC(req, res, next) {
285
   async getOHLC(req, res, next) {
273
     try {
286
     try {
274
-      const { symbolId, period = '1h', limit = 100 } = req.query;
287
+      const { symbolId, timeframe = '1h', limit = 100 } = req.query;
275
 
288
 
276
       // Verify symbol exists
289
       // Verify symbol exists
277
       const symbol = await Symbol.findByPk(symbolId);
290
       const symbol = await Symbol.findByPk(symbolId);
@@ -281,15 +294,11 @@ class CandleController {
281
         return next(error);
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
         limit: parseInt(limit),
302
         limit: parseInt(limit),
294
         order: [['openTime', 'DESC']],
303
         order: [['openTime', 'DESC']],
295
         attributes: ['openTime', 'open', 'high', 'low', 'close', 'volume']
304
         attributes: ['openTime', 'open', 'high', 'low', 'close', 'volume']
@@ -298,7 +307,7 @@ class CandleController {
298
       res.json({
307
       res.json({
299
         success: true,
308
         success: true,
300
         data: candles,
309
         data: candles,
301
-        period,
310
+        timeframe,
302
         symbol: symbol.symbol
311
         symbol: symbol.symbol
303
       });
312
       });
304
     } catch (error) {
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
   async cleanupCandles(req, res, next) {
319
   async cleanupCandles(req, res, next) {
311
     try {
320
     try {
312
       const { symbolId } = req.params;
321
       const { symbolId } = req.params;
313
-      const { keep = 1000 } = req.query;
322
+      const { timeframe = '1h', keep = 1000 } = req.query;
314
 
323
 
315
       // Verify symbol exists
324
       // Verify symbol exists
316
       const symbol = await Symbol.findByPk(symbolId);
325
       const symbol = await Symbol.findByPk(symbolId);
@@ -320,22 +329,29 @@ class CandleController {
320
         return next(error);
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
       if (totalCandles <= keep) {
340
       if (totalCandles <= keep) {
329
         return res.json({
341
         return res.json({
330
           success: true,
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
         order: [['openTime', 'DESC']],
355
         order: [['openTime', 'DESC']],
340
         limit: parseInt(keep),
356
         limit: parseInt(keep),
341
         attributes: ['id']
357
         attributes: ['id']
@@ -344,9 +360,10 @@ class CandleController {
344
       const keepIds = candlesToKeep.map(candle => candle.id);
360
       const keepIds = candlesToKeep.map(candle => candle.id);
345
 
361
 
346
       // Delete older candles (those not in keepIds)
362
       // Delete older candles (those not in keepIds)
347
-      const deletedCount = await Candle1h.destroy({
363
+      const deletedCount = await Candle.destroy({
348
         where: {
364
         where: {
349
           symbolId: parseInt(symbolId),
365
           symbolId: parseInt(symbolId),
366
+          timeframe: timeframe,
350
           id: {
367
           id: {
351
             [Op.notIn]: keepIds
368
             [Op.notIn]: keepIds
352
           }
369
           }
@@ -355,10 +372,11 @@ class CandleController {
355
 
372
 
356
       res.json({
373
       res.json({
357
         success: true,
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
         deletedCount,
376
         deletedCount,
360
         keptCount: keepIds.length,
377
         keptCount: keepIds.length,
361
-        symbol: symbol.symbol
378
+        symbol: symbol.symbol,
379
+        timeframe
362
       });
380
       });
363
     } catch (error) {
381
     } catch (error) {
364
       next(error);
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
       res.json({
30
       res.json({
21
         success: true,
31
         success: true,
22
         data: livePrices.rows,
32
         data: livePrices.rows,
@@ -28,6 +38,7 @@ class LivePriceController {
28
         }
38
         }
29
       });
39
       });
30
     } catch (error) {
40
     } catch (error) {
41
+      console.error('getAllLivePrices error:', error);
31
       next(error);
42
       next(error);
32
     }
43
     }
33
   }
44
   }

+ 20 - 5
src/models/Candle1h.js

@@ -2,7 +2,7 @@ const { DataTypes } = require('sequelize');
2
 const { sequelize } = require('../config/database');
2
 const { sequelize } = require('../config/database');
3
 const Symbol = require('./Symbol');
3
 const Symbol = require('./Symbol');
4
 
4
 
5
-const Candle1h = sequelize.define('Candle1h', {
5
+const Candle = sequelize.define('Candle', {
6
   id: {
6
   id: {
7
     type: DataTypes.BIGINT,
7
     type: DataTypes.BIGINT,
8
     primaryKey: true,
8
     primaryKey: true,
@@ -17,6 +17,11 @@ const Candle1h = sequelize.define('Candle1h', {
17
       key: 'id'
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
   openTime: {
25
   openTime: {
21
     type: DataTypes.DATE,
26
     type: DataTypes.DATE,
22
     field: 'open_time',
27
     field: 'open_time',
@@ -59,12 +64,22 @@ const Candle1h = sequelize.define('Candle1h', {
59
     field: 'created_at'
64
     field: 'created_at'
60
   }
65
   }
61
 }, {
66
 }, {
62
-  tableName: 'candles_1h',
67
+  tableName: 'candles',
63
   indexes: [
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
 const { sequelize } = require('../config/database');
1
 const { sequelize } = require('../config/database');
2
 const Symbol = require('./Symbol');
2
 const Symbol = require('./Symbol');
3
-const Candle1h = require('./Candle1h');
3
+const Candle = require('./Candle');
4
 const LivePrice = require('./LivePrice');
4
 const LivePrice = require('./LivePrice');
5
 
5
 
6
 // Define associations
6
 // Define associations
7
-Symbol.hasMany(Candle1h, { foreignKey: 'symbolId', as: 'candles1h' });
8
-Candle1h.belongsTo(Symbol, { foreignKey: 'symbolId', as: 'symbol' });
9
-
10
 Symbol.hasOne(LivePrice, { foreignKey: 'symbolId', as: 'livePrice' });
7
 Symbol.hasOne(LivePrice, { foreignKey: 'symbolId', as: 'livePrice' });
11
 LivePrice.belongsTo(Symbol, { foreignKey: 'symbolId', as: 'livePriceSymbol' });
8
 LivePrice.belongsTo(Symbol, { foreignKey: 'symbolId', as: 'livePriceSymbol' });
12
 
9
 
@@ -24,6 +21,6 @@ if (process.env.NODE_ENV === 'development') {
24
 module.exports = {
21
 module.exports = {
25
   sequelize,
22
   sequelize,
26
   Symbol,
23
   Symbol,
27
-  Candle1h,
24
+  Candle,
28
   LivePrice
25
   LivePrice
29
 };
26
 };

+ 7 - 1
src/routes/candles.js

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

+ 13 - 10
tests/candleController.test.js

@@ -1,6 +1,6 @@
1
 const request = require('supertest');
1
 const request = require('supertest');
2
 const app = require('../src/app');
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
 describe('Candle Controller Integration Tests', () => {
5
 describe('Candle Controller Integration Tests', () => {
6
   let testSymbol;
6
   let testSymbol;
@@ -19,7 +19,7 @@ describe('Candle Controller Integration Tests', () => {
19
 
19
 
20
   afterAll(async () => {
20
   afterAll(async () => {
21
     // Cleanup test data
21
     // Cleanup test data
22
-    await Candle1h.destroy({ where: {} });
22
+    await Candle.destroy({ where: {} });
23
     await Symbol.destroy({ where: {} });
23
     await Symbol.destroy({ where: {} });
24
     await sequelize.close();
24
     await sequelize.close();
25
   });
25
   });
@@ -29,8 +29,9 @@ describe('Candle Controller Integration Tests', () => {
29
       const mockCandles = [
29
       const mockCandles = [
30
         {
30
         {
31
           symbolId: testSymbol.id,
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
           open: 1.1000,
35
           open: 1.1000,
35
           high: 1.1050,
36
           high: 1.1050,
36
           low: 1.0990,
37
           low: 1.0990,
@@ -39,8 +40,9 @@ describe('Candle Controller Integration Tests', () => {
39
         },
40
         },
40
         {
41
         {
41
           symbolId: testSymbol.id,
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
           open: 1.1025,
46
           open: 1.1025,
45
           high: 1.1075,
47
           high: 1.1075,
46
           low: 1.1005,
48
           low: 1.1005,
@@ -59,8 +61,8 @@ describe('Candle Controller Integration Tests', () => {
59
       expect(response.body.data.length).toBe(2);
61
       expect(response.body.data.length).toBe(2);
60
 
62
 
61
       // Verify database persistence
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
         order: [['openTime', 'ASC']]
66
         order: [['openTime', 'ASC']]
65
       });
67
       });
66
 
68
 
@@ -82,8 +84,9 @@ describe('Candle Controller Integration Tests', () => {
82
     it('should handle invalid symbol IDs', async () => {
84
     it('should handle invalid symbol IDs', async () => {
83
       const invalidCandles = [{
85
       const invalidCandles = [{
84
         symbolId: 999,
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
         open: 1.1000,
90
         open: 1.1000,
88
         high: 1.1050,
91
         high: 1.1050,
89
         low: 1.0990,
92
         low: 1.0990,