ソースを参照

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 ヶ月 前
コミット
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[];
 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,