Bladeren bron

project architecture

project architecture
uzairrizwan1 9 maanden geleden
bovenliggende
commit
72dc6c92b6
12 gewijzigde bestanden met toevoegingen van 2484 en 0 verwijderingen
  1. 45 0
      .env.example
  2. 1 0
      config/database.js
  3. 129 0
      middleware/errorHandler.js
  4. 46 0
      package.json
  5. 366 0
      public/index.html
  6. 373 0
      routes/priceRoutes.js
  7. 367 0
      services/priceUpdateService.js
  8. 159 0
      src/server.js
  9. 203 0
      tests/candleValidation.test.js
  10. 166 0
      tests/server.test.js
  11. 494 0
      utils/databaseInit.js
  12. 135 0
      utils/logger.js

+ 45 - 0
.env.example

@@ -0,0 +1,45 @@
+# Server Configuration
+NODE_ENV=development
+PORT=3001
+CLIENT_URL=http://localhost:3000
+
+# Database Configuration
+DB_TYPE=mysql
+DB_HOST=localhost
+DB_PORT=3306
+DB_NAME=financial_data
+DB_USER=root
+DB_PASSWORD=password
+
+# Alternative Database Options (SQLite for development)
+# DB_TYPE=sqlite
+# DB_FILE=./data/financial_data.db
+
+# Redis Configuration (for caching and session storage)
+REDIS_HOST=localhost
+REDIS_PORT=6379
+REDIS_PASSWORD=
+
+# Security
+JWT_SECRET=your-super-secret-jwt-key-here
+API_KEY=your-api-key-here
+
+# Logging
+LOG_LEVEL=info
+LOG_FILE=./logs/server.log
+
+# Rate Limiting
+RATE_LIMIT_WINDOW_MS=900000
+RATE_LIMIT_MAX_REQUESTS=1000
+
+# Price Updates
+PRICE_UPDATE_INTERVAL=1000
+SUPPORTED_SYMBOLS=AUDUSD,EURUSD,GBPUSD,USDJPY,USDCHF,USDCAD,NZDUSD
+
+# External APIs (if needed)
+ALPHA_VANTAGE_API_KEY=
+FINNHUB_API_KEY=
+
+# WebSocket Configuration
+WS_HEARTBEAT_INTERVAL=30000
+WS_MAX_CONNECTIONS=10000

+ 1 - 0
config/database.js

@@ -0,0 +1 @@
+

+ 129 - 0
middleware/errorHandler.js

@@ -0,0 +1,129 @@
+const { logger } = require('../utils/logger');
+
+// Custom Error class
+class AppError extends Error {
+  constructor(message, statusCode, isOperational = true) {
+    super(message);
+    this.statusCode = statusCode;
+    this.isOperational = isOperational;
+    this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
+
+    Error.captureStackTrace(this, this.constructor);
+  }
+}
+
+// Global error handler middleware
+const errorHandler = (err, req, res, next) => {
+  let error = { ...err };
+  error.message = err.message;
+
+  // Log error
+  logger.error('Error occurred:', {
+    message: err.message,
+    stack: err.stack,
+    url: req.originalUrl,
+    method: req.method,
+    ip: req.ip,
+    userAgent: req.get('User-Agent')
+  });
+
+  // Mongoose bad ObjectId
+  if (err.name === 'CastError') {
+    const message = 'Resource not found';
+    error = new AppError(message, 404);
+  }
+
+  // Mongoose duplicate key
+  if (err.code === 11000) {
+    const message = 'Duplicate field value entered';
+    error = new AppError(message, 400);
+  }
+
+  // Mongoose validation error
+  if (err.name === 'ValidationError') {
+    const message = Object.values(err.errors).map(val => val.message).join(', ');
+    error = new AppError(message, 400);
+  }
+
+  // JWT errors
+  if (err.name === 'JsonWebTokenError') {
+    const message = 'Invalid token';
+    error = new AppError(message, 401);
+  }
+
+  if (err.name === 'TokenExpiredError') {
+    const message = 'Token expired';
+    error = new AppError(message, 401);
+  }
+
+  // Database connection errors
+  if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
+    const message = 'Database connection failed';
+    error = new AppError(message, 503);
+  }
+
+  // Send error response
+  const statusCode = error.statusCode || 500;
+  const status = error.status || 'error';
+  const message = error.message || 'Something went wrong';
+
+  res.status(statusCode).json({
+    status,
+    message,
+    ...(process.env.NODE_ENV === 'development' && {
+      stack: err.stack,
+      error: err
+    })
+  });
+};
+
+// 404 handler
+const notFound = (req, res, next) => {
+  const error = new AppError(`Not found - ${req.originalUrl}`, 404);
+  next(error);
+};
+
+// Async error wrapper
+const catchAsync = (fn) => {
+  return (req, res, next) => {
+    fn(req, res, next).catch(next);
+  };
+};
+
+// Development error sender (for debugging)
+const sendErrorDev = (err, res) => {
+  res.status(err.statusCode || 500).json({
+    status: err.status,
+    error: err,
+    message: err.message,
+    stack: err.stack
+  });
+};
+
+// Production error sender (security-conscious)
+const sendErrorProd = (err, res) => {
+  // Operational, trusted error: send message to client
+  if (err.isOperational) {
+    res.status(err.statusCode).json({
+      status: err.status,
+      message: err.message
+    });
+  } else {
+    // Programming or other unknown error: don't leak error details
+    logger.error('ERROR 💥', err);
+
+    res.status(500).json({
+      status: 'error',
+      message: 'Something went wrong!'
+    });
+  }
+};
+
+module.exports = {
+  AppError,
+  errorHandler,
+  notFound,
+  catchAsync,
+  sendErrorDev,
+  sendErrorProd
+};

+ 46 - 0
package.json

@@ -0,0 +1,46 @@
+{
+  "name": "financial-data-server",
+  "version": "1.0.0",
+  "description": "Real-time financial data server with WebSocket support for 2013 symbols",
+  "main": "src/server.js",
+  "scripts": {
+    "start": "node src/server.js",
+    "dev": "nodemon src/server.js",
+    "test": "jest"
+  },
+  "keywords": [
+    "financial-data",
+    "websocket",
+    "real-time",
+    "trading"
+  ],
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "compression": "^1.7.4",
+    "cors": "^2.8.5",
+    "dotenv": "^16.3.1",
+    "express": "^4.18.2",
+    "express-rate-limit": "^6.10.0",
+    "helmet": "^7.0.0",
+    "morgan": "^1.10.0",
+    "mysql2": "^3.6.0",
+    "pg": "^8.16.3",
+    "pg-hstore": "^2.3.4",
+    "redis": "^4.6.7",
+    "sequelize": "^6.37.7",
+    "socket.io": "^4.7.2",
+    "sqlite3": "^5.1.6",
+    "winston": "^3.10.0"
+  },
+  "devDependencies": {
+    "chai": "^4.3.10",
+    "jest": "^29.6.2",
+    "mocha": "^10.2.0",
+    "nodemon": "^3.0.1",
+    "supertest": "^6.3.3"
+  },
+  "engines": {
+    "node": ">=16.0.0"
+  }
+}

+ 366 - 0
public/index.html

@@ -0,0 +1,366 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Financial Data Charts</title>
+    <script src="https://unpkg.com/lightweight-charts@4.1.0/dist/lightweight-charts.standalone.production.mjs"></script>
+    <style>
+        body {
+            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
+            margin: 0;
+            padding: 20px;
+            background-color: #f5f5f5;
+        }
+        .container {
+            max-width: 1200px;
+            margin: 0 auto;
+        }
+        .controls {
+            background: white;
+            padding: 20px;
+            border-radius: 8px;
+            margin-bottom: 20px;
+            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+        }
+        .chart-container {
+            background: white;
+            border-radius: 8px;
+            padding: 20px;
+            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+            height: 600px;
+        }
+        .control-group {
+            margin-bottom: 15px;
+        }
+        label {
+            display: inline-block;
+            width: 100px;
+            font-weight: 500;
+        }
+        select, input, button {
+            padding: 8px 12px;
+            border: 1px solid #ddd;
+            border-radius: 4px;
+            margin-right: 10px;
+        }
+        button {
+            background: #007bff;
+            color: white;
+            cursor: pointer;
+            border: none;
+        }
+        button:hover {
+            background: #0056b3;
+        }
+        .stats {
+            background: #e9ecef;
+            padding: 10px;
+            border-radius: 4px;
+            margin-top: 10px;
+            font-family: monospace;
+        }
+    </style>
+</head>
+<body>
+    <div class="container">
+        <h1>Financial Data Charts & API Testing</h1>
+
+        <div class="controls">
+            <div class="control-group">
+                <label for="symbol">Symbol:</label>
+                <select id="symbol">
+                    <option value="EURUSD">EURUSD</option>
+                    <option value="GBPUSD">GBPUSD</option>
+                    <option value="USDJPY">USDJPY</option>
+                </select>
+
+                <label for="timeframe">Timeframe:</label>
+                <select id="timeframe">
+                    <option value="M1">M1</option>
+                    <option value="M5">M5</option>
+                    <option value="M15">M15</option>
+                    <option value="H1">H1</option>
+                    <option value="H4">H4</option>
+                    <option value="D1">D1</option>
+                </select>
+
+                <label for="limit">Candles:</label>
+                <input type="number" id="limit" value="100" min="10" max="1000">
+
+                <button onclick="loadChart()">Load Chart</button>
+                <button onclick="testCandles()">Test Candles</button>
+            </div>
+
+            <div class="control-group">
+                <button onclick="startRealTime()">Start Real-time</button>
+                <button onclick="stopRealTime()">Stop Real-time</button>
+                <button onclick="validateCandles()">Validate Candles</button>
+            </div>
+
+            <div class="stats" id="stats">
+                Ready to load chart data...
+            </div>
+        </div>
+
+        <div class="chart-container">
+            <div id="chart"></div>
+        </div>
+
+        <div style="margin-top: 20px; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
+            <h3>API Testing Results</h3>
+            <div id="test-results" style="font-family: monospace; white-space: pre-wrap;"></div>
+        </div>
+    </div>
+
+    <script>
+        let chart = null;
+        let candlestickSeries = null;
+        let realTimeInterval = null;
+
+        // Initialize chart
+        function initChart() {
+            const chartContainer = document.getElementById('chart');
+            chart = LightweightCharts.createChart(chartContainer, {
+                width: chartContainer.clientWidth,
+                height: 500,
+                layout: {
+                    background: { color: '#ffffff' },
+                    textColor: '#333',
+                },
+                grid: {
+                    vertLines: { color: '#e1e1e1' },
+                    horzLines: { color: '#e1e1e1' },
+                },
+                crosshair: {
+                    mode: LightweightCharts.CrosshairMode.Normal,
+                },
+                rightPriceScale: {
+                    borderColor: '#485158',
+                },
+                timeScale: {
+                    borderColor: '#485158',
+                    timeVisible: true,
+                    secondsVisible: false,
+                },
+            });
+
+            candlestickSeries = chart.addCandlestickSeries({
+                upColor: '#26a69a',
+                downColor: '#ef5350',
+                borderVisible: false,
+                wickUpColor: '#26a69a',
+                wickDownColor: '#ef5350',
+            });
+        }
+
+        // Load chart data
+        async function loadChart() {
+            const symbol = document.getElementById('symbol').value;
+            const timeframe = document.getElementById('timeframe').value;
+            const limit = document.getElementById('limit').value;
+
+            updateStats(`Loading ${symbol} ${timeframe} candles...`);
+
+            try {
+                const response = await fetch(`/api/prices/historical/${symbol}?timeframe=${timeframe}&limit=${limit}`);
+                const result = await response.json();
+
+                if (result.status === 'success') {
+                    const candles = result.data.map(candle => ({
+                        time: Math.floor(new Date(candle.timestamp).getTime() / 1000),
+                        open: parseFloat(candle.open),
+                        high: parseFloat(candle.high),
+                        low: parseFloat(candle.low),
+                        close: parseFloat(candle.close),
+                    }));
+
+                    if (!chart) {
+                        initChart();
+                    }
+
+                    candlestickSeries.setData(candles);
+                    chart.timeScale().fitContent();
+
+                    updateStats(`Loaded ${candles.length} candles for ${symbol} ${timeframe}`);
+                } else {
+                    updateStats(`Error: ${result.message}`);
+                }
+            } catch (error) {
+                updateStats(`Error loading chart: ${error.message}`);
+            }
+        }
+
+        // Test candles for correctness
+        async function testCandles() {
+            const symbol = document.getElementById('symbol').value;
+            const timeframe = document.getElementById('timeframe').value;
+            const limit = document.getElementById('limit').value;
+
+            updateStats(`Testing ${symbol} ${timeframe} candles...`);
+
+            try {
+                const response = await fetch(`/api/prices/historical/${symbol}?timeframe=${timeframe}&limit=${limit}`);
+                const result = await response.json();
+
+                if (result.status === 'success') {
+                    const tests = validateCandleData(result.data);
+                    displayTestResults(tests);
+                } else {
+                    updateStats(`Error: ${result.message}`);
+                }
+            } catch (error) {
+                updateStats(`Error testing candles: ${error.message}`);
+            }
+        }
+
+        // Validate candle data
+        function validateCandleData(candles) {
+            const tests = {
+                total: candles.length,
+                validOHLC: 0,
+                validPriceLogic: 0,
+                validTimestamps: 0,
+                issues: []
+            };
+
+            candles.forEach((candle, index) => {
+                const open = parseFloat(candle.open);
+                const high = parseFloat(candle.high);
+                const low = parseFloat(candle.low);
+                const close = parseFloat(candle.close);
+
+                // Test 1: OHLC values are valid numbers
+                if (!isNaN(open) && !isNaN(high) && !isNaN(low) && !isNaN(close)) {
+                    tests.validOHLC++;
+                } else {
+                    tests.issues.push(`Candle ${index}: Invalid OHLC values`);
+                }
+
+                // Test 2: Price logic (high >= open, high >= close, low <= open, low <= close)
+                if (high >= Math.max(open, close) && low <= Math.min(open, close)) {
+                    tests.validPriceLogic++;
+                } else {
+                    tests.issues.push(`Candle ${index}: Invalid price logic (H: ${high}, L: ${low}, O: ${open}, C: ${close})`);
+                }
+
+                // Test 3: Valid timestamp
+                const timestamp = new Date(candle.timestamp);
+                if (!isNaN(timestamp.getTime())) {
+                    tests.validTimestamps++;
+                } else {
+                    tests.issues.push(`Candle ${index}: Invalid timestamp`);
+                }
+            });
+
+            return tests;
+        }
+
+        // Display test results
+        function displayTestResults(tests) {
+            const resultsDiv = document.getElementById('test-results');
+            let output = `Candle Validation Results:\n`;
+            output += `Total Candles: ${tests.total}\n`;
+            output += `Valid OHLC Values: ${tests.validOHLC}/${tests.total}\n`;
+            output += `Valid Price Logic: ${tests.validPriceLogic}/${tests.total}\n`;
+            output += `Valid Timestamps: ${tests.validTimestamps}/${tests.total}\n\n`;
+
+            if (tests.issues.length > 0) {
+                output += `Issues Found:\n`;
+                tests.issues.slice(0, 10).forEach(issue => {
+                    output += `  - ${issue}\n`;
+                });
+                if (tests.issues.length > 10) {
+                    output += `  ... and ${tests.issues.length - 10} more issues\n`;
+                }
+            } else {
+                output += `✅ All candles passed validation!\n`;
+            }
+
+            resultsDiv.textContent = output;
+        }
+
+        // Start real-time updates
+        function startRealTime() {
+            if (realTimeInterval) {
+                clearInterval(realTimeInterval);
+            }
+
+            realTimeInterval = setInterval(async () => {
+                const symbol = document.getElementById('symbol').value;
+                try {
+                    const response = await fetch(`/api/prices/current/${symbol}`);
+                    const result = await response.json();
+
+                    if (result.status === 'success') {
+                        const data = result.data;
+                        const newCandle = {
+                            time: Math.floor(new Date(data.timestamp).getTime() / 1000),
+                            open: parseFloat(data.price),
+                            high: parseFloat(data.high),
+                            low: parseFloat(data.low),
+                            close: parseFloat(data.price),
+                        };
+
+                        if (candlestickSeries) {
+                            candlestickSeries.update(newCandle);
+                        }
+                    }
+                } catch (error) {
+                    console.error('Real-time update error:', error);
+                }
+            }, 1000);
+
+            updateStats('Real-time updates started');
+        }
+
+        // Stop real-time updates
+        function stopRealTime() {
+            if (realTimeInterval) {
+                clearInterval(realTimeInterval);
+                realTimeInterval = null;
+            }
+            updateStats('Real-time updates stopped');
+        }
+
+        // Validate candles via API
+        async function validateCandles() {
+            const symbol = document.getElementById('symbol').value;
+            const timeframe = document.getElementById('timeframe').value;
+
+            updateStats(`Validating ${symbol} ${timeframe} candles via API...`);
+
+            try {
+                const response = await fetch(`/api/prices/validate-candles/${symbol}?timeframe=${timeframe}`);
+                const result = await response.json();
+
+                if (result.status === 'success') {
+                    displayTestResults(result.data);
+                } else {
+                    updateStats(`Validation error: ${result.message}`);
+                }
+            } catch (error) {
+                updateStats(`Error validating candles: ${error.message}`);
+            }
+        }
+
+        // Update stats display
+        function updateStats(message) {
+            document.getElementById('stats').textContent = message;
+        }
+
+        // Initialize on page load
+        window.addEventListener('load', () => {
+            // Load initial chart
+            loadChart();
+        });
+
+        // Handle window resize
+        window.addEventListener('resize', () => {
+            if (chart) {
+                chart.applyOptions({ width: document.getElementById('chart').clientWidth });
+            }
+        });
+    </script>
+</body>
+</html>

+ 373 - 0
routes/priceRoutes.js

@@ -0,0 +1,373 @@
+const express = require('express');
+const router = express.Router();
+const { catchAsync } = require('../middleware/errorHandler');
+const { logger } = require('../utils/logger');
+const {
+  getCurrentPrice,
+  getCurrentPrices,
+  getHistoricalData,
+  getPriceUpdateStats
+} = require('../services/priceUpdateService');
+const {
+  getHistoricalCandles,
+  validateCandles,
+  generateTestCandles
+} = require('../services/candlestickService');
+
+// GET /api/prices/current/:symbol
+router.get('/current/:symbol', catchAsync(async (req, res) => {
+  const { symbol } = req.params;
+
+  if (!symbol) {
+    return res.status(400).json({
+      status: 'error',
+      message: 'Symbol parameter is required'
+    });
+  }
+
+  const priceData = getCurrentPrice(symbol.toUpperCase());
+
+  if (!priceData) {
+    return res.status(404).json({
+      status: 'error',
+      message: `Price data not found for symbol: ${symbol}`
+    });
+  }
+
+  res.json({
+    status: 'success',
+    data: {
+      symbol: priceData.symbol,
+      price: priceData.close,
+      change: priceData.close - priceData.open,
+      changePercent: ((priceData.close - priceData.open) / priceData.open) * 100,
+      high: priceData.high,
+      low: priceData.low,
+      volume: priceData.volume,
+      timestamp: priceData.timestamp,
+      timeframe: priceData.timeframe
+    }
+  });
+}));
+
+// GET /api/prices/current
+router.get('/current', catchAsync(async (req, res) => {
+  const { symbols } = req.query;
+
+  let targetSymbols = null;
+  if (symbols) {
+    targetSymbols = symbols.split(',').map(s => s.trim().toUpperCase());
+  }
+
+  const pricesData = getCurrentPrices(targetSymbols);
+
+  const formattedData = Object.entries(pricesData).map(([symbol, data]) => ({
+    symbol,
+    price: data.close,
+    change: data.close - data.open,
+    changePercent: ((data.close - data.open) / data.open) * 100,
+    high: data.high,
+    low: data.low,
+    volume: data.volume,
+    timestamp: data.timestamp,
+    timeframe: data.timeframe
+  }));
+
+  res.json({
+    status: 'success',
+    data: formattedData,
+    count: formattedData.length
+  });
+}));
+
+// GET /api/prices/historical/:symbol
+router.get('/historical/:symbol', catchAsync(async (req, res) => {
+  const { symbol } = req.params;
+  const { timeframe = 'H1', limit = 1000 } = req.query;
+
+  if (!symbol) {
+    return res.status(400).json({
+      status: 'error',
+      message: 'Symbol parameter is required'
+    });
+  }
+
+  const historicalData = await getHistoricalData(
+    symbol.toUpperCase(),
+    timeframe,
+    parseInt(limit)
+  );
+
+  if (historicalData.length === 0) {
+    return res.status(404).json({
+      status: 'error',
+      message: `Historical data not found for symbol: ${symbol}`
+    });
+  }
+
+  res.json({
+    status: 'success',
+    data: historicalData,
+    symbol: symbol.toUpperCase(),
+    timeframe,
+    count: historicalData.length,
+    meta: {
+      requestedLimit: parseInt(limit),
+      actualCount: historicalData.length,
+      startDate: historicalData[0]?.timestamp,
+      endDate: historicalData[historicalData.length - 1]?.timestamp
+    }
+  });
+}));
+
+// GET /api/prices/bulk-historical
+router.get('/bulk-historical', catchAsync(async (req, res) => {
+  const { symbols, timeframe = 'H1', limit = 1000 } = req.query;
+
+  if (!symbols) {
+    return res.status(400).json({
+      status: 'error',
+      message: 'Symbols parameter is required (comma-separated)'
+    });
+  }
+
+  const symbolList = symbols.split(',').map(s => s.trim().toUpperCase());
+  const results = {};
+
+  for (const symbol of symbolList) {
+    try {
+      const data = await getHistoricalData(symbol, timeframe, parseInt(limit));
+      results[symbol] = {
+        status: 'success',
+        data,
+        count: data.length
+      };
+    } catch (error) {
+      logger.error(`Error fetching historical data for ${symbol}:`, error);
+      results[symbol] = {
+        status: 'error',
+        message: error.message,
+        count: 0
+      };
+    }
+  }
+
+  res.json({
+    status: 'success',
+    data: results,
+    timeframe,
+    requestedLimit: parseInt(limit),
+    symbols: symbolList
+  });
+}));
+
+// GET /api/prices/stats
+router.get('/stats', catchAsync(async (req, res) => {
+  const stats = getPriceUpdateStats();
+
+  res.json({
+    status: 'success',
+    data: stats
+  });
+}));
+
+// POST /api/prices/subscribe
+router.post('/subscribe', catchAsync(async (req, res) => {
+  const { symbols } = req.body;
+
+  if (!Array.isArray(symbols) || symbols.length === 0) {
+    return res.status(400).json({
+      status: 'error',
+      message: 'Symbols array is required and cannot be empty'
+    });
+  }
+
+  const validSymbols = symbols.map(s => s.toUpperCase());
+
+  res.json({
+    status: 'success',
+    message: `Subscribed to ${validSymbols.length} symbols`,
+    data: {
+      symbols: validSymbols,
+      timestamp: new Date().toISOString()
+    }
+  });
+}));
+
+// GET /api/prices/search
+router.get('/search', catchAsync(async (req, res) => {
+  const { q: query, limit = 50 } = req.query;
+
+  if (!query) {
+    return res.status(400).json({
+      status: 'error',
+      message: 'Search query parameter (q) is required'
+    });
+  }
+
+  // This would typically search your database or external API
+  // For now, we'll return a placeholder response
+  res.json({
+    status: 'success',
+    data: [],
+    query,
+    count: 0,
+    message: 'Search functionality not yet implemented'
+  });
+}));
+
+// GET /api/prices/validate-candles/:symbol
+router.get('/validate-candles/:symbol', catchAsync(async (req, res) => {
+  const { symbol } = req.params;
+  const { timeframe = 'H1', limit = 100 } = req.query;
+
+  if (!symbol) {
+    return res.status(400).json({
+      status: 'error',
+      message: 'Symbol parameter is required'
+    });
+  }
+
+  try {
+    // Get candles for validation
+    const candles = await getHistoricalCandles(symbol.toUpperCase(), timeframe, parseInt(limit));
+
+    if (candles.length === 0) {
+      return res.status(404).json({
+        status: 'error',
+        message: `No candle data found for symbol: ${symbol}`
+      });
+    }
+
+    // Validate the candles
+    const validationResults = validateCandles(candles);
+
+    res.json({
+      status: 'success',
+      data: validationResults,
+      symbol: symbol.toUpperCase(),
+      timeframe,
+      count: candles.length,
+      meta: {
+        validationPassed: validationResults.valid === validationResults.total,
+        validCandles: validationResults.valid,
+        invalidCandles: validationResults.invalid,
+        successRate: `${((validationResults.valid / validationResults.total) * 100).toFixed(2)}%`
+      }
+    });
+  } catch (error) {
+    logger.error(`Error validating candles for ${symbol}:`, error);
+    res.status(500).json({
+      status: 'error',
+      message: 'Error validating candles',
+      error: error.message
+    });
+  }
+}));
+
+// GET /api/prices/generate-test-candles/:symbol
+router.get('/generate-test-candles/:symbol', catchAsync(async (req, res) => {
+  const { symbol } = req.params;
+  const { timeframe = 'H1', count = 100 } = req.query;
+
+  if (!symbol) {
+    return res.status(400).json({
+      status: 'error',
+      message: 'Symbol parameter is required'
+    });
+  }
+
+  try {
+    const testCandles = generateTestCandles(symbol.toUpperCase(), timeframe, parseInt(count));
+
+    // Validate the generated test candles
+    const validationResults = validateCandles(testCandles);
+
+    res.json({
+      status: 'success',
+      data: testCandles,
+      validation: validationResults,
+      symbol: symbol.toUpperCase(),
+      timeframe,
+      count: testCandles.length,
+      meta: {
+        description: 'Test candles generated with realistic price movements for validation purposes',
+        validationPassed: validationResults.valid === validationResults.total,
+        successRate: `${((validationResults.valid / validationResults.total) * 100).toFixed(2)}%`
+      }
+    });
+  } catch (error) {
+    logger.error(`Error generating test candles for ${symbol}:`, error);
+    res.status(500).json({
+      status: 'error',
+      message: 'Error generating test candles',
+      error: error.message
+    });
+  }
+}));
+
+// GET /api/prices/candle-info/:symbol
+router.get('/candle-info/:symbol', catchAsync(async (req, res) => {
+  const { symbol } = req.params;
+  const { timeframe = 'H1' } = req.query;
+
+  if (!symbol) {
+    return res.status(400).json({
+      status: 'error',
+      message: 'Symbol parameter is required'
+    });
+  }
+
+  try {
+    const candles = await getHistoricalCandles(symbol.toUpperCase(), timeframe, 1000);
+
+    if (candles.length === 0) {
+      return res.status(404).json({
+        status: 'error',
+        message: `No candle data found for symbol: ${symbol}`
+      });
+    }
+
+    // Calculate statistics
+    const closes = candles.map(c => parseFloat(c.close));
+    const highs = candles.map(c => parseFloat(c.high));
+    const lows = candles.map(c => parseFloat(c.low));
+    const volumes = candles.map(c => parseInt(c.volume));
+
+    const stats = {
+      count: candles.length,
+      priceRange: {
+        min: Math.min(...closes),
+        max: Math.max(...closes),
+        avg: closes.reduce((a, b) => a + b, 0) / closes.length
+      },
+      highLowRange: {
+        overallHigh: Math.max(...highs),
+        overallLow: Math.min(...lows)
+      },
+      volume: {
+        total: volumes.reduce((a, b) => a + b, 0),
+        avg: volumes.reduce((a, b) => a + b, 0) / volumes.length,
+        max: Math.max(...volumes),
+        min: Math.min(...volumes)
+      },
+      timeframe,
+      symbol: symbol.toUpperCase(),
+      lastUpdated: candles[candles.length - 1]?.timestamp
+    };
+
+    res.json({
+      status: 'success',
+      data: stats
+    });
+  } catch (error) {
+    logger.error(`Error getting candle info for ${symbol}:`, error);
+    res.status(500).json({
+      status: 'error',
+      message: 'Error getting candle information',
+      error: error.message
+    });
+  }
+}));
+
+module.exports = router;

+ 367 - 0
services/priceUpdateService.js

@@ -0,0 +1,367 @@
+const { logger } = require('../utils/logger');
+const { executeQuery } = require('../config/database');
+const { broadcastPriceUpdate, broadcastMultiplePriceUpdates } = require('./websocketService');
+const { addTick } = require('./candlestickService');
+
+// In-memory cache for current prices
+const priceCache = new Map();
+const symbolSubscriptions = new Map();
+
+// Simulated price data for development
+const generateMockPriceData = (symbol) => {
+  const basePrice = priceCache.get(symbol)?.close || 1.0;
+  const change = (Math.random() - 0.5) * 0.02; // ±1% change
+  const newPrice = basePrice * (1 + change);
+
+  return {
+    symbol,
+    open: basePrice,
+    high: Math.max(basePrice, newPrice) * (1 + Math.random() * 0.001),
+    low: Math.min(basePrice, newPrice) * (1 - Math.random() * 0.001),
+    close: newPrice,
+    volume: Math.floor(Math.random() * 1000000) + 100000,
+    timestamp: new Date(),
+    timeframe: 'H1'
+  };
+};
+
+class PriceUpdateService {
+  constructor() {
+    this.isRunning = false;
+    this.updateInterval = null;
+    this.supportedSymbols = [];
+    this.updateCallbacks = [];
+
+    this.initialize();
+  }
+
+  initialize() {
+    // Load supported symbols from environment
+    this.supportedSymbols = (process.env.SUPPORTED_SYMBOLS || 'EURUSD,GBPUSD,USDJPY')
+      .split(',')
+      .map(symbol => symbol.trim());
+
+    logger.info(`Price update service initialized with symbols: ${this.supportedSymbols.join(', ')}`);
+
+    // Initialize price cache with mock data
+    this.initializePriceCache();
+  }
+
+  initializePriceCache() {
+    this.supportedSymbols.forEach(symbol => {
+      priceCache.set(symbol, generateMockPriceData(symbol));
+    });
+    logger.info('Price cache initialized');
+  }
+
+  start() {
+    if (this.isRunning) {
+      logger.warn('Price update service is already running');
+      return;
+    }
+
+    const interval = parseInt(process.env.PRICE_UPDATE_INTERVAL) || 1000;
+
+    this.updateInterval = setInterval(() => {
+      this.updatePrices();
+    }, interval);
+
+    this.isRunning = true;
+    logger.info(`Price update service started with ${interval}ms interval`);
+
+    // Start cleanup interval
+    setInterval(() => {
+      this.cleanup();
+    }, 60000); // Cleanup every minute
+  }
+
+  stop() {
+    if (!this.isRunning) {
+      return;
+    }
+
+    if (this.updateInterval) {
+      clearInterval(this.updateInterval);
+      this.updateInterval = null;
+    }
+
+    this.isRunning = false;
+    logger.info('Price update service stopped');
+  }
+
+  async updatePrices() {
+    try {
+      const updates = [];
+
+      for (const symbol of this.supportedSymbols) {
+        const priceData = await this.generatePriceUpdate(symbol);
+        updates.push(priceData);
+
+        // Update cache
+        priceCache.set(symbol, priceData);
+
+        // Store in database (if needed)
+        await this.storePriceData(priceData);
+      }
+
+      // Broadcast updates to WebSocket clients
+      if (updates.length > 0) {
+        broadcastMultiplePriceUpdates(updates);
+      }
+
+      // Call registered update callbacks
+      this.updateCallbacks.forEach(callback => {
+        try {
+          callback(updates);
+        } catch (error) {
+          logger.error('Error in price update callback:', error);
+        }
+      });
+
+    } catch (error) {
+      logger.error('Error updating prices:', error);
+    }
+  }
+
+  async generatePriceUpdate(symbol) {
+    // In a real implementation, this would fetch from external APIs
+    // For now, we'll generate mock data
+    const currentPrice = priceCache.get(symbol);
+    const change = (Math.random() - 0.5) * 0.02; // ±1% change
+    const newPrice = currentPrice ? currentPrice.close * (1 + change) : 1.0;
+
+    const priceData = {
+      symbol,
+      open: currentPrice ? currentPrice.close : newPrice,
+      high: Math.max(currentPrice ? currentPrice.close : newPrice, newPrice) * (1 + Math.random() * 0.001),
+      low: Math.min(currentPrice ? currentPrice.close : newPrice, newPrice) * (1 - Math.random() * 0.001),
+      close: newPrice,
+      volume: Math.floor(Math.random() * 1000000) + 100000,
+      timestamp: new Date(),
+      timeframe: 'H1'
+    };
+
+    // Add tick data for candlestick aggregation
+    // Generate multiple ticks within this price update period
+    const tickCount = Math.floor(Math.random() * 5) + 3; // 3-7 ticks per update
+    for (let i = 0; i < tickCount; i++) {
+      const tickPrice = newPrice + (Math.random() - 0.5) * 0.001; // Small variations
+      const tickTimestamp = new Date(priceData.timestamp.getTime() - (tickCount - i) * 100); // Spread over the interval
+      addTick(symbol, tickPrice, tickTimestamp);
+    }
+
+    return priceData;
+  }
+
+  async storePriceData(priceData) {
+    try {
+      const dbType = process.env.DB_TYPE || 'sqlite';
+
+      if (dbType === 'sqlite') {
+        await this.storePriceDataSQLite(priceData);
+      } else if (dbType === 'mysql') {
+        await this.storePriceDataMySQL(priceData);
+      } else if (dbType === 'postgres') {
+        await this.storePriceDataPostgreSQL(priceData);
+      }
+    } catch (error) {
+      logger.error('Error storing price data:', error);
+    }
+  }
+
+  async storePriceDataSQLite(priceData) {
+    const query = `
+      INSERT OR IGNORE INTO price_data (symbol, timeframe, timestamp, open, high, low, close, volume)
+      VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+    `;
+
+    await executeQuery(query, [
+      priceData.symbol,
+      priceData.timeframe,
+      priceData.timestamp,
+      priceData.open,
+      priceData.high,
+      priceData.low,
+      priceData.close,
+      priceData.volume
+    ]);
+  }
+
+  async storePriceDataMySQL(priceData) {
+    const query = `
+      INSERT IGNORE INTO price_data (symbol, timeframe, timestamp, open, high, low, close, volume)
+      VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+    `;
+
+    await executeQuery(query, [
+      priceData.symbol,
+      priceData.timeframe,
+      priceData.timestamp,
+      priceData.open,
+      priceData.high,
+      priceData.low,
+      priceData.close,
+      priceData.volume
+    ]);
+  }
+
+  async storePriceDataPostgreSQL(priceData) {
+    const query = `
+      INSERT INTO price_data (symbol, timeframe, timestamp, open, high, low, close, volume)
+      VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+      ON CONFLICT DO NOTHING
+    `;
+
+    await executeQuery(query, [
+      priceData.symbol,
+      priceData.timeframe,
+      priceData.timestamp,
+      priceData.open,
+      priceData.high,
+      priceData.low,
+      priceData.close,
+      priceData.volume
+    ]);
+  }
+
+  // Get current price for a symbol
+  getCurrentPrice(symbol) {
+    return priceCache.get(symbol);
+  }
+
+  // Get current prices for multiple symbols
+  getCurrentPrices(symbols = null) {
+    const targetSymbols = symbols || this.supportedSymbols;
+    const prices = {};
+
+    targetSymbols.forEach(symbol => {
+      prices[symbol] = priceCache.get(symbol);
+    });
+
+    return prices;
+  }
+
+  // Get historical data for a symbol
+  async getHistoricalData(symbol, timeframe = 'H1', limit = 1000) {
+    try {
+      const dbType = process.env.DB_TYPE || 'sqlite';
+      let query;
+
+      if (dbType === 'sqlite') {
+        query = `
+          SELECT * FROM price_data
+          WHERE symbol = ? AND timeframe = ?
+          ORDER BY timestamp DESC
+          LIMIT ?
+        `;
+      } else {
+        query = `
+          SELECT * FROM price_data
+          WHERE symbol = ? AND timeframe = ?
+          ORDER BY timestamp DESC
+          LIMIT ?
+        `;
+      }
+
+      const params = [symbol, timeframe, limit];
+      const data = await executeQuery(query, params);
+
+      return data.reverse(); // Return in chronological order
+    } catch (error) {
+      logger.error(`Error fetching historical data for ${symbol}:`, error);
+      return [];
+    }
+  }
+
+  // Register callback for price updates
+  onPriceUpdate(callback) {
+    this.updateCallbacks.push(callback);
+  }
+
+  // Remove price update callback
+  removePriceUpdateCallback(callback) {
+    const index = this.updateCallbacks.indexOf(callback);
+    if (index > -1) {
+      this.updateCallbacks.splice(index, 1);
+    }
+  }
+
+  // Cleanup old data
+  async cleanup() {
+    try {
+      const cutoffDate = new Date();
+      cutoffDate.setDate(cutoffDate.getDate() - 30); // Keep 30 days of data
+
+      const dbType = process.env.DB_TYPE || 'sqlite';
+      let query;
+
+      if (dbType === 'sqlite') {
+        query = 'DELETE FROM price_data WHERE timestamp < ?';
+      } else {
+        query = 'DELETE FROM price_data WHERE timestamp < ?';
+      }
+
+      const params = [cutoffDate];
+      await executeQuery(query, params);
+
+      logger.info('Price data cleanup completed');
+    } catch (error) {
+      logger.error('Error during price data cleanup:', error);
+    }
+  }
+
+  // Get service statistics
+  getStats() {
+    return {
+      isRunning: this.isRunning,
+      supportedSymbols: this.supportedSymbols,
+      cachedSymbols: Array.from(priceCache.keys()),
+      updateCallbacks: this.updateCallbacks.length,
+      cacheSize: priceCache.size,
+      uptime: process.uptime()
+    };
+  }
+}
+
+// Create singleton instance
+const priceUpdateService = new PriceUpdateService();
+
+// Export functions for external use
+const startPriceUpdates = (io) => {
+  priceUpdateService.start();
+};
+
+const stopPriceUpdates = () => {
+  priceUpdateService.stop();
+};
+
+const getCurrentPrice = (symbol) => {
+  return priceUpdateService.getCurrentPrice(symbol);
+};
+
+const getCurrentPrices = (symbols) => {
+  return priceUpdateService.getCurrentPrices(symbols);
+};
+
+const getHistoricalData = (symbol, timeframe, limit) => {
+  return priceUpdateService.getHistoricalData(symbol, timeframe, limit);
+};
+
+const onPriceUpdate = (callback) => {
+  priceUpdateService.onPriceUpdate(callback);
+};
+
+const getPriceUpdateStats = () => {
+  return priceUpdateService.getStats();
+};
+
+module.exports = {
+  startPriceUpdates,
+  stopPriceUpdates,
+  getCurrentPrice,
+  getCurrentPrices,
+  getHistoricalData,
+  onPriceUpdate,
+  getPriceUpdateStats,
+  priceUpdateService
+};

+ 159 - 0
src/server.js

@@ -0,0 +1,159 @@
+const express = require('express');
+const http = require('http');
+const socketIo = require('socket.io');
+const cors = require('cors');
+const helmet = require('helmet');
+const morgan = require('morgan');
+const compression = require('compression');
+const rateLimit = require('express-rate-limit');
+
+require('dotenv').config();
+
+// Import custom modules
+const { errorHandler } = require('../middleware/errorHandler');
+const { logger } = require('../utils/logger');
+const { connectDatabase } = require('../config/database');
+const { initializeDatabase } = require('../utils/databaseInit');
+const priceRoutes = require('../routes/priceRoutes');
+const symbolRoutes = require('../routes/symbolRoutes');
+const { initializeWebSocket } = require('../services/websocketService');
+const { startPriceUpdates } = require('../services/priceUpdateService');
+
+class FinancialDataServer {
+  constructor() {
+    this.app = express();
+    this.server = http.createServer(this.app);
+    this.io = socketIo(this.server, {
+      cors: {
+        origin: process.env.CLIENT_URL || "http://localhost:3000",
+        methods: ["GET", "POST"]
+      }
+    });
+    this.port = process.env.PORT || 3001;
+
+    this.initializeMiddleware();
+    this.initializeRoutes();
+    this.initializeWebSocket();
+    this.initializeErrorHandling();
+    this.initializeDatabase();
+  }
+
+  initializeMiddleware() {
+    // Security middleware
+    this.app.use(helmet());
+
+    // CORS configuration
+    this.app.use(cors({
+      origin: process.env.CLIENT_URL || "http://localhost:3000",
+      credentials: true
+    }));
+
+    // Rate limiting
+    const limiter = rateLimit({
+      windowMs: 15 * 60 * 1000, // 15 minutes
+      max: 1000, // limit each IP to 1000 requests per windowMs
+      message: 'Too many requests from this IP, please try again later.'
+    });
+    this.app.use('/api/', limiter);
+
+    // Body parsing middleware
+    this.app.use(express.json({ limit: '10mb' }));
+    this.app.use(express.urlencoded({ extended: true, limit: '10mb' }));
+
+    // Compression middleware
+    this.app.use(compression());
+
+    // Logging middleware
+    this.app.use(morgan('combined', { stream: { write: msg => logger.info(msg.trim()) } }));
+  }
+
+  initializeRoutes() {
+    // Health check endpoint
+    this.app.get('/health', (req, res) => {
+      res.status(200).json({
+        status: 'OK',
+        timestamp: new Date().toISOString(),
+        uptime: process.uptime()
+      });
+    });
+
+    // API routes
+    this.app.use('/api/prices', priceRoutes);
+    this.app.use('/api/symbols', symbolRoutes);
+
+    // Serve static files from public directory
+    this.app.use(express.static('public'));
+
+    // Catch all handler: send back React's index.html file for client-side routing
+    if (process.env.NODE_ENV === 'production') {
+      this.app.get('*', (req, res) => {
+        res.sendFile(path.join(__dirname, '../public/index.html'));
+      });
+    }
+  }
+
+  initializeWebSocket() {
+    initializeWebSocket(this.io);
+  }
+
+  initializeErrorHandling() {
+    this.app.use(errorHandler);
+  }
+
+  async initializeDatabase() {
+    try {
+      await connectDatabase();
+      logger.info('Database connected successfully');
+
+      // Initialize database tables and sample data
+      await initializeDatabase();
+      logger.info('Database initialized successfully');
+    } catch (error) {
+      logger.error('Database initialization failed:', error);
+      process.exit(1);
+    }
+  }
+
+  startPriceUpdates() {
+    startPriceUpdates(this.io);
+  }
+
+  async start() {
+    try {
+      this.server.listen(this.port, () => {
+        logger.info(`Financial Data Server running on port ${this.port}`);
+        logger.info(`Environment: ${process.env.NODE_ENV || 'development'}`);
+      });
+
+      // Start real-time price updates
+      this.startPriceUpdates();
+
+    } catch (error) {
+      logger.error('Failed to start server:', error);
+      process.exit(1);
+    }
+  }
+
+  async stop() {
+    logger.info('Shutting down server...');
+    this.server.close(() => {
+      logger.info('Server closed');
+      process.exit(0);
+    });
+  }
+}
+
+// Create and start server
+const server = new FinancialDataServer();
+
+// Graceful shutdown
+process.on('SIGTERM', () => server.stop());
+process.on('SIGINT', () => server.stop());
+
+// Start the server
+server.start().catch(error => {
+  logger.error('Failed to start server:', error);
+  process.exit(1);
+});
+
+module.exports = server;

+ 203 - 0
tests/candleValidation.test.js

@@ -0,0 +1,203 @@
+const request = require('supertest');
+const { expect } = require('chai');
+const app = require('../src/server');
+const { getHistoricalCandles, validateCandles, generateTestCandles } = require('../services/candlestickService');
+
+describe('Candlestick API Tests', () => {
+  const testSymbol = 'EURUSD';
+  const testTimeframe = 'H1';
+
+  describe('GET /api/prices/validate-candles/:symbol', () => {
+    it('should validate candles for a valid symbol', async () => {
+      const response = await request(app)
+        .get(`/api/prices/validate-candles/${testSymbol}`)
+        .query({ timeframe: testTimeframe, limit: 50 })
+        .expect(200);
+
+      expect(response.body.status).to.equal('success');
+      expect(response.body.data).to.have.property('total');
+      expect(response.body.data).to.have.property('valid');
+      expect(response.body.data).to.have.property('invalid');
+      expect(response.body.data.total).to.be.greaterThan(0);
+    });
+
+    it('should return 400 for missing symbol', async () => {
+      const response = await request(app)
+        .get('/api/prices/validate-candles/')
+        .expect(404); // Express will return 404 for missing route param
+    });
+
+    it('should handle different timeframes', async () => {
+      const timeframes = ['M1', 'M5', 'M15', 'H1', 'H4', 'D1'];
+
+      for (const timeframe of timeframes) {
+        const response = await request(app)
+          .get(`/api/prices/validate-candles/${testSymbol}`)
+          .query({ timeframe, limit: 10 })
+          .expect(200);
+
+        expect(response.body.status).to.equal('success');
+        expect(response.body.timeframe).to.equal(timeframe);
+      }
+    });
+  });
+
+  describe('GET /api/prices/generate-test-candles/:symbol', () => {
+    it('should generate test candles for a valid symbol', async () => {
+      const response = await request(app)
+        .get(`/api/prices/generate-test-candles/${testSymbol}`)
+        .query({ timeframe: testTimeframe, count: 20 })
+        .expect(200);
+
+      expect(response.body.status).to.equal('success');
+      expect(response.body.data).to.be.an('array');
+      expect(response.body.data.length).to.equal(20);
+      expect(response.body.validation).to.have.property('total', 20);
+    });
+
+    it('should validate generated test candles', async () => {
+      const response = await request(app)
+        .get(`/api/prices/generate-test-candles/${testSymbol}`)
+        .query({ timeframe: testTimeframe, count: 10 })
+        .expect(200);
+
+      expect(response.body.meta.validationPassed).to.equal(true);
+      expect(response.body.meta.successRate).to.equal('100.00%');
+    });
+  });
+
+  describe('GET /api/prices/candle-info/:symbol', () => {
+    it('should return candle statistics for a valid symbol', async () => {
+      const response = await request(app)
+        .get(`/api/prices/candle-info/${testSymbol}`)
+        .query({ timeframe: testTimeframe })
+        .expect(200);
+
+      expect(response.body.status).to.equal('success');
+      expect(response.body.data).to.have.property('count');
+      expect(response.body.data).to.have.property('priceRange');
+      expect(response.body.data).to.have.property('volume');
+      expect(response.body.data.symbol).to.equal(testSymbol.toUpperCase());
+    });
+  });
+
+  describe('Candlestick Service Unit Tests', () => {
+    it('should generate valid test candles', () => {
+      const candles = generateTestCandles(testSymbol, testTimeframe, 10);
+      expect(candles).to.have.length(10);
+
+      candles.forEach(candle => {
+        expect(candle.symbol).to.equal(testSymbol);
+        expect(candle.timeframe).to.equal(testTimeframe);
+        expect(candle).to.have.property('open');
+        expect(candle).to.have.property('high');
+        expect(candle).to.have.property('low');
+        expect(candle).to.have.property('close');
+        expect(candle).to.have.property('volume');
+        expect(candle).to.have.property('timestamp');
+      });
+    });
+
+    it('should validate correct candles', () => {
+      const validCandles = generateTestCandles(testSymbol, testTimeframe, 5);
+      const results = validateCandles(validCandles);
+
+      expect(results.total).to.equal(5);
+      expect(results.valid).to.equal(5);
+      expect(results.invalid).to.equal(0);
+      expect(results.issues).to.have.length(0);
+    });
+
+    it('should detect invalid candles', () => {
+      const invalidCandles = [
+        {
+          symbol: testSymbol,
+          timeframe: testTimeframe,
+          timestamp: new Date(),
+          open: 'invalid',
+          high: 1.1,
+          low: 1.0,
+          close: 1.05,
+          volume: 1000
+        },
+        {
+          symbol: testSymbol,
+          timeframe: testTimeframe,
+          timestamp: new Date(),
+          open: 1.0,
+          high: 0.9, // Invalid: high < open
+          low: 1.0,
+          close: 1.05,
+          volume: 1000
+        }
+      ];
+
+      const results = validateCandles(invalidCandles);
+      expect(results.invalid).to.be.greaterThan(0);
+      expect(results.issues.length).to.be.greaterThan(0);
+    });
+  });
+
+  describe('GET /api/prices/historical/:symbol (Integration)', () => {
+    it('should return historical data that can be validated', async () => {
+      const response = await request(app)
+        .get(`/api/prices/historical/${testSymbol}`)
+        .query({ timeframe: testTimeframe, limit: 20 })
+        .expect(200);
+
+      expect(response.body.status).to.equal('success');
+      expect(response.body.data).to.be.an('array');
+
+      if (response.body.data.length > 0) {
+        // Validate the returned candles
+        const validationResults = validateCandles(response.body.data);
+        expect(validationResults.total).to.equal(response.body.data.length);
+
+        // Log validation results for debugging
+        console.log(`Historical data validation: ${validationResults.valid}/${validationResults.total} valid candles`);
+      }
+    });
+  });
+
+  describe('Chart Data Format Tests', () => {
+    it('should return data in correct format for charting', async () => {
+      const response = await request(app)
+        .get(`/api/prices/historical/${testSymbol}`)
+        .query({ timeframe: testTimeframe, limit: 10 })
+        .expect(200);
+
+      expect(response.body.status).to.equal('success');
+      expect(response.body.data).to.be.an('array');
+
+      if (response.body.data.length > 0) {
+        const candle = response.body.data[0];
+
+        // Check required fields for charting
+        expect(candle).to.have.property('timestamp');
+        expect(candle).to.have.property('open');
+        expect(candle).to.have.property('high');
+        expect(candle).to.have.property('low');
+        expect(candle).to.have.property('close');
+
+        // Validate data types
+        expect(typeof candle.open).to.equal('number');
+        expect(typeof candle.high).to.equal('number');
+        expect(typeof candle.low).to.equal('number');
+        expect(typeof candle.close).to.equal('number');
+
+        // Validate price logic
+        expect(candle.high).to.be.at.least(Math.max(candle.open, candle.close));
+        expect(candle.low).to.be.at.most(Math.min(candle.open, candle.close));
+      }
+    });
+  });
+});
+
+// Helper function to wait for price updates
+const waitForPriceUpdate = (ms = 2000) => {
+  return new Promise(resolve => setTimeout(resolve, ms));
+};
+
+module.exports = {
+  waitForPriceUpdate
+};

+ 166 - 0
tests/server.test.js

@@ -0,0 +1,166 @@
+const request = require('supertest');
+const { expect } = require('chai');
+const server = require('../src/server');
+
+describe('Financial Data Server', () => {
+  let app;
+
+  before(async () => {
+    // Get the Express app instance from the server
+    app = server.app;
+  });
+
+  describe('Health Check', () => {
+    it('should return server health status', (done) => {
+      request(app)
+        .get('/health')
+        .expect(200)
+        .end((err, res) => {
+          if (err) return done(err);
+
+          expect(res.body).to.have.property('status', 'OK');
+          expect(res.body).to.have.property('timestamp');
+          expect(res.body).to.have.property('uptime');
+          done();
+        });
+    });
+  });
+
+  describe('Price API', () => {
+    it('should return current price for valid symbol', (done) => {
+      request(app)
+        .get('/api/prices/current/EURUSD')
+        .expect(200)
+        .end((err, res) => {
+          if (err) return done(err);
+
+          expect(res.body).to.have.property('status', 'success');
+          expect(res.body).to.have.property('data');
+          expect(res.body.data).to.have.property('symbol', 'EURUSD');
+          expect(res.body.data).to.have.property('price');
+          expect(res.body.data).to.have.property('change');
+          expect(res.body.data).to.have.property('changePercent');
+          done();
+        });
+    });
+
+    it('should return 404 for invalid symbol', (done) => {
+      request(app)
+        .get('/api/prices/current/INVALID')
+        .expect(404)
+        .end((err, res) => {
+          if (err) return done(err);
+
+          expect(res.body).to.have.property('status', 'error');
+          expect(res.body).to.have.property('message');
+          done();
+        });
+    });
+
+    it('should return historical data for valid symbol', (done) => {
+      request(app)
+        .get('/api/prices/historical/EURUSD?timeframe=H1&limit=10')
+        .expect(200)
+        .end((err, res) => {
+          if (err) return done(err);
+
+          expect(res.body).to.have.property('status', 'success');
+          expect(res.body).to.have.property('data');
+          expect(res.body.data).to.be.an('array');
+          expect(res.body).to.have.property('symbol', 'EURUSD');
+          expect(res.body).to.have.property('timeframe', 'H1');
+          done();
+        });
+    });
+  });
+
+  describe('Symbol API', () => {
+    it('should return list of symbols', (done) => {
+      request(app)
+        .get('/api/symbols')
+        .expect(200)
+        .end((err, res) => {
+          if (err) return done(err);
+
+          expect(res.body).to.have.property('status', 'success');
+          expect(res.body).to.have.property('data');
+          expect(res.body.data).to.be.an('array');
+          expect(res.body).to.have.property('pagination');
+          done();
+        });
+    });
+
+    it('should return specific symbol details', (done) => {
+      request(app)
+        .get('/api/symbols/EURUSD')
+        .expect(200)
+        .end((err, res) => {
+          if (err) return done(err);
+
+          expect(res.body).to.have.property('status', 'success');
+          expect(res.body).to.have.property('data');
+          expect(res.body.data).to.have.property('symbol', 'EURUSD');
+          expect(res.body.data).to.have.property('name');
+          expect(res.body.data).to.have.property('category');
+          expect(res.body.data).to.have.property('exchange');
+          done();
+        });
+    });
+
+    it('should return 404 for non-existent symbol', (done) => {
+      request(app)
+        .get('/api/symbols/NONEXISTENT')
+        .expect(404)
+        .end((err, res) => {
+          if (err) return done(err);
+
+          expect(res.body).to.have.property('status', 'error');
+          expect(res.body).to.have.property('message');
+          done();
+        });
+    });
+
+    it('should return symbol categories', (done) => {
+      request(app)
+        .get('/api/symbols/categories/list')
+        .expect(200)
+        .end((err, res) => {
+          if (err) return done(err);
+
+          expect(res.body).to.have.property('status', 'success');
+          expect(res.body).to.have.property('data');
+          expect(res.body.data).to.be.an('array');
+          done();
+        });
+    });
+  });
+
+  describe('Error Handling', () => {
+    it('should handle 404 for unknown routes', (done) => {
+      request(app)
+        .get('/api/unknown-route')
+        .expect(404)
+        .end((err, res) => {
+          if (err) return done(err);
+
+          expect(res.body).to.have.property('status', 'error');
+          expect(res.body).to.have.property('message');
+          done();
+        });
+    });
+
+    it('should handle malformed JSON', (done) => {
+      request(app)
+        .post('/api/prices/subscribe')
+        .set('Content-Type', 'application/json')
+        .send('{ malformed json }')
+        .expect(400)
+        .end((err, res) => {
+          if (err) return done(err);
+
+          expect(res.body).to.have.property('status', 'error');
+          done();
+        });
+    });
+  });
+});

+ 494 - 0
utils/databaseInit.js

@@ -0,0 +1,494 @@
+const { executeQuery, executeNonQuery } = require('../config/database');
+const { logger } = require('./logger');
+const fs = require('fs');
+const path = require('path');
+
+class DatabaseInitializer {
+  constructor() {
+    this.initialized = false;
+  }
+
+  async initialize() {
+    if (this.initialized) {
+      logger.info('Database already initialized');
+      return;
+    }
+
+    try {
+      logger.info('Starting database initialization...');
+
+      // Create tables based on database type
+      const dbType = process.env.DB_TYPE || 'sqlite';
+
+      switch (dbType) {
+        case 'mysql':
+        case 'postgres':
+          await this.initializeRelationalDatabase();
+          break;
+        case 'sqlite':
+        default:
+          await this.initializeSQLite();
+          break;
+      }
+
+      // Insert sample data
+      await this.insertSampleData();
+
+      this.initialized = true;
+      logger.info('Database initialization completed successfully');
+
+    } catch (error) {
+      logger.error('Database initialization failed:', error);
+      throw error;
+    }
+  }
+
+  async initializeRelationalDatabase() {
+    try {
+      const dbType = process.env.DB_TYPE || 'mysql';
+
+      if (dbType === 'mysql') {
+        await this.initializeMySQL();
+      } else if (dbType === 'postgres') {
+        await this.initializePostgreSQL();
+      }
+
+    } catch (error) {
+      logger.error('Error creating relational database tables:', error);
+      throw error;
+    }
+  }
+
+  async initializeMySQL() {
+    try {
+      // Create price_data table for MySQL
+      const createPriceTableQuery = `
+        CREATE TABLE IF NOT EXISTS price_data (
+          id INT PRIMARY KEY AUTO_INCREMENT,
+          symbol VARCHAR(10) NOT NULL,
+          timeframe VARCHAR(5) NOT NULL,
+          timestamp DATETIME NOT NULL,
+          open DECIMAL(15,8) NOT NULL,
+          high DECIMAL(15,8) NOT NULL,
+          low DECIMAL(15,8) NOT NULL,
+          close DECIMAL(15,8) NOT NULL,
+          volume BIGINT NOT NULL,
+          created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+          INDEX idx_symbol_timeframe (symbol, timeframe),
+          INDEX idx_timestamp (timestamp),
+          INDEX idx_symbol_timestamp (symbol, timestamp)
+        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+      `;
+
+      await executeNonQuery(createPriceTableQuery);
+      logger.info('MySQL price data table created successfully');
+
+      // Create symbols table for MySQL
+      const createSymbolsTableQuery = `
+        CREATE TABLE IF NOT EXISTS symbols (
+          id INT PRIMARY KEY AUTO_INCREMENT,
+          symbol VARCHAR(10) UNIQUE NOT NULL,
+          name VARCHAR(100) NOT NULL,
+          category VARCHAR(20) NOT NULL,
+          exchange VARCHAR(20) NOT NULL,
+          is_active BOOLEAN DEFAULT TRUE,
+          created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+          updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+          INDEX idx_symbol (symbol),
+          INDEX idx_category (category),
+          INDEX idx_exchange (exchange)
+        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+      `;
+
+      await executeNonQuery(createSymbolsTableQuery);
+      logger.info('MySQL symbols table created successfully');
+
+    } catch (error) {
+      logger.error('Error creating MySQL tables:', error);
+      throw error;
+    }
+  }
+
+  async initializePostgreSQL() {
+    try {
+      // Create price_data table for PostgreSQL
+      const createPriceTableQuery = `
+        CREATE TABLE IF NOT EXISTS price_data (
+          id SERIAL PRIMARY KEY,
+          symbol VARCHAR(10) NOT NULL,
+          timeframe VARCHAR(5) NOT NULL,
+          timestamp TIMESTAMP NOT NULL,
+          open DECIMAL(15,8) NOT NULL,
+          high DECIMAL(15,8) NOT NULL,
+          low DECIMAL(15,8) NOT NULL,
+          close DECIMAL(15,8) NOT NULL,
+          volume BIGINT NOT NULL,
+          created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+        );
+      `;
+
+      await executeNonQuery(createPriceTableQuery);
+      logger.info('PostgreSQL price data table created successfully');
+
+      // Create indexes for PostgreSQL
+      const createIndexes = [
+        'CREATE INDEX IF NOT EXISTS idx_symbol_timeframe ON price_data(symbol, timeframe);',
+        'CREATE INDEX IF NOT EXISTS idx_timestamp ON price_data(timestamp);',
+        'CREATE INDEX IF NOT EXISTS idx_symbol_timestamp ON price_data(symbol, timestamp);'
+      ];
+
+      for (const indexQuery of createIndexes) {
+        await executeNonQuery(indexQuery);
+      }
+
+      logger.info('PostgreSQL indexes created successfully');
+
+      // Create symbols table for PostgreSQL
+      const createSymbolsTableQuery = `
+        CREATE TABLE IF NOT EXISTS symbols (
+          id SERIAL PRIMARY KEY,
+          symbol VARCHAR(10) UNIQUE NOT NULL,
+          name VARCHAR(100) NOT NULL,
+          category VARCHAR(20) NOT NULL,
+          exchange VARCHAR(20) NOT NULL,
+          is_active BOOLEAN DEFAULT TRUE,
+          created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+          updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+        );
+      `;
+
+      await executeNonQuery(createSymbolsTableQuery);
+      logger.info('PostgreSQL symbols table created successfully');
+
+    } catch (error) {
+      logger.error('Error creating PostgreSQL tables:', error);
+      throw error;
+    }
+  }
+
+  async initializeSQLite() {
+    try {
+      // Create price_data table
+      const createPriceTableQuery = `
+        CREATE TABLE IF NOT EXISTS price_data (
+          id INTEGER PRIMARY KEY AUTOINCREMENT,
+          symbol TEXT NOT NULL,
+          timeframe TEXT NOT NULL,
+          timestamp DATETIME NOT NULL,
+          open REAL NOT NULL,
+          high REAL NOT NULL,
+          low REAL NOT NULL,
+          close REAL NOT NULL,
+          volume INTEGER NOT NULL,
+          created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+        );
+      `;
+
+      await executeNonQuery(createPriceTableQuery);
+      logger.info('SQLite price data table created successfully');
+
+      // Create indexes for better performance
+      const createIndexes = [
+        'CREATE INDEX IF NOT EXISTS idx_symbol_timeframe ON price_data(symbol, timeframe);',
+        'CREATE INDEX IF NOT EXISTS idx_timestamp ON price_data(timestamp);',
+        'CREATE INDEX IF NOT EXISTS idx_symbol_timestamp ON price_data(symbol, timestamp);'
+      ];
+
+      for (const indexQuery of createIndexes) {
+        await executeNonQuery(indexQuery);
+      }
+
+      logger.info('SQLite indexes created successfully');
+
+      // Create symbols table
+      const createSymbolsTableQuery = `
+        CREATE TABLE IF NOT EXISTS symbols (
+          id INTEGER PRIMARY KEY AUTOINCREMENT,
+          symbol TEXT UNIQUE NOT NULL,
+          name TEXT NOT NULL,
+          category TEXT NOT NULL,
+          exchange TEXT NOT NULL,
+          is_active INTEGER DEFAULT 1,
+          created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+          updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
+        );
+      `;
+
+      await executeNonQuery(createSymbolsTableQuery);
+      logger.info('SQLite symbols table created successfully');
+
+    } catch (error) {
+      logger.error('Error creating SQLite tables:', error);
+      throw error;
+    }
+  }
+
+  async insertSampleData() {
+    try {
+      const dbType = process.env.DB_TYPE || 'sqlite';
+
+      // Check if symbols already exist
+      let existingSymbols;
+      if (dbType === 'sqlite') {
+        existingSymbols = await executeQuery('SELECT COUNT(*) as count FROM symbols');
+      } else {
+        existingSymbols = await executeQuery('SELECT COUNT(*) as count FROM symbols');
+      }
+
+      // Handle different database result formats
+      let symbolCount = 0;
+      if (existingSymbols && existingSymbols.length > 0) {
+        symbolCount = existingSymbols[0].count || 0;
+      }
+
+      if (symbolCount > 0) {
+        logger.info('Sample symbols already exist, skipping insertion');
+        return;
+      }
+
+      // Insert sample symbols
+      const sampleSymbols = [
+        { symbol: 'EURUSD', name: 'Euro vs US Dollar', category: 'forex', exchange: 'FX' },
+        { symbol: 'GBPUSD', name: 'British Pound vs US Dollar', category: 'forex', exchange: 'FX' },
+        { symbol: 'USDJPY', name: 'US Dollar vs Japanese Yen', category: 'forex', exchange: 'FX' },
+        { symbol: 'USDCHF', name: 'US Dollar vs Swiss Franc', category: 'forex', exchange: 'FX' },
+        { symbol: 'USDCAD', name: 'US Dollar vs Canadian Dollar', category: 'forex', exchange: 'FX' },
+        { symbol: 'AUDUSD', name: 'Australian Dollar vs US Dollar', category: 'forex', exchange: 'FX' },
+        { symbol: 'NZDUSD', name: 'New Zealand Dollar vs US Dollar', category: 'forex', exchange: 'FX' },
+        { symbol: 'BTCUSD', name: 'Bitcoin vs US Dollar', category: 'crypto', exchange: 'Crypto' },
+        { symbol: 'ETHUSD', name: 'Ethereum vs US Dollar', category: 'crypto', exchange: 'Crypto' },
+        { symbol: 'AAPL', name: 'Apple Inc.', category: 'stocks', exchange: 'NASDAQ' },
+        { symbol: 'GOOGL', name: 'Alphabet Inc.', category: 'stocks', exchange: 'NASDAQ' },
+        { symbol: 'MSFT', name: 'Microsoft Corporation', category: 'stocks', exchange: 'NASDAQ' },
+        { symbol: 'TSLA', name: 'Tesla Inc.', category: 'stocks', exchange: 'NASDAQ' },
+        { symbol: 'AMZN', name: 'Amazon.com Inc.', category: 'stocks', exchange: 'NASDAQ' },
+        { symbol: 'META', name: 'Meta Platforms Inc.', category: 'stocks', exchange: 'NASDAQ' }
+      ];
+
+      for (const symbolData of sampleSymbols) {
+        let insertQuery;
+        if (dbType === 'sqlite') {
+          insertQuery = `
+            INSERT OR IGNORE INTO symbols (symbol, name, category, exchange, is_active)
+            VALUES (?, ?, ?, ?, 1)
+          `;
+        } else if (dbType === 'postgres') {
+          insertQuery = `
+            INSERT INTO symbols (symbol, name, category, exchange, is_active)
+            VALUES (?, ?, ?, ?, ?)
+            ON CONFLICT (symbol) DO NOTHING
+          `;
+        } else {
+          insertQuery = `
+            INSERT IGNORE INTO symbols (symbol, name, category, exchange, is_active)
+            VALUES (?, ?, ?, ?, 1)
+          `;
+        }
+
+        await executeNonQuery(insertQuery, [
+          symbolData.symbol,
+          symbolData.name,
+          symbolData.category,
+          symbolData.exchange,
+          true
+        ]);
+      }
+
+      logger.info(`Inserted ${sampleSymbols.length} sample symbols`);
+
+      // Generate sample price data for the last 1000 H1 candles
+      await this.generateSamplePriceData(sampleSymbols.slice(0, 7)); // Generate for first 7 symbols
+
+    } catch (error) {
+      logger.error('Error inserting sample data:', error);
+      throw error;
+    }
+  }
+
+  async generateSamplePriceData(symbols) {
+    try {
+      logger.info('Generating sample price data...');
+
+      const now = new Date();
+      const dbType = process.env.DB_TYPE || 'sqlite';
+
+      for (const symbolData of symbols) {
+        logger.info(`Generating price data for ${symbolData.symbol}...`);
+
+        // Generate 1000 H1 candles (about 41 days of data)
+        const priceData = [];
+        let currentPrice = this.getInitialPrice(symbolData.symbol);
+
+        for (let i = 999; i >= 0; i--) {
+          const timestamp = new Date(now.getTime() - (i * 60 * 60 * 1000)); // H1 intervals
+          const open = currentPrice;
+
+          // Generate realistic price movement (±1%)
+          const change = (Math.random() - 0.5) * 0.02;
+          const close = open * (1 + change);
+
+          const high = Math.max(open, close) * (1 + Math.random() * 0.001);
+          const low = Math.min(open, close) * (1 - Math.random() * 0.001);
+          const volume = Math.floor(Math.random() * 1000000) + 100000;
+
+          priceData.push({
+            symbol: symbolData.symbol,
+            timeframe: 'H1',
+            timestamp,
+            open: open.toFixed(8),
+            high: high.toFixed(8),
+            low: low.toFixed(8),
+            close: close.toFixed(8),
+            volume
+          });
+
+          currentPrice = close;
+        }
+
+        // Insert price data in batches
+        const batchSize = 100;
+        for (let i = 0; i < priceData.length; i += batchSize) {
+          const batch = priceData.slice(i, i + batchSize);
+
+          for (const price of batch) {
+            let insertQuery;
+            if (dbType === 'sqlite') {
+              insertQuery = `
+                INSERT OR IGNORE INTO price_data (symbol, timeframe, timestamp, open, high, low, close, volume)
+                VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+              `;
+            } else if (dbType === 'postgres') {
+              insertQuery = `
+                INSERT INTO price_data (symbol, timeframe, timestamp, open, high, low, close, volume)
+                VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+                ON CONFLICT DO NOTHING
+              `;
+            } else {
+              insertQuery = `
+                INSERT IGNORE INTO price_data (symbol, timeframe, timestamp, open, high, low, close, volume)
+                VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+              `;
+            }
+
+            await executeNonQuery(insertQuery, [
+              price.symbol,
+              price.timeframe,
+              price.timestamp,
+              price.open,
+              price.high,
+              price.low,
+              price.close,
+              price.volume
+            ]);
+          }
+        }
+
+        logger.info(`Generated ${priceData.length} price records for ${symbolData.symbol}`);
+      }
+
+      logger.info('Sample price data generation completed');
+
+    } catch (error) {
+      logger.error('Error generating sample price data:', error);
+      throw error;
+    }
+  }
+
+  getInitialPrice(symbol) {
+    const initialPrices = {
+      'EURUSD': 1.0850,
+      'GBPUSD': 1.2650,
+      'USDJPY': 150.50,
+      'USDCHF': 0.9150,
+      'USDCAD': 1.3450,
+      'AUDUSD': 0.6550,
+      'NZDUSD': 0.6050
+    };
+
+    return initialPrices[symbol] || 1.0000;
+  }
+
+  async reset() {
+    try {
+      logger.info('Resetting database...');
+
+      const dbType = process.env.DB_TYPE || 'sqlite';
+
+      // Drop tables
+      if (dbType === 'sqlite') {
+        await executeNonQuery('DROP TABLE IF EXISTS price_data');
+        await executeNonQuery('DROP TABLE IF EXISTS symbols');
+      } else {
+        await executeNonQuery('DROP TABLE IF EXISTS price_data');
+        await executeNonQuery('DROP TABLE IF EXISTS symbols');
+      }
+
+      this.initialized = false;
+      logger.info('Database reset completed');
+
+      // Reinitialize
+      await this.initialize();
+
+    } catch (error) {
+      logger.error('Error resetting database:', error);
+      throw error;
+    }
+  }
+
+  async getStats() {
+    try {
+      const dbType = process.env.DB_TYPE || 'sqlite';
+
+      let symbolCount, priceCount;
+
+      if (dbType === 'sqlite') {
+        symbolCount = await executeQuery('SELECT COUNT(*) as count FROM symbols');
+        priceCount = await executeQuery('SELECT COUNT(*) as count FROM price_data');
+      } else {
+        symbolCount = await executeQuery('SELECT COUNT(*) as count FROM symbols');
+        priceCount = await executeQuery('SELECT COUNT(*) as count FROM price_data');
+      }
+
+      // Handle different database result formats
+      let symbols = 0;
+      let priceRecords = 0;
+
+      if (symbolCount && symbolCount.length > 0) {
+        symbols = symbolCount[0].count || 0;
+      }
+
+      if (priceCount && priceCount.length > 0) {
+        priceRecords = priceCount[0].count || 0;
+      }
+
+      return {
+        symbols,
+        price_records: priceRecords,
+        initialized: this.initialized
+      };
+
+    } catch (error) {
+      logger.error('Error getting database stats:', error);
+      return {
+        symbols: 0,
+        price_records: 0,
+        initialized: false
+      };
+    }
+  }
+}
+
+const databaseInitializer = new DatabaseInitializer();
+
+// Export functions for external use
+const initializeDatabase = () => databaseInitializer.initialize();
+const resetDatabase = () => databaseInitializer.reset();
+const getDatabaseStats = () => databaseInitializer.getStats();
+
+module.exports = {
+  DatabaseInitializer,
+  initializeDatabase,
+  resetDatabase,
+  getDatabaseStats,
+  databaseInitializer
+};

+ 135 - 0
utils/logger.js

@@ -0,0 +1,135 @@
+const winston = require('winston');
+const path = require('path');
+
+// Define log levels
+const logLevels = {
+  error: 0,
+  warn: 1,
+  info: 2,
+  http: 3,
+  debug: 4,
+};
+
+// Define log colors
+const logColors = {
+  error: 'red',
+  warn: 'yellow',
+  info: 'green',
+  http: 'magenta',
+  debug: 'white',
+};
+
+winston.addColors(logColors);
+
+// Create logs directory if it doesn't exist
+const fs = require('fs');
+const logDir = path.join(__dirname, '../logs');
+if (!fs.existsSync(logDir)) {
+  fs.mkdirSync(logDir, { recursive: true });
+}
+
+// Define log format
+const logFormat = winston.format.combine(
+  winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
+  winston.format.errors({ stack: true }),
+  winston.format.json(),
+  winston.format.printf(({ timestamp, level, message, stack, ...meta }) => {
+    let log = `${timestamp} [${level.toUpperCase()}]: ${message}`;
+
+    if (Object.keys(meta).length > 0) {
+      log += ` | ${JSON.stringify(meta)}`;
+    }
+
+    if (stack) {
+      log += `\n${stack}`;
+    }
+
+    return log;
+  })
+);
+
+// Create console format for development
+const consoleFormat = winston.format.combine(
+  winston.format.colorize({ all: true }),
+  winston.format.timestamp({ format: 'HH:mm:ss' }),
+  winston.format.printf(({ timestamp, level, message, ...meta }) => {
+    let log = `${timestamp} [${level}]: ${message}`;
+
+    if (Object.keys(meta).length > 0) {
+      log += ` | ${JSON.stringify(meta)}`;
+    }
+
+    return log;
+  })
+);
+
+// Create the logger instance
+const logger = winston.createLogger({
+  level: process.env.LOG_LEVEL || 'info',
+  levels: logLevels,
+  format: logFormat,
+  defaultMeta: { service: 'financial-data-server' },
+  transports: [
+    // Write all logs with importance level of `error` or less to `error.log`
+    new winston.transports.File({
+      filename: path.join(logDir, 'error.log'),
+      level: 'error',
+      maxsize: 5242880, // 5MB
+      maxFiles: 5,
+    }),
+    // Write all logs with importance level of `info` or less to `combined.log`
+    new winston.transports.File({
+      filename: path.join(logDir, 'combined.log'),
+      maxsize: 5242880, // 5MB
+      maxFiles: 5,
+    }),
+  ],
+});
+
+// If we're not in production, log to the console as well
+if (process.env.NODE_ENV !== 'production') {
+  logger.add(new winston.transports.Console({
+    format: consoleFormat,
+    level: process.env.LOG_LEVEL || 'debug',
+  }));
+}
+
+// Create a stream object for Morgan HTTP logger
+const stream = {
+  write: (message) => {
+    logger.http(message.trim());
+  },
+};
+
+// Handle uncaught exceptions and unhandled rejections
+logger.exceptions.handle(
+  new winston.transports.File({
+    filename: path.join(logDir, 'exceptions.log')
+  })
+);
+
+logger.rejections.handle(
+  new winston.transports.File({
+    filename: path.join(logDir, 'rejections.log')
+  })
+);
+
+// Graceful shutdown
+process.on('SIGTERM', () => {
+  logger.info('SIGTERM received, shutting down gracefully');
+  logger.end(() => {
+    process.exit(0);
+  });
+});
+
+process.on('SIGINT', () => {
+  logger.info('SIGINT received, shutting down gracefully');
+  logger.end(() => {
+    process.exit(0);
+  });
+});
+
+module.exports = {
+  logger,
+  stream
+};