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

feat: initial project setup with complete market data service architecture

- Add comprehensive Node.js/Express.js backend structure
- Implement PostgreSQL database schema with Sequelize ORM
- Create RESTful API endpoints for symbols, candles, and live prices
- Add data validation using Joi and error handling middleware
- Configure Winston logging and security with Helmet.js
- Set up modular architecture with controllers, routes, and models
- Update .gitignore with standard Node.js exclusions
- Enhance README with detailed documentation, installation, and API guides

This commit establishes the foundational architecture for a high-performance
financial data API supporting multiple asset classes including cryptocurrencies,
stocks, forex, and commodities with both RESTful endpoints and real-time
WebSocket capabilities.
Hussain Afzal месяцев назад: 4
Родитель
Сommit
7ece7a970d

+ 84 - 15
.gitignore

@@ -1,30 +1,99 @@
1
-# ---> Node
1
+# Dependencies
2
+node_modules/
3
+npm-debug.log*
4
+yarn-debug.log*
5
+yarn-error.log*
6
+
7
+# Environment variables
8
+.env
9
+.env.local
10
+.env.development.local
11
+.env.test.local
12
+.env.production.local
13
+
2 14
 # Logs
3
-logs
15
+logs/
4 16
 *.log
5 17
 npm-debug.log*
18
+yarn-debug.log*
19
+yarn-error.log*
20
+lerna-debug.log*
6 21
 
7 22
 # Runtime data
8 23
 pids
9 24
 *.pid
10 25
 *.seed
11
-
12
-# Directory for instrumented libs generated by jscoverage/JSCover
13
-lib-cov
26
+*.pid.lock
14 27
 
15 28
 # Coverage directory used by tools like istanbul
16
-coverage
29
+coverage/
30
+*.lcov
31
+
32
+# nyc test coverage
33
+.nyc_output
34
+
35
+# Dependency directories
36
+node_modules/
37
+jspm_packages/
38
+
39
+# Optional npm cache directory
40
+.npm
41
+
42
+# Optional eslint cache
43
+.eslintcache
44
+
45
+# Microbundle cache
46
+.rpt2_cache/
47
+.rts2_cache_cjs/
48
+.rts2_cache_es/
49
+.rts2_cache_umd/
50
+
51
+# Optional REPL history
52
+.node_repl_history
53
+
54
+# Output of 'npm pack'
55
+*.tgz
56
+
57
+# Yarn Integrity file
58
+.yarn-integrity
59
+
60
+# parcel-bundler cache (https://parceljs.org/)
61
+.cache
62
+.parcel-cache
63
+
64
+# Next.js build output
65
+.next
66
+
67
+# Nuxt.js build / generate output
68
+.nuxt
69
+dist
70
+
71
+# Gatsby files
72
+.cache/
73
+public
17 74
 
18
-# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
19
-.grunt
75
+# Storybook build outputs
76
+.out
77
+.storybook-out
20 78
 
21
-# node-waf configuration
22
-.lock-wscript
79
+# Temporary folders
80
+tmp/
81
+temp/
23 82
 
24
-# Compiled binary addons (http://nodejs.org/api/addons.html)
25
-build/Release
83
+# Editor directories and files
84
+.vscode/*
85
+!.vscode/extensions.json
86
+.idea
87
+.DS_Store
88
+*.suo
89
+*.ntvs*
90
+*.njsproj
91
+*.sln
92
+*.sw?
26 93
 
27
-# Dependency directory
28
-# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
29
-node_modules
94
+# Database
95
+*.sqlite
96
+*.db
30 97
 
98
+# OS generated files
99
+Thumbs.db

+ 204 - 2
README.md

@@ -1,3 +1,205 @@
1
-# market-data-service
1
+# Market Data Service
2 2
 
3
-Market Data Service is a high-performance financial data API that provides comprehensive Symbol prices of different markets  through both RESTful endpoints and real-time WebSocket connections.
3
+A high-performance financial data API that provides comprehensive market data for various financial instruments including cryptocurrencies, stocks, forex, and commodities through both RESTful endpoints and real-time WebSocket connections.
4
+
5
+## Features
6
+
7
+- **Multi-Asset Support**: Handles cryptocurrencies, stocks, forex, and commodities
8
+- **Real-time Data**: Live price feeds with bid/ask spreads
9
+- **Historical Data**: OHLCV candle data with flexible timeframes
10
+- **RESTful API**: Well-structured endpoints for all operations
11
+- **Data Validation**: Comprehensive input validation using Joi
12
+- **Error Handling**: Robust error handling with detailed responses
13
+- **Security**: Helmet.js for security headers, CORS support
14
+- **Logging**: Winston-based logging with multiple transports
15
+- **Database**: PostgreSQL with Sequelize ORM
16
+- **Scalable Architecture**: Modular design with controllers, routes, and middleware
17
+
18
+## Tech Stack
19
+
20
+- **Backend**: Node.js, Express.js
21
+- **Database**: PostgreSQL
22
+- **ORM**: Sequelize
23
+- **Validation**: Joi
24
+- **Security**: Helmet, CORS
25
+- **Logging**: Winston, Morgan
26
+- **Testing**: Jest
27
+- **Development**: Nodemon, ESLint
28
+
29
+## Project Structure
30
+
31
+```
32
+market-data-service/
33
+├── src/
34
+│   ├── config/
35
+│   │   └── database.js          # Database configuration
36
+│   ├── controllers/
37
+│   │   ├── symbolController.js      # Symbol CRUD operations
38
+│   │   ├── candleController.js      # Candle data operations
39
+│   │   └── livePriceController.js   # Live price operations
40
+│   ├── middleware/
41
+│   │   ├── errorHandler.js          # Global error handling
42
+│   │   └── validation.js            # Request validation
43
+│   ├── models/
44
+│   │   ├── Symbol.js                # Symbol model
45
+│   │   ├── Candle1h.js              # 1-hour candle model
46
+│   │   ├── LivePrice.js             # Live price model
47
+│   │   └── index.js                 # Model associations
48
+│   ├── routes/
49
+│   │   ├── symbols.js               # Symbol routes
50
+│   │   ├── candles.js               # Candle routes
51
+│   │   └── livePrices.js            # Live price routes
52
+│   ├── utils/
53
+│   │   └── logger.js                # Logging utility
54
+│   ├── app.js                       # Express app configuration
55
+│   └── server.js                    # Server startup
56
+├── tests/                           # Test files
57
+├── schema.sql                       # Database schema
58
+├── .env                             # Environment variables
59
+├── .gitignore                       # Git ignore rules
60
+├── package.json                     # Dependencies and scripts
61
+└── README.md                        # This file
62
+```
63
+
64
+## Installation
65
+
66
+1. **Clone the repository**
67
+   ```bash
68
+   git clone https://git.mqldevelopment.com/muhammad.uzair/market-data-service.git
69
+   cd market-data-service
70
+   ```
71
+
72
+2. **Install dependencies**
73
+   ```bash
74
+   npm install
75
+   ```
76
+
77
+3. **Set up environment variables**
78
+   ```bash
79
+   cp .env.example .env
80
+   ```
81
+   Edit `.env` with your database credentials and other configuration.
82
+
83
+4. **Set up the database**
84
+   ```bash
85
+   # Create PostgreSQL database
86
+   createdb market_data
87
+
88
+   # Run the schema
89
+   psql -d market_data -f schema.sql
90
+   ```
91
+
92
+5. **Start the development server**
93
+   ```bash
94
+   npm run dev
95
+   ```
96
+
97
+The server will start on `http://localhost:3000`
98
+
99
+## Environment Variables
100
+
101
+Create a `.env` file in the root directory with the following variables:
102
+
103
+```env
104
+# Database Configuration
105
+DB_HOST=localhost
106
+DB_PORT=5432
107
+DB_NAME=market_data
108
+DB_USER=your_username
109
+DB_PASSWORD=your_password
110
+
111
+# Server Configuration
112
+PORT=3000
113
+NODE_ENV=development
114
+
115
+# JWT Configuration (if needed for authentication)
116
+JWT_SECRET=your_jwt_secret_key
117
+
118
+# API Keys (if needed for external services)
119
+# BINANCE_API_KEY=your_api_key
120
+# BINANCE_API_SECRET=your_api_secret
121
+
122
+# CORS Configuration
123
+CORS_ORIGIN=*
124
+```
125
+
126
+## API Endpoints
127
+
128
+### Health Check
129
+- `GET /health` - Check service health
130
+
131
+### Symbols
132
+- `GET /api/symbols` - Get all symbols (with filtering)
133
+- `GET /api/symbols/search` - Search symbols by name
134
+- `GET /api/symbols/:id` - Get symbol by ID
135
+- `POST /api/symbols` - Create new symbol
136
+- `PUT /api/symbols/:id` - Update symbol
137
+- `DELETE /api/symbols/:id` - Delete symbol (soft delete)
138
+
139
+### Candles
140
+- `GET /api/candles` - Get candles with filtering
141
+- `GET /api/candles/ohlc` - Get OHLC data
142
+- `GET /api/candles/:symbolId/latest` - Get latest candle for symbol
143
+- `POST /api/candles` - Create new candle
144
+- `POST /api/candles/bulk` - Bulk create candles
145
+
146
+### Live Prices
147
+- `GET /api/live-prices` - Get all live prices
148
+- `GET /api/live-prices/exchange/:exchange` - Get live prices by exchange
149
+- `GET /api/live-prices/type/:type` - Get live prices by instrument type
150
+- `GET /api/live-prices/:symbolId` - Get live price for symbol
151
+- `POST /api/live-prices` - Create/update live price
152
+- `POST /api/live-prices/bulk` - Bulk update live prices
153
+- `DELETE /api/live-prices/:symbolId` - Delete live price
154
+
155
+## Database Schema
156
+
157
+The service uses three main tables:
158
+
159
+1. **symbols** - Trading symbols metadata
160
+2. **candles_1h** - Hourly OHLCV data
161
+3. **live_prices** - Current market prices
162
+
163
+See `schema.sql` for complete table definitions.
164
+
165
+## Development
166
+
167
+### Available Scripts
168
+
169
+- `npm start` - Start production server
170
+- `npm run dev` - Start development server with auto-reload
171
+- `npm test` - Run tests
172
+- `npm run test:watch` - Run tests in watch mode
173
+- `npm run lint` - Run ESLint
174
+- `npm run lint:fix` - Fix ESLint issues
175
+
176
+### Code Style
177
+
178
+This project uses ESLint for code linting. Run `npm run lint` to check for issues and `npm run lint:fix` to automatically fix them.
179
+
180
+### Testing
181
+
182
+Tests are written using Jest. Add your test files in the `tests/` directory.
183
+
184
+## Deployment
185
+
186
+1. Set `NODE_ENV=production` in your environment
187
+2. Run `npm start` to start the production server
188
+3. Consider using a process manager like PM2 for production deployments
189
+
190
+## Contributing
191
+
192
+1. Fork the repository
193
+2. Create a feature branch
194
+3. Make your changes
195
+4. Add tests for new features
196
+5. Ensure all tests pass
197
+6. Submit a pull request
198
+
199
+## License
200
+
201
+ISC License - see LICENSE file for details.
202
+
203
+## Support
204
+
205
+For support or questions, please contact the development team.

Разница между файлами не показана из-за своего большого размера
+ 1629 - 0
package-lock.json


+ 35 - 0
package.json

@@ -0,0 +1,35 @@
1
+{
2
+  "name": "market-data-service",
3
+  "version": "1.0.0",
4
+  "description": "Market Data Service is a high-performance financial data API that provides comprehensive Symbol prices of different markets  through both RESTful endpoints and real-time WebSocket connections.",
5
+  "main": "index.js",
6
+  "scripts": {
7
+    "start": "node src/server.js",
8
+    "dev": "nodemon src/server.js",
9
+    "test": "jest",
10
+    "test:watch": "jest --watch",
11
+    "lint": "eslint src/**/*.js",
12
+    "lint:fix": "eslint src/**/*.js --fix",
13
+    "db:migrate": "sequelize-cli db:migrate",
14
+    "db:seed": "sequelize-cli db:seed:all"
15
+  },
16
+  "repository": {
17
+    "type": "git",
18
+    "url": "https://git.mqldevelopment.com/muhammad.uzair/market-data-service.git"
19
+  },
20
+  "keywords": [],
21
+  "author": "",
22
+  "license": "ISC",
23
+  "type": "commonjs",
24
+  "dependencies": {
25
+    "cors": "^2.8.5",
26
+    "dotenv": "^17.2.3",
27
+    "express": "^5.1.0",
28
+    "helmet": "^8.1.0",
29
+    "joi": "^18.0.1",
30
+    "morgan": "^1.10.1",
31
+    "pg": "^8.16.3",
32
+    "sequelize": "^6.37.7",
33
+    "winston": "^3.18.3"
34
+  }
35
+}

+ 50 - 0
schema.sql

@@ -0,0 +1,50 @@
1
+-- symbols table
2
+-- Stores metadata for each trading symbol
3
+CREATE TABLE symbols (
4
+    id SERIAL PRIMARY KEY,
5
+    symbol VARCHAR(50) NOT NULL UNIQUE,
6
+    base_asset VARCHAR(50),
7
+    quote_asset VARCHAR(50),
8
+    exchange VARCHAR(50),
9
+    instrument_type VARCHAR(20) CHECK (instrument_type IN ('crypto', 'stock', 'forex', 'commodity')),
10
+    is_active BOOLEAN DEFAULT TRUE,
11
+    created_at TIMESTAMPTZ DEFAULT NOW(),
12
+    updated_at TIMESTAMPTZ DEFAULT NOW()
13
+);
14
+
15
+CREATE INDEX idx_symbols_exchange ON symbols(exchange);
16
+CREATE INDEX idx_symbols_type ON symbols(instrument_type);
17
+
18
+-- candles_1h table
19
+-- Stores hourly OHLCV data for each symbol
20
+CREATE TABLE candles_1h (
21
+    id BIGSERIAL PRIMARY KEY,
22
+    symbol_id INT NOT NULL REFERENCES symbols(id) ON DELETE CASCADE,
23
+    open_time TIMESTAMPTZ NOT NULL,
24
+    close_time TIMESTAMPTZ NOT NULL,
25
+    open NUMERIC(18,8) NOT NULL,
26
+    high NUMERIC(18,8) NOT NULL,
27
+    low NUMERIC(18,8) NOT NULL,
28
+    close NUMERIC(18,8) NOT NULL,
29
+    volume NUMERIC(20,8),
30
+    trades_count INT,
31
+    quote_volume NUMERIC(20,8),
32
+    created_at TIMESTAMPTZ DEFAULT NOW()
33
+);
34
+
35
+CREATE UNIQUE INDEX idx_candles_symbol_time ON candles_1h(symbol_id, open_time);
36
+CREATE INDEX idx_candles_open_time ON candles_1h(open_time);
37
+
38
+-- live_prices table
39
+-- Stores the latest live market prices per symbol
40
+CREATE TABLE live_prices (
41
+    symbol_id INT PRIMARY KEY REFERENCES symbols(id) ON DELETE CASCADE,
42
+    price NUMERIC(18,8) NOT NULL,
43
+    bid NUMERIC(18,8),
44
+    ask NUMERIC(18,8),
45
+    bid_size NUMERIC(18,8),
46
+    ask_size NUMERIC(18,8),
47
+    last_updated TIMESTAMPTZ DEFAULT NOW()
48
+);
49
+
50
+CREATE INDEX idx_live_prices_price ON live_prices(price);

+ 70 - 0
src/app.js

@@ -0,0 +1,70 @@
1
+const express = require('express');
2
+const cors = require('cors');
3
+const helmet = require('helmet');
4
+const morgan = require('morgan');
5
+require('dotenv').config();
6
+
7
+// Import routes
8
+const symbolRoutes = require('./routes/symbols');
9
+const candleRoutes = require('./routes/candles');
10
+const livePriceRoutes = require('./routes/livePrices');
11
+
12
+// Import middleware
13
+const errorHandler = require('./middleware/errorHandler');
14
+
15
+// Import utilities
16
+const logger = require('./utils/logger');
17
+
18
+const app = express();
19
+
20
+// Security middleware
21
+app.use(helmet());
22
+
23
+// CORS configuration
24
+app.use(cors({
25
+  origin: process.env.CORS_ORIGIN || '*',
26
+  credentials: true
27
+}));
28
+
29
+// Body parsing middleware
30
+app.use(express.json({ limit: '10mb' }));
31
+app.use(express.urlencoded({ extended: true, limit: '10mb' }));
32
+
33
+// Logging middleware
34
+if (process.env.NODE_ENV === 'development') {
35
+  app.use(morgan('combined'));
36
+} else {
37
+  app.use(morgan('combined', {
38
+    stream: {
39
+      write: (message) => logger.http(message.trim())
40
+    }
41
+  }));
42
+}
43
+
44
+// Health check endpoint
45
+app.get('/health', (req, res) => {
46
+  res.json({
47
+    success: true,
48
+    message: 'Market Data Service is running',
49
+    timestamp: new Date().toISOString(),
50
+    environment: process.env.NODE_ENV
51
+  });
52
+});
53
+
54
+// API routes
55
+app.use('/api/symbols', symbolRoutes);
56
+app.use('/api/candles', candleRoutes);
57
+app.use('/api/live-prices', livePriceRoutes);
58
+
59
+// 404 handler
60
+app.use('*', (req, res) => {
61
+  res.status(404).json({
62
+    success: false,
63
+    message: 'Route not found'
64
+  });
65
+});
66
+
67
+// Error handling middleware (must be last)
68
+app.use(errorHandler);
69
+
70
+module.exports = app;

+ 37 - 0
src/config/database.js

@@ -0,0 +1,37 @@
1
+const { Sequelize } = require('sequelize');
2
+require('dotenv').config();
3
+
4
+const sequelize = new Sequelize(
5
+  process.env.DB_NAME,
6
+  process.env.DB_USER,
7
+  process.env.DB_PASSWORD,
8
+  {
9
+    host: process.env.DB_HOST,
10
+    port: process.env.DB_PORT,
11
+    dialect: 'postgres',
12
+    logging: process.env.NODE_ENV === 'development' ? console.log : false,
13
+    pool: {
14
+      max: 5,
15
+      min: 0,
16
+      acquire: 30000,
17
+      idle: 10000
18
+    },
19
+    define: {
20
+      timestamps: true,
21
+      underscored: true,
22
+      freezeTableName: true
23
+    }
24
+  }
25
+);
26
+
27
+// Test the connection
28
+const testConnection = async () => {
29
+  try {
30
+    await sequelize.authenticate();
31
+    console.log('Database connection has been established successfully.');
32
+  } catch (error) {
33
+    console.error('Unable to connect to the database:', error);
34
+  }
35
+};
36
+
37
+module.exports = { sequelize, testConnection };

+ 211 - 0
src/controllers/candleController.js

@@ -0,0 +1,211 @@
1
+const { Candle1h, Symbol } = require('../models');
2
+const { Op } = require('sequelize');
3
+
4
+class CandleController {
5
+  // Get candles for a symbol
6
+  async getCandles(req, res, next) {
7
+    try {
8
+      const {
9
+        symbolId,
10
+        startTime,
11
+        endTime,
12
+        limit = 100,
13
+        offset = 0
14
+      } = req.query;
15
+
16
+      // Verify symbol exists
17
+      const symbol = await Symbol.findByPk(symbolId);
18
+      if (!symbol) {
19
+        const error = new Error('Symbol not found');
20
+        error.statusCode = 404;
21
+        return next(error);
22
+      }
23
+
24
+      const where = { symbolId: parseInt(symbolId) };
25
+
26
+      if (startTime) {
27
+        where.openTime = {
28
+          ...where.openTime,
29
+          [Op.gte]: new Date(startTime)
30
+        };
31
+      }
32
+
33
+      if (endTime) {
34
+        where.openTime = {
35
+          ...where.openTime,
36
+          [Op.lte]: new Date(endTime)
37
+        };
38
+      }
39
+
40
+      const candles = await Candle1h.findAndCountAll({
41
+        where,
42
+        limit: parseInt(limit),
43
+        offset: parseInt(offset),
44
+        order: [['openTime', 'DESC']],
45
+        include: [{
46
+          model: Symbol,
47
+          as: 'symbol',
48
+          attributes: ['symbol', 'exchange', 'instrumentType']
49
+        }]
50
+      });
51
+
52
+      res.json({
53
+        success: true,
54
+        data: candles.rows,
55
+        pagination: {
56
+          total: candles.count,
57
+          limit: parseInt(limit),
58
+          offset: parseInt(offset),
59
+          hasMore: offset + candles.rows.length < candles.count
60
+        }
61
+      });
62
+    } catch (error) {
63
+      next(error);
64
+    }
65
+  }
66
+
67
+  // Get latest candle for a symbol
68
+  async getLatestCandle(req, res, next) {
69
+    try {
70
+      const { symbolId } = req.params;
71
+
72
+      // Verify symbol exists
73
+      const symbol = await Symbol.findByPk(symbolId);
74
+      if (!symbol) {
75
+        const error = new Error('Symbol not found');
76
+        error.statusCode = 404;
77
+        return next(error);
78
+      }
79
+
80
+      const candle = await Candle1h.findOne({
81
+        where: { symbolId: parseInt(symbolId) },
82
+        order: [['openTime', 'DESC']],
83
+        include: [{
84
+          model: Symbol,
85
+          as: 'symbol',
86
+          attributes: ['symbol', 'exchange', 'instrumentType']
87
+        }]
88
+      });
89
+
90
+      if (!candle) {
91
+        const error = new Error('No candle data found for this symbol');
92
+        error.statusCode = 404;
93
+        return next(error);
94
+      }
95
+
96
+      res.json({
97
+        success: true,
98
+        data: candle
99
+      });
100
+    } catch (error) {
101
+      next(error);
102
+    }
103
+  }
104
+
105
+  // Create new candle
106
+  async createCandle(req, res, next) {
107
+    try {
108
+      const candleData = {
109
+        ...req.body,
110
+        symbolId: parseInt(req.body.symbolId)
111
+      };
112
+
113
+      // Verify symbol exists
114
+      const symbol = await Symbol.findByPk(candleData.symbolId);
115
+      if (!symbol) {
116
+        const error = new Error('Symbol not found');
117
+        error.statusCode = 404;
118
+        return next(error);
119
+      }
120
+
121
+      const candle = await Candle1h.create(candleData);
122
+
123
+      res.status(201).json({
124
+        success: true,
125
+        data: candle,
126
+        message: 'Candle created successfully'
127
+      });
128
+    } catch (error) {
129
+      next(error);
130
+    }
131
+  }
132
+
133
+  // Bulk create candles
134
+  async bulkCreateCandles(req, res, next) {
135
+    try {
136
+      const { candles } = req.body;
137
+
138
+      if (!Array.isArray(candles) || candles.length === 0) {
139
+        const error = new Error('Candles array is required');
140
+        error.statusCode = 400;
141
+        return next(error);
142
+      }
143
+
144
+      // Verify all symbols exist
145
+      const symbolIds = [...new Set(candles.map(c => c.symbolId))];
146
+      const existingSymbols = await Symbol.findAll({
147
+        where: { id: symbolIds },
148
+        attributes: ['id']
149
+      });
150
+
151
+      const existingSymbolIds = existingSymbols.map(s => s.id);
152
+      const invalidSymbolIds = symbolIds.filter(id => !existingSymbolIds.includes(id));
153
+
154
+      if (invalidSymbolIds.length > 0) {
155
+        const error = new Error(`Invalid symbol IDs: ${invalidSymbolIds.join(', ')}`);
156
+        error.statusCode = 400;
157
+        return next(error);
158
+      }
159
+
160
+      const createdCandles = await Candle1h.bulkCreate(candles);
161
+
162
+      res.status(201).json({
163
+        success: true,
164
+        data: createdCandles,
165
+        message: `${createdCandles.length} candles created successfully`
166
+      });
167
+    } catch (error) {
168
+      next(error);
169
+    }
170
+  }
171
+
172
+  // Get OHLC data aggregated by time period
173
+  async getOHLC(req, res, next) {
174
+    try {
175
+      const { symbolId, period = '1h', limit = 100 } = req.query;
176
+
177
+      // Verify symbol exists
178
+      const symbol = await Symbol.findByPk(symbolId);
179
+      if (!symbol) {
180
+        const error = new Error('Symbol not found');
181
+        error.statusCode = 404;
182
+        return next(error);
183
+      }
184
+
185
+      // For now, only support 1h period since we only have candles_1h table
186
+      if (period !== '1h') {
187
+        const error = new Error('Only 1h period is currently supported');
188
+        error.statusCode = 400;
189
+        return next(error);
190
+      }
191
+
192
+      const candles = await Candle1h.findAll({
193
+        where: { symbolId: parseInt(symbolId) },
194
+        limit: parseInt(limit),
195
+        order: [['openTime', 'DESC']],
196
+        attributes: ['openTime', 'open', 'high', 'low', 'close', 'volume']
197
+      });
198
+
199
+      res.json({
200
+        success: true,
201
+        data: candles,
202
+        period,
203
+        symbol: symbol.symbol
204
+      });
205
+    } catch (error) {
206
+      next(error);
207
+    }
208
+  }
209
+}
210
+
211
+module.exports = new CandleController();

+ 251 - 0
src/controllers/livePriceController.js

@@ -0,0 +1,251 @@
1
+const { LivePrice, Symbol } = require('../models');
2
+
3
+class LivePriceController {
4
+  // Get all live prices
5
+  async getAllLivePrices(req, res, next) {
6
+    try {
7
+      const { limit = 100, offset = 0 } = req.query;
8
+
9
+      const livePrices = await LivePrice.findAndCountAll({
10
+        limit: parseInt(limit),
11
+        offset: parseInt(offset),
12
+        order: [['lastUpdated', 'DESC']],
13
+        include: [{
14
+          model: Symbol,
15
+          as: 'symbol',
16
+          attributes: ['symbol', 'exchange', 'instrumentType', 'isActive']
17
+        }]
18
+      });
19
+
20
+      res.json({
21
+        success: true,
22
+        data: livePrices.rows,
23
+        pagination: {
24
+          total: livePrices.count,
25
+          limit: parseInt(limit),
26
+          offset: parseInt(offset),
27
+          hasMore: offset + livePrices.rows.length < livePrices.count
28
+        }
29
+      });
30
+    } catch (error) {
31
+      next(error);
32
+    }
33
+  }
34
+
35
+  // Get live price for a specific symbol
36
+  async getLivePrice(req, res, next) {
37
+    try {
38
+      const { symbolId } = req.params;
39
+
40
+      const livePrice = await LivePrice.findOne({
41
+        where: { symbolId: parseInt(symbolId) },
42
+        include: [{
43
+          model: Symbol,
44
+          as: 'symbol',
45
+          attributes: ['symbol', 'exchange', 'instrumentType', 'isActive']
46
+        }]
47
+      });
48
+
49
+      if (!livePrice) {
50
+        const error = new Error('Live price not found for this symbol');
51
+        error.statusCode = 404;
52
+        return next(error);
53
+      }
54
+
55
+      res.json({
56
+        success: true,
57
+        data: livePrice
58
+      });
59
+    } catch (error) {
60
+      next(error);
61
+    }
62
+  }
63
+
64
+  // Update or create live price
65
+  async upsertLivePrice(req, res, next) {
66
+    try {
67
+      const { symbolId, price, bid, ask, bidSize, askSize } = req.body;
68
+
69
+      // Verify symbol exists and is active
70
+      const symbol = await Symbol.findByPk(symbolId);
71
+      if (!symbol) {
72
+        const error = new Error('Symbol not found');
73
+        error.statusCode = 404;
74
+        return next(error);
75
+      }
76
+
77
+      if (!symbol.isActive) {
78
+        const error = new Error('Cannot update live price for inactive symbol');
79
+        error.statusCode = 400;
80
+        return next(error);
81
+      }
82
+
83
+      const [livePrice, created] = await LivePrice.upsert({
84
+        symbolId: parseInt(symbolId),
85
+        price,
86
+        bid,
87
+        ask,
88
+        bidSize,
89
+        askSize,
90
+        lastUpdated: new Date()
91
+      });
92
+
93
+      res.status(created ? 201 : 200).json({
94
+        success: true,
95
+        data: livePrice,
96
+        message: created ? 'Live price created successfully' : 'Live price updated successfully'
97
+      });
98
+    } catch (error) {
99
+      next(error);
100
+    }
101
+  }
102
+
103
+  // Bulk update live prices
104
+  async bulkUpdateLivePrices(req, res, next) {
105
+    try {
106
+      const { prices } = req.body;
107
+
108
+      if (!Array.isArray(prices) || prices.length === 0) {
109
+        const error = new Error('Prices array is required');
110
+        error.statusCode = 400;
111
+        return next(error);
112
+      }
113
+
114
+      // Verify all symbols exist and are active
115
+      const symbolIds = prices.map(p => p.symbolId);
116
+      const existingSymbols = await Symbol.findAll({
117
+        where: {
118
+          id: symbolIds,
119
+          isActive: true
120
+        },
121
+        attributes: ['id']
122
+      });
123
+
124
+      const existingSymbolIds = existingSymbols.map(s => s.id);
125
+      const invalidSymbolIds = symbolIds.filter(id => !existingSymbolIds.includes(id));
126
+
127
+      if (invalidSymbolIds.length > 0) {
128
+        const error = new Error(`Invalid or inactive symbol IDs: ${invalidSymbolIds.join(', ')}`);
129
+        error.statusCode = 400;
130
+        return next(error);
131
+      }
132
+
133
+      // Prepare data for bulk upsert
134
+      const upsertData = prices.map(price => ({
135
+        symbolId: parseInt(price.symbolId),
136
+        price: price.price,
137
+        bid: price.bid,
138
+        ask: price.ask,
139
+        bidSize: price.bidSize,
140
+        askSize: price.askSize,
141
+        lastUpdated: new Date()
142
+      }));
143
+
144
+      const updatedPrices = [];
145
+      for (const data of upsertData) {
146
+        const [livePrice] = await LivePrice.upsert(data);
147
+        updatedPrices.push(livePrice);
148
+      }
149
+
150
+      res.json({
151
+        success: true,
152
+        data: updatedPrices,
153
+        message: `${updatedPrices.length} live prices updated successfully`
154
+      });
155
+    } catch (error) {
156
+      next(error);
157
+    }
158
+  }
159
+
160
+  // Get live prices by exchange
161
+  async getLivePricesByExchange(req, res, next) {
162
+    try {
163
+      const { exchange } = req.params;
164
+      const { limit = 100, offset = 0 } = req.query;
165
+
166
+      const livePrices = await LivePrice.findAndCountAll({
167
+        limit: parseInt(limit),
168
+        offset: parseInt(offset),
169
+        order: [['lastUpdated', 'DESC']],
170
+        include: [{
171
+          model: Symbol,
172
+          as: 'symbol',
173
+          where: { exchange, isActive: true },
174
+          attributes: ['symbol', 'exchange', 'instrumentType']
175
+        }]
176
+      });
177
+
178
+      res.json({
179
+        success: true,
180
+        data: livePrices.rows,
181
+        pagination: {
182
+          total: livePrices.count,
183
+          limit: parseInt(limit),
184
+          offset: parseInt(offset),
185
+          hasMore: offset + livePrices.rows.length < livePrices.count
186
+        }
187
+      });
188
+    } catch (error) {
189
+      next(error);
190
+    }
191
+  }
192
+
193
+  // Get live prices by instrument type
194
+  async getLivePricesByType(req, res, next) {
195
+    try {
196
+      const { type } = req.params;
197
+      const { limit = 100, offset = 0 } = req.query;
198
+
199
+      const livePrices = await LivePrice.findAndCountAll({
200
+        limit: parseInt(limit),
201
+        offset: parseInt(offset),
202
+        order: [['lastUpdated', 'DESC']],
203
+        include: [{
204
+          model: Symbol,
205
+          as: 'symbol',
206
+          where: { instrumentType: type, isActive: true },
207
+          attributes: ['symbol', 'exchange', 'instrumentType']
208
+        }]
209
+      });
210
+
211
+      res.json({
212
+        success: true,
213
+        data: livePrices.rows,
214
+        pagination: {
215
+          total: livePrices.count,
216
+          limit: parseInt(limit),
217
+          offset: parseInt(offset),
218
+          hasMore: offset + livePrices.rows.length < livePrices.count
219
+        }
220
+      });
221
+    } catch (error) {
222
+      next(error);
223
+    }
224
+  }
225
+
226
+  // Delete live price for a symbol
227
+  async deleteLivePrice(req, res, next) {
228
+    try {
229
+      const { symbolId } = req.params;
230
+
231
+      const deletedRowsCount = await LivePrice.destroy({
232
+        where: { symbolId: parseInt(symbolId) }
233
+      });
234
+
235
+      if (deletedRowsCount === 0) {
236
+        const error = new Error('Live price not found for this symbol');
237
+        error.statusCode = 404;
238
+        return next(error);
239
+      }
240
+
241
+      res.json({
242
+        success: true,
243
+        message: 'Live price deleted successfully'
244
+      });
245
+    } catch (error) {
246
+      next(error);
247
+    }
248
+  }
249
+}
250
+
251
+module.exports = new LivePriceController();

+ 164 - 0
src/controllers/symbolController.js

@@ -0,0 +1,164 @@
1
+const { Symbol } = require('../models');
2
+const { Op } = require('sequelize');
3
+
4
+class SymbolController {
5
+  // Get all symbols with optional filtering
6
+  async getAllSymbols(req, res, next) {
7
+    try {
8
+      const {
9
+        exchange,
10
+        instrumentType,
11
+        isActive = true,
12
+        limit = 100,
13
+        offset = 0
14
+      } = req.query;
15
+
16
+      const where = {};
17
+      if (exchange) where.exchange = exchange;
18
+      if (instrumentType) where.instrumentType = instrumentType;
19
+      if (isActive !== undefined) where.isActive = isActive === 'true';
20
+
21
+      const symbols = await Symbol.findAndCountAll({
22
+        where,
23
+        limit: parseInt(limit),
24
+        offset: parseInt(offset),
25
+        order: [['symbol', 'ASC']]
26
+      });
27
+
28
+      res.json({
29
+        success: true,
30
+        data: symbols.rows,
31
+        pagination: {
32
+          total: symbols.count,
33
+          limit: parseInt(limit),
34
+          offset: parseInt(offset),
35
+          hasMore: offset + symbols.rows.length < symbols.count
36
+        }
37
+      });
38
+    } catch (error) {
39
+      next(error);
40
+    }
41
+  }
42
+
43
+  // Get symbol by ID
44
+  async getSymbolById(req, res, next) {
45
+    try {
46
+      const { id } = req.params;
47
+
48
+      const symbol = await Symbol.findByPk(id);
49
+
50
+      if (!symbol) {
51
+        const error = new Error('Symbol not found');
52
+        error.statusCode = 404;
53
+        return next(error);
54
+      }
55
+
56
+      res.json({
57
+        success: true,
58
+        data: symbol
59
+      });
60
+    } catch (error) {
61
+      next(error);
62
+    }
63
+  }
64
+
65
+  // Create new symbol
66
+  async createSymbol(req, res, next) {
67
+    try {
68
+      const symbol = await Symbol.create(req.body);
69
+
70
+      res.status(201).json({
71
+        success: true,
72
+        data: symbol,
73
+        message: 'Symbol created successfully'
74
+      });
75
+    } catch (error) {
76
+      next(error);
77
+    }
78
+  }
79
+
80
+  // Update symbol
81
+  async updateSymbol(req, res, next) {
82
+    try {
83
+      const { id } = req.params;
84
+
85
+      const [updatedRowsCount] = await Symbol.update(req.body, {
86
+        where: { id }
87
+      });
88
+
89
+      if (updatedRowsCount === 0) {
90
+        const error = new Error('Symbol not found');
91
+        error.statusCode = 404;
92
+        return next(error);
93
+      }
94
+
95
+      const updatedSymbol = await Symbol.findByPk(id);
96
+
97
+      res.json({
98
+        success: true,
99
+        data: updatedSymbol,
100
+        message: 'Symbol updated successfully'
101
+      });
102
+    } catch (error) {
103
+      next(error);
104
+    }
105
+  }
106
+
107
+  // Delete symbol (soft delete by setting isActive to false)
108
+  async deleteSymbol(req, res, next) {
109
+    try {
110
+      const { id } = req.params;
111
+
112
+      const [updatedRowsCount] = await Symbol.update(
113
+        { isActive: false },
114
+        { where: { id } }
115
+      );
116
+
117
+      if (updatedRowsCount === 0) {
118
+        const error = new Error('Symbol not found');
119
+        error.statusCode = 404;
120
+        return next(error);
121
+      }
122
+
123
+      res.json({
124
+        success: true,
125
+        message: 'Symbol deactivated successfully'
126
+      });
127
+    } catch (error) {
128
+      next(error);
129
+    }
130
+  }
131
+
132
+  // Search symbols by symbol name
133
+  async searchSymbols(req, res, next) {
134
+    try {
135
+      const { q, limit = 20 } = req.query;
136
+
137
+      if (!q) {
138
+        const error = new Error('Search query is required');
139
+        error.statusCode = 400;
140
+        return next(error);
141
+      }
142
+
143
+      const symbols = await Symbol.findAll({
144
+        where: {
145
+          symbol: {
146
+            [Op.iLike]: `%${q}%`
147
+          },
148
+          isActive: true
149
+        },
150
+        limit: parseInt(limit),
151
+        order: [['symbol', 'ASC']]
152
+      });
153
+
154
+      res.json({
155
+        success: true,
156
+        data: symbols
157
+      });
158
+    } catch (error) {
159
+      next(error);
160
+    }
161
+  }
162
+}
163
+
164
+module.exports = new SymbolController();

+ 60 - 0
src/middleware/errorHandler.js

@@ -0,0 +1,60 @@
1
+const errorHandler = (err, req, res, next) => {
2
+  console.error(err.stack);
3
+
4
+  // Sequelize validation error
5
+  if (err.name === 'SequelizeValidationError') {
6
+    const errors = err.errors.map(error => ({
7
+      field: error.path,
8
+      message: error.message,
9
+      value: error.value
10
+    }));
11
+
12
+    return res.status(400).json({
13
+      success: false,
14
+      message: 'Validation error',
15
+      errors
16
+    });
17
+  }
18
+
19
+  // Sequelize unique constraint error
20
+  if (err.name === 'SequelizeUniqueConstraintError') {
21
+    return res.status(409).json({
22
+      success: false,
23
+      message: 'Duplicate entry',
24
+      error: err.errors[0].message
25
+    });
26
+  }
27
+
28
+  // Sequelize foreign key constraint error
29
+  if (err.name === 'SequelizeForeignKeyConstraintError') {
30
+    return res.status(400).json({
31
+      success: false,
32
+      message: 'Foreign key constraint error',
33
+      error: err.message
34
+    });
35
+  }
36
+
37
+  // Joi validation error
38
+  if (err.isJoi) {
39
+    return res.status(400).json({
40
+      success: false,
41
+      message: 'Validation error',
42
+      details: err.details.map(detail => ({
43
+        field: detail.path.join('.'),
44
+        message: detail.message
45
+      }))
46
+    });
47
+  }
48
+
49
+  // Default error
50
+  const statusCode = err.statusCode || 500;
51
+  const message = err.message || 'Internal server error';
52
+
53
+  res.status(statusCode).json({
54
+    success: false,
55
+    message,
56
+    ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
57
+  });
58
+};
59
+
60
+module.exports = errorHandler;

+ 90 - 0
src/middleware/validation.js

@@ -0,0 +1,90 @@
1
+const Joi = require('joi');
2
+
3
+// Symbol validation schemas
4
+const symbolSchema = Joi.object({
5
+  symbol: Joi.string().max(50).required(),
6
+  baseAsset: Joi.string().max(50),
7
+  quoteAsset: Joi.string().max(50),
8
+  exchange: Joi.string().max(50),
9
+  instrumentType: Joi.string().valid('crypto', 'stock', 'forex', 'commodity').required(),
10
+  isActive: Joi.boolean().default(true)
11
+});
12
+
13
+const symbolIdSchema = Joi.object({
14
+  id: Joi.number().integer().positive().required()
15
+});
16
+
17
+// Candle validation schemas
18
+const candleQuerySchema = Joi.object({
19
+  symbolId: Joi.number().integer().positive().required(),
20
+  startTime: Joi.date().iso(),
21
+  endTime: Joi.date().iso().when('startTime', {
22
+    is: Joi.exist(),
23
+    then: Joi.date().iso().greater(Joi.ref('startTime'))
24
+  }),
25
+  limit: Joi.number().integer().min(1).max(1000).default(100),
26
+  offset: Joi.number().integer().min(0).default(0)
27
+});
28
+
29
+// Live price validation schemas
30
+const livePriceSchema = Joi.object({
31
+  symbolId: Joi.number().integer().positive().required(),
32
+  price: Joi.number().precision(8).positive().required(),
33
+  bid: Joi.number().precision(8).positive(),
34
+  ask: Joi.number().precision(8).positive(),
35
+  bidSize: Joi.number().precision(8).positive(),
36
+  askSize: Joi.number().precision(8).positive()
37
+});
38
+
39
+// Middleware functions
40
+const validate = (schema) => {
41
+  return (req, res, next) => {
42
+    const { error, value } = schema.validate(req.body, { abortEarly: false });
43
+
44
+    if (error) {
45
+      error.isJoi = true;
46
+      return next(error);
47
+    }
48
+
49
+    req.body = value;
50
+    next();
51
+  };
52
+};
53
+
54
+const validateQuery = (schema) => {
55
+  return (req, res, next) => {
56
+    const { error, value } = schema.validate(req.query, { abortEarly: false });
57
+
58
+    if (error) {
59
+      error.isJoi = true;
60
+      return next(error);
61
+    }
62
+
63
+    req.query = value;
64
+    next();
65
+  };
66
+};
67
+
68
+const validateParams = (schema) => {
69
+  return (req, res, next) => {
70
+    const { error, value } = schema.validate(req.params, { abortEarly: false });
71
+
72
+    if (error) {
73
+      error.isJoi = true;
74
+      return next(error);
75
+    }
76
+
77
+    req.params = value;
78
+    next();
79
+  };
80
+};
81
+
82
+module.exports = {
83
+  symbolSchema,
84
+  symbolIdSchema,
85
+  candleQuerySchema,
86
+  livePriceSchema,
87
+  validate,
88
+  validateQuery,
89
+  validateParams
90
+};

+ 73 - 0
src/models/Candle1h.js

@@ -0,0 +1,73 @@
1
+const { DataTypes } = require('sequelize');
2
+const { sequelize } = require('../config/database');
3
+const Symbol = require('./Symbol');
4
+
5
+const Candle1h = sequelize.define('Candle1h', {
6
+  id: {
7
+    type: DataTypes.BIGINT,
8
+    primaryKey: true,
9
+    autoIncrement: true
10
+  },
11
+  symbolId: {
12
+    type: DataTypes.INTEGER,
13
+    field: 'symbol_id',
14
+    allowNull: false,
15
+    references: {
16
+      model: Symbol,
17
+      key: 'id'
18
+    }
19
+  },
20
+  openTime: {
21
+    type: DataTypes.DATE,
22
+    field: 'open_time',
23
+    allowNull: false
24
+  },
25
+  closeTime: {
26
+    type: DataTypes.DATE,
27
+    field: 'close_time',
28
+    allowNull: false
29
+  },
30
+  open: {
31
+    type: DataTypes.DECIMAL(18, 8),
32
+    allowNull: false
33
+  },
34
+  high: {
35
+    type: DataTypes.DECIMAL(18, 8),
36
+    allowNull: false
37
+  },
38
+  low: {
39
+    type: DataTypes.DECIMAL(18, 8),
40
+    allowNull: false
41
+  },
42
+  close: {
43
+    type: DataTypes.DECIMAL(18, 8),
44
+    allowNull: false
45
+  },
46
+  volume: {
47
+    type: DataTypes.DECIMAL(20, 8)
48
+  },
49
+  tradesCount: {
50
+    type: DataTypes.INTEGER,
51
+    field: 'trades_count'
52
+  },
53
+  quoteVolume: {
54
+    type: DataTypes.DECIMAL(20, 8),
55
+    field: 'quote_volume'
56
+  },
57
+  createdAt: {
58
+    type: DataTypes.DATE,
59
+    field: 'created_at'
60
+  }
61
+}, {
62
+  tableName: 'candles_1h',
63
+  indexes: [
64
+    { unique: true, fields: ['symbol_id', 'open_time'] },
65
+    { fields: ['open_time'] }
66
+  ]
67
+});
68
+
69
+// Define associations
70
+Candle1h.belongsTo(Symbol, { foreignKey: 'symbolId', as: 'symbol' });
71
+Symbol.hasMany(Candle1h, { foreignKey: 'symbolId', as: 'candles1h' });
72
+
73
+module.exports = Candle1h;

+ 49 - 0
src/models/LivePrice.js

@@ -0,0 +1,49 @@
1
+const { DataTypes } = require('sequelize');
2
+const { sequelize } = require('../config/database');
3
+const Symbol = require('./Symbol');
4
+
5
+const LivePrice = sequelize.define('LivePrice', {
6
+  symbolId: {
7
+    type: DataTypes.INTEGER,
8
+    primaryKey: true,
9
+    field: 'symbol_id',
10
+    references: {
11
+      model: Symbol,
12
+      key: 'id'
13
+    }
14
+  },
15
+  price: {
16
+    type: DataTypes.DECIMAL(18, 8),
17
+    allowNull: false
18
+  },
19
+  bid: {
20
+    type: DataTypes.DECIMAL(18, 8)
21
+  },
22
+  ask: {
23
+    type: DataTypes.DECIMAL(18, 8)
24
+  },
25
+  bidSize: {
26
+    type: DataTypes.DECIMAL(18, 8),
27
+    field: 'bid_size'
28
+  },
29
+  askSize: {
30
+    type: DataTypes.DECIMAL(18, 8),
31
+    field: 'ask_size'
32
+  },
33
+  lastUpdated: {
34
+    type: DataTypes.DATE,
35
+    field: 'last_updated',
36
+    defaultValue: DataTypes.NOW
37
+  }
38
+}, {
39
+  tableName: 'live_prices',
40
+  indexes: [
41
+    { fields: ['price'] }
42
+  ]
43
+});
44
+
45
+// Define associations
46
+LivePrice.belongsTo(Symbol, { foreignKey: 'symbolId', as: 'symbol' });
47
+Symbol.hasOne(LivePrice, { foreignKey: 'symbolId', as: 'livePrice' });
48
+
49
+module.exports = LivePrice;

+ 52 - 0
src/models/Symbol.js

@@ -0,0 +1,52 @@
1
+const { DataTypes } = require('sequelize');
2
+const { sequelize } = require('../config/database');
3
+
4
+const Symbol = sequelize.define('Symbol', {
5
+  id: {
6
+    type: DataTypes.INTEGER,
7
+    primaryKey: true,
8
+    autoIncrement: true
9
+  },
10
+  symbol: {
11
+    type: DataTypes.STRING(50),
12
+    allowNull: false,
13
+    unique: true
14
+  },
15
+  baseAsset: {
16
+    type: DataTypes.STRING(50),
17
+    field: 'base_asset'
18
+  },
19
+  quoteAsset: {
20
+    type: DataTypes.STRING(50),
21
+    field: 'quote_asset'
22
+  },
23
+  exchange: {
24
+    type: DataTypes.STRING(50)
25
+  },
26
+  instrumentType: {
27
+    type: DataTypes.ENUM('crypto', 'stock', 'forex', 'commodity'),
28
+    field: 'instrument_type',
29
+    allowNull: false
30
+  },
31
+  isActive: {
32
+    type: DataTypes.BOOLEAN,
33
+    field: 'is_active',
34
+    defaultValue: true
35
+  },
36
+  createdAt: {
37
+    type: DataTypes.DATE,
38
+    field: 'created_at'
39
+  },
40
+  updatedAt: {
41
+    type: DataTypes.DATE,
42
+    field: 'updated_at'
43
+  }
44
+}, {
45
+  tableName: 'symbols',
46
+  indexes: [
47
+    { fields: ['exchange'] },
48
+    { fields: ['instrument_type'] }
49
+  ]
50
+});
51
+
52
+module.exports = Symbol;

+ 29 - 0
src/models/index.js

@@ -0,0 +1,29 @@
1
+const { sequelize } = require('../config/database');
2
+const Symbol = require('./Symbol');
3
+const Candle1h = require('./Candle1h');
4
+const LivePrice = require('./LivePrice');
5
+
6
+// Define associations
7
+Symbol.hasMany(Candle1h, { foreignKey: 'symbolId', as: 'candles1h' });
8
+Candle1h.belongsTo(Symbol, { foreignKey: 'symbolId', as: 'symbol' });
9
+
10
+Symbol.hasOne(LivePrice, { foreignKey: 'symbolId', as: 'livePrice' });
11
+LivePrice.belongsTo(Symbol, { foreignKey: 'symbolId', as: 'symbol' });
12
+
13
+// Sync database (only in development)
14
+if (process.env.NODE_ENV === 'development') {
15
+  sequelize.sync({ alter: true })
16
+    .then(() => {
17
+      console.log('Database synchronized successfully.');
18
+    })
19
+    .catch((error) => {
20
+      console.error('Error synchronizing database:', error);
21
+    });
22
+}
23
+
24
+module.exports = {
25
+  sequelize,
26
+  Symbol,
27
+  Candle1h,
28
+  LivePrice
29
+};

+ 61 - 0
src/routes/candles.js

@@ -0,0 +1,61 @@
1
+const express = require('express');
2
+const router = express.Router();
3
+const candleController = require('../controllers/candleController');
4
+const { validate, validateQuery, validateParams } = require('../middleware/validation');
5
+const Joi = require('joi');
6
+
7
+// GET /api/candles - Get candles with filtering
8
+router.get('/', validateQuery(Joi.object({
9
+  symbolId: Joi.number().integer().positive().required(),
10
+  startTime: Joi.date().iso(),
11
+  endTime: Joi.date().iso().when('startTime', {
12
+    is: Joi.exist(),
13
+    then: Joi.date().iso().greater(Joi.ref('startTime'))
14
+  }),
15
+  limit: Joi.number().integer().min(1).max(1000).default(100),
16
+  offset: Joi.number().integer().min(0).default(0)
17
+})), candleController.getCandles);
18
+
19
+// GET /api/candles/ohlc - Get OHLC data
20
+router.get('/ohlc', validateQuery(Joi.object({
21
+  symbolId: Joi.number().integer().positive().required(),
22
+  period: Joi.string().valid('1h').default('1h'),
23
+  limit: Joi.number().integer().min(1).max(1000).default(100)
24
+})), candleController.getOHLC);
25
+
26
+// GET /api/candles/:symbolId/latest - Get latest candle for a symbol
27
+router.get('/:symbolId/latest', validateParams(Joi.object({
28
+  symbolId: Joi.number().integer().positive().required()
29
+})), candleController.getLatestCandle);
30
+
31
+// POST /api/candles - Create new candle
32
+router.post('/', validate(Joi.object({
33
+  symbolId: Joi.number().integer().positive().required(),
34
+  openTime: Joi.date().iso().required(),
35
+  closeTime: Joi.date().iso().required(),
36
+  open: Joi.number().precision(8).positive().required(),
37
+  high: Joi.number().precision(8).positive().required(),
38
+  low: Joi.number().precision(8).positive().required(),
39
+  close: Joi.number().precision(8).positive().required(),
40
+  volume: Joi.number().precision(8).positive(),
41
+  tradesCount: Joi.number().integer().min(0),
42
+  quoteVolume: Joi.number().precision(8).positive()
43
+})), candleController.createCandle);
44
+
45
+// POST /api/candles/bulk - Bulk create candles
46
+router.post('/bulk', validate(Joi.object({
47
+  candles: Joi.array().items(Joi.object({
48
+    symbolId: Joi.number().integer().positive().required(),
49
+    openTime: Joi.date().iso().required(),
50
+    closeTime: Joi.date().iso().required(),
51
+    open: Joi.number().precision(8).positive().required(),
52
+    high: Joi.number().precision(8).positive().required(),
53
+    low: Joi.number().precision(8).positive().required(),
54
+    close: Joi.number().precision(8).positive().required(),
55
+    volume: Joi.number().precision(8).positive(),
56
+    tradesCount: Joi.number().integer().min(0),
57
+    quoteVolume: Joi.number().precision(8).positive()
58
+  })).min(1).required()
59
+})), candleController.bulkCreateCandles);
60
+
61
+module.exports = router;

+ 47 - 0
src/routes/livePrices.js

@@ -0,0 +1,47 @@
1
+const express = require('express');
2
+const router = express.Router();
3
+const livePriceController = require('../controllers/livePriceController');
4
+const { validate, validateQuery, validateParams, livePriceSchema } = require('../middleware/validation');
5
+const Joi = require('joi');
6
+
7
+// GET /api/live-prices - Get all live prices
8
+router.get('/', validateQuery(Joi.object({
9
+  limit: Joi.number().integer().min(1).max(1000).default(100),
10
+  offset: Joi.number().integer().min(0).default(0)
11
+})), livePriceController.getAllLivePrices);
12
+
13
+// GET /api/live-prices/exchange/:exchange - Get live prices by exchange
14
+router.get('/exchange/:exchange', validateParams(Joi.object({
15
+  exchange: Joi.string().max(50).required()
16
+})), validateQuery(Joi.object({
17
+  limit: Joi.number().integer().min(1).max(1000).default(100),
18
+  offset: Joi.number().integer().min(0).default(0)
19
+})), livePriceController.getLivePricesByExchange);
20
+
21
+// GET /api/live-prices/type/:type - Get live prices by instrument type
22
+router.get('/type/:type', validateParams(Joi.object({
23
+  type: Joi.string().valid('crypto', 'stock', 'forex', 'commodity').required()
24
+})), validateQuery(Joi.object({
25
+  limit: Joi.number().integer().min(1).max(1000).default(100),
26
+  offset: Joi.number().integer().min(0).default(0)
27
+})), livePriceController.getLivePricesByType);
28
+
29
+// GET /api/live-prices/:symbolId - Get live price for a specific symbol
30
+router.get('/:symbolId', validateParams(Joi.object({
31
+  symbolId: Joi.number().integer().positive().required()
32
+})), livePriceController.getLivePrice);
33
+
34
+// POST /api/live-prices - Create or update live price
35
+router.post('/', validate(livePriceSchema), livePriceController.upsertLivePrice);
36
+
37
+// POST /api/live-prices/bulk - Bulk update live prices
38
+router.post('/bulk', validate(Joi.object({
39
+  prices: Joi.array().items(livePriceSchema).min(1).required()
40
+})), livePriceController.bulkUpdateLivePrices);
41
+
42
+// DELETE /api/live-prices/:symbolId - Delete live price for a symbol
43
+router.delete('/:symbolId', validateParams(Joi.object({
44
+  symbolId: Joi.number().integer().positive().required()
45
+})), livePriceController.deleteLivePrice);
46
+
47
+module.exports = router;

+ 33 - 0
src/routes/symbols.js

@@ -0,0 +1,33 @@
1
+const express = require('express');
2
+const router = express.Router();
3
+const symbolController = require('../controllers/symbolController');
4
+const { validate, validateQuery, validateParams, symbolSchema, symbolIdSchema } = require('../middleware/validation');
5
+
6
+// GET /api/symbols - Get all symbols with optional filtering
7
+router.get('/', validateQuery(require('joi').object({
8
+  exchange: require('joi').string().max(50),
9
+  instrumentType: require('joi').string().valid('crypto', 'stock', 'forex', 'commodity'),
10
+  isActive: require('joi').string().valid('true', 'false'),
11
+  limit: require('joi').number().integer().min(1).max(1000).default(100),
12
+  offset: require('joi').number().integer().min(0).default(0)
13
+})), symbolController.getAllSymbols);
14
+
15
+// GET /api/symbols/search - Search symbols by name
16
+router.get('/search', validateQuery(require('joi').object({
17
+  q: require('joi').string().required(),
18
+  limit: require('joi').number().integer().min(1).max(100).default(20)
19
+})), symbolController.searchSymbols);
20
+
21
+// GET /api/symbols/:id - Get symbol by ID
22
+router.get('/:id', validateParams(symbolIdSchema), symbolController.getSymbolById);
23
+
24
+// POST /api/symbols - Create new symbol
25
+router.post('/', validate(symbolSchema), symbolController.createSymbol);
26
+
27
+// PUT /api/symbols/:id - Update symbol
28
+router.put('/:id', validateParams(symbolIdSchema), validate(symbolSchema), symbolController.updateSymbol);
29
+
30
+// DELETE /api/symbols/:id - Delete symbol (soft delete)
31
+router.delete('/:id', validateParams(symbolIdSchema), symbolController.deleteSymbol);
32
+
33
+module.exports = router;

+ 55 - 0
src/server.js

@@ -0,0 +1,55 @@
1
+const app = require('./app');
2
+const { testConnection } = require('./config/database');
3
+const logger = require('./utils/logger');
4
+
5
+const PORT = process.env.PORT || 3000;
6
+
7
+// Test database connection and start server
8
+const startServer = async () => {
9
+  try {
10
+    // Test database connection
11
+    await testConnection();
12
+
13
+    // Start the server
14
+    const server = app.listen(PORT, () => {
15
+      logger.info(`Market Data Service is running on port ${PORT}`);
16
+      logger.info(`Environment: ${process.env.NODE_ENV || 'development'}`);
17
+      logger.info(`Health check available at: http://localhost:${PORT}/health`);
18
+    });
19
+
20
+    // Graceful shutdown
21
+    process.on('SIGTERM', () => {
22
+      logger.info('SIGTERM received, shutting down gracefully');
23
+      server.close(() => {
24
+        logger.info('Process terminated');
25
+        process.exit(0);
26
+      });
27
+    });
28
+
29
+    process.on('SIGINT', () => {
30
+      logger.info('SIGINT received, shutting down gracefully');
31
+      server.close(() => {
32
+        logger.info('Process terminated');
33
+        process.exit(0);
34
+      });
35
+    });
36
+
37
+  } catch (error) {
38
+    logger.error('Failed to start server:', error);
39
+    process.exit(1);
40
+  }
41
+};
42
+
43
+// Handle uncaught exceptions
44
+process.on('uncaughtException', (error) => {
45
+  logger.error('Uncaught Exception:', error);
46
+  process.exit(1);
47
+});
48
+
49
+// Handle unhandled promise rejections
50
+process.on('unhandledRejection', (reason, promise) => {
51
+  logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
52
+  process.exit(1);
53
+});
54
+
55
+startServer();

+ 54 - 0
src/utils/logger.js

@@ -0,0 +1,54 @@
1
+const winston = require('winston');
2
+
3
+// Define log levels
4
+const levels = {
5
+  error: 0,
6
+  warn: 1,
7
+  info: 2,
8
+  http: 3,
9
+  debug: 4,
10
+};
11
+
12
+// Define colors for each level
13
+const colors = {
14
+  error: 'red',
15
+  warn: 'yellow',
16
+  info: 'green',
17
+  http: 'magenta',
18
+  debug: 'white',
19
+};
20
+
21
+// Tell winston that we want to link the colors
22
+winston.addColors(colors);
23
+
24
+// Define the format for logs
25
+const format = winston.format.combine(
26
+  winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }),
27
+  winston.format.colorize({ all: true }),
28
+  winston.format.printf(
29
+    (info) => `${info.timestamp} ${info.level}: ${info.message}`,
30
+  ),
31
+);
32
+
33
+// Define which transports the logger must use
34
+const transports = [
35
+  // Allow the use the console to print the messages
36
+  new winston.transports.Console(),
37
+  // Allow to print all the error level messages inside the error.log file
38
+  new winston.transports.File({
39
+    filename: 'logs/error.log',
40
+    level: 'error',
41
+  }),
42
+  // Allow to print all the error message inside the all.log file
43
+  new winston.transports.File({ filename: 'logs/all.log' }),
44
+];
45
+
46
+// Create the logger instance
47
+const logger = winston.createLogger({
48
+  level: process.env.NODE_ENV === 'development' ? 'debug' : 'warn',
49
+  levels,
50
+  format,
51
+  transports,
52
+});
53
+
54
+module.exports = logger;