diff --git a/docker-compose.yml b/docker-compose.yml
index 4483d65..83e344a 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,5 +1,3 @@
-version: '3.8'
-
services:
redis:
image: redis:7-alpine
diff --git a/frontend/package.json b/frontend/package.json
index 3dfa28d..b172953 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -3,17 +3,42 @@
"version": "1.0.0",
"private": true,
"dependencies": {
+ "@testing-library/jest-dom": "^5.16.4",
+ "@testing-library/react": "^13.3.0",
+ "@testing-library/user-event": "^13.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "react-scripts": "5.0.1",
"react-router-dom": "^6.3.0",
+ "react-scripts": "5.0.1",
"axios": "^1.4.0",
"@mui/material": "^5.14.1",
- "@mui/icons-material": "^5.14.1"
+ "@mui/icons-material": "^5.14.1",
+ "@emotion/react": "^11.11.1",
+ "@emotion/styled": "^11.11.0",
+ "web-vitals": "^2.1.4"
},
"scripts": {
- "start": "rea[<35;82;26Mct-scripts start",
+ "start": "react-scripts start",
"build": "react-scripts build",
- "test": "react-scripts test"
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
}
}
diff --git a/frontend/public/index.html b/frontend/public/index.html
new file mode 100644
index 0000000..488a532
--- /dev/null
+++ b/frontend/public/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+ Trading Platform
+
+
+
+
+
+
diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json
new file mode 100644
index 0000000..f4bd73e
--- /dev/null
+++ b/frontend/public/manifest.json
@@ -0,0 +1,15 @@
+{
+ "short_name": "Trading",
+ "name": "Trading Platform",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#1976d2",
+ "background_color": "#0a0e27"
+}
diff --git a/frontend/public/robots.txt b/frontend/public/robots.txt
new file mode 100644
index 0000000..3b39592
--- /dev/null
+++ b/frontend/public/robots.txt
@@ -0,0 +1,3 @@
+# Allow all robot
+User-agent: *
+Disallow:
diff --git a/frontend/src/App.css b/frontend/src/App.css
new file mode 100644
index 0000000..f25157f
--- /dev/null
+++ b/frontend/src/App.css
@@ -0,0 +1,15 @@
+.App {
+ min-height: 100vh;
+ background-color: #0a0e27;
+}
+
+.App-header {
+ background-color: #1a1a2e;
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ font-size: calc(10px + 2vmin);
+ color: white;
+}
diff --git a/frontend/src/App.js b/frontend/src/App.js
new file mode 100644
index 0000000..1b598ef
--- /dev/null
+++ b/frontend/src/App.js
@@ -0,0 +1,80 @@
+import React from 'react';
+import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
+import { ThemeProvider, createTheme } from '@mui/material/styles';
+import CssBaseline from '@mui/material/CssBaseline';
+import { AuthProvider } from './context/AuthContext';
+import { WebSocketProvider } from './context/WebSocketContext';
+import ProtectedRoute from './components/common/ProtectedRoute';
+import LoginPage from './pages/LoginPage';
+import DashboardPage from './pages/DashboardPage';
+import OrderBookPage from './pages/OrderBookPage';
+import PortfolioPage from './pages/PortfolioPage';
+import MarketDataPage from './pages/MarketDataPage';
+import SettingsPage from './pages/SettingsPage';
+import './App.css';
+
+const darkTheme = createTheme({
+ palette: {
+ mode: 'dark',
+ primary: {
+ main: '#1976d2',
+ },
+ secondary: {
+ main: '#dc004e',
+ },
+ background: {
+ default: '#0a0e27',
+ paper: '#1a1a2e',
+ },
+ },
+ typography: {
+ fontFamily: 'Roboto, Arial, sans-serif',
+ },
+});
+
+function App() {
+ return (
+
+
+
+
+
+
+
+ } />
+
+
+
+ } />
+
+
+
+ } />
+
+
+
+ } />
+
+
+
+ } />
+
+
+
+ } />
+ } />
+
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/frontend/src/components/common/Header.js b/frontend/src/components/common/Header.js
new file mode 100644
index 0000000..e41930e
--- /dev/null
+++ b/frontend/src/components/common/Header.js
@@ -0,0 +1,88 @@
+import React from 'react';
+import {
+ AppBar,
+ Toolbar,
+ Typography,
+ IconButton,
+ Menu,
+ MenuItem,
+ Avatar,
+ Box
+} from '@mui/material';
+import { AccountCircle, Notifications } from '@mui/icons-material';
+import { useAuth } from '../../context/AuthContext';
+import { useWebSocket } from '../../context/WebSocketContext';
+
+const Header = () => {
+ const { user, logout } = useAuth();
+ const { connected } = useWebSocket();
+ const [anchorEl, setAnchorEl] = React.useState(null);
+
+ const handleMenu = (event) => {
+ setAnchorEl(event.currentTarget);
+ };
+
+ const handleClose = () => {
+ setAnchorEl(null);
+ };
+
+ const handleLogout = () => {
+ handleClose();
+ logout();
+ };
+
+ return (
+
+
+
+ Trading Platform
+
+
+
+ {/* Connection Status */}
+
+
+
+ {connected ? 'Connected' : 'Disconnected'}
+
+
+
+ {/* Notifications */}
+
+
+
+
+ {/* User Menu */}
+
+
+ {user?.firstName?.charAt(0) || 'U'}
+
+
+
+
+
+
+ );
+};
+
+export default Header;
diff --git a/frontend/src/components/common/ProtectedRoute.js b/frontend/src/components/common/ProtectedRoute.js
new file mode 100644
index 0000000..a67c212
--- /dev/null
+++ b/frontend/src/components/common/ProtectedRoute.js
@@ -0,0 +1,20 @@
+import React from 'react';
+import { Navigate } from 'react-router-dom';
+import { useAuth } from '../../context/AuthContext';
+import { CircularProgress, Box } from '@mui/material';
+
+const ProtectedRoute = ({ children }) => {
+ const { isAuthenticated, loading } = useAuth();
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ return isAuthenticated ? children : ;
+};
+
+export default ProtectedRoute;
diff --git a/frontend/src/components/common/Sidebar.js b/frontend/src/components/common/Sidebar.js
new file mode 100644
index 0000000..fa465ff
--- /dev/null
+++ b/frontend/src/components/common/Sidebar.js
@@ -0,0 +1,74 @@
+import React from 'react';
+import { useNavigate, useLocation } from 'react-router-dom';
+import {
+ Drawer,
+ List,
+ ListItem,
+ ListItemButton,
+ ListItemIcon,
+ ListItemText,
+ Box
+} from '@mui/material';
+import {
+ Dashboard,
+ ShowChart,
+ AccountBalance,
+ TrendingUp,
+ Settings
+} from '@mui/icons-material';
+
+const Sidebar = () => {
+ const navigate = useNavigate();
+ const location = useLocation();
+
+ const menuItems = [
+ { text: 'Dashboard', icon: , path: '/' },
+ { text: 'OrderBook', icon: , path: '/orderbook' },
+ { text: 'Portfolio', icon: , path: '/portfolio' },
+ { text: 'Market Data', icon: , path: '/market-data' },
+ { text: 'Settings', icon: , path: '/settings' }
+ ];
+
+ return (
+
+
+
+ {menuItems.map((item) => (
+
+ navigate(item.path)}
+ sx={{
+ '&.Mui-selected': {
+ bgcolor: 'rgba(25, 118, 210, 0.2)',
+ borderRight: '3px solid #1976d2'
+ }
+ }}
+ >
+
+ {item.icon}
+
+
+
+
+ ))}
+
+
+
+ );
+};
+
+export default Sidebar;
diff --git a/frontend/src/context/AuthContext.js b/frontend/src/context/AuthContext.js
new file mode 100644
index 0000000..283e135
--- /dev/null
+++ b/frontend/src/context/AuthContext.js
@@ -0,0 +1,89 @@
+import React, { createContext, useContext, useState, useEffect } from 'react';
+import authService from '../services/authService';
+
+const AuthContext = createContext();
+
+export const useAuth = () => {
+ const context = useContext(AuthContext);
+ if (!context) {
+ throw new Error('useAuth must be used within an AuthProvider');
+ }
+ return context;
+};
+
+export const AuthProvider = ({ children }) => {
+ const [user, setUser] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [isAuthenticated, setIsAuthenticated] = useState(false);
+
+ useEffect(() => {
+ const initializeAuth = async () => {
+ const token = localStorage.getItem('token');
+ if (token) {
+ try {
+ // Parse JWT to get user data (simple decode, don't use in production without validation)
+ const base64Url = token.split('.')[1];
+ const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
+ const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
+ return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
+ }).join(''));
+
+ const userData = JSON.parse(jsonPayload);
+ setUser(userData);
+ setIsAuthenticated(true);
+ } catch (error) {
+ console.error('Token validation failed:', error);
+ localStorage.removeItem('token');
+ }
+ }
+ setLoading(false);
+ };
+
+ initializeAuth();
+ }, []);
+
+ const login = async (email, password) => {
+ try {
+ const response = await authService.login(email, password);
+ if (response.success) {
+ localStorage.setItem('token', response.token);
+ setUser(response.user);
+ setIsAuthenticated(true);
+ return { success: true };
+ }
+ return { success: false, message: response.message };
+ } catch (error) {
+ return { success: false, message: 'Login failed' };
+ }
+ };
+
+ const logout = () => {
+ localStorage.removeItem('token');
+ setUser(null);
+ setIsAuthenticated(false);
+ };
+
+ const register = async (userData) => {
+ try {
+ const response = await authService.register(userData);
+ return response;
+ } catch (error) {
+ return { success: false, message: 'Registration failed' };
+ }
+ };
+
+ const value = {
+ user,
+ loading,
+ isAuthenticated,
+ login,
+ logout,
+ register
+ };
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/frontend/src/context/WebSocketContext.js b/frontend/src/context/WebSocketContext.js
new file mode 100644
index 0000000..a96a6d2
--- /dev/null
+++ b/frontend/src/context/WebSocketContext.js
@@ -0,0 +1,126 @@
+import React, { createContext, useContext, useEffect, useState } from 'react';
+import { useAuth } from './AuthContext';
+
+const WebSocketContext = createContext();
+
+export const useWebSocket = () => {
+ const context = useContext(WebSocketContext);
+ if (!context) {
+ throw new Error('useWebSocket must be used within a WebSocketProvider');
+ }
+ return context;
+};
+
+export const WebSocketProvider = ({ children }) => {
+ const [ws, setWs] = useState(null);
+ const [connected, setConnected] = useState(false);
+ const [marketData, setMarketData] = useState({});
+ const [orderBookData, setOrderBookData] = useState({});
+ const { isAuthenticated } = useAuth();
+
+ useEffect(() => {
+ if (!isAuthenticated) return;
+
+ const wsUrl = process.env.REACT_APP_WS_URL || 'ws://localhost:8080';
+ const websocket = new WebSocket(wsUrl);
+
+ websocket.onopen = () => {
+ console.log('WebSocket connected');
+ setConnected(true);
+ setWs(websocket);
+ };
+
+ websocket.onmessage = (event) => {
+ try {
+ const data = JSON.parse(event.data);
+ handleWebSocketMessage(data);
+ } catch (error) {
+ console.error('Error parsing WebSocket message:', error);
+ }
+ };
+
+ websocket.onclose = () => {
+ console.log('WebSocket disconnected');
+ setConnected(false);
+ setWs(null);
+ };
+
+ websocket.onerror = (error) => {
+ console.error('WebSocket error:', error);
+ };
+
+ return () => {
+ if (websocket.readyState === WebSocket.OPEN) {
+ websocket.close();
+ }
+ };
+ }, [isAuthenticated]);
+
+ const handleWebSocketMessage = (data) => {
+ switch (data.type) {
+ case 'broadcast':
+ if (data.channel === 'market_data_updates') {
+ handleMarketDataUpdate(data.data);
+ }
+ break;
+ case 'connection':
+ console.log('WebSocket connection confirmed:', data.clientId);
+ break;
+ case 'subscription_confirmed':
+ console.log('Subscribed to:', data.channel);
+ break;
+ default:
+ console.log('Unknown WebSocket message type:', data.type);
+ }
+ };
+
+ const handleMarketDataUpdate = (data) => {
+ if (data.type === 'price_update') {
+ setMarketData(prev => ({
+ ...prev,
+ [data.symbol]: {
+ ...prev[data.symbol],
+ price: data.price,
+ timestamp: data.timestamp
+ }
+ }));
+ } else if (data.type === 'orderbook_update') {
+ setOrderBookData(prev => ({
+ ...prev,
+ [data.symbol]: data.data
+ }));
+ }
+ };
+
+ const subscribe = (channel) => {
+ if (ws && connected) {
+ ws.send(JSON.stringify({
+ type: 'subscribe',
+ channel
+ }));
+ }
+ };
+
+ const unsubscribe = (channel) => {
+ if (ws && connected) {
+ ws.send(JSON.stringify({
+ type: 'unsubscribe',
+ channel
+ }));
+ }
+ };
+
+ const value = {
+ connected,
+ marketData,
+ orderBookData,
+ subscribe,
+ unsubscribe
+ };
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/frontend/src/index.css b/frontend/src/index.css
new file mode 100644
index 0000000..4f3d9b7
--- /dev/null
+++ b/frontend/src/index.css
@@ -0,0 +1,19 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+code {
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+ monospace;
+}
diff --git a/frontend/src/index.js b/frontend/src/index.js
new file mode 100644
index 0000000..0dd56bb
--- /dev/null
+++ b/frontend/src/index.js
@@ -0,0 +1,11 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import './index.css';
+import App from './App';
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(
+
+
+
+);
diff --git a/frontend/src/pages/DashboardPage.js b/frontend/src/pages/DashboardPage.js
new file mode 100644
index 0000000..0a2b0c6
--- /dev/null
+++ b/frontend/src/pages/DashboardPage.js
@@ -0,0 +1,58 @@
+import React from 'react';
+import { Box, Grid, Paper, Typography } from '@mui/material';
+import Header from '../components/common/Header';
+import Sidebar from '../components/common/Sidebar';
+import { useAuth } from '../context/AuthContext';
+
+const DashboardPage = () => {
+ const { user } = useAuth();
+
+ return (
+
+
+
+
+
+
+ Welcome back, {user?.firstName || 'Trader'}!
+
+
+
+
+
+ Portfolio Value
+ $125,000.50
+ +2.5% Today
+
+
+
+
+
+ Daily P&L
+ +$2,500.75
+ +2.04%
+
+
+
+
+
+ Active Orders
+ 12
+ 3 Pending
+
+
+
+
+
+ Quick Actions
+
+ Navigate using the sidebar to access OrderBook, Portfolio, Market Data, and Settings.
+
+
+
+
+
+ );
+};
+
+export default DashboardPage;
diff --git a/frontend/src/pages/LoginPage.js b/frontend/src/pages/LoginPage.js
new file mode 100644
index 0000000..f5e61f0
--- /dev/null
+++ b/frontend/src/pages/LoginPage.js
@@ -0,0 +1,105 @@
+import React, { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import {
+ Container,
+ Box,
+ TextField,
+ Button,
+ Typography,
+ Paper,
+ Alert
+} from '@mui/material';
+import { useAuth } from '../context/AuthContext';
+
+const LoginPage = () => {
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
+ const { login } = useAuth();
+ const navigate = useNavigate();
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ setError('');
+
+ const result = await login(email, password);
+ if (result.success) {
+ navigate('/');
+ } else {
+ setError(result.message || 'Login failed');
+ }
+ };
+
+ return (
+
+
+
+
+ Trading Platform
+
+
+ Sign in to continue
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+ setEmail(e.target.value)}
+ />
+ setPassword(e.target.value)}
+ />
+
+
+
+
+ Demo Credentials:
+
+
+ Email: trader@example.com
+
+
+ Password: demo123
+
+
+
+
+
+
+ );
+};
+
+export default LoginPage;
diff --git a/frontend/src/pages/MarketDataPage.js b/frontend/src/pages/MarketDataPage.js
new file mode 100644
index 0000000..fc20c2f
--- /dev/null
+++ b/frontend/src/pages/MarketDataPage.js
@@ -0,0 +1,23 @@
+import React from 'react';
+import { Box, Paper, Typography } from '@mui/material';
+import Header from '../components/common/Header';
+import Sidebar from '../components/common/Sidebar';
+
+const MarketDataPage = () => {
+ return (
+
+
+
+
+
+ Market Data
+
+ Real-time market data will be displayed here.
+
+
+
+
+ );
+};
+
+export default MarketDataPage;
diff --git a/frontend/src/pages/OrderBookPage.js b/frontend/src/pages/OrderBookPage.js
new file mode 100644
index 0000000..b6b5cb5
--- /dev/null
+++ b/frontend/src/pages/OrderBookPage.js
@@ -0,0 +1,167 @@
+import React, { useState, useEffect } from 'react';
+import { Box, Paper, Typography, Grid, Button, TextField, MenuItem } from '@mui/material';
+import Header from '../components/common/Header';
+import Sidebar from '../components/common/Sidebar';
+import orderService from '../services/orderService';
+import { useAuth } from '../context/AuthContext';
+
+const OrderBookPage = () => {
+ const [orderBook, setOrderBook] = useState({ bids: [], asks: [] });
+ const [symbol, setSymbol] = useState('BTC/USD');
+ const [side, setSide] = useState('BUY');
+ const [quantity, setQuantity] = useState('');
+ const [price, setPrice] = useState('');
+ const [message, setMessage] = useState('');
+ const { user } = useAuth();
+
+ useEffect(() => {
+ loadOrderBook();
+ const interval = setInterval(loadOrderBook, 5000);
+ return () => clearInterval(interval);
+ }, [symbol]);
+
+ const loadOrderBook = async () => {
+ try {
+ const response = await orderService.getOrderBook(symbol);
+ if (response.success) {
+ setOrderBook(response.data);
+ }
+ } catch (error) {
+ console.error('Failed to load order book:', error);
+ }
+ };
+
+ const handlePlaceOrder = async () => {
+ try {
+ const response = await orderService.placeOrder({
+ symbol,
+ side,
+ quantity: parseFloat(quantity),
+ price: parseFloat(price),
+ type: 'LIMIT'
+ });
+
+ if (response.success) {
+ setMessage(`Order placed successfully! Order ID: ${response.orderId}`);
+ setQuantity('');
+ setPrice('');
+ loadOrderBook();
+ }
+ } catch (error) {
+ setMessage('Failed to place order: ' + error.message);
+ }
+ };
+
+ return (
+
+
+
+
+
+ Order Book
+
+
+ {/* Order Book Display */}
+
+
+ {symbol}
+
+
+ {/* Bids */}
+
+ BIDS
+ {orderBook.bids?.slice(0, 10).map((bid, idx) => (
+
+ {bid.price.toFixed(2)}
+ {bid.quantity.toFixed(4)}
+
+ ))}
+
+
+ {/* Asks */}
+
+ ASKS
+ {orderBook.asks?.slice(0, 10).map((ask, idx) => (
+
+ {ask.price.toFixed(2)}
+ {ask.quantity.toFixed(4)}
+
+ ))}
+
+
+
+
+
+ {/* Order Entry Form */}
+
+
+ Place Order
+
+ setSymbol(e.target.value)}
+ margin="normal"
+ >
+
+
+
+
+ setSide(e.target.value)}
+ margin="normal"
+ >
+
+
+
+
+ setQuantity(e.target.value)}
+ margin="normal"
+ />
+
+ setPrice(e.target.value)}
+ margin="normal"
+ />
+
+
+
+ {message && (
+
+ {message}
+
+ )}
+
+
+
+
+
+
+ );
+};
+
+export default OrderBookPage;
diff --git a/frontend/src/pages/PortfolioPage.js b/frontend/src/pages/PortfolioPage.js
new file mode 100644
index 0000000..539059e
--- /dev/null
+++ b/frontend/src/pages/PortfolioPage.js
@@ -0,0 +1,57 @@
+import React from 'react';
+import { Box, Paper, Typography, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material';
+import Header from '../components/common/Header';
+import Sidebar from '../components/common/Sidebar';
+
+const PortfolioPage = () => {
+ const positions = [
+ { symbol: 'BTC/USD', quantity: 2.5, avgPrice: 45000, currentPrice: 45250.50, pnl: 626.25 },
+ { symbol: 'ETH/USD', quantity: 10.0, avgPrice: 3100, currentPrice: 3150.25, pnl: 502.50 },
+ ];
+
+ return (
+
+
+
+
+
+ Portfolio
+
+
+ Total Portfolio Value: $125,000.50
+ Daily P&L: +$2,500.75 (+2.04%)
+
+
+
+
+
+
+ Symbol
+ Quantity
+ Avg Price
+ Current Price
+ P&L
+
+
+
+ {positions.map((position) => (
+
+ {position.symbol}
+ {position.quantity}
+ ${position.avgPrice.toFixed(2)}
+ ${position.currentPrice.toFixed(2)}
+ = 0 ? 'success.main' : 'error.main' }}>
+ ${position.pnl.toFixed(2)}
+
+
+ ))}
+
+
+
+
+
+
+ );
+};
+
+export default PortfolioPage;
diff --git a/frontend/src/pages/SettingsPage.js b/frontend/src/pages/SettingsPage.js
new file mode 100644
index 0000000..ed00c0d
--- /dev/null
+++ b/frontend/src/pages/SettingsPage.js
@@ -0,0 +1,23 @@
+import React from 'react';
+import { Box, Paper, Typography } from '@mui/material';
+import Header from '../components/common/Header';
+import Sidebar from '../components/common/Sidebar';
+
+const SettingsPage = () => {
+ return (
+
+
+
+
+
+ Settings
+
+ User settings and preferences will be displayed here.
+
+
+
+
+ );
+};
+
+export default SettingsPage;
diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js
new file mode 100644
index 0000000..f8dd1ed
--- /dev/null
+++ b/frontend/src/services/api.js
@@ -0,0 +1,41 @@
+import axios from 'axios';
+
+const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:8080/api';
+
+const api = axios.create({
+ baseURL: API_BASE_URL,
+ timeout: 10000,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+});
+
+// Request interceptor to add auth token
+api.interceptors.request.use(
+ (config) => {
+ const token = localStorage.getItem('token');
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+ return config;
+ },
+ (error) => {
+ return Promise.reject(error);
+ }
+);
+
+// Response interceptor to handle errors
+api.interceptors.response.use(
+ (response) => {
+ return response;
+ },
+ (error) => {
+ if (error.response?.status === 401) {
+ localStorage.removeItem('token');
+ window.location.href = '/login';
+ }
+ return Promise.reject(error);
+ }
+);
+
+export default api;
diff --git a/frontend/src/services/authService.js b/frontend/src/services/authService.js
new file mode 100644
index 0000000..d2196b1
--- /dev/null
+++ b/frontend/src/services/authService.js
@@ -0,0 +1,35 @@
+import api from './api';
+
+class AuthService {
+ async login(email, password) {
+ try {
+ const response = await api.post('/auth/login', { email, password });
+ return response.data;
+ } catch (error) {
+ throw error.response?.data || { message: 'Login failed' };
+ }
+ }
+
+ async register(userData) {
+ try {
+ const response = await api.post('/auth/register', userData);
+ return response.data;
+ } catch (error) {
+ throw error.response?.data || { message: 'Registration failed' };
+ }
+ }
+
+ logout() {
+ localStorage.removeItem('token');
+ }
+
+ getToken() {
+ return localStorage.getItem('token');
+ }
+
+ isAuthenticated() {
+ return !!this.getToken();
+ }
+}
+
+export default new AuthService();
diff --git a/frontend/src/services/marketDataService.js b/frontend/src/services/marketDataService.js
new file mode 100644
index 0000000..954e862
--- /dev/null
+++ b/frontend/src/services/marketDataService.js
@@ -0,0 +1,23 @@
+import api from './api';
+
+class MarketDataService {
+ async getMarketData(symbol) {
+ try {
+ const response = await api.get(`/market-data/${symbol}`);
+ return response.data;
+ } catch (error) {
+ throw error.response?.data || { message: 'Failed to fetch market data' };
+ }
+ }
+
+ async getSymbols() {
+ try {
+ const response = await api.get('/symbols');
+ return response.data;
+ } catch (error) {
+ throw error.response?.data || { message: 'Failed to fetch symbols' };
+ }
+ }
+}
+
+export default new MarketDataService();
diff --git a/frontend/src/services/orderService.js b/frontend/src/services/orderService.js
new file mode 100644
index 0000000..f2024ef
--- /dev/null
+++ b/frontend/src/services/orderService.js
@@ -0,0 +1,41 @@
+import api from './api';
+
+class OrderService {
+ async placeOrder(orderData) {
+ try {
+ const response = await api.post('/orders', orderData);
+ return response.data;
+ } catch (error) {
+ throw error.response?.data || { message: 'Failed to place order' };
+ }
+ }
+
+ async cancelOrder(orderId) {
+ try {
+ const response = await api.delete(`/orders/${orderId}`);
+ return response.data;
+ } catch (error) {
+ throw error.response?.data || { message: 'Failed to cancel order' };
+ }
+ }
+
+ async getUserOrders(userId) {
+ try {
+ const response = await api.get(`/orders/user/${userId}`);
+ return response.data;
+ } catch (error) {
+ throw error.response?.data || { message: 'Failed to fetch orders' };
+ }
+ }
+
+ async getOrderBook(symbol) {
+ try {
+ const response = await api.get(`/orderbook/${symbol}`);
+ return response.data;
+ } catch (error) {
+ throw error.response?.data || { message: 'Failed to fetch order book' };
+ }
+ }
+}
+
+export default new OrderService();
diff --git a/services/api-gateway/src/middleware/auth.js b/services/api-gateway/src/middleware/auth.js
new file mode 100644
index 0000000..ddaa3ed
--- /dev/null
+++ b/services/api-gateway/src/middleware/auth.js
@@ -0,0 +1,35 @@
+const AuthService = require('../services/AuthService');
+
+const authMiddleware = async (req, res, next) => {
+ try {
+ const authHeader = req.headers.authorization;
+
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
+ return res.status(401).json({
+ success: false,
+ message: 'No token provided'
+ });
+ }
+
+ const token = authHeader.substring(7);
+ const validation = await AuthService.validateToken(token);
+
+ if (!validation.valid) {
+ return res.status(401).json({
+ success: false,
+ message: validation.message
+ });
+ }
+
+ req.user = validation.user;
+ next();
+ } catch (error) {
+ console.error('Auth middleware error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Authentication error'
+ });
+ }
+};
+
+module.exports = authMiddleware;
diff --git a/services/api-gateway/src/middleware/errorHandler.js b/services/api-gateway/src/middleware/errorHandler.js
new file mode 100644
index 0000000..251b0e0
--- /dev/null
+++ b/services/api-gateway/src/middleware/errorHandler.js
@@ -0,0 +1,33 @@
+const errorHandler = (err, req, res, next) => {
+ console.error('Error:', err);
+
+ // Default error
+ let error = { ...err };
+ error.message = err.message;
+
+ // Mongoose bad ObjectId
+ if (err.name === 'CastError') {
+ const message = 'Resource not found';
+ error = { message, statusCode: 404 };
+ }
+
+ // Mongoose duplicate key
+ if (err.code === 11000) {
+ const message = 'Duplicate field value entered';
+ error = { message, statusCode: 400 };
+ }
+
+ // Mongoose validation error
+ if (err.name === 'ValidationError') {
+ const message = Object.values(err.errors).map(val => val.message).join(', ');
+ error = { message, statusCode: 400 };
+ }
+
+ res.status(error.statusCode || 500).json({
+ success: false,
+ message: error.message || 'Server Error',
+ ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
+ });
+};
+
+module.exports = errorHandler;
diff --git a/services/api-gateway/src/server.js b/services/api-gateway/src/server.js
new file mode 100644
index 0000000..cd5d26d
--- /dev/null
+++ b/services/api-gateway/src/server.js
@@ -0,0 +1,137 @@
+const express = require('express');
+const http = require('http');
+const WebSocket = require('ws');
+const cors = require('cors');
+const helmet = require('helmet');
+const rateLimit = require('express-rate-limit');
+const { createProxyMiddleware } = require('http-proxy-middleware');
+const path = require('path');
+require('dotenv').config();
+
+const AuthService = require('./services/AuthService');
+const WebSocketManager = require('./services/WebSocketManager');
+const authMiddleware = require('./middleware/auth');
+const errorHandler = require('./middleware/errorHandler');
+
+const app = express();
+const server = http.createServer(app);
+
+// Security middleware
+app.use(helmet());
+app.use(cors({
+ origin: process.env.FRONTEND_URL || 'http://localhost:3000',
+ credentials: true
+}));
+
+// Rate limiting
+const limiter = rateLimit({
+ windowMs: 15 * 60 * 1000, // 15 minutes
+ max: 100,
+ message: 'Too many requests from this IP'
+});
+app.use('/api/', limiter);
+
+app.use(express.json({ limit: '10mb' }));
+app.use(express.urlencoded({ extended: true }));
+
+// Serve static frontend files
+app.use(express.static(path.join(__dirname, '../../../frontend/build')));
+
+// Authentication routes
+app.post('/api/auth/login', async (req, res) => {
+ try {
+ const { email, password } = req.body;
+ const result = await AuthService.login(email, password);
+
+ if (result.success) {
+ res.json({
+ success: true,
+ token: result.token,
+ user: result.user,
+ expiresIn: '24h'
+ });
+ } else {
+ res.status(401).json({
+ success: false,
+ message: 'Invalid credentials'
+ });
+ }
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ message: 'Authentication failed'
+ });
+ }
+});
+
+app.post('/api/auth/register', async (req, res) => {
+ try {
+ const { email, password, firstName, lastName } = req.body;
+ const result = await AuthService.register(email, password, firstName, lastName);
+
+ if (result.success) {
+ res.json({
+ success: true,
+ message: 'User created successfully',
+ userId: result.userId
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.message
+ });
+ }
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ message: 'Registration failed'
+ });
+ }
+});
+
+// Health check
+app.get('/api/health', (req, res) => {
+ res.json({
+ status: 'healthy',
+ timestamp: new Date().toISOString(),
+ uptime: process.uptime()
+ });
+});
+
+// Protected API routes with microservice proxying
+app.use('/api/orders', authMiddleware, createProxyMiddleware({
+ target: process.env.ORDERBOOK_SERVICE_URL || 'http://localhost:8082',
+ changeOrigin: true,
+ pathRewrite: { '^/api/orders': '/orders' }
+}));
+
+app.use('/api/market-data', authMiddleware, createProxyMiddleware({
+ target: process.env.MARKET_DATA_SERVICE_URL || 'http://localhost:8083',
+ changeOrigin: true,
+ pathRewrite: { '^/api/market-data': '/market-data' }
+}));
+
+app.use('/api/users', authMiddleware, createProxyMiddleware({
+ target: process.env.USER_SERVICE_URL || 'http://localhost:8084',
+ changeOrigin: true,
+ pathRewrite: { '^/api/users': '/users' }
+}));
+
+// Serve React app for all other routes
+app.get('*', (req, res) => {
+ res.sendFile(path.join(__dirname, '../../../frontend/build/index.html'));
+});
+
+// Error handling
+app.use(errorHandler);
+
+// WebSocket server for real-time data
+const wss = new WebSocket.Server({ server });
+const wsManager = new WebSocketManager(wss);
+
+const PORT = process.env.PORT || 8080;
+server.listen(PORT, () => {
+ console.log(`🚀 Trading Platform API Gateway running on port ${PORT}`);
+ console.log(`📱 Frontend available at: http://localhost:${PORT}`);
+ console.log(`🔌 WebSocket server ready for real-time data`);
+});
diff --git a/services/api-gateway/src/services/AuthService.js b/services/api-gateway/src/services/AuthService.js
new file mode 100644
index 0000000..cd2c450
--- /dev/null
+++ b/services/api-gateway/src/services/AuthService.js
@@ -0,0 +1,133 @@
+const jwt = require('jsonwebtoken');
+const bcrypt = require('bcryptjs');
+const redis = require('redis');
+
+class AuthService {
+ constructor() {
+ this.redisClient = redis.createClient({
+ url: process.env.REDIS_URL || 'redis://localhost:6379'
+ });
+ this.redisClient.connect()
+ .then(() => console.log("Redis connected!"))
+ .catch(err => console.error("Redis connection error:", err));
+ this.jwtSecret = process.env.JWT_SECRET || 'your-super-secret-key';
+
+ // Demo users for testing
+ this.demoUsers = [
+ {
+ id: 1,
+ email: 'trader@example.com',
+ username: 'trader123',
+ password: bcrypt.hashSync('demo123', 10),
+ firstName: 'John',
+ lastName: 'Trader',
+ accountType: 'Premium',
+ joinDate: '2024-01-15'
+ },
+ {
+ id: 2,
+ email: 'admin@example.com',
+ username: 'admin',
+ password: bcrypt.hashSync('admin123', 10),
+ firstName: 'Admin',
+ lastName: 'User',
+ accountType: 'Admin',
+ joinDate: '2023-12-01'
+ }
+ ];
+ }
+
+ async login(email, password) {
+ try {
+ const user = this.demoUsers.find(u => u.email === email || u.username === email);
+
+ if (!user) {
+ return { success: false, message: 'User not found' };
+ }
+
+ const isValidPassword = bcrypt.compareSync(password, user.password);
+ if (!isValidPassword) {
+ return { success: false, message: 'Invalid password' };
+ }
+
+ const token = jwt.sign(
+ {
+ userId: user.id,
+ email: user.email,
+ accountType: user.accountType
+ },
+ this.jwtSecret,
+ { expiresIn: '24h' }
+ );
+
+ await this.redisClient.setEx(`session:${user.id}`, 86400, JSON.stringify({
+ userId: user.id,
+ email: user.email,
+ loginTime: new Date().toISOString()
+ }));
+
+ const { password: _, ...userWithoutPassword } = user;
+
+ return {
+ success: true,
+ token,
+ user: userWithoutPassword
+ };
+
+ } catch (error) {
+ console.error('Login error:', error);
+ return { success: false, message: 'Authentication failed' };
+ }
+ }
+
+ async register(email, password, firstName, lastName) {
+ try {
+ const existingUser = this.demoUsers.find(u => u.email === email);
+ if (existingUser) {
+ return { success: false, message: 'User already exists' };
+ }
+
+ const hashedPassword = bcrypt.hashSync(password, 10);
+ const newUser = {
+ id: this.demoUsers.length + 1,
+ email,
+ username: email.split('@')[0],
+ password: hashedPassword,
+ firstName,
+ lastName,
+ accountType: 'Standard',
+ joinDate: new Date().toISOString().split('T')[0]
+ };
+
+ this.demoUsers.push(newUser);
+
+ return {
+ success: true,
+ userId: newUser.id,
+ message: 'User registered successfully'
+ };
+
+ } catch (error) {
+ console.error('Registration error:', error);
+ return { success: false, message: 'Registration failed' };
+ }
+ }
+
+ async validateToken(token) {
+ try {
+ const decoded = jwt.verify(token, this.jwtSecret);
+ const sessionData = await this.redisClient.get(`session:${decoded.userId}`);
+
+ if (!sessionData) {
+ return { valid: false, message: 'Session expired' };
+ }
+
+ return { valid: true, user: decoded };
+
+ } catch (error) {
+ return { valid: false, message: 'Invalid token' };
+ }
+ }
+}
+
+module.exports = new AuthService();
diff --git a/services/api-gateway/src/services/WebSocketManager.js b/services/api-gateway/src/services/WebSocketManager.js
new file mode 100644
index 0000000..a60ea9f
--- /dev/null
+++ b/services/api-gateway/src/services/WebSocketManager.js
@@ -0,0 +1,188 @@
+const WebSocket = require('ws');
+const redis = require('redis');
+
+class WebSocketManager {
+ constructor(wss) {
+ this.wss = wss;
+ this.clients = new Map();
+ this.subscriptions = new Map();
+
+ this.redisSubscriber = redis.createClient({
+ url: process.env.REDIS_URL || 'redis://localhost:6379'
+ });
+ this.redisClient = redis.createClient({ url: process.env.REDIS_URL });
+ this.redisClient.connect()
+ .then(() => console.log("Redis connected!"))
+ .catch(err => console.error("Redis connection error:", err));
+
+ this.setupWebSocketServer();
+ this.setupRedisSubscriptions();
+ }
+
+ setupWebSocketServer() {
+ this.wss.on('connection', (ws, req) => {
+ const clientId = this.generateClientId();
+ this.clients.set(clientId, {
+ ws,
+ subscriptions: new Set(),
+ lastPing: Date.now()
+ });
+
+ console.log(`WebSocket client connected: ${clientId}`);
+
+ ws.on('message', (message) => {
+ this.handleMessage(clientId, message);
+ });
+
+ ws.on('close', () => {
+ this.handleDisconnect(clientId);
+ });
+
+ ws.on('pong', () => {
+ const client = this.clients.get(clientId);
+ if (client) {
+ client.lastPing = Date.now();
+ }
+ });
+
+ // Send a welcome message
+ this.sendToClient(clientId, {
+ type: 'connection',
+ status: 'connected',
+ clientId
+ });
+ });
+
+ // Heartbeat mechanism
+ setInterval(() => {
+ this.wss.clients.forEach((ws) => {
+ if (ws.readyState === WebSocket.OPEN) {
+ ws.ping();
+ }
+ });
+ }, 30000);
+ }
+
+ setupRedisSubscriptions() {
+ // Subscribe to market data updates
+ this.redisSubscriber.subscribe('market_data_updates');
+ this.redisSubscriber.subscribe('order_updates');
+ this.redisSubscriber.subscribe('trade_updates');
+
+ this.redisSubscriber.on('message', (channel, message) => {
+ this.broadcastToSubscribers(channel, JSON.parse(message));
+ });
+ }
+
+ handleMessage(clientId, message) {
+ try {
+ const data = JSON.parse(message);
+
+ switch (data.type) {
+ case 'subscribe':
+ this.handleSubscription(clientId, data.channel);
+ break;
+ case 'unsubscribe':
+ this.handleUnsubscription(clientId, data.channel);
+ break;
+ case 'ping':
+ this.sendToClient(clientId, { type: 'pong' });
+ break;
+ default:
+ console.log(`Unknown message type: ${data.type}`);
+ }
+ } catch (error) {
+ console.error('Error handling WebSocket message:', error);
+ }
+ }
+
+ handleSubscription(clientId, channel) {
+ const client = this.clients.get(clientId);
+ if (client) {
+ client.subscriptions.add(channel);
+
+ if (!this.subscriptions.has(channel)) {
+ this.subscriptions.set(channel, new Set());
+ }
+ this.subscriptions.get(channel).add(clientId);
+
+ this.sendToClient(clientId, {
+ type: 'subscription_confirmed',
+ channel
+ });
+
+ console.log(`Client ${clientId} subscribed to ${channel}`);
+ }
+ }
+
+ handleUnsubscription(clientId, channel) {
+ const client = this.clients.get(clientId);
+ if (client) {
+ client.subscriptions.delete(channel);
+
+ if (this.subscriptions.has(channel)) {
+ this.subscriptions.get(channel).delete(clientId);
+
+ if (this.subscriptions.get(channel).size === 0) {
+ this.subscriptions.delete(channel);
+ }
+ }
+
+ this.sendToClient(clientId, {
+ type: 'unsubscription_confirmed',
+ channel
+ });
+ }
+ }
+
+ handleDisconnect(clientId) {
+ const client = this.clients.get(clientId);
+ if (client) {
+ // Remove from all subscriptions
+ client.subscriptions.forEach(channel => {
+ if (this.subscriptions.has(channel)) {
+ this.subscriptions.get(channel).delete(clientId);
+ if (this.subscriptions.get(channel).size === 0) {
+ this.subscriptions.delete(channel);
+ }
+ }
+ });
+
+ this.clients.delete(clientId);
+ console.log(`WebSocket client disconnected: ${clientId}`);
+ }
+ }
+
+ sendToClient(clientId, data) {
+ const client = this.clients.get(clientId);
+ if (client && client.ws.readyState === WebSocket.OPEN) {
+ client.ws.send(JSON.stringify(data));
+ }
+ }
+
+ broadcastToSubscribers(channel, data) {
+ if (this.subscriptions.has(channel)) {
+ const subscribers = this.subscriptions.get(channel);
+ subscribers.forEach(clientId => {
+ this.sendToClient(clientId, {
+ type: 'broadcast',
+ channel,
+ data
+ });
+ });
+ }
+ }
+
+ broadcast(data) {
+ this.clients.forEach((client, clientId) => {
+ this.sendToClient(clientId, data);
+ });
+ }
+
+ generateClientId() {
+ return Math.random().toString(36).slice(2, 11);
+ }
+
+}
+
+module.exports = WebSocketManager;
diff --git a/services/market-data-service/package.json b/services/market-data-service/package.json
new file mode 100644
index 0000000..e402ddf
--- /dev/null
+++ b/services/market-data-service/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "market-data-service",
+ "version": "1.0.0",
+ "description": "Market data and analytics microservice",
+ "main": "src/server.js",
+ "scripts": {
+ "start": "node src/server.js",
+ "dev": "nodemon src/server.js"
+ },
+ "dependencies": {
+ "express": "^4.18.2",
+ "redis": "^4.6.7",
+ "dotenv": "^16.3.1"
+ },
+ "devDependencies": {
+ "nodemon": "^3.0.1"
+ }
+}
diff --git a/services/market-data-service/src/controllers/MarketDataController.js b/services/market-data-service/src/controllers/MarketDataController.js
new file mode 100644
index 0000000..3fe5b90
--- /dev/null
+++ b/services/market-data-service/src/controllers/MarketDataController.js
@@ -0,0 +1,120 @@
+class MarketDataController {
+ constructor(marketDataService) {
+ this.marketDataService = marketDataService;
+ }
+
+ async getMarketData(req, res) {
+ try {
+ const { symbol } = req.params;
+ const marketData = await this.marketDataService.getMarketData(symbol);
+
+ res.json({
+ success: true,
+ data: marketData
+ });
+
+ } catch (error) {
+ console.error('Get market data error:', error);
+ res.status(500).json({
+ success: false,
+ message: error.message
+ });
+ }
+ }
+
+ async getPriceHistory(req, res) {
+ try {
+ const { symbol } = req.params;
+ const interval = req.query.interval || '1h';
+ const limit = parseInt(req.query.limit) || 100;
+
+ const history = await this.marketDataService.getPriceHistory(symbol, interval, limit);
+
+ res.json({
+ success: true,
+ data: {
+ symbol,
+ interval,
+ history
+ }
+ });
+
+ } catch (error) {
+ console.error('Get price history error:', error);
+ res.status(500).json({
+ success: false,
+ message: error.message
+ });
+ }
+ }
+
+ async getMarketStats(req, res) {
+ try {
+ const { symbol } = req.params;
+ const stats = await this.marketDataService.getMarketStats(symbol);
+
+ res.json({
+ success: true,
+ data: {
+ symbol,
+ ...stats
+ }
+ });
+
+ } catch (error) {
+ console.error('Get market stats error:', error);
+ res.status(500).json({
+ success: false,
+ message: error.message
+ });
+ }
+ }
+
+ async getSymbols(req, res) {
+ try {
+ const symbols = await this.marketDataService.getSymbols();
+
+ res.json({
+ success: true,
+ data: symbols
+ });
+
+ } catch (error) {
+ console.error('Get symbols error:', error);
+ res.status(500).json({
+ success: false,
+ message: error.message
+ });
+ }
+ }
+
+ async getWatchlist(req, res) {
+ try {
+ const { userId } = req.params;
+ const requestingUserId = req.user?.userId;
+
+ if (parseInt(userId) !== requestingUserId && req.user?.accountType !== 'Admin') {
+ return res.status(403).json({
+ success: false,
+ message: 'Access denied'
+ });
+ }
+
+ const watchlist = await this.marketDataService.getWatchlist(userId);
+
+ res.json({
+ success: true,
+ data: watchlist
+ });
+
+ } catch (error) {
+ console.error('Get watchlist error:', error);
+ res.status(500).json({
+ success: false,
+ message: error.message
+ });
+ }
+ }
+}
+
+module.exports = MarketDataController;
diff --git a/services/market-data-service/src/server.js b/services/market-data-service/src/server.js
new file mode 100644
index 0000000..5ff02b9
--- /dev/null
+++ b/services/market-data-service/src/server.js
@@ -0,0 +1,38 @@
+const express = require('express');
+const MarketDataController = require('./controllers/MarketDataController');
+const MarketDataService = require('./services/MarketDataService');
+const PriceService = require('./services/PriceService');
+
+const app = express();
+app.use(express.json());
+
+// Initialize services
+const priceService = new PriceService();
+const marketDataService = new MarketDataService(priceService);
+
+// Initialize controller
+const marketDataController = new MarketDataController(marketDataService);
+
+// Market data routes
+app.get('/market-data/:symbol', (req, res) => marketDataController.getMarketData(req, res));
+app.get('/market-data/:symbol/history', (req, res) => marketDataController.getPriceHistory(req, res));
+app.get('/market-data/:symbol/stats', (req, res) => marketDataController.getMarketStats(req, res));
+app.get('/symbols', (req, res) => marketDataController.getSymbols(req, res));
+app.get('/watchlist/:userId', (req, res) => marketDataController.getWatchlist(req, res));
+
+// Health check
+app.get('/health', (req, res) => {
+ res.json({
+ status: 'healthy',
+ service: 'market-data-service',
+ timestamp: new Date().toISOString()
+ });
+});
+
+const PORT = process.env.PORT || 8083;
+app.listen(PORT, () => {
+ console.log(`📈 Market Data Service running on port ${PORT}`);
+});
+
+// Start price simulation
+priceService.startPriceSimulation();
diff --git a/services/market-data-service/src/services/MarketDataService.js b/services/market-data-service/src/services/MarketDataService.js
new file mode 100644
index 0000000..9c1a833
--- /dev/null
+++ b/services/market-data-service/src/services/MarketDataService.js
@@ -0,0 +1,147 @@
+const redis = require('redis');
+
+class MarketDataService {
+ constructor(priceService) {
+ this.priceService = priceService;
+ this.redisClient = redis.createClient({
+ url: process.env.REDIS_URL || 'redis://localhost:6379'
+ });
+ this.redisClient.connect();
+
+ this.symbols = [
+ 'BTC/USD', 'ETH/USD', 'BNB/USD', 'ADA/USD', 'SOL/USD',
+ 'AAPL', 'GOOGL', 'MSFT', 'TSLA', 'AMZN'
+ ];
+
+ // Initialize market stats
+ this.initializeMarketStats();
+ }
+
+ async initializeMarketStats() {
+ const initialStats = {
+ 'BTC/USD': {
+ high24h: 46100.00,
+ low24h: 43800.00,
+ volume24h: 234567.89,
+ priceChange24h: 1250.75,
+ priceChangePercent24h: 2.84
+ },
+ 'ETH/USD': {
+ high24h: 3280.00,
+ low24h: 3050.00,
+ volume24h: 567890.12,
+ priceChange24h: -125.50,
+ priceChangePercent24h: -3.83
+ },
+ 'AAPL': {
+ high24h: 178.50,
+ low24h: 172.00,
+ volume24h: 89234567,
+ priceChange24h: 2.25,
+ priceChangePercent24h: 1.29
+ }
+ };
+
+ for (const [symbol, stats] of Object.entries(initialStats)) {
+ await this.redisClient.setEx(
+ `market_stats:${symbol}`,
+ 3600,
+ JSON.stringify(stats)
+ );
+ }
+ }
+
+ async getMarketData(symbol) {
+ try {
+ const currentPrice = await this.priceService.getCurrentPrice(symbol);
+ const stats = await this.getMarketStats(symbol);
+
+ return {
+ symbol,
+ price: currentPrice,
+ ...stats,
+ timestamp: Date.now()
+ };
+ } catch (error) {
+ console.error('Get market data error:', error);
+ throw error;
+ }
+ }
+
+ async getMarketStats(symbol) {
+ try {
+ const cached = await this.redisClient.get(`market_stats:${symbol}`);
+ if (cached) {
+ return JSON.parse(cached);
+ }
+
+ // Default stats if not found
+ return {
+ high24h: 0,
+ low24h: 0,
+ volume24h: 0,
+ priceChange24h: 0,
+ priceChangePercent24h: 0
+ };
+ } catch (error) {
+ console.error('Get market stats error:', error);
+ throw error;
+ }
+ }
+
+ async getPriceHistory(symbol, interval = '1h', limit = 100) {
+ try {
+ return await this.priceService.getPriceHistory(symbol, interval, limit);
+ } catch (error) {
+ console.error('Get price history error:', error);
+ throw error;
+ }
+ }
+
+ async getSymbols() {
+ return this.symbols.map(symbol => ({
+ symbol,
+ type: symbol.includes('/') ? 'crypto' : 'stock',
+ isActive: true
+ }));
+ }
+
+ async getWatchlist(userId) {
+ try {
+ const watchlist = await this.redisClient.sMembers(`watchlist:${userId}`);
+ const watchlistData = [];
+
+ for (const symbol of watchlist) {
+ const marketData = await this.getMarketData(symbol);
+ watchlistData.push(marketData);
+ }
+
+ return watchlistData;
+ } catch (error) {
+ console.error('Get watchlist error:', error);
+ return [];
+ }
+ }
+
+ async addToWatchlist(userId, symbol) {
+ try {
+ await this.redisClient.sAdd(`watchlist:${userId}`, symbol);
+ return true;
+ } catch (error) {
+ console.error('Add to watchlist error:', error);
+ return false;
+ }
+ }
+
+ async removeFromWatchlist(userId, symbol) {
+ try {
+ await this.redisClient.sRem(`watchlist:${userId}`, symbol);
+ return true;
+ } catch (error) {
+ console.error('Remove from watchlist error:', error);
+ return false;
+ }
+ }
+}
+
+module.exports = MarketDataService;
diff --git a/services/market-data-service/src/services/PriceService.js b/services/market-data-service/src/services/PriceService.js
new file mode 100644
index 0000000..6074d7a
--- /dev/null
+++ b/services/market-data-service/src/services/PriceService.js
@@ -0,0 +1,155 @@
+const redis = require('redis');
+
+class PriceService {
+ constructor() {
+ this.redisClient = redis.createClient({
+ url: process.env.REDIS_URL || 'redis://localhost:6379'
+ });
+ this.redisClient.connect();
+
+ // Initial prices
+ this.prices = new Map([
+ ['BTC/USD', 45250.50],
+ ['ETH/USD', 3150.25],
+ ['BNB/USD', 320.75],
+ ['ADA/USD', 0.65],
+ ['SOL/USD', 98.50],
+ ['AAPL', 175.50],
+ ['GOOGL', 2750.25],
+ ['MSFT', 415.75],
+ ['TSLA', 245.30],
+ ['AMZN', 3420.80]
+ ]);
+
+ this.priceHistory = new Map();
+ this.isSimulating = false;
+ }
+
+ async getCurrentPrice(symbol) {
+ try {
+ // Try cache first
+ const cached = await this.redisClient.get(`price:${symbol}`);
+ if (cached) {
+ return parseFloat(cached);
+ }
+
+ // Get from memory
+ const price = this.prices.get(symbol) || 0;
+
+ // Cache for 1 second
+ await this.redisClient.setEx(`price:${symbol}`, 1, price.toString());
+
+ return price;
+ } catch (error) {
+ console.error('Get current price error:', error);
+ return this.prices.get(symbol) || 0;
+ }
+ }
+
+ async updatePrice(symbol, price) {
+ try {
+ this.prices.set(symbol, price);
+
+ // Update cache
+ await this.redisClient.setEx(`price:${symbol}`, 1, price.toString());
+
+ // Store in history
+ await this.storePriceHistory(symbol, price);
+
+ // Broadcast price update
+ await this.broadcastPriceUpdate(symbol, price);
+
+ } catch (error) {
+ console.error('Update price error:', error);
+ }
+ }
+
+ async storePriceHistory(symbol, price) {
+ try {
+ const timestamp = Date.now();
+ const historyKey = `price_history:${symbol}`;
+
+ // Store as sorted a set with timestamp as score
+ await this.redisClient.zAdd(historyKey, {
+ score: timestamp,
+ value: JSON.stringify({ price, timestamp })
+ });
+
+ // Keep only the last 1000 entries
+ await this.redisClient.zRemRangeByRank(historyKey, 0, -1001);
+
+ } catch (error) {
+ console.error('Store price history error:', error);
+ }
+ }
+
+ async getPriceHistory(symbol, interval = '1h', limit = 100) {
+ try {
+ const historyKey = `price_history:${symbol}`;
+ const history = await this.redisClient.zRevRange(historyKey, 0, limit - 1, {
+ WITHSCORES: true
+ });
+
+ const formattedHistory = [];
+ for (let i = 0; i < history.length; i += 2) {
+ const data = JSON.parse(history[i]);
+ formattedHistory.push({
+ timestamp: data.timestamp,
+ price: data.price,
+ date: new Date(data.timestamp).toISOString()
+ });
+ }
+
+ return formattedHistory;
+ } catch (error) {
+ console.error('Get price history error:', error);
+ return [];
+ }
+ }
+
+ async broadcastPriceUpdate(symbol, price) {
+ try {
+ await this.redisClient.publish('market_data_updates', JSON.stringify({
+ type: 'price_update',
+ symbol,
+ price,
+ timestamp: Date.now()
+ }));
+ } catch (error) {
+ console.error('Broadcast price update error:', error);
+ }
+ }
+
+ startPriceSimulation() {
+ if (this.isSimulating) return;
+
+ this.isSimulating = true;
+ console.log('Starting price simulation...');
+
+ setInterval(() => {
+ this.simulatePriceMovements();
+ }, 1000); // Update every second
+ }
+
+ simulatePriceMovements() {
+ for (const [symbol, currentPrice] of this.prices.entries()) {
+ // Generate random price movement (-2% to +2%)
+ const changePercent = (Math.random() - 0.5) * 0.04;
+ const newPrice = currentPrice * (1 + changePercent);
+
+ // Round to appropriate decimal places
+ const roundedPrice = symbol.includes('/')
+ ? Math.round(newPrice * 100) / 100 // Crypto: 2 decimals
+ : Math.round(newPrice * 100) / 100; // Stocks: 2 decimals
+
+ this.updatePrice(symbol, roundedPrice);
+ }
+ }
+
+ stopPriceSimulation() {
+ this.isSimulating = false;
+ console.log('Price simulation stopped');
+ }
+}
+
+module.exports = PriceService;
diff --git a/services/orderbook-service/package.json b/services/orderbook-service/package.json
new file mode 100644
index 0000000..3b24b43
--- /dev/null
+++ b/services/orderbook-service/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "orderbook-service",
+ "version": "1.0.0",
+ "description": "C++ OrderBook microservice wrapper",
+ "main": "src/server.js",
+ "scripts": {
+ "start": "node src/server.js",
+ "dev": "nodemon src/server.js"
+ },
+ "dependencies": {
+ "express": "^4.18.2",
+ "redis": "^4.6.7",
+ "uuid": "^9.0.0",
+ "dotenv": "^16.3.1"
+ },
+ "devDependencies": {
+ "nodemon": "^3.0.1"
+ }
+}
diff --git a/services/orderbook-service/src/OrderBookWrapper.js b/services/orderbook-service/src/OrderBookWrapper.js
new file mode 100644
index 0000000..14dce28
--- /dev/null
+++ b/services/orderbook-service/src/OrderBookWrapper.js
@@ -0,0 +1,262 @@
+const { spawn } = require('child_process');
+const redis = require('redis');
+const EventEmitter = require('events');
+
+class OrderBookWrapper extends EventEmitter {
+ constructor() {
+ super();
+ this.redisClient = redis.createClient({
+ url: process.env.REDIS_URL || 'redis://localhost:6379'
+ });
+ this.redisClient.connect();
+
+ this.isReady = false;
+ this.orderIdCounter = 1;
+ this.activeOrders = new Map();
+
+ // For demo purposes, we'll simulate the C++ OrderBook
+ // In production, this would spawn your actual C++ process
+ this.initializeSimulatedOrderBook();
+
+ setTimeout(() => {
+ this.isReady = true;
+ console.log('OrderBook service is ready');
+ }, 2000);
+ }
+
+ initializeSimulatedOrderBook() {
+ // Simulated market data for demo
+ this.orderBook = {
+ 'BTC/USD': {
+ bids: [
+ { price: 45248.50, quantity: 2.5, orders: [] },
+ { price: 45247.25, quantity: 1.8, orders: [] },
+ { price: 45246.00, quantity: 3.2, orders: [] }
+ ],
+ asks: [
+ { price: 45251.00, quantity: 1.7, orders: [] },
+ { price: 45252.25, quantity: 2.3, orders: [] },
+ { price: 45253.50, quantity: 1.5, orders: [] }
+ ]
+ }
+ };
+ }
+
+ async placeOrder(orderData) {
+ if (!this.isReady) {
+ throw new Error('OrderBook service not ready');
+ }
+
+ try {
+ const orderId = this.generateOrderId();
+ const order = {
+ ...orderData,
+ orderId,
+ status: 'PENDING',
+ timestamp: new Date().toISOString()
+ };
+
+ // Store in active orders
+ this.activeOrders.set(orderId, order);
+
+ // Cache in Redis
+ await this.redisClient.hSet(
+ `orders:active:${orderData.userId}`,
+ orderId,
+ JSON.stringify(order)
+ );
+
+ // Simulate order book update
+ this.simulateOrderBookUpdate(order);
+
+ // Emit order update event
+ this.emit('orderUpdate', {
+ type: 'ORDER_PLACED',
+ order
+ });
+
+ return {
+ success: true,
+ orderId,
+ status: 'PENDING',
+ message: 'Order submitted successfully'
+ };
+
+ } catch (error) {
+ console.error('Place order error:', error);
+ return {
+ success: false,
+ message: 'Failed to place order'
+ };
+ }
+ }
+
+ async cancelOrder(orderId, userId) {
+ try {
+ const order = this.activeOrders.get(parseInt(orderId));
+ if (!order || order.userId !== userId) {
+ return {
+ success: false,
+ message: 'Order not found or access denied'
+ };
+ }
+
+ // Remove from active orders
+ this.activeOrders.delete(parseInt(orderId));
+
+ // Update cache
+ await this.redisClient.hDel(`orders:active:${userId}`, orderId);
+ await this.redisClient.hSet(
+ `orders:cancelled:${userId}`,
+ orderId,
+ JSON.stringify({
+ ...order,
+ status: 'CANCELLED',
+ cancelledAt: new Date().toISOString()
+ })
+ );
+
+ // Emit order update event
+ this.emit('orderUpdate', {
+ type: 'ORDER_CANCELLED',
+ orderId,
+ userId
+ });
+
+ return {
+ success: true,
+ message: 'Order cancelled successfully'
+ };
+
+ } catch (error) {
+ console.error('Cancel order error:', error);
+ return {
+ success: false,
+ message: 'Failed to cancel order'
+ };
+ }
+ }
+
+ async getOrderBook(symbol = 'BTC/USD') {
+ try {
+ // Try to get from Redis cache first
+ const cached = await this.redisClient.get(`orderbook:${symbol}`);
+ if (cached) {
+ return JSON.parse(cached);
+ }
+
+ // Return simulated data
+ const orderBookData = this.orderBook[symbol] || {
+ bids: [],
+ asks: []
+ };
+
+ // Cache the result
+ await this.redisClient.setEx(
+ `orderbook:${symbol}`,
+ 30,
+ JSON.stringify({
+ symbol,
+ timestamp: Date.now(),
+ ...orderBookData
+ })
+ );
+
+ return {
+ symbol,
+ timestamp: Date.now(),
+ ...orderBookData
+ };
+
+ } catch (error) {
+ console.error('Get orderbook error:', error);
+ throw error;
+ }
+ }
+
+ async getUserOrders(userId) {
+ try {
+ const activeOrders = await this.redisClient.hGetAll(`orders:active:${userId}`);
+ const cancelledOrders = await this.redisClient.hGetAll(`orders:cancelled:${userId}`);
+
+ const orders = [];
+
+ // Parse active orders
+ Object.values(activeOrders).forEach(orderStr => {
+ orders.push(JSON.parse(orderStr));
+ });
+
+ // Parse cancelled orders
+ Object.values(cancelledOrders).forEach(orderStr => {
+ orders.push(JSON.parse(orderStr));
+ });
+
+ return orders.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
+
+ } catch (error) {
+ console.error('Get user orders error:', error);
+ return [];
+ }
+ }
+
+ simulateOrderBookUpdate(order) {
+ const symbol = order.symbol || 'BTC/USD';
+ if (!this.orderBook[symbol]) {
+ this.orderBook[symbol] = { bids: [], asks: [] };
+ }
+
+ // Simulate adding order to the appropriate side
+ const side = order.side === 'BUY' ? 'bids' : 'asks';
+ const priceLevel = this.orderBook[symbol][side].find(level => level.price === order.price);
+
+ if (priceLevel) {
+ priceLevel.quantity += order.quantity;
+ priceLevel.orders.push(order);
+ } else {
+ this.orderBook[symbol][side].push({
+ price: order.price,
+ quantity: order.quantity,
+ orders: [order]
+ });
+
+ // Sort bids descending, asks ascending
+ if (side === 'bids') {
+ this.orderBook[symbol][side].sort((a, b) => b.price - a.price);
+ } else {
+ this.orderBook[symbol][side].sort((a, b) => a.price - b.price);
+ }
+ }
+
+ // Broadcast update
+ this.broadcastMarketUpdate(symbol);
+ }
+
+ async broadcastMarketUpdate(symbol) {
+ const orderBookData = await this.getOrderBook(symbol);
+
+ // Publish to Redis for WebSocket broadcast
+ this.redisClient.publish('market_data_updates', JSON.stringify({
+ type: 'orderbook_update',
+ symbol,
+ data: orderBookData
+ }));
+ }
+
+ generateOrderId() {
+ return this.orderIdCounter++;
+ }
+
+ getBestBid(symbol = 'BTC/USD') {
+ const orderBook = this.orderBook[symbol];
+ if (!orderBook || !orderBook.bids.length) return null;
+ return orderBook.bids[0].price;
+ }
+
+ getBestAsk(symbol = 'BTC/USD') {
+ const orderBook = this.orderBook[symbol];
+ if (!orderBook || !orderBook.asks.length) return null;
+ return orderBook.asks[0].price;
+ }
+}
+
+module.exports = OrderBookWrapper;
diff --git a/services/orderbook-service/src/controllers/MarketDataController.js b/services/orderbook-service/src/controllers/MarketDataController.js
new file mode 100644
index 0000000..6837436
--- /dev/null
+++ b/services/orderbook-service/src/controllers/MarketDataController.js
@@ -0,0 +1,60 @@
+class MarketDataController {
+ constructor(orderBookWrapper) {
+ this.orderBookWrapper = orderBookWrapper;
+ }
+
+ async getOrderBook(req, res) {
+ try {
+ const symbol = req.params.symbol || 'BTC/USD';
+ const data = await this.orderBookWrapper.getOrderBook(symbol);
+
+ res.json({
+ success: true,
+ data
+ });
+
+ } catch (error) {
+ console.error('Get order book error:', error);
+ res.status(500).json({
+ success: false,
+ message: error.message
+ });
+ }
+ }
+
+ async getMarketData(req, res) {
+ try {
+ const symbol = req.params.symbol;
+ const orderBookData = await this.orderBookWrapper.getOrderBook(symbol);
+
+ const marketData = {
+ symbol,
+ bestBid: this.orderBookWrapper.getBestBid(symbol),
+ bestAsk: this.orderBookWrapper.getBestAsk(symbol),
+ spread: null,
+ lastPrice: null,
+ volume24h: 0,
+ priceChange24h: 0,
+ timestamp: Date.now()
+ };
+
+ if (marketData.bestBid && marketData.bestAsk) {
+ marketData.spread = marketData.bestAsk - marketData.bestBid;
+ }
+
+ res.json({
+ success: true,
+ data: marketData
+ });
+
+ } catch (error) {
+ console.error('Get market data error:', error);
+ res.status(500).json({
+ success: false,
+ message: error.message
+ });
+ }
+ }
+}
+
+module.exports = MarketDataController;
diff --git a/services/orderbook-service/src/controllers/OrderController.js b/services/orderbook-service/src/controllers/OrderController.js
new file mode 100644
index 0000000..8e08a8a
--- /dev/null
+++ b/services/orderbook-service/src/controllers/OrderController.js
@@ -0,0 +1,108 @@
+class OrderController {
+ constructor(orderBookWrapper) {
+ this.orderBookWrapper = orderBookWrapper;
+ }
+
+ async placeOrder(req, res) {
+ try {
+ const orderData = {
+ userId: req.user?.userId || req.body.userId || 1,
+ symbol: req.body.symbol || 'BTC/USD',
+ side: req.body.side,
+ quantity: parseFloat(req.body.quantity),
+ price: parseFloat(req.body.price),
+ type: req.body.type || 'LIMIT'
+ };
+
+ // Validate order data
+ if (!orderData.side || !['BUY', 'SELL'].includes(orderData.side)) {
+ return res.status(400).json({
+ success: false,
+ message: 'Invalid order side. Must be BUY or SELL'
+ });
+ }
+
+ if (!orderData.quantity || orderData.quantity <= 0) {
+ return res.status(400).json({
+ success: false,
+ message: 'Invalid quantity. Must be greater than 0'
+ });
+ }
+
+ if (orderData.type === 'LIMIT' && (!orderData.price || orderData.price <= 0)) {
+ return res.status(400).json({
+ success: false,
+ message: 'Invalid price for limit order'
+ });
+ }
+
+ const result = await this.orderBookWrapper.placeOrder(orderData);
+
+ if (result.success) {
+ res.status(201).json(result);
+ } else {
+ res.status(400).json(result);
+ }
+
+ } catch (error) {
+ console.error('Place order error:', error);
+ res.status(500).json({
+ success: false,
+ message: error.message
+ });
+ }
+ }
+
+ async cancelOrder(req, res) {
+ try {
+ const { orderId } = req.params;
+ const userId = req.user?.userId || req.body.userId || 1;
+
+ const result = await this.orderBookWrapper.cancelOrder(orderId, userId);
+
+ if (result.success) {
+ res.json(result);
+ } else {
+ res.status(404).json(result);
+ }
+
+ } catch (error) {
+ console.error('Cancel order error:', error);
+ res.status(500).json({
+ success: false,
+ message: error.message
+ });
+ }
+ }
+
+ async getUserOrders(req, res) {
+ try {
+ const { userId } = req.params;
+ const requestingUserId = req.user?.userId || userId;
+
+ // Users can only see their own orders (unless admin)
+ if (parseInt(userId) !== requestingUserId && req.user?.accountType !== 'Admin') {
+ return res.status(403).json({
+ success: false,
+ message: 'Access denied'
+ });
+ }
+
+ const orders = await this.orderBookWrapper.getUserOrders(parseInt(userId));
+
+ res.json({
+ success: true,
+ data: orders
+ });
+
+ } catch (error) {
+ console.error('Get user orders error:', error);
+ res.status(500).json({
+ success: false,
+ message: error.message
+ });
+ }
+ }
+}
+
+module.exports = OrderController;
diff --git a/services/orderbook-service/src/server.js b/services/orderbook-service/src/server.js
new file mode 100644
index 0000000..2f9b0d9
--- /dev/null
+++ b/services/orderbook-service/src/server.js
@@ -0,0 +1,37 @@
+const express = require('express');
+const OrderBookWrapper = require('./OrderBookWrapper');
+const OrderController = require('./controllers/OrderController');
+const MarketDataController = require('./controllers/MarketDataController');
+
+const app = express();
+app.use(express.json());
+
+// Initialize OrderBook wrapper
+const orderBookWrapper = new OrderBookWrapper();
+
+// Inject OrderBook into controllers
+const orderController = new OrderController(orderBookWrapper);
+const marketDataController = new MarketDataController(orderBookWrapper);
+
+// Order management endpoints
+app.post('/orders', (req, res) => orderController.placeOrder(req, res));
+app.delete('/orders/:orderId', (req, res) => orderController.cancelOrder(req, res));
+app.get('/orders/user/:userId', (req, res) => orderController.getUserOrders(req, res));
+
+// Market data endpoints
+app.get('/orderbook/:symbol?', (req, res) => marketDataController.getOrderBook(req, res));
+app.get('/market-data/:symbol', (req, res) => marketDataController.getMarketData(req, res));
+
+// Health check
+app.get('/health', (req, res) => {
+ res.json({
+ status: 'healthy',
+ service: 'orderbook-service',
+ timestamp: new Date().toISOString()
+ });
+});
+
+const PORT = process.env.PORT || 8082;
+app.listen(PORT, () => {
+ console.log(`🔧 OrderBook Service running on port ${PORT}`);
+});
diff --git a/services/user-service/package.json b/services/user-service/package.json
new file mode 100644
index 0000000..b487b6e
--- /dev/null
+++ b/services/user-service/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "user-service",
+ "version": "1.0.0",
+ "description": "User management microservice",
+ "main": "src/server.js",
+ "scripts": {
+ "start": "node src/server.js",
+ "dev": "nodemon src/server.js"
+ },
+ "dependencies": {
+ "express": "^4.18.2",
+ "redis": "^4.6.7",
+ "dotenv": "^16.3.1"
+ },
+ "devDependencies": {
+ "nodemon": "^3.0.1"
+ }
+}
diff --git a/services/user-service/src/controllers/PortfolioController.js b/services/user-service/src/controllers/PortfolioController.js
new file mode 100644
index 0000000..e6537d6
--- /dev/null
+++ b/services/user-service/src/controllers/PortfolioController.js
@@ -0,0 +1,93 @@
+class PortfolioController {
+ constructor(portfolioService) {
+ this.portfolioService = portfolioService;
+ }
+
+ async getPortfolio(req, res) {
+ try {
+ const { userId } = req.params;
+ const requestingUserId = req.user?.userId;
+
+ if (parseInt(userId) !== requestingUserId && req.user?.accountType !== 'Admin') {
+ return res.status(403).json({
+ success: false,
+ message: 'Access denied'
+ });
+ }
+
+ const portfolio = await this.portfolioService.getPortfolio(userId);
+
+ res.json({
+ success: true,
+ data: portfolio
+ });
+
+ } catch (error) {
+ console.error('Get portfolio error:', error);
+ res.status(500).json({
+ success: false,
+ message: error.message
+ });
+ }
+ }
+
+ async getPositions(req, res) {
+ try {
+ const { userId } = req.params;
+ const requestingUserId = req.user?.userId;
+
+ if (parseInt(userId) !== requestingUserId && req.user?.accountType !== 'Admin') {
+ return res.status(403).json({
+ success: false,
+ message: 'Access denied'
+ });
+ }
+
+ const positions = await this.portfolioService.getPositions(userId);
+
+ res.json({
+ success: true,
+ data: positions
+ });
+
+ } catch (error) {
+ console.error('Get positions error:', error);
+ res.status(500).json({
+ success: false,
+ message: error.message
+ });
+ }
+ }
+
+ async getTradeHistory(req, res) {
+ try {
+ const { userId } = req.params;
+ const requestingUserId = req.user?.userId;
+ const limit = parseInt(req.query.limit) || 50;
+ const offset = parseInt(req.query.offset) || 0;
+
+ if (parseInt(userId) !== requestingUserId && req.user?.accountType !== 'Admin') {
+ return res.status(403).json({
+ success: false,
+ message: 'Access denied'
+ });
+ }
+
+ const tradeHistory = await this.portfolioService.getTradeHistory(userId, limit, offset);
+
+ res.json({
+ success: true,
+ data: tradeHistory
+ });
+
+ } catch (error) {
+ console.error('Get trade history error:', error);
+ res.status(500).json({
+ success: false,
+ message: error.message
+ });
+ }
+ }
+}
+
+module.exports = PortfolioController;
diff --git a/services/user-service/src/controllers/UserController.js b/services/user-service/src/controllers/UserController.js
new file mode 100644
index 0000000..c288353
--- /dev/null
+++ b/services/user-service/src/controllers/UserController.js
@@ -0,0 +1,111 @@
+class UserController {
+ constructor(userService) {
+ this.userService = userService;
+ }
+
+ async getUser(req, res) {
+ try {
+ const { userId } = req.params;
+ const requestingUserId = req.user?.userId;
+
+ // Users can only access their own data (unless admin)
+ if (parseInt(userId) !== requestingUserId && req.user?.accountType !== 'Admin') {
+ return res.status(403).json({
+ success: false,
+ message: 'Access denied'
+ });
+ }
+
+ const user = await this.userService.getUser(userId);
+ if (!user) {
+ return res.status(404).json({
+ success: false,
+ message: 'User not found'
+ });
+ }
+
+ res.json({
+ success: true,
+ data: user
+ });
+
+ } catch (error) {
+ console.error('Get user error:', error);
+ res.status(500).json({
+ success: false,
+ message: error.message
+ });
+ }
+ }
+
+ async updateUser(req, res) {
+ try {
+ const { userId } = req.params;
+ const requestingUserId = req.user?.userId;
+
+ // Users can only update their own data
+ if (parseInt(userId) !== requestingUserId && req.user?.accountType !== 'Admin') {
+ return res.status(403).json({
+ success: false,
+ message: 'Access denied'
+ });
+ }
+
+ const updatedUser = await this.userService.updateUser(userId, req.body);
+ if (!updatedUser) {
+ return res.status(404).json({
+ success: false,
+ message: 'User not found'
+ });
+ }
+
+ res.json({
+ success: true,
+ data: updatedUser
+ });
+
+ } catch (error) {
+ console.error('Update user error:', error);
+ res.status(500).json({
+ success: false,
+ message: error.message
+ });
+ }
+ }
+
+ async getUserProfile(req, res) {
+ try {
+ const { userId } = req.params;
+ const requestingUserId = req.user?.userId;
+
+ if (parseInt(userId) !== requestingUserId && req.user?.accountType !== 'Admin') {
+ return res.status(403).json({
+ success: false,
+ message: 'Access denied'
+ });
+ }
+
+ const profile = await this.userService.getUserProfile(userId);
+ if (!profile) {
+ return res.status(404).json({
+ success: false,
+ message: 'User not found'
+ });
+ }
+
+ res.json({
+ success: true,
+ data: profile
+ });
+
+ } catch (error) {
+ console.error('Get user profile error:', error);
+ res.status(500).json({
+ success: false,
+ message: error.message
+ });
+ }
+ }
+}
+
+module.exports = UserController;
diff --git a/services/user-service/src/server.js b/services/user-service/src/server.js
new file mode 100644
index 0000000..5d6753c
--- /dev/null
+++ b/services/user-service/src/server.js
@@ -0,0 +1,40 @@
+const express = require('express');
+const UserController = require('./controllers/UserController');
+const PortfolioController = require('./controllers/PortfolioController');
+const UserService = require('./services/UserService');
+const PortfolioService = require('./services/PortfolioService');
+
+const app = express();
+app.use(express.json());
+
+// Initialize services
+const userService = new UserService();
+const portfolioService = new PortfolioService();
+
+// Initialize controllers
+const userController = new UserController(userService);
+const portfolioController = new PortfolioController(portfolioService);
+
+// User management routes
+app.get('/users/:userId', (req, res) => userController.getUser(req, res));
+app.put('/users/:userId', (req, res) => userController.updateUser(req, res));
+app.get('/users/:userId/profile', (req, res) => userController.getUserProfile(req, res));
+
+// Portfolio routes
+app.get('/users/:userId/portfolio', (req, res) => portfolioController.getPortfolio(req, res));
+app.get('/users/:userId/positions', (req, res) => portfolioController.getPositions(req, res));
+app.get('/users/:userId/trades', (req, res) => portfolioController.getTradeHistory(req, res));
+
+// Health check
+app.get('/health', (req, res) => {
+ res.json({
+ status: 'healthy',
+ service: 'user-service',
+ timestamp: new Date().toISOString()
+ });
+});
+
+const PORT = process.env.PORT || 8084;
+app.listen(PORT, () => {
+ console.log(`👥 User Service running on port ${PORT}`);
+});
diff --git a/services/user-service/src/services/PortfolioService.js b/services/user-service/src/services/PortfolioService.js
new file mode 100644
index 0000000..1972a66
--- /dev/null
+++ b/services/user-service/src/services/PortfolioService.js
@@ -0,0 +1,199 @@
+const redis = require('redis');
+
+class PortfolioService {
+ constructor() {
+ this.redisClient = redis.createClient({
+ url: process.env.REDIS_URL || 'redis://localhost:6379'
+ });
+ this.redisClient.connect();
+
+ // Demo portfolio data
+ this.portfolios = new Map([
+ [1, {
+ userId: 1,
+ totalValue: 125000.50,
+ cashBalance: 25000.00,
+ dailyPnL: 2500.75,
+ totalPnL: 25000.50,
+ positions: [
+ {
+ symbol: 'BTC/USD',
+ quantity: 2.5,
+ avgPrice: 45000,
+ currentPrice: 45250.50,
+ marketValue: 113126.25,
+ unrealizedPnL: 626.25,
+ realizedPnL: 1250.00
+ },
+ {
+ symbol: 'ETH/USD',
+ quantity: 10.0,
+ avgPrice: 3100,
+ currentPrice: 3150.25,
+ marketValue: 31502.50,
+ unrealizedPnL: 502.50,
+ realizedPnL: 750.25
+ }
+ ],
+ lastUpdated: new Date().toISOString()
+ }]
+ ]);
+
+ // Demo trade history
+ this.tradeHistory = new Map([
+ [1, [
+ {
+ id: 1,
+ symbol: 'BTC/USD',
+ side: 'BUY',
+ quantity: 1.0,
+ price: 44000,
+ value: 44000,
+ fee: 44.00,
+ timestamp: '2024-01-15T10:30:00Z'
+ },
+ {
+ id: 2,
+ symbol: 'BTC/USD',
+ side: 'BUY',
+ quantity: 1.5,
+ price: 46000,
+ value: 69000,
+ fee: 69.00,
+ timestamp: '2024-01-16T14:20:00Z'
+ },
+ {
+ id: 3,
+ symbol: 'ETH/USD',
+ side: 'BUY',
+ quantity: 10.0,
+ price: 3100,
+ value: 31000,
+ fee: 31.00,
+ timestamp: '2024-01-17T09:15:00Z'
+ }
+ ]]
+ ]);
+ }
+
+ async getPortfolio(userId) {
+ try {
+ // Try cache first
+ const cached = await this.redisClient.get(`portfolio:${userId}`);
+ if (cached) {
+ return JSON.parse(cached);
+ }
+
+ const portfolio = this.portfolios.get(parseInt(userId));
+ if (!portfolio) {
+ return {
+ userId: parseInt(userId),
+ totalValue: 0,
+ cashBalance: 0,
+ dailyPnL: 0,
+ totalPnL: 0,
+ positions: [],
+ lastUpdated: new Date().toISOString()
+ };
+ }
+
+ // Cache for 5 minutes
+ await this.redisClient.setEx(`portfolio:${userId}`, 300, JSON.stringify(portfolio));
+
+ return portfolio;
+ } catch (error) {
+ console.error('Get portfolio error:', error);
+ throw error;
+ }
+ }
+
+ async getPositions(userId) {
+ try {
+ const portfolio = await this.getPortfolio(userId);
+ return portfolio.positions || [];
+ } catch (error) {
+ console.error('Get positions error:', error);
+ throw error;
+ }
+ }
+
+ async getTradeHistory(userId, limit = 50, offset = 0) {
+ try {
+ const trades = this.tradeHistory.get(parseInt(userId)) || [];
+
+ // Sort by timestamp descending
+ const sortedTrades = trades.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
+
+ // Apply pagination
+ const paginatedTrades = sortedTrades.slice(offset, offset + limit);
+
+ return {
+ trades: paginatedTrades,
+ total: trades.length,
+ limit,
+ offset
+ };
+ } catch (error) {
+ console.error('Get trade history error:', error);
+ throw error;
+ }
+ }
+
+ async updatePosition(userId, symbol, quantity, price) {
+ try {
+ const portfolio = await this.getPortfolio(userId);
+ const existingPosition = portfolio.positions.find(p => p.symbol === symbol);
+
+ if (existingPosition) {
+ // Update existing position
+ const totalQuantity = existingPosition.quantity + quantity;
+ const totalValue = (existingPosition.quantity * existingPosition.avgPrice) + (quantity * price);
+
+ existingPosition.quantity = totalQuantity;
+ existingPosition.avgPrice = totalValue / totalQuantity;
+ } else {
+ // Add new position
+ portfolio.positions.push({
+ symbol,
+ quantity,
+ avgPrice: price,
+ currentPrice: price,
+ marketValue: quantity * price,
+ unrealizedPnL: 0,
+ realizedPnL: 0
+ });
+ }
+
+ portfolio.lastUpdated = new Date().toISOString();
+ this.portfolios.set(parseInt(userId), portfolio);
+
+ // Update cache
+ await this.redisClient.setEx(`portfolio:${userId}`, 300, JSON.stringify(portfolio));
+
+ return portfolio;
+ } catch (error) {
+ console.error('Update position error:', error);
+ throw error;
+ }
+ }
+
+ async addTrade(userId, trade) {
+ try {
+ const trades = this.tradeHistory.get(parseInt(userId)) || [];
+ trades.push({
+ ...trade,
+ id: trades.length + 1,
+ timestamp: new Date().toISOString()
+ });
+
+ this.tradeHistory.set(parseInt(userId), trades);
+
+ return trade;
+ } catch (error) {
+ console.error('Add trade error:', error);
+ throw error;
+ }
+ }
+}
+
+module.exports = PortfolioService;
diff --git a/services/user-service/src/services/UserService.js b/services/user-service/src/services/UserService.js
new file mode 100644
index 0000000..70555f0
--- /dev/null
+++ b/services/user-service/src/services/UserService.js
@@ -0,0 +1,144 @@
+const redis = require('redis');
+
+class UserService {
+ constructor() {
+ this.redisClient = redis.createClient({
+ url: process.env.REDIS_URL || 'redis://localhost:6379'
+ });
+ this.redisClient.connect();
+
+ // Demo users data
+ this.users = new Map([
+ [1, {
+ id: 1,
+ email: 'trader@example.com',
+ username: 'trader123',
+ firstName: 'John',
+ lastName: 'Trader',
+ accountType: 'Premium',
+ joinDate: '2024-01-15',
+ isActive: true,
+ preferences: {
+ currency: 'USD',
+ timezone: 'UTC',
+ notifications: {
+ email: true,
+ sms: false,
+ push: true
+ }
+ }
+ }],
+ [2, {
+ id: 2,
+ email: 'admin@example.com',
+ username: 'admin',
+ firstName: 'Admin',
+ lastName: 'User',
+ accountType: 'Admin',
+ joinDate: '2023-12-01',
+ isActive: true,
+ preferences: {
+ currency: 'USD',
+ timezone: 'UTC',
+ notifications: {
+ email: true,
+ sms: true,
+ push: true
+ }
+ }
+ }]
+ ]);
+ }
+
+ async getUser(userId) {
+ try {
+ // Try cache first
+ const cached = await this.redisClient.get(`user:${userId}`);
+ if (cached) {
+ return JSON.parse(cached);
+ }
+
+ // Get from "database" (demo data)
+ const user = this.users.get(parseInt(userId));
+ if (!user) {
+ return null;
+ }
+
+ // Cache for 1 hour
+ await this.redisClient.setEx(`user:${userId}`, 3600, JSON.stringify(user));
+
+ return user;
+ } catch (error) {
+ console.error('Get user error:', error);
+ throw error;
+ }
+ }
+
+ async updateUser(userId, updateData) {
+ try {
+ const user = this.users.get(parseInt(userId));
+ if (!user) {
+ return null;
+ }
+
+ // Update user data
+ const updatedUser = {
+ ...user,
+ ...updateData,
+ id: user.id, // Prevent ID changes
+ updatedAt: new Date().toISOString()
+ };
+
+ this.users.set(parseInt(userId), updatedUser);
+
+ // Update cache
+ await this.redisClient.setEx(`user:${userId}`, 3600, JSON.stringify(updatedUser));
+
+ return updatedUser;
+ } catch (error) {
+ console.error('Update user error:', error);
+ throw error;
+ }
+ }
+
+ async getUserProfile(userId) {
+ try {
+ const user = await this.getUser(userId);
+ if (!user) {
+ return null;
+ }
+
+ // Return profile without sensitive data
+ const { password, ...profile } = user;
+ return profile;
+ } catch (error) {
+ console.error('Get user profile error:', error);
+ throw error;
+ }
+ }
+
+ async updateUserPreferences(userId, preferences) {
+ try {
+ const user = await this.getUser(userId);
+ if (!user) {
+ return null;
+ }
+
+ const updatedUser = {
+ ...user,
+ preferences: {
+ ...user.preferences,
+ ...preferences
+ },
+ updatedAt: new Date().toISOString()
+ };
+
+ return await this.updateUser(userId, updatedUser);
+ } catch (error) {
+ console.error('Update user preferences error:', error);
+ throw error;
+ }
+ }
+}
+
+module.exports = UserService;