Преглед изворни кода

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

muhammad.uzair пре 3 месеци
родитељ
комит
dc9e7a193e

+ 1 - 1
README.md

@@ -48,7 +48,7 @@ The service includes an MT5 Expert Advisor (EA) that automatically sends histori
48 48
 - Error recovery mechanisms  
49 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 52
 - **Real-time Data**: Live price feeds with bid/ask spreads
53 53
 - **Historical Data**: OHLCV candle data with flexible timeframes
54 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 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 98
       res.json({
@@ -165,7 +167,12 @@ class CandleController {
165 167
       }
166 168
 
167 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 176
       const existingSymbols = await Symbol.findAll({
170 177
         where: { id: symbolIds },
171 178
         attributes: ['id']
@@ -180,7 +187,35 @@ class CandleController {
180 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 220
       // Emit WebSocket events for real-time updates
186 221
       const io = req.app.get('io');
@@ -226,7 +261,7 @@ class CandleController {
226 261
       res.status(201).json({
227 262
         success: true,
228 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 266
     } catch (error) {
232 267
       next(error);

+ 17 - 6
src/controllers/symbolController.js

@@ -67,13 +67,24 @@ class SymbolController {
67 67
   // Create new symbol
68 68
   async createSymbol(req, res, next) {
69 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 88
     } catch (error) {
78 89
       next(error);
79 90
     }

+ 1 - 1
src/middleware/validation.js

@@ -6,7 +6,7 @@ const symbolSchema = Joi.object({
6 6
   baseAsset: Joi.string().max(50),
7 7
   quoteAsset: Joi.string().max(50),
8 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 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 24
     type: DataTypes.STRING(50)
25 25
   },
26 26
   instrumentType: {
27
-    type: DataTypes.ENUM('crypto', 'stock', 'forex', 'commodity'),
27
+    type: DataTypes.ENUM('crypto', 'stock', 'forex', 'commodity', 'index'),
28 28
     field: 'instrument_type',
29 29
     allowNull: false
30 30
   },

+ 1 - 1
src/routes/symbols.js

@@ -6,7 +6,7 @@ const { validate, validateQuery, validateParams, symbolSchema, symbolIdSchema }
6 6
 // GET /api/symbols - Get all symbols with optional filtering
7 7
 router.get('/', validateQuery(require('joi').object({
8 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 10
   isActive: require('joi').string().valid('true', 'false'),
11 11
   limit: require('joi').number().integer().min(1).max(1000).default(100),
12 12
   offset: require('joi').number().integer().min(0).default(0)