Kaynağa Gözat

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 8 ay önce
ebeveyn
işleme
893ec7bead

+ 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[];
 int symbolIds[];
 datetime lastSend = 0;
 datetime lastSend = 0;
 datetime lastCandleSync = 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()
 int OnInit()
 {
 {
@@ -290,9 +294,9 @@ int CreateSymbolInDatabase(string symbolName)
 //+------------------------------------------------------------------+
 //+------------------------------------------------------------------+
 //| Fetch latest stored candle openTime from API                     |
 //| 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 headers = "Content-Type: application/json\r\n";
    string resultHeaders = "";
    string resultHeaders = "";
    char result[];
    char result[];
@@ -303,7 +307,7 @@ datetime GetLatestCandleTime(int symbolId)
 
 
    if(res != 200)
    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;
       return 0;
    }
    }
 
 
@@ -311,7 +315,7 @@ datetime GetLatestCandleTime(int symbolId)
    int pos = StringFind(response, "\"openTime\":\"");
    int pos = StringFind(response, "\"openTime\":\"");
    if(pos < 0)
    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;
       return 0;
    }
    }
 
 
@@ -319,7 +323,6 @@ datetime GetLatestCandleTime(int symbolId)
    int end = StringFind(response, "\"", pos);
    int end = StringFind(response, "\"", pos);
    string openTimeStr = StringSubstr(response, pos, end - pos);
    string openTimeStr = StringSubstr(response, pos, end - pos);
 
 
-   // --- Parse ISO8601 to datetime ---
    int year  = (int)StringToInteger(StringSubstr(openTimeStr, 0, 4));
    int year  = (int)StringToInteger(StringSubstr(openTimeStr, 0, 4));
    int month = (int)StringToInteger(StringSubstr(openTimeStr, 5, 2));
    int month = (int)StringToInteger(StringSubstr(openTimeStr, 5, 2));
    int day   = (int)StringToInteger(StringSubstr(openTimeStr, 8, 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;
    t.hour = hour; t.min = min; t.sec = sec;
 
 
    datetime dt = StructToTime(t);
    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;
    return dt;
 }
 }
 
 
@@ -345,7 +348,7 @@ datetime GetLatestCandleTime(int symbolId)
 //+------------------------------------------------------------------+
 //+------------------------------------------------------------------+
 void SendAllHistoricalCandles()
 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++)
    for(int i = 0; i < ArraySize(symbols); i++)
    {
    {
@@ -353,90 +356,91 @@ void SendAllHistoricalCandles()
       int symbolId = symbolIds[i];
       int symbolId = symbolIds[i];
       if(symbolId <= 0) continue;
       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);
    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\":[";
    string json = "{\"candles\":[";
    bool first = true;
    bool first = true;
@@ -595,7 +599,7 @@ string BuildCandleJSONFromRates(int symbolId, MqlRates &rates[], int startIndex,
       if(r.time <= 0) continue;
       if(r.time <= 0) continue;
 
 
       datetime open_dt  = (datetime)r.time;
       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 openTime  = ToISO8601(open_dt);
       string closeTime = ToISO8601(close_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);
       double quoteVolume  = (r.real_volume > 0 ? r.real_volume : volume);
 
 
       string one = StringFormat(
       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,
          r.open, r.high, r.low, r.close,
          volume, (int)volume, quoteVolume
          volume, (int)volume, quoteVolume
       );
       );

+ 1 - 1
README.md

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

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

@@ -3,8 +3,8 @@
 /** @type {import('sequelize-cli').Migration} */
 /** @type {import('sequelize-cli').Migration} */
 module.exports = {
 module.exports = {
   async up (queryInterface, Sequelize) {
   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'));");
     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),
     base_asset VARCHAR(50),
     quote_asset VARCHAR(50),
     quote_asset VARCHAR(50),
     exchange 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,
     is_active BOOLEAN DEFAULT TRUE,
     created_at TIMESTAMPTZ DEFAULT NOW(),
     created_at TIMESTAMPTZ DEFAULT NOW(),
     updated_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_exchange ON symbols(exchange);
 CREATE INDEX idx_symbols_type ON symbols(instrument_type);
 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,
     id BIGSERIAL PRIMARY KEY,
     symbol_id INT NOT NULL REFERENCES symbols(id) ON DELETE CASCADE,
     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,
     open_time TIMESTAMPTZ NOT NULL,
     close_time TIMESTAMPTZ NOT NULL,
     close_time TIMESTAMPTZ NOT NULL,
     open NUMERIC(18,8) NOT NULL,
     open NUMERIC(18,8) NOT NULL,
@@ -33,8 +34,8 @@ CREATE TABLE candles_1h (
     updated_at TIMESTAMPTZ DEFAULT NOW()
     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
 -- live_prices table
 -- Stores the latest live market prices per symbol
 -- Stores the latest live market prices per symbol

+ 8 - 0
src/config/database.js

@@ -1,6 +1,14 @@
 const { Sequelize } = require('sequelize');
 const { Sequelize } = require('sequelize');
 require('dotenv').config();
 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(
 const sequelize = new Sequelize(
   process.env.DB_NAME,
   process.env.DB_NAME,
   process.env.DB_USER,
   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');
 const { Op } = require('sequelize');
 
 
 class CandleController {
 class CandleController {
@@ -7,6 +7,7 @@ class CandleController {
     try {
     try {
       const {
       const {
         symbolId,
         symbolId,
+        timeframe = '1h',
         startTime,
         startTime,
         endTime,
         endTime,
         limit = 100,
         limit = 100,
@@ -21,7 +22,10 @@ class CandleController {
         return next(error);
         return next(error);
       }
       }
 
 
-      const where = { symbolId: parseInt(symbolId) };
+      const where = {
+        symbolId: parseInt(symbolId),
+        timeframe: timeframe
+      };
 
 
       if (startTime) {
       if (startTime) {
         where.openTime = {
         where.openTime = {
@@ -37,7 +41,7 @@ class CandleController {
         };
         };
       }
       }
 
 
-      const candles = await Candle1h.findAndCountAll({
+      const candles = await Candle.findAndCountAll({
         where,
         where,
         limit: parseInt(limit),
         limit: parseInt(limit),
         offset: parseInt(offset),
         offset: parseInt(offset),
@@ -52,6 +56,7 @@ class CandleController {
       res.json({
       res.json({
         success: true,
         success: true,
         data: candles.rows,
         data: candles.rows,
+        timeframe,
         pagination: {
         pagination: {
           total: candles.count,
           total: candles.count,
           limit: parseInt(limit),
           limit: parseInt(limit),
@@ -68,6 +73,7 @@ class CandleController {
   async getLatestCandle(req, res, next) {
   async getLatestCandle(req, res, next) {
     try {
     try {
       const { symbolId } = req.params;
       const { symbolId } = req.params;
+      const { timeframe = '1h' } = req.query;
 
 
       // Verify symbol exists
       // Verify symbol exists
       const symbol = await Symbol.findByPk(symbolId);
       const symbol = await Symbol.findByPk(symbolId);
@@ -77,8 +83,11 @@ class CandleController {
         return next(error);
         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']],
         order: [['openTime', 'DESC']],
         include: [{
         include: [{
           model: Symbol,
           model: Symbol,
@@ -91,13 +100,14 @@ class CandleController {
         return res.json({
         return res.json({
           success: true,
           success: true,
           data: null,
           data: null,
-          message: 'No candle data found for this symbol'
+          message: 'No candle data found for this symbol and timeframe'
         });
         });
       }
       }
 
 
       res.json({
       res.json({
         success: true,
         success: true,
-        data: candle
+        data: candle,
+        timeframe
       });
       });
     } catch (error) {
     } catch (error) {
       next(error);
       next(error);
@@ -109,7 +119,8 @@ class CandleController {
     try {
     try {
       const candleData = {
       const candleData = {
         ...req.body,
         ...req.body,
-        symbolId: parseInt(req.body.symbolId)
+        symbolId: parseInt(req.body.symbolId),
+        timeframe: req.body.timeframe || '1h'
       };
       };
 
 
       // Verify symbol exists
       // Verify symbol exists
@@ -120,7 +131,7 @@ class CandleController {
         return next(error);
         return next(error);
       }
       }
 
 
-      const candle = await Candle1h.create(candleData);
+      const candle = await Candle.create(candleData);
 
 
       // Emit WebSocket event for real-time updates
       // Emit WebSocket event for real-time updates
       const io = req.app.get('io');
       const io = req.app.get('io');
@@ -128,6 +139,7 @@ class CandleController {
         const eventData = {
         const eventData = {
           symbol: symbol.symbol,
           symbol: symbol.symbol,
           symbolId: symbol.id,
           symbolId: symbol.id,
+          timeframe: candle.timeframe,
           openTime: candle.openTime,
           openTime: candle.openTime,
           open: candle.open,
           open: candle.open,
           high: candle.high,
           high: candle.high,
@@ -188,24 +200,25 @@ class CandleController {
       }
       }
 
 
       // Check for existing candles to identify duplicates
       // Check for existing candles to identify duplicates
-      const existingCandles = await Candle1h.findAll({
+      const existingCandles = await Candle.findAll({
         where: {
         where: {
           [Op.or]: processedCandles.map(candle => ({
           [Op.or]: processedCandles.map(candle => ({
             symbolId: candle.symbolId,
             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
       // Create a set of existing keys for quick lookup
       const existingKeys = new Set(
       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
       // Filter out duplicates
       const newCandles = processedCandles.filter(candle =>
       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;
       const duplicateCount = processedCandles.length - newCandles.length;
@@ -215,7 +228,7 @@ class CandleController {
         console.log(`Bulk create candles: ${duplicateCount} duplicates skipped`);
         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
       // Emit WebSocket events for real-time updates
       const io = req.app.get('io');
       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) {
   async getOHLC(req, res, next) {
     try {
     try {
-      const { symbolId, period = '1h', limit = 100 } = req.query;
+      const { symbolId, timeframe = '1h', limit = 100 } = req.query;
 
 
       // Verify symbol exists
       // Verify symbol exists
       const symbol = await Symbol.findByPk(symbolId);
       const symbol = await Symbol.findByPk(symbolId);
@@ -281,15 +294,11 @@ class CandleController {
         return next(error);
         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),
         limit: parseInt(limit),
         order: [['openTime', 'DESC']],
         order: [['openTime', 'DESC']],
         attributes: ['openTime', 'open', 'high', 'low', 'close', 'volume']
         attributes: ['openTime', 'open', 'high', 'low', 'close', 'volume']
@@ -298,7 +307,7 @@ class CandleController {
       res.json({
       res.json({
         success: true,
         success: true,
         data: candles,
         data: candles,
-        period,
+        timeframe,
         symbol: symbol.symbol
         symbol: symbol.symbol
       });
       });
     } catch (error) {
     } 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) {
   async cleanupCandles(req, res, next) {
     try {
     try {
       const { symbolId } = req.params;
       const { symbolId } = req.params;
-      const { keep = 1000 } = req.query;
+      const { timeframe = '1h', keep = 1000 } = req.query;
 
 
       // Verify symbol exists
       // Verify symbol exists
       const symbol = await Symbol.findByPk(symbolId);
       const symbol = await Symbol.findByPk(symbolId);
@@ -320,22 +329,29 @@ class CandleController {
         return next(error);
         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) {
       if (totalCandles <= keep) {
         return res.json({
         return res.json({
           success: true,
           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']],
         order: [['openTime', 'DESC']],
         limit: parseInt(keep),
         limit: parseInt(keep),
         attributes: ['id']
         attributes: ['id']
@@ -344,9 +360,10 @@ class CandleController {
       const keepIds = candlesToKeep.map(candle => candle.id);
       const keepIds = candlesToKeep.map(candle => candle.id);
 
 
       // Delete older candles (those not in keepIds)
       // Delete older candles (those not in keepIds)
-      const deletedCount = await Candle1h.destroy({
+      const deletedCount = await Candle.destroy({
         where: {
         where: {
           symbolId: parseInt(symbolId),
           symbolId: parseInt(symbolId),
+          timeframe: timeframe,
           id: {
           id: {
             [Op.notIn]: keepIds
             [Op.notIn]: keepIds
           }
           }
@@ -355,10 +372,11 @@ class CandleController {
 
 
       res.json({
       res.json({
         success: true,
         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,
         deletedCount,
         keptCount: keepIds.length,
         keptCount: keepIds.length,
-        symbol: symbol.symbol
+        symbol: symbol.symbol,
+        timeframe
       });
       });
     } catch (error) {
     } catch (error) {
       next(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({
       res.json({
         success: true,
         success: true,
         data: livePrices.rows,
         data: livePrices.rows,
@@ -28,6 +38,7 @@ class LivePriceController {
         }
         }
       });
       });
     } catch (error) {
     } catch (error) {
+      console.error('getAllLivePrices error:', error);
       next(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 { sequelize } = require('../config/database');
 const Symbol = require('./Symbol');
 const Symbol = require('./Symbol');
 
 
-const Candle1h = sequelize.define('Candle1h', {
+const Candle = sequelize.define('Candle', {
   id: {
   id: {
     type: DataTypes.BIGINT,
     type: DataTypes.BIGINT,
     primaryKey: true,
     primaryKey: true,
@@ -17,6 +17,11 @@ const Candle1h = sequelize.define('Candle1h', {
       key: 'id'
       key: 'id'
     }
     }
   },
   },
+  timeframe: {
+    type: DataTypes.ENUM('15m', '30m', '1h', '1D', '1W', '1M'),
+    allowNull: false,
+    defaultValue: '1h'
+  },
   openTime: {
   openTime: {
     type: DataTypes.DATE,
     type: DataTypes.DATE,
     field: 'open_time',
     field: 'open_time',
@@ -59,12 +64,22 @@ const Candle1h = sequelize.define('Candle1h', {
     field: 'created_at'
     field: 'created_at'
   }
   }
 }, {
 }, {
-  tableName: 'candles_1h',
+  tableName: 'candles',
   indexes: [
   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 { sequelize } = require('../config/database');
 const Symbol = require('./Symbol');
 const Symbol = require('./Symbol');
-const Candle1h = require('./Candle1h');
+const Candle = require('./Candle');
 const LivePrice = require('./LivePrice');
 const LivePrice = require('./LivePrice');
 
 
 // Define associations
 // Define associations
-Symbol.hasMany(Candle1h, { foreignKey: 'symbolId', as: 'candles1h' });
-Candle1h.belongsTo(Symbol, { foreignKey: 'symbolId', as: 'symbol' });
-
 Symbol.hasOne(LivePrice, { foreignKey: 'symbolId', as: 'livePrice' });
 Symbol.hasOne(LivePrice, { foreignKey: 'symbolId', as: 'livePrice' });
 LivePrice.belongsTo(Symbol, { foreignKey: 'symbolId', as: 'livePriceSymbol' });
 LivePrice.belongsTo(Symbol, { foreignKey: 'symbolId', as: 'livePriceSymbol' });
 
 
@@ -24,6 +21,6 @@ if (process.env.NODE_ENV === 'development') {
 module.exports = {
 module.exports = {
   sequelize,
   sequelize,
   Symbol,
   Symbol,
-  Candle1h,
+  Candle,
   LivePrice
   LivePrice
 };
 };

+ 7 - 1
src/routes/candles.js

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

+ 13 - 10
tests/candleController.test.js

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