Browse Source

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

muhammad.uzair 8 tháng trước cách đây
mục cha
commit
4d743413fe

+ 20 - 0
.env.example

@@ -0,0 +1,20 @@
+# Database Configuration
+DB_TYPE=postgres
+DB_HOST=localhost
+DB_PORT=5432
+DB_NAME=financial_data
+DB_USER=postgres
+DB_PASSWORD=your_secure_password_here
+
+# Server Configuration
+PORT=3001
+NODE_ENV=development
+
+# JWT Configuration (if needed for authentication)
+JWT_SECRET=your_secure_jwt_secret_key_here
+
+# CORS Configuration
+CORS_ORIGIN=*
+
+# Logging
+LOG_LEVEL=debug

+ 79 - 75
MT5/Experts/MarketDataSender.mq5

@@ -16,6 +16,10 @@ string symbols[];
 int symbolIds[];
 datetime lastSend = 0;
 datetime lastCandleSync = 0;
+
+// --- Supported timeframes ---
+ENUM_TIMEFRAMES Timeframes[] = { PERIOD_M15, PERIOD_M30, PERIOD_H1, PERIOD_D1, PERIOD_W1, PERIOD_MN1 };
+string TimeframeStrings[]    = { "15m", "30m", "1h", "1D", "1W", "1M" };
 //+------------------------------------------------------------------+
 int OnInit()
 {
@@ -290,9 +294,9 @@ int CreateSymbolInDatabase(string symbolName)
 //+------------------------------------------------------------------+
 //| Fetch latest stored candle openTime from API                     |
 //+------------------------------------------------------------------+
-datetime GetLatestCandleTime(int symbolId)
+datetime GetLatestCandleTime(int symbolId, string timeframe)
 {
-   string url = ApiBaseUrl + "/api/candles/" + IntegerToString(symbolId) + "/latest";
+   string url = ApiBaseUrl + "/api/candles/" + IntegerToString(symbolId) + "/latest?timeframe=" + timeframe;
    string headers = "Content-Type: application/json\r\n";
    string resultHeaders = "";
    char result[];
@@ -303,7 +307,7 @@ datetime GetLatestCandleTime(int symbolId)
 
    if(res != 200)
    {
-      Print("⚠️ Could not fetch latest candle for symbolId=", symbolId, " (HTTP ", res, ")");
+      Print("⚠️ Could not fetch latest candle for symbolId=", symbolId, " timeframe=", timeframe, " (HTTP ", res, ")");
       return 0;
    }
 
@@ -311,7 +315,7 @@ datetime GetLatestCandleTime(int symbolId)
    int pos = StringFind(response, "\"openTime\":\"");
    if(pos < 0)
    {
-      Print("⚠️ No openTime found in response for symbolId=", symbolId);
+      Print("⚠️ No openTime found in response for symbolId=", symbolId, " timeframe=", timeframe);
       return 0;
    }
 
@@ -319,7 +323,6 @@ datetime GetLatestCandleTime(int symbolId)
    int end = StringFind(response, "\"", pos);
    string openTimeStr = StringSubstr(response, pos, end - pos);
 
-   // --- Parse ISO8601 to datetime ---
    int year  = (int)StringToInteger(StringSubstr(openTimeStr, 0, 4));
    int month = (int)StringToInteger(StringSubstr(openTimeStr, 5, 2));
    int day   = (int)StringToInteger(StringSubstr(openTimeStr, 8, 2));
@@ -332,7 +335,7 @@ datetime GetLatestCandleTime(int symbolId)
    t.hour = hour; t.min = min; t.sec = sec;
 
    datetime dt = StructToTime(t);
-   Print("🕓 Latest stored candle openTime for symbolId=", symbolId, " → ", TimeToString(dt, TIME_DATE|TIME_SECONDS));
+   PrintFormat("🕓 Latest stored candle for %s (symbolId=%d) = %s", timeframe, symbolId, TimeToString(dt, TIME_DATE|TIME_SECONDS));
    return dt;
 }
 
@@ -345,7 +348,7 @@ datetime GetLatestCandleTime(int symbolId)
 //+------------------------------------------------------------------+
 void SendAllHistoricalCandles()
 {
-   Print("Starting historical upload for ", ArraySize(symbols), " symbols...");
+   Print("Starting multi-timeframe historical upload for ", ArraySize(symbols), " symbols...");
 
    for(int i = 0; i < ArraySize(symbols); i++)
    {
@@ -353,90 +356,91 @@ void SendAllHistoricalCandles()
       int symbolId = symbolIds[i];
       if(symbolId <= 0) continue;
 
-      // --- Get last stored candle time ---
-      datetime latestApiTime = GetLatestCandleTime(symbolId);
+      // --- Loop through all timeframes ---
+      for(int tfIndex = 0; tfIndex < ArraySize(Timeframes); tfIndex++)
+      {
+         ENUM_TIMEFRAMES tf = Timeframes[tfIndex];
+         string tfStr = TimeframeStrings[tfIndex];
+         PrintFormat("📊 Processing %s timeframe for %s", tfStr, sym);
+
+         datetime latestApiTime = GetLatestCandleTime(symbolId, tfStr);
 
-      // --- Ensure history data is available ---
-      Sleep(300);
-      int tries = 0;
-      bool historyReady = false;
+         Sleep(300);
+         int tries = 0;
+         bool historyReady = false;
 
-      while(tries < 10)
-      {
-         if(SeriesInfoInteger(sym, HistoricalTimeframe, SERIES_SYNCHRONIZED))
+         while(tries < 10)
          {
-            historyReady = true;
-            break;
+            if(SeriesInfoInteger(sym, tf, SERIES_SYNCHRONIZED))
+            {
+               historyReady = true;
+               break;
+            }
+            PrintFormat("⏳ Waiting for %s (%s) history to load... (try %d/10)", sym, tfStr, tries + 1);
+            Sleep(500);
+            tries++;
          }
-         PrintFormat("⏳ Waiting for %s history to load... (try %d/10)", sym, tries + 1);
-         Sleep(500);
-         tries++;
-      }
 
-      if(!historyReady)
-      {
-         PrintFormat("⚠️ Skipping %s — history not loaded after 10 tries (~5s timeout).", sym);
-         continue;
-      }
+         if(!historyReady)
+         {
+            PrintFormat("⚠️ Skipping %s (%s) — history not loaded.", sym, tfStr);
+            continue;
+         }
 
-      // --- Copy rates ---
-      MqlRates rates[];
-      ResetLastError();
-      int copied = CopyRates(sym, HistoricalTimeframe, 0, HistoricalCandleCount, rates);
+         MqlRates rates[];
+         ResetLastError();
+         int copied = CopyRates(sym, tf, 0, HistoricalCandleCount, rates);
 
-      if(copied <= 0)
-      {
-         int err = GetLastError();
-         PrintFormat("⚠️ Failed to copy candles for %s (error %d)", sym, err);
-         continue;
-      }
+         if(copied <= 0)
+         {
+            int err = GetLastError();
+            PrintFormat("⚠️ Failed to copy %s candles (%s) (error %d)", sym, tfStr, err);
+            continue;
+         }
 
-      PrintFormat("✅ Copied %d candles for %s", copied, sym);
+         int startIndex = 0;
+         for(int j = 0; j < copied; j++)
+         {
+            if(rates[j].time > latestApiTime)
+            {
+               startIndex = j;
+               break;
+            }
+         }
 
-      // --- Filter new candles ---
-      int startIndex = 0;
-      for(int j = 0; j < copied; j++)
-      {
-         if(rates[j].time > latestApiTime)
+         int newCount = copied - startIndex;
+         if(newCount <= 0)
          {
-            startIndex = j;
-            break;
+            PrintFormat("ℹ️ No new %s candles for %s", tfStr, sym);
+            continue;
          }
-      }
 
-      int newCount = copied - startIndex;
-      if(newCount <= 0)
-      {
-         PrintFormat("ℹ️ No new candles to send for %s", sym);
-         continue;
-      }
+         PrintFormat("🆕 Sending %d new %s candles for %s", newCount, tfStr, sym);
 
-      PrintFormat("🆕 Sending %d new candles for %s after %s", newCount, sym, TimeToString(latestApiTime, TIME_DATE|TIME_SECONDS));
+         int batchSize = 200;
+         int sentTotal = 0;
 
-      // --- Send new candles in batches ---
-      int batchSize = 200;
-      int sentTotal = 0;
+         for(int start = startIndex; start < copied; start += batchSize)
+         {
+            int size = MathMin(batchSize, copied - start);
+            string json = BuildCandleJSONFromRates(symbolId, rates, start, size, tfStr, tf);
+            string url = ApiBaseUrl + "/api/candles/bulk";
+            string response;
 
-      for(int start = startIndex; start < copied; start += batchSize)
-      {
-         int size = MathMin(batchSize, copied - start);
-         string json = BuildCandleJSONFromRates(symbolId, rates, start, size);
-         string url = ApiBaseUrl + "/api/candles/bulk";
-         string response;
+            bool ok = SendJSON(url, json, response);
+            if(!ok)
+            {
+               PrintFormat("❌ Failed to send %s batch for %s (start=%d)", tfStr, sym, start);
+               break;
+            }
 
-         bool ok = SendJSON(url, json, response);
-         if(!ok)
-         {
-            PrintFormat("❌ Failed to send candle batch for %s (start=%d)", sym, start);
-            break;
+            sentTotal += size;
+            PrintFormat("📤 Sent %d/%d %s candles for %s", sentTotal, newCount, tfStr, sym);
          }
-
-         sentTotal += size;
-         PrintFormat("📤 Sent %d/%d new candles for %s", sentTotal, newCount, sym);
       }
    }
 
-   Print("✅ Incremental candle upload finished.");
+   Print("✅ Multi-timeframe candle upload finished.");
 }
 
 
@@ -583,7 +587,7 @@ string ToISO8601(datetime t)
    return StringFormat("%04d-%02d-%02dT%02d:%02d:%02d.000Z", st.year, st.mon, st.day, st.hour, st.min, st.sec);
 }
 
-string BuildCandleJSONFromRates(int symbolId, MqlRates &rates[], int startIndex, int count)
+string BuildCandleJSONFromRates(int symbolId, MqlRates &rates[], int startIndex, int count, string timeframe, ENUM_TIMEFRAMES tf)
 {
    string json = "{\"candles\":[";
    bool first = true;
@@ -595,7 +599,7 @@ string BuildCandleJSONFromRates(int symbolId, MqlRates &rates[], int startIndex,
       if(r.time <= 0) continue;
 
       datetime open_dt  = (datetime)r.time;
-      datetime close_dt = (datetime)(r.time + (datetime)PeriodSeconds(HistoricalTimeframe));
+      datetime close_dt = (datetime)(r.time + (datetime)PeriodSeconds(tf));
 
       string openTime  = ToISO8601(open_dt);
       string closeTime = ToISO8601(close_dt);
@@ -604,8 +608,8 @@ string BuildCandleJSONFromRates(int symbolId, MqlRates &rates[], int startIndex,
       double quoteVolume  = (r.real_volume > 0 ? r.real_volume : volume);
 
       string one = StringFormat(
-         "{\"symbolId\":%d,\"openTime\":\"%s\",\"closeTime\":\"%s\",\"open\":%.5f,\"high\":%.5f,\"low\":%.5f,\"close\":%.5f,\"volume\":%.5f,\"tradesCount\":%d,\"quoteVolume\":%.5f}",
-         symbolId, openTime, closeTime,
+         "{\"symbolId\":%d,\"timeframe\":\"%s\",\"openTime\":\"%s\",\"closeTime\":\"%s\",\"open\":%.5f,\"high\":%.5f,\"low\":%.5f,\"close\":%.5f,\"volume\":%.5f,\"tradesCount\":%d,\"quoteVolume\":%.5f}",
+         symbolId, timeframe, openTime, closeTime,
          r.open, r.high, r.low, r.close,
          volume, (int)volume, quoteVolume
       );

+ 1 - 1
README.md

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

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

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

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

@@ -0,0 +1,69 @@
+'use strict';
+
+/** @type {import('sequelize-cli').Migration} */
+module.exports = {
+  async up (queryInterface, Sequelize) {
+    // Rename table from candles_1h to candles
+    await queryInterface.renameTable('candles_1h', 'candles');
+
+    // Add timeframe column with enum
+    await queryInterface.addColumn('candles', 'timeframe', {
+      type: Sequelize.ENUM('15m', '30m', '1h', '1D', '1W', '1M'),
+      allowNull: false,
+      defaultValue: '1h'
+    });
+
+    // Update existing records to have '1h' timeframe
+    await queryInterface.sequelize.query('UPDATE candles SET timeframe = \'1h\' WHERE timeframe IS NULL');
+
+    // Remove the default value after setting existing records
+    await queryInterface.changeColumn('candles', 'timeframe', {
+      type: Sequelize.ENUM('15m', '30m', '1h', '1D', '1W', '1M'),
+      allowNull: false
+    });
+
+    // Drop the old unique constraint
+    await queryInterface.removeConstraint('candles', 'unique_symbol_open_time');
+
+    // Add new unique constraint including timeframe
+    await queryInterface.addConstraint('candles', {
+      fields: ['symbol_id', 'open_time', 'timeframe'],
+      type: 'unique',
+      name: 'unique_symbol_open_time_timeframe'
+    });
+
+    // Update indexes to include timeframe
+    await queryInterface.removeIndex('candles', 'idx_candles_open_time');
+    await queryInterface.addIndex('candles', ['open_time', 'timeframe'], {
+      name: 'idx_candles_open_time_timeframe'
+    });
+  },
+
+  async down (queryInterface, Sequelize) {
+    // Reverse the changes
+
+    // Remove new indexes
+    await queryInterface.removeIndex('candles', 'idx_candles_open_time_timeframe');
+
+    // Add back old index
+    await queryInterface.addIndex('candles', ['open_time'], {
+      name: 'idx_candles_open_time'
+    });
+
+    // Remove new constraint
+    await queryInterface.removeConstraint('candles', 'unique_symbol_open_time_timeframe');
+
+    // Add back old constraint
+    await queryInterface.addConstraint('candles', {
+      fields: ['symbol_id', 'open_time'],
+      type: 'unique',
+      name: 'unique_symbol_open_time'
+    });
+
+    // Remove timeframe column
+    await queryInterface.removeColumn('candles', 'timeframe');
+
+    // Rename table back to candles_1h
+    await queryInterface.renameTable('candles', 'candles_1h');
+  }
+};

+ 7 - 6
schema.sql

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

+ 8 - 0
src/config/database.js

@@ -1,6 +1,14 @@
 const { Sequelize } = require('sequelize');
 require('dotenv').config();
 
+console.log('Database connection config:', {
+  database: process.env.DB_NAME,
+  username: process.env.DB_USER,
+  host: process.env.DB_HOST,
+  port: process.env.DB_PORT,
+  dialect: 'postgres'
+});
+
 const sequelize = new Sequelize(
   process.env.DB_NAME,
   process.env.DB_USER,

+ 58 - 40
src/controllers/candleController.js

@@ -1,4 +1,4 @@
-const { Candle1h, Symbol } = require('../models');
+const { Candle, Symbol } = require('../models');
 const { Op } = require('sequelize');
 
 class CandleController {
@@ -7,6 +7,7 @@ class CandleController {
     try {
       const {
         symbolId,
+        timeframe = '1h',
         startTime,
         endTime,
         limit = 100,
@@ -21,7 +22,10 @@ class CandleController {
         return next(error);
       }
 
-      const where = { symbolId: parseInt(symbolId) };
+      const where = {
+        symbolId: parseInt(symbolId),
+        timeframe: timeframe
+      };
 
       if (startTime) {
         where.openTime = {
@@ -37,7 +41,7 @@ class CandleController {
         };
       }
 
-      const candles = await Candle1h.findAndCountAll({
+      const candles = await Candle.findAndCountAll({
         where,
         limit: parseInt(limit),
         offset: parseInt(offset),
@@ -52,6 +56,7 @@ class CandleController {
       res.json({
         success: true,
         data: candles.rows,
+        timeframe,
         pagination: {
           total: candles.count,
           limit: parseInt(limit),
@@ -68,6 +73,7 @@ class CandleController {
   async getLatestCandle(req, res, next) {
     try {
       const { symbolId } = req.params;
+      const { timeframe = '1h' } = req.query;
 
       // Verify symbol exists
       const symbol = await Symbol.findByPk(symbolId);
@@ -77,8 +83,11 @@ class CandleController {
         return next(error);
       }
 
-      const candle = await Candle1h.findOne({
-        where: { symbolId: parseInt(symbolId) },
+      const candle = await Candle.findOne({
+        where: {
+          symbolId: parseInt(symbolId),
+          timeframe: timeframe
+        },
         order: [['openTime', 'DESC']],
         include: [{
           model: Symbol,
@@ -91,13 +100,14 @@ class CandleController {
         return res.json({
           success: true,
           data: null,
-          message: 'No candle data found for this symbol'
+          message: 'No candle data found for this symbol and timeframe'
         });
       }
 
       res.json({
         success: true,
-        data: candle
+        data: candle,
+        timeframe
       });
     } catch (error) {
       next(error);
@@ -109,7 +119,8 @@ class CandleController {
     try {
       const candleData = {
         ...req.body,
-        symbolId: parseInt(req.body.symbolId)
+        symbolId: parseInt(req.body.symbolId),
+        timeframe: req.body.timeframe || '1h'
       };
 
       // Verify symbol exists
@@ -120,7 +131,7 @@ class CandleController {
         return next(error);
       }
 
-      const candle = await Candle1h.create(candleData);
+      const candle = await Candle.create(candleData);
 
       // Emit WebSocket event for real-time updates
       const io = req.app.get('io');
@@ -128,6 +139,7 @@ class CandleController {
         const eventData = {
           symbol: symbol.symbol,
           symbolId: symbol.id,
+          timeframe: candle.timeframe,
           openTime: candle.openTime,
           open: candle.open,
           high: candle.high,
@@ -188,24 +200,25 @@ class CandleController {
       }
 
       // Check for existing candles to identify duplicates
-      const existingCandles = await Candle1h.findAll({
+      const existingCandles = await Candle.findAll({
         where: {
           [Op.or]: processedCandles.map(candle => ({
             symbolId: candle.symbolId,
-            openTime: candle.openTime
+            openTime: candle.openTime,
+            timeframe: candle.timeframe || '1h'
           }))
         },
-        attributes: ['symbolId', 'openTime']
+        attributes: ['symbolId', 'openTime', 'timeframe']
       });
 
       // Create a set of existing keys for quick lookup
       const existingKeys = new Set(
-        existingCandles.map(c => `${c.symbolId}-${c.openTime.toISOString()}`)
+        existingCandles.map(c => `${c.symbolId}-${c.openTime.toISOString()}-${c.timeframe}`)
       );
 
       // Filter out duplicates
       const newCandles = processedCandles.filter(candle =>
-        !existingKeys.has(`${candle.symbolId}-${candle.openTime.toISOString()}`)
+        !existingKeys.has(`${candle.symbolId}-${candle.openTime.toISOString()}-${candle.timeframe || '1h'}`)
       );
 
       const duplicateCount = processedCandles.length - newCandles.length;
@@ -215,7 +228,7 @@ class CandleController {
         console.log(`Bulk create candles: ${duplicateCount} duplicates skipped`);
       }
 
-      const createdCandles = await Candle1h.bulkCreate(newCandles);
+      const createdCandles = await Candle.bulkCreate(newCandles);
 
       // Emit WebSocket events for real-time updates
       const io = req.app.get('io');
@@ -268,10 +281,10 @@ class CandleController {
     }
   }
 
-  // Get OHLC data aggregated by time period
+  // Get OHLC data aggregated by timeframe
   async getOHLC(req, res, next) {
     try {
-      const { symbolId, period = '1h', limit = 100 } = req.query;
+      const { symbolId, timeframe = '1h', limit = 100 } = req.query;
 
       // Verify symbol exists
       const symbol = await Symbol.findByPk(symbolId);
@@ -281,15 +294,11 @@ class CandleController {
         return next(error);
       }
 
-      // For now, only support 1h period since we only have candles_1h table
-      if (period !== '1h') {
-        const error = new Error('Only 1h period is currently supported');
-        error.statusCode = 400;
-        return next(error);
-      }
-
-      const candles = await Candle1h.findAll({
-        where: { symbolId: parseInt(symbolId) },
+      const candles = await Candle.findAll({
+        where: {
+          symbolId: parseInt(symbolId),
+          timeframe: timeframe
+        },
         limit: parseInt(limit),
         order: [['openTime', 'DESC']],
         attributes: ['openTime', 'open', 'high', 'low', 'close', 'volume']
@@ -298,7 +307,7 @@ class CandleController {
       res.json({
         success: true,
         data: candles,
-        period,
+        timeframe,
         symbol: symbol.symbol
       });
     } catch (error) {
@@ -306,11 +315,11 @@ class CandleController {
     }
   }
 
-  // Clean up old candles, keep latest N candles
+  // Clean up old candles, keep latest N candles for a specific timeframe
   async cleanupCandles(req, res, next) {
     try {
       const { symbolId } = req.params;
-      const { keep = 1000 } = req.query;
+      const { timeframe = '1h', keep = 1000 } = req.query;
 
       // Verify symbol exists
       const symbol = await Symbol.findByPk(symbolId);
@@ -320,22 +329,29 @@ class CandleController {
         return next(error);
       }
 
-      // Get total count of candles for this symbol
-      const totalCandles = await Candle1h.count({
-        where: { symbolId: parseInt(symbolId) }
+      // Get total count of candles for this symbol and timeframe
+      const totalCandles = await Candle.count({
+        where: {
+          symbolId: parseInt(symbolId),
+          timeframe: timeframe
+        }
       });
 
       if (totalCandles <= keep) {
         return res.json({
           success: true,
-          message: `No cleanup needed. Only ${totalCandles} candles exist (keep: ${keep})`,
-          deletedCount: 0
+          message: `No cleanup needed. Only ${totalCandles} candles exist for timeframe ${timeframe} (keep: ${keep})`,
+          deletedCount: 0,
+          timeframe
         });
       }
 
-      // Get the IDs of candles to keep (latest N candles)
-      const candlesToKeep = await Candle1h.findAll({
-        where: { symbolId: parseInt(symbolId) },
+      // Get the IDs of candles to keep (latest N candles for this timeframe)
+      const candlesToKeep = await Candle.findAll({
+        where: {
+          symbolId: parseInt(symbolId),
+          timeframe: timeframe
+        },
         order: [['openTime', 'DESC']],
         limit: parseInt(keep),
         attributes: ['id']
@@ -344,9 +360,10 @@ class CandleController {
       const keepIds = candlesToKeep.map(candle => candle.id);
 
       // Delete older candles (those not in keepIds)
-      const deletedCount = await Candle1h.destroy({
+      const deletedCount = await Candle.destroy({
         where: {
           symbolId: parseInt(symbolId),
+          timeframe: timeframe,
           id: {
             [Op.notIn]: keepIds
           }
@@ -355,10 +372,11 @@ class CandleController {
 
       res.json({
         success: true,
-        message: `Cleanup completed. Deleted ${deletedCount} old candles, kept ${keepIds.length} latest candles`,
+        message: `Cleanup completed. Deleted ${deletedCount} old candles for timeframe ${timeframe}, kept ${keepIds.length} latest candles`,
         deletedCount,
         keptCount: keepIds.length,
-        symbol: symbol.symbol
+        symbol: symbol.symbol,
+        timeframe
       });
     } catch (error) {
       next(error);

+ 11 - 0
src/controllers/livePriceController.js

@@ -17,6 +17,16 @@ class LivePriceController {
         }]
       });
 
+      console.log(`getAllLivePrices: Returning ${livePrices.rows.length} records out of ${livePrices.count} total`);
+      if (livePrices.rows.length > 0) {
+        console.log('First record sample:', {
+          symbolId: livePrices.rows[0].symbolId,
+          price: livePrices.rows[0].price,
+          lastUpdated: livePrices.rows[0].lastUpdated,
+          symbol: livePrices.rows[0].livePriceSymbol?.symbol
+        });
+      }
+
       res.json({
         success: true,
         data: livePrices.rows,
@@ -28,6 +38,7 @@ class LivePriceController {
         }
       });
     } catch (error) {
+      console.error('getAllLivePrices error:', error);
       next(error);
     }
   }

+ 20 - 5
src/models/Candle1h.js → src/models/Candle.js

@@ -2,7 +2,7 @@ const { DataTypes } = require('sequelize');
 const { sequelize } = require('../config/database');
 const Symbol = require('./Symbol');
 
-const Candle1h = sequelize.define('Candle1h', {
+const Candle = sequelize.define('Candle', {
   id: {
     type: DataTypes.BIGINT,
     primaryKey: true,
@@ -17,6 +17,11 @@ const Candle1h = sequelize.define('Candle1h', {
       key: 'id'
     }
   },
+  timeframe: {
+    type: DataTypes.ENUM('15m', '30m', '1h', '1D', '1W', '1M'),
+    allowNull: false,
+    defaultValue: '1h'
+  },
   openTime: {
     type: DataTypes.DATE,
     field: 'open_time',
@@ -59,12 +64,22 @@ const Candle1h = sequelize.define('Candle1h', {
     field: 'created_at'
   }
 }, {
-  tableName: 'candles_1h',
+  tableName: 'candles',
   indexes: [
-    { unique: true, fields: ['symbol_id', 'open_time'] },
-    { fields: ['open_time'] }
+    { unique: true, fields: ['symbol_id', 'open_time', 'timeframe'] },
+    { fields: ['open_time', 'timeframe'] }
   ]
 });
 
+// Define associations
+Candle.belongsTo(Symbol, {
+  foreignKey: 'symbolId',
+  as: 'symbol'
+});
+
+Symbol.hasMany(Candle, {
+  foreignKey: 'symbolId',
+  as: 'candles'
+});
 
-module.exports = Candle1h;
+module.exports = Candle;

+ 2 - 5
src/models/index.js

@@ -1,12 +1,9 @@
 const { sequelize } = require('../config/database');
 const Symbol = require('./Symbol');
-const Candle1h = require('./Candle1h');
+const Candle = require('./Candle');
 const LivePrice = require('./LivePrice');
 
 // Define associations
-Symbol.hasMany(Candle1h, { foreignKey: 'symbolId', as: 'candles1h' });
-Candle1h.belongsTo(Symbol, { foreignKey: 'symbolId', as: 'symbol' });
-
 Symbol.hasOne(LivePrice, { foreignKey: 'symbolId', as: 'livePrice' });
 LivePrice.belongsTo(Symbol, { foreignKey: 'symbolId', as: 'livePriceSymbol' });
 
@@ -24,6 +21,6 @@ if (process.env.NODE_ENV === 'development') {
 module.exports = {
   sequelize,
   Symbol,
-  Candle1h,
+  Candle,
   LivePrice
 };

+ 7 - 1
src/routes/candles.js

@@ -7,6 +7,7 @@ const Joi = require('joi');
 // GET /api/candles - Get candles with filtering
 router.get('/', validateQuery(Joi.object({
   symbolId: Joi.number().integer().positive().required(),
+  timeframe: Joi.string().valid('15m', '30m', '1h', '1D', '1W', '1M').default('1h'),
   startTime: Joi.date().iso(),
   endTime: Joi.date().iso().when('startTime', {
     is: Joi.exist(),
@@ -19,18 +20,21 @@ router.get('/', validateQuery(Joi.object({
 // GET /api/candles/ohlc - Get OHLC data
 router.get('/ohlc', validateQuery(Joi.object({
   symbolId: Joi.number().integer().positive().required(),
-  period: Joi.string().valid('1h').default('1h'),
+  timeframe: Joi.string().valid('15m', '30m', '1h', '1D', '1W', '1M').default('1h'),
   limit: Joi.number().integer().min(1).max(1000).default(100)
 })), candleController.getOHLC);
 
 // GET /api/candles/:symbolId/latest - Get latest candle for a symbol
 router.get('/:symbolId/latest', validateParams(Joi.object({
   symbolId: Joi.number().integer().positive().required()
+})), validateQuery(Joi.object({
+  timeframe: Joi.string().valid('15m', '30m', '1h', '1D', '1W', '1M').default('1h')
 })), candleController.getLatestCandle);
 
 // POST /api/candles - Create new candle
 router.post('/', validate(Joi.object({
   symbolId: Joi.number().integer().positive().required(),
+  timeframe: Joi.string().valid('15m', '30m', '1h', '1D', '1W', '1M').default('1h'),
   openTime: Joi.date().iso().required(),
   closeTime: Joi.date().iso().required(),
   open: Joi.number().precision(8).positive().required(),
@@ -46,6 +50,7 @@ router.post('/', validate(Joi.object({
 router.post('/bulk', validate(Joi.object({
   candles: Joi.array().items(Joi.object({
     symbolId: Joi.number().integer().positive().required(),
+    timeframe: Joi.string().valid('15m', '30m', '1h', '1D', '1W', '1M').default('1h'),
     openTime: Joi.date().iso().required(),
     closeTime: Joi.date().iso().required(),
     open: Joi.number().precision(8).positive().required(),
@@ -62,6 +67,7 @@ router.post('/bulk', validate(Joi.object({
 router.delete('/cleanup/:symbolId', validateParams(Joi.object({
   symbolId: Joi.number().integer().positive().required()
 })), validateQuery(Joi.object({
+  timeframe: Joi.string().valid('15m', '30m', '1h', '1D', '1W', '1M').default('1h'),
   keep: Joi.number().integer().min(1).default(1000)
 })), candleController.cleanupCandles);
 

+ 13 - 10
tests/candleController.test.js

@@ -1,6 +1,6 @@
 const request = require('supertest');
 const app = require('../src/app');
-const { Candle1h, Symbol, sequelize } = require('../src/models');
+const { Candle, Symbol, sequelize } = require('../src/models');
 
 describe('Candle Controller Integration Tests', () => {
   let testSymbol;
@@ -19,7 +19,7 @@ describe('Candle Controller Integration Tests', () => {
 
   afterAll(async () => {
     // Cleanup test data
-    await Candle1h.destroy({ where: {} });
+    await Candle.destroy({ where: {} });
     await Symbol.destroy({ where: {} });
     await sequelize.close();
   });
@@ -29,8 +29,9 @@ describe('Candle Controller Integration Tests', () => {
       const mockCandles = [
         {
           symbolId: testSymbol.id,
-          openTime: '2025-10-17 00:00:00',
-          closeTime: '2025-10-17 01:00:00',
+          timeframe: '1h',
+          openTime: '2025-10-17T00:00:00.000Z',
+          closeTime: '2025-10-17T01:00:00.000Z',
           open: 1.1000,
           high: 1.1050,
           low: 1.0990,
@@ -39,8 +40,9 @@ describe('Candle Controller Integration Tests', () => {
         },
         {
           symbolId: testSymbol.id,
-          openTime: '2025-10-17 01:00:00',
-          closeTime: '2025-10-17 02:00:00',
+          timeframe: '1h',
+          openTime: '2025-10-17T01:00:00.000Z',
+          closeTime: '2025-10-17T02:00:00.000Z',
           open: 1.1025,
           high: 1.1075,
           low: 1.1005,
@@ -59,8 +61,8 @@ describe('Candle Controller Integration Tests', () => {
       expect(response.body.data.length).toBe(2);
 
       // Verify database persistence
-      const dbCandles = await Candle1h.findAll({
-        where: { symbolId: testSymbol.id },
+      const dbCandles = await Candle.findAll({
+        where: { symbolId: testSymbol.id, timeframe: '1h' },
         order: [['openTime', 'ASC']]
       });
 
@@ -82,8 +84,9 @@ describe('Candle Controller Integration Tests', () => {
     it('should handle invalid symbol IDs', async () => {
       const invalidCandles = [{
         symbolId: 999,
-        openTime: '2025-10-17 00:00:00',
-        closeTime: '2025-10-17 01:00:00',
+        timeframe: '1h',
+        openTime: '2025-10-17T00:00:00.000Z',
+        closeTime: '2025-10-17T01:00:00.000Z',
         open: 1.1000,
         high: 1.1050,
         low: 1.0990,