Просмотр исходного кода

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

muhammad.uzair месяцев назад: 3
Родитель
Сommit
dc9e7a193e

+ 1 - 1
README.md

@@ -48,7 +48,7 @@ The service includes an MT5 Expert Advisor (EA) that automatically sends histori
48
 - Error recovery mechanisms  
48
 - Error recovery mechanisms  
49
 - Comprehensive test coverage  
49
 - Comprehensive test coverage  
50
 
50
 
51
-- **Multi-Asset Support**: Handles cryptocurrencies, stocks, forex, and commodities
51
+- **Multi-Asset Support**: Handles cryptocurrencies, stocks, forex, commodities, and indices
52
 - **Real-time Data**: Live price feeds with bid/ask spreads
52
 - **Real-time Data**: Live price feeds with bid/ask spreads
53
 - **Historical Data**: OHLCV candle data with flexible timeframes
53
 - **Historical Data**: OHLCV candle data with flexible timeframes
54
 - **RESTful API**: Well-structured endpoints for all operations
54
 - **RESTful API**: Well-structured endpoints for all operations

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

@@ -0,0 +1,16 @@
1
+'use strict';
2
+
3
+/** @type {import('sequelize-cli').Migration} */
4
+module.exports = {
5
+  async up (queryInterface, Sequelize) {
6
+    // Drop the existing CHECK constraint and add a new one with 'index'
7
+    await queryInterface.sequelize.query("ALTER TABLE symbols DROP CONSTRAINT symbols_instrument_type_check;");
8
+    await queryInterface.sequelize.query("ALTER TABLE symbols ADD CONSTRAINT symbols_instrument_type_check CHECK (instrument_type IN ('crypto', 'stock', 'forex', 'commodity', 'index'));");
9
+  },
10
+
11
+  async down (queryInterface, Sequelize) {
12
+    // Revert the CHECK constraint without 'index'
13
+    await queryInterface.sequelize.query("ALTER TABLE symbols DROP CONSTRAINT symbols_instrument_type_check;");
14
+    await queryInterface.sequelize.query("ALTER TABLE symbols ADD CONSTRAINT symbols_instrument_type_check CHECK (instrument_type IN ('crypto', 'stock', 'forex', 'commodity'));");
15
+  }
16
+};

+ 41 - 6
src/controllers/candleController.js

@@ -88,9 +88,11 @@ class CandleController {
88
       });
88
       });
89
 
89
 
90
       if (!candle) {
90
       if (!candle) {
91
-        const error = new Error('No candle data found for this symbol');
92
-        error.statusCode = 404;
93
-        return next(error);
91
+        return res.json({
92
+          success: true,
93
+          data: null,
94
+          message: 'No candle data found for this symbol'
95
+        });
94
       }
96
       }
95
 
97
 
96
       res.json({
98
       res.json({
@@ -165,7 +167,12 @@ class CandleController {
165
       }
167
       }
166
 
168
 
167
       // Verify all symbols exist
169
       // Verify all symbols exist
168
-      const symbolIds = [...new Set(candles.map(c => c.symbolId))];
170
+      const processedCandles = candles.map(candle => ({
171
+        ...candle,
172
+        symbolId: parseInt(candle.symbolId)
173
+      }));
174
+
175
+      const symbolIds = [...new Set(processedCandles.map(c => c.symbolId))];
169
       const existingSymbols = await Symbol.findAll({
176
       const existingSymbols = await Symbol.findAll({
170
         where: { id: symbolIds },
177
         where: { id: symbolIds },
171
         attributes: ['id']
178
         attributes: ['id']
@@ -180,7 +187,35 @@ class CandleController {
180
         return next(error);
187
         return next(error);
181
       }
188
       }
182
 
189
 
183
-      const createdCandles = await Candle1h.bulkCreate(candles);
190
+      // Check for existing candles to identify duplicates
191
+      const existingCandles = await Candle1h.findAll({
192
+        where: {
193
+          [Op.or]: processedCandles.map(candle => ({
194
+            symbolId: candle.symbolId,
195
+            openTime: candle.openTime
196
+          }))
197
+        },
198
+        attributes: ['symbolId', 'openTime']
199
+      });
200
+
201
+      // Create a set of existing keys for quick lookup
202
+      const existingKeys = new Set(
203
+        existingCandles.map(c => `${c.symbolId}-${c.openTime.toISOString()}`)
204
+      );
205
+
206
+      // Filter out duplicates
207
+      const newCandles = processedCandles.filter(candle =>
208
+        !existingKeys.has(`${candle.symbolId}-${candle.openTime.toISOString()}`)
209
+      );
210
+
211
+      const duplicateCount = processedCandles.length - newCandles.length;
212
+
213
+      // Log duplicates if any
214
+      if (duplicateCount > 0) {
215
+        console.log(`Bulk create candles: ${duplicateCount} duplicates skipped`);
216
+      }
217
+
218
+      const createdCandles = await Candle1h.bulkCreate(newCandles);
184
 
219
 
185
       // Emit WebSocket events for real-time updates
220
       // Emit WebSocket events for real-time updates
186
       const io = req.app.get('io');
221
       const io = req.app.get('io');
@@ -226,7 +261,7 @@ class CandleController {
226
       res.status(201).json({
261
       res.status(201).json({
227
         success: true,
262
         success: true,
228
         data: createdCandles,
263
         data: createdCandles,
229
-        message: `${createdCandles.length} candles created successfully`
264
+        message: `${createdCandles.length} candles created successfully${duplicateCount > 0 ? `, ${duplicateCount} duplicates skipped` : ''}`
230
       });
265
       });
231
     } catch (error) {
266
     } catch (error) {
232
       next(error);
267
       next(error);

+ 17 - 6
src/controllers/symbolController.js

@@ -67,13 +67,24 @@ class SymbolController {
67
   // Create new symbol
67
   // Create new symbol
68
   async createSymbol(req, res, next) {
68
   async createSymbol(req, res, next) {
69
     try {
69
     try {
70
-      const symbol = await Symbol.create(req.body);
71
-
72
-      res.status(201).json({
73
-        success: true,
74
-        data: symbol,
75
-        message: 'Symbol created successfully'
70
+      const [symbol, created] = await Symbol.findOrCreate({
71
+        where: { symbol: req.body.symbol },
72
+        defaults: req.body
76
       });
73
       });
74
+
75
+      if (created) {
76
+        res.status(201).json({
77
+          success: true,
78
+          data: symbol,
79
+          message: 'Symbol created successfully'
80
+        });
81
+      } else {
82
+        res.status(200).json({
83
+          success: true,
84
+          data: symbol,
85
+          message: 'Symbol already exists'
86
+        });
87
+      }
77
     } catch (error) {
88
     } catch (error) {
78
       next(error);
89
       next(error);
79
     }
90
     }

+ 1 - 1
src/middleware/validation.js

@@ -6,7 +6,7 @@ const symbolSchema = Joi.object({
6
   baseAsset: Joi.string().max(50),
6
   baseAsset: Joi.string().max(50),
7
   quoteAsset: Joi.string().max(50),
7
   quoteAsset: Joi.string().max(50),
8
   exchange: Joi.string().max(50),
8
   exchange: Joi.string().max(50),
9
-  instrumentType: Joi.string().valid('crypto', 'stock', 'forex', 'commodity').required(),
9
+  instrumentType: Joi.string().valid('crypto', 'stock', 'forex', 'commodity', 'index').required(),
10
   isActive: Joi.boolean().default(true)
10
   isActive: Joi.boolean().default(true)
11
 });
11
 });
12
 
12
 

+ 1 - 1
src/models/Symbol.js

@@ -24,7 +24,7 @@ const Symbol = sequelize.define('Symbol', {
24
     type: DataTypes.STRING(50)
24
     type: DataTypes.STRING(50)
25
   },
25
   },
26
   instrumentType: {
26
   instrumentType: {
27
-    type: DataTypes.ENUM('crypto', 'stock', 'forex', 'commodity'),
27
+    type: DataTypes.ENUM('crypto', 'stock', 'forex', 'commodity', 'index'),
28
     field: 'instrument_type',
28
     field: 'instrument_type',
29
     allowNull: false
29
     allowNull: false
30
   },
30
   },

+ 1 - 1
src/routes/symbols.js

@@ -6,7 +6,7 @@ const { validate, validateQuery, validateParams, symbolSchema, symbolIdSchema }
6
 // GET /api/symbols - Get all symbols with optional filtering
6
 // GET /api/symbols - Get all symbols with optional filtering
7
 router.get('/', validateQuery(require('joi').object({
7
 router.get('/', validateQuery(require('joi').object({
8
   exchange: require('joi').string().max(50),
8
   exchange: require('joi').string().max(50),
9
-  instrumentType: require('joi').string().valid('crypto', 'stock', 'forex', 'commodity'),
9
+  instrumentType: require('joi').string().valid('crypto', 'stock', 'forex', 'commodity', 'index'),
10
   isActive: require('joi').string().valid('true', 'false'),
10
   isActive: require('joi').string().valid('true', 'false'),
11
   limit: require('joi').number().integer().min(1).max(1000).default(100),
11
   limit: require('joi').number().integer().min(1).max(1000).default(100),
12
   offset: require('joi').number().integer().min(0).default(0)
12
   offset: require('joi').number().integer().min(0).default(0)