Преглед на файлове

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 преди 3 месеца
родител
ревизия
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
 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,