diff --git a/Backend/services/recommendation_service.py b/Backend/services/recommendation_service.py index 2f861bc..57d473c 100644 --- a/Backend/services/recommendation_service.py +++ b/Backend/services/recommendation_service.py @@ -3,6 +3,8 @@ import yfinance as yf from concurrent.futures import ThreadPoolExecutor, as_completed from datetime import datetime +import numpy as np +import pandas as pd from transformers import AutoTokenizer, AutoModelForSequenceClassification import torch from .stock_service import StockService, sanitize_value @@ -71,26 +73,40 @@ def get_news_sentiment(ticker: str) -> Dict: Returns average sentiment and individual news sentiments """ try: - news = StockService.format_news(yf.Ticker(ticker).get_news(count=20, tab='all')) - formatted_news = StockService.format_news(news) - + stock = yf.Ticker(ticker) + news = stock.get_news(count=20) + + if not news: + logger.warning(f"No news found for {ticker}") + return {'average_sentiment': 'neutral', 'sentiment_score': 0.0, 'news_count': 0} + + # Format news using StockService + formatted_news = StockService.format_news(news) + if not formatted_news: + logger.warning(f"No formatted news for {ticker}") return {'average_sentiment': 'neutral', 'sentiment_score': 0.0, 'news_count': 0} sentiments = [] for article in formatted_news: - content = article.get('content', {}) - title = content.get('title', '') - summary = content.get('summary', '') + # Handle both flattened format and nested content format + title = article.get('title', '') + summary = article.get('summary', '') # Combine title and summary for analysis text = f"{title}. {summary}" if summary else title - if text: + if text and len(text.strip()) > 10: # Ensure meaningful text sentiment_result = RecommendationService.analyze_sentiment(text) sentiments.append(sentiment_result) + logger.info(f"Analyzed article for {ticker}: '{title[:50]}...' -> {sentiment_result}") + else: + logger.debug(f"Skipping article with insufficient text for {ticker}") + + logger.info(f"Total sentiments analyzed for {ticker}: {len(sentiments)} out of {len(formatted_news)} articles") if not sentiments: + logger.warning(f"No sentiments extracted from {len(formatted_news)} news articles for {ticker}") return {'average_sentiment': 'neutral', 'sentiment_score': 0.0, 'news_count': 0} # Calculate weighted average sentiment score @@ -182,6 +198,119 @@ def get_analyst_recommendation(ticker: str) -> Dict: logger.error(f"Error getting analyst recommendation for {ticker}: {e}") return {'recommendation': 'hold', 'confidence': 0.0} + @staticmethod + def calculate_volatility(ticker: str, period: str = '3mo') -> Dict: + """ + Calculate historical volatility (annualized standard deviation of returns) + Returns volatility percentage and risk level + """ + try: + stock = yf.Ticker(ticker) + history = stock.history(period=period) + + if history.empty or len(history) < 2: + logger.warning(f"Insufficient data for volatility calculation: {ticker}") + return {'volatility': 0.0, 'risk_level': 'unknown'} + + # Calculate daily returns + history['returns'] = history['Close'].pct_change() + + # Calculate standard deviation of returns + std_dev = history['returns'].std() + + # Annualize volatility (assuming 252 trading days) + annualized_volatility = std_dev * np.sqrt(252) * 100 # Convert to percentage + + # Determine risk level + if annualized_volatility < 15: + risk_level = 'low' + risk_score = 0.8 # Low volatility is good + elif annualized_volatility < 30: + risk_level = 'moderate' + risk_score = 0.5 + elif annualized_volatility < 50: + risk_level = 'high' + risk_score = 0.2 + else: + risk_level = 'very_high' + risk_score = 0.0 + + logger.info(f"Volatility for {ticker}: {annualized_volatility:.2f}% ({risk_level})") + + return { + 'volatility': round(annualized_volatility, 2), + 'risk_level': risk_level, + 'risk_score': risk_score # 0-1, higher is better (lower volatility) + } + + except Exception as e: + logger.error(f"Error calculating volatility for {ticker}: {e}") + return {'volatility': 0.0, 'risk_level': 'unknown', 'risk_score': 0.5} + + @staticmethod + def calculate_rsi(ticker: str, period: str = '3mo', rsi_period: int = 14) -> Dict: + """ + Calculate Relative Strength Index (RSI) + RSI ranges from 0-100: + - Above 70: Overbought (potential sell signal) + - Below 30: Oversold (potential buy signal) + - 40-60: Neutral + """ + try: + stock = yf.Ticker(ticker) + history = stock.history(period=period) + + if history.empty or len(history) < rsi_period + 1: + logger.warning(f"Insufficient data for RSI calculation: {ticker}") + return {'rsi': 50.0, 'signal': 'neutral', 'rsi_score': 0.0} + + # Calculate price changes + delta = history['Close'].diff() + + # Separate gains and losses + gains = delta.where(delta > 0, 0) + losses = -delta.where(delta < 0, 0) + + # Calculate average gains and losses using EMA (Exponential Moving Average) + avg_gain = gains.ewm(span=rsi_period, adjust=False).mean() + avg_loss = losses.ewm(span=rsi_period, adjust=False).mean() + + # Calculate RS (Relative Strength) + rs = avg_gain / avg_loss + + # Calculate RSI + rsi = 100 - (100 / (1 + rs)) + current_rsi = float(rsi.iloc[-1]) + + # Determine signal and score + if current_rsi > 70: + signal = 'overbought' + rsi_score = -0.5 # Negative score for overbought (sell signal) + elif current_rsi > 60: + signal = 'slightly_overbought' + rsi_score = -0.2 + elif current_rsi < 30: + signal = 'oversold' + rsi_score = 0.8 # Positive score for oversold (buy signal) + elif current_rsi < 40: + signal = 'slightly_oversold' + rsi_score = 0.4 + else: + signal = 'neutral' + rsi_score = 0.0 + + logger.info(f"RSI for {ticker}: {current_rsi:.2f} ({signal})") + + return { + 'rsi': round(current_rsi, 2), + 'signal': signal, + 'rsi_score': rsi_score # -1 to 1, positive favors buy + } + + except Exception as e: + logger.error(f"Error calculating RSI for {ticker}: {e}") + return {'rsi': 50.0, 'signal': 'neutral', 'rsi_score': 0.0} + @staticmethod def analyze_holding(holding: Dict, in_portfolio: bool = True) -> Optional[Dict]: """ @@ -217,6 +346,10 @@ def analyze_holding(holding: Dict, in_portfolio: bool = True) -> Optional[Dict]: # Get analyst recommendations analyst_rec = RecommendationService.get_analyst_recommendation(ticker) + # Get technical indicators + volatility_data = RecommendationService.calculate_volatility(ticker) + rsi_data = RecommendationService.calculate_rsi(ticker) + # Convert analyst recommendation to score rec_map = { 'strong_buy': 1.0, @@ -236,6 +369,10 @@ def analyze_holding(holding: Dict, in_portfolio: bool = True) -> Optional[Dict]: 'sentimentScore': news_sentiment['sentiment_score'], 'analystRecommendation': analyst_rec['recommendation'], 'analystConfidence': analyst_rec['confidence'], + 'volatility': volatility_data['volatility'], + 'riskLevel': volatility_data['risk_level'], + 'rsi': rsi_data['rsi'], + 'rsiSignal': rsi_data['signal'], 'inPortfolio': in_portfolio } @@ -245,11 +382,14 @@ def analyze_holding(holding: Dict, in_portfolio: bool = True) -> Optional[Dict]: gain_loss_percent = (gain_loss / buy_price) * 100 if buy_price > 0 else 0 performance_score = max(min(gain_loss_percent / 50, 1), -1) # Normalize to -1 to 1 - # Weighted composite score: performance (30%), sentiment (35%), analyst (35%) + # Weighted composite score for portfolio holdings: + # Performance (20%), Sentiment (25%), Analyst (25%), RSI (15%), Risk (15%) composite_score = ( - performance_score * 0.30 + - sentiment_score * 0.35 + - analyst_score * 0.35 + performance_score * 0.20 + + sentiment_score * 0.25 + + analyst_score * 0.25 + + rsi_data['rsi_score'] * 0.15 + + volatility_data['risk_score'] * 0.15 ) # Determine action for existing holdings @@ -274,14 +414,18 @@ def analyze_holding(holding: Dict, in_portfolio: bool = True) -> Optional[Dict]: gain_loss_percent, news_sentiment['average_sentiment'], analyst_rec['recommendation'], + rsi_data['signal'], + volatility_data['risk_level'], in_portfolio ) }) else: - # For stocks not in portfolio: only sentiment (50%) + analyst (50%) + # For stocks not in portfolio: Sentiment (30%) + Analyst (30%) + RSI (20%) + Risk (20%) composite_score = ( - sentiment_score * 0.50 + - analyst_score * 0.50 + sentiment_score * 0.30 + + analyst_score * 0.30 + + rsi_data['rsi_score'] * 0.20 + + volatility_data['risk_score'] * 0.20 ) # Determine action for new investments @@ -301,6 +445,8 @@ def analyze_holding(holding: Dict, in_portfolio: bool = True) -> Optional[Dict]: None, news_sentiment['average_sentiment'], analyst_rec['recommendation'], + rsi_data['signal'], + volatility_data['risk_level'], in_portfolio ) }) @@ -311,7 +457,8 @@ def analyze_holding(holding: Dict, in_portfolio: bool = True) -> Optional[Dict]: return None @staticmethod - def _generate_reasoning(performance_pct: Optional[float], sentiment: str, analyst_rec: str, in_portfolio: bool) -> str: + def _generate_reasoning(performance_pct: Optional[float], sentiment: str, analyst_rec: str, + rsi_signal: str, risk_level: str, in_portfolio: bool) -> str: """Generate human-readable reasoning for recommendation""" parts = [] @@ -325,9 +472,29 @@ def _generate_reasoning(performance_pct: Optional[float], sentiment: str, analys else: parts.append(f"Negative returns ({performance_pct:.1f}%)") + # Add RSI signal + if rsi_signal == 'oversold': + parts.append("RSI oversold (buy opportunity)") + elif rsi_signal == 'overbought': + parts.append("RSI overbought (sell signal)") + elif rsi_signal == 'slightly_oversold': + parts.append("RSI slightly oversold") + elif rsi_signal == 'slightly_overbought': + parts.append("RSI slightly overbought") + parts.append(f"{sentiment} news sentiment") parts.append(f"analysts {analyst_rec.replace('_', ' ')}") + # Add risk level + if risk_level == 'low': + parts.append("low volatility (stable)") + elif risk_level == 'very_high': + parts.append("very high volatility (risky)") + elif risk_level == 'high': + parts.append("high volatility") + elif risk_level == 'moderate': + parts.append("moderate volatility") + if not in_portfolio: parts.append("not currently in portfolio") diff --git a/Backend/services/stock_service.py b/Backend/services/stock_service.py index edf94ba..53a9ef2 100644 --- a/Backend/services/stock_service.py +++ b/Backend/services/stock_service.py @@ -829,17 +829,22 @@ def handle_connect(): emit('connected', {'message': 'Successfully connected to stock price stream'}) @socketio.on('disconnect') - def handle_disconnect(): - logger.info(f"Client disconnected: {request.sid}") + def handle_disconnect(reason=None): + logger.info(f"Client disconnected: {request.sid}, reason: {reason}") # Stop any active price streaming for this client if request.sid in active_connections: - active_connections[request.sid]['active'] = False - # Wait for thread to finish (with timeout) - thread = active_connections[request.sid].get('thread') - if thread and thread.is_alive(): - thread.join(timeout=1.0) # Wait max 1 second - del active_connections[request.sid] - logger.info(f"Cleaned up thread for client {request.sid}") + try: + active_connections[request.sid]['active'] = False + # Wait for thread to finish (with timeout) + thread = active_connections[request.sid].get('thread') + if thread and thread.is_alive(): + thread.join(timeout=1.0) # Wait max 1 second + del active_connections[request.sid] + logger.info(f"Cleaned up thread for client {request.sid}") + except KeyError: + logger.warning(f"Client {request.sid} already removed from active_connections") + except Exception as e: + logger.error(f"Error cleaning up connection {request.sid}: {str(e)}") @socketio.on('subscribe_ticker') def handle_subscribe_ticker(data): @@ -882,41 +887,46 @@ def stream_prices(sid): stock_info = stock.info # Get latest price data history = stock.history(period="1d", interval="1m") + + if history.empty: + logger.warning(f"No history data available for {ticker}") + socketio.emit('error', {'message': f'No data available for {ticker}'}, room=sid) + time.sleep(120) + continue + latest_timestamp = history.index[-1] # Get the datetime index print(f"Latest timestamp for {ticker}: {latest_timestamp}") print(f"Streaming price for {stock_info.get('shortName', ticker)}, history length: {len(history)}") - if not history.empty: - latest = history.iloc[-1] - current_price = float(latest['Close']) - open_price = float(history.iloc[0]['Open']) - high_price = float(max(history['High'])) - low_price = float(min(history['Low'])) - volume = int(latest['Volume'] if latest['Volume'] != 0 else stock.info.get('volume', 0)) - - # Calculate change - day_open = history.iloc[0]['Open'] - change = current_price - day_open - change_percent = (change / day_open * 100) if day_open > 0 else 0 - - price_data = { - 'ticker': ticker, - 'currency': stock_info.get('currency', 'USD'), - 'name': stock_info.get('shortName', ticker), - - 'price': round(current_price, 2), - 'open': round(open_price, 2), - 'high': round(high_price, 2), - 'low': round(low_price, 2), - 'volume': volume, - 'change': round(change, 2), - 'changePercent': round(change_percent, 2), - 'timestamp': latest_timestamp.strftime('%Y-%m-%d %H:%M:%S') - } + + latest = history.iloc[-1] + current_price = float(latest['Close']) + open_price = float(history.iloc[0]['Open']) + high_price = float(max(history['High'])) + low_price = float(min(history['Low'])) + volume = int(latest['Volume'] if latest['Volume'] != 0 else stock.info.get('volume', 0)) + + # Calculate change + day_open = history.iloc[0]['Open'] + change = current_price - day_open + change_percent = (change / day_open * 100) if day_open > 0 else 0 + + price_data = { + 'ticker': ticker, + 'currency': stock_info.get('currency', 'USD'), + 'name': stock_info.get('shortName', ticker), - socketio.emit('price_update', price_data, room=sid) - logger.debug(f"Sent price update for {ticker} to {sid}: ${current_price}") - else: - logger.warning(f"No price data available for {ticker}") + 'price': round(current_price, 2), + 'open': round(open_price, 2), + 'high': round(high_price, 2), + 'low': round(low_price, 2), + 'volume': volume, + 'change': round(change, 2), + 'changePercent': round(change_percent, 2), + 'timestamp': latest_timestamp.strftime('%Y-%m-%d %H:%M:%S') + } + + socketio.emit('price_update', price_data, room=sid) + logger.debug(f"Sent price update for {ticker} to {sid}: ${current_price}") # Wait before next update (2 minutes to prevent rate limiting) time.sleep(120) diff --git a/reactpotfolio/package-lock.json b/reactpotfolio/package-lock.json index ab94325..5be7a0d 100644 --- a/reactpotfolio/package-lock.json +++ b/reactpotfolio/package-lock.json @@ -16,6 +16,7 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^13.5.0", + "axios": "^1.13.4", "lightweight-charts": "^5.1.0", "react": "^19.2.4", "react-dom": "^19.2.4", @@ -5371,6 +5372,33 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", + "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -14227,6 +14255,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", diff --git a/reactpotfolio/package.json b/reactpotfolio/package.json index 21b4bc8..d014e3d 100644 --- a/reactpotfolio/package.json +++ b/reactpotfolio/package.json @@ -11,6 +11,7 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^13.5.0", + "axios": "^1.13.4", "lightweight-charts": "^5.1.0", "react": "^19.2.4", "react-dom": "^19.2.4", diff --git a/reactpotfolio/src/api/accountApi.js b/reactpotfolio/src/api/accountApi.js index 465fdf7..801371e 100644 --- a/reactpotfolio/src/api/accountApi.js +++ b/reactpotfolio/src/api/accountApi.js @@ -2,31 +2,58 @@ let creditBalance = 250000; export const getCreditBalance = async () => { - return new Promise((resolve) => { - setTimeout(() => { - resolve(creditBalance); - }, 400); - }); + try { + const response = await fetch('http://localhost:8080/wallet/balance'); + if (!response.ok) { + throw new Error('Failed to fetch credit balance'); + } + return await response.json(); + } catch (error) { + console.error('Error fetching credit balance:', error); + return 0; + } }; export const increaseCredit = async (amount) => { - return new Promise((resolve) => { - setTimeout(() => { - creditBalance += Number(amount); - resolve(creditBalance); - }, 400); - }); + try { + const response = await fetch(`http://localhost:8080/wallet/add?amount=${amount}`, { + method: 'POST', + }); + if (!response.ok) { + throw new Error('Failed to add credit'); + } + return await response.json(); + } catch (error) { + console.error('Error adding credit:', error); + throw error; + } }; export const decreaseCredit = async (amount) => { - return new Promise((resolve, reject) => { - setTimeout(() => { - if (creditBalance >= amount) { - creditBalance -= Number(amount); - resolve(creditBalance); - } else { - reject(new Error("Insufficient funds")); - } - }, 400); - }); + try { + const response = await fetch(`http://localhost:8080/wallet/deduct?amount=${amount}`, { + method: 'POST', + }); + if (!response.ok) { + const error = await response.json().catch(() => ({ message: 'Insufficient funds' })); + throw new Error(error.message || 'Failed to deduct credit'); + } + return await response.json(); + } catch (error) { + console.error('Error deducting credit:', error); + throw error; + } +}; + +export const getWalletSummary = async () => { + try { + const response = await fetch('http://localhost:8080/wallet/summary'); + if (!response.ok) { + throw new Error('Failed to fetch wallet summary'); + } + return await response.json(); + } catch (error) { + console.error('Error fetching wallet summary:', error); + throw error; + } }; diff --git a/reactpotfolio/src/api/assetsApi.js b/reactpotfolio/src/api/assetsApi.js index 106a4a8..8a680eb 100644 --- a/reactpotfolio/src/api/assetsApi.js +++ b/reactpotfolio/src/api/assetsApi.js @@ -47,77 +47,114 @@ let mockAssets = [ ]; export const getAssets = async () => { - return new Promise((resolve) => { - setTimeout(() => { - resolve([...mockAssets]); - }, 400); - }); + try { + const response = await fetch('http://localhost:8080/api/pms/all'); + if (!response.ok) { + throw new Error('Failed to fetch assets'); + } + return await response.json(); + } catch (error) { + console.error('Error fetching assets:', error); + return []; + } }; export const addAsset = async (assetData) => { - return new Promise((resolve) => { - setTimeout(() => { - const currentValue = assetData.quantity * assetData.buyPrice; - const newAsset = { - id: Date.now(), // Normalized unique ID - companyName: assetData.companyName, + try { + const response = await fetch('http://localhost:8080/api/pms/add', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ symbol: assetData.symbol.toUpperCase(), + companyName: assetData.companyName, quantity: assetData.quantity, - currentValue: currentValue, - percentageChange: 0, // Initial change + buyPrice: assetData.buyPrice, + currentPrice: assetData.buyPrice, assetType: assetData.assetType - }; - mockAssets.push(newAsset); - resolve(newAsset); - }, 600); - }); + }), + }); + if (!response.ok) { + throw new Error('Failed to add asset'); + } + return await response.json(); + } catch (error) { + console.error('Error adding asset:', error); + throw error; + } }; export const sellAsset = async (assetId) => { - return new Promise((resolve) => { - setTimeout(() => { - mockAssets = mockAssets.filter(asset => asset.id !== assetId); - resolve(true); - }, 600); - }); + try { + const response = await fetch(`http://localhost:8080/api/pms/remove/${assetId}`, { + method: 'DELETE', + }); + if (!response.ok) { + throw new Error('Failed to remove asset'); + } + return true; + } catch (error) { + console.error('Error removing asset:', error); + throw error; + } }; // Helper function to increase asset quantity (for future transaction integration) -export const increaseAssetQuantity = async (symbol, quantity) => { - return new Promise((resolve, reject) => { - setTimeout(() => { - const assetIndex = mockAssets.findIndex(a => a.symbol === symbol.toUpperCase()); - if (assetIndex !== -1) { - const asset = mockAssets[assetIndex]; - const currentPrice = asset.currentValue / asset.quantity; - asset.quantity += quantity; - asset.currentValue = asset.quantity * currentPrice; - resolve(asset); - } else { - reject(new Error("Asset not found")); - } - }, 200); - }); +export const increaseAssetQuantity = async (assetId, quantity) => { + try { + const response = await fetch(`http://localhost:8080/api/pms/update-quantity/${assetId}?quantity=${quantity}`, { + method: 'PUT', + }); + if (!response.ok) { + throw new Error('Failed to update asset quantity'); + } + return await response.json(); + } catch (error) { + console.error('Error updating asset quantity:', error); + throw error; + } }; -// Helper function to decrease asset quantity (for future transaction integration) -export const decreaseAssetQuantity = async (symbol, quantity) => { - return new Promise((resolve, reject) => { - setTimeout(() => { - const assetIndex = mockAssets.findIndex(a => a.symbol === symbol.toUpperCase()); - if (assetIndex !== -1) { - const asset = mockAssets[assetIndex]; - if (asset.quantity >= quantity) { - const currentPrice = asset.currentValue / asset.quantity; - asset.quantity -= quantity; - asset.currentValue = asset.quantity * currentPrice; - resolve(asset); - } else { - reject(new Error("Insufficient quantity")); - } - } else { - reject(new Error("Asset not found")); - } - }, 200); - }); +// Note: To decrease quantity, use increaseAssetQuantity with negative value or remove the asset +// Backend only supports update-quantity endpoint which sets the quantity +export const getAssetPL = async (assetId) => { + try { + const response = await fetch(`http://localhost:8080/api/pms/pl/${assetId}`); + if (!response.ok) { + throw new Error('Failed to fetch P/L'); + } + return await response.json(); + } catch (error) { + console.error('Error fetching P/L:', error); + throw error; + } +}; + +export const getTotalPortfolioValue = async () => { + try { + const response = await fetch('http://localhost:8080/api/pms/total-value'); + if (!response.ok) { + throw new Error('Failed to fetch total portfolio value'); + } + return await response.json(); + } catch (error) { + console.error('Error fetching total portfolio value:', error); + throw error; + } +}; + +export const updateCurrentPrice = async (symbol, price) => { + try { + const response = await fetch(`http://localhost:8080/api/pms/update-price/${symbol}?price=${price}`, { + method: 'PUT', + }); + if (!response.ok) { + throw new Error('Failed to update current price'); + } + return await response.json(); + } catch (error) { + console.error('Error updating current price:', error); + throw error; + } }; diff --git a/reactpotfolio/src/api/portfolioApi.js b/reactpotfolio/src/api/portfolioApi.js index 6d35b16..fa3c655 100644 --- a/reactpotfolio/src/api/portfolioApi.js +++ b/reactpotfolio/src/api/portfolioApi.js @@ -1,80 +1,53 @@ -import { getAssets } from './assetsApi.js'; +import axios from 'axios'; -export const getPortfolioSummary = async () => { - const assets = await getAssets(); - - let totalPortfolioValue = 0; - let totalInvestedValue = 0; - - assets.forEach(asset => { - const currentValue = asset.currentValue; - const percentageChange = asset.percentageChange; - - totalPortfolioValue += currentValue; - - const gainFactor = 1 + (percentageChange / 100); - if (gainFactor !== 0) { - totalInvestedValue += currentValue / gainFactor; - } - }); - - const totalGainValue = totalPortfolioValue - totalInvestedValue; - const gainPercentageValue = totalInvestedValue > 0 ? (totalGainValue / totalInvestedValue) * 100 : 0; +const API_BASE_URL = 'http://localhost:8080/api/portfolio'; - return new Promise((resolve) => { - setTimeout(() => { - resolve({ - userName: 'Alex Johnson', - portfolioValue: totalPortfolioValue, - totalGain: totalGainValue, - gainPercentage: gainPercentageValue - }); - }, 500); - }); +export const getPortfolioSummary = async () => { + try { + const response = await axios.get(`${API_BASE_URL}/summary`); + return response.data; + } catch (error) { + console.error('Error fetching portfolio summary:', error); + throw error; + } }; export const getPortfolioPerformance = async () => { - // We want the chart to end at the current portfolio value. - const summary = await getPortfolioSummary(); - const currentTotal = summary.portfolioValue; - - return new Promise((resolve) => { - setTimeout(() => { - // Generate deterministic history - const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']; - const data = months.map((month, index) => { - // Create a curve that ends at currentTotal - // factor grows from 0.8 to 1.0 (approx) - const factor = 0.8 + (index * 0.04) + (Math.sin(index) * 0.05); - return { - date: month, - value: Math.round(currentTotal * factor) - }; - }); - - // Adjust last point to be exactly current - data[data.length - 1].value = currentTotal; - - resolve(data); - }, 600); - }); + try { + const response = await axios.get(`${API_BASE_URL}/performance`); + return response.data; + } catch (error) { + console.error('Error fetching portfolio performance:', error); + throw error; + } }; export const getAssetAllocation = async () => { - const assets = await getAssets(); - const allocation = {}; + try { + const response = await axios.get(`${API_BASE_URL}/allocation`); + return response.data; + } catch (error) { + console.error('Error fetching asset allocation:', error); + throw error; + } +}; - assets.forEach(asset => { - const value = asset.currentValue; - if (allocation[asset.assetType]) { - allocation[asset.assetType] += value; - } else { - allocation[asset.assetType] = value; - } - }); +export const getInvestmentBreakdown = async () => { + try { + const response = await axios.get(`${API_BASE_URL}/breakdown`); + return response.data; + } catch (error) { + console.error('Error fetching investment breakdown:', error); + throw error; + } +}; - return Object.keys(allocation).map(type => ({ - assetType: type, - value: allocation[type] - })); +export const getPerformers = async () => { + try { + const response = await axios.get(`${API_BASE_URL}/performers`); + return response.data; + } catch (error) { + console.error('Error fetching performers:', error); + throw error; + } }; diff --git a/reactpotfolio/src/api/transactionsApi.js b/reactpotfolio/src/api/transactionsApi.js index 184deff..3aab75a 100644 --- a/reactpotfolio/src/api/transactionsApi.js +++ b/reactpotfolio/src/api/transactionsApi.js @@ -84,18 +84,46 @@ const transactions = [ ]; export const getTransactions = async () => { - return new Promise((resolve) => { - setTimeout(() => { - resolve([...transactions]); - }, 400); - }); + try { + const response = await fetch('http://localhost:8080/transactions/all'); + if (!response.ok) { + throw new Error('Failed to fetch transactions'); + } + return await response.json(); + } catch (error) { + console.error('Error fetching transactions:', error); + return []; + } }; export const addTransaction = async (transaction) => { - return new Promise((resolve) => { - setTimeout(() => { - transactions.unshift(transaction); // Add to beginning of list - resolve(transaction); - }, 400); - }); + try { + const response = await fetch('http://localhost:8080/transactions/add', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(transaction), + }); + if (!response.ok) { + throw new Error('Failed to add transaction'); + } + return await response.json(); + } catch (error) { + console.error('Error adding transaction:', error); + throw error; + } +}; + +export const getTransactionsBySymbol = async (symbol) => { + try { + const response = await fetch(`http://localhost:8080/transactions/symbol/${symbol}`); + if (!response.ok) { + throw new Error('Failed to fetch transactions by symbol'); + } + return await response.json(); + } catch (error) { + console.error('Error fetching transactions by symbol:', error); + return []; + } }; diff --git a/reactpotfolio/src/components/cards/PerformanceCard.jsx b/reactpotfolio/src/components/cards/PerformanceCard.jsx index f2e8748..c52b67d 100644 --- a/reactpotfolio/src/components/cards/PerformanceCard.jsx +++ b/reactpotfolio/src/components/cards/PerformanceCard.jsx @@ -11,10 +11,10 @@ const PerformanceCard = ({ label, name, value, percentage, hideLabel }) => {
{!hideLabel &&
{label}
}
-

{name}

+

{name}

- {formatCurrency(value)} + {formatCurrency(value)} { }); return ( -
-

Asset Allocation

-
+
+
{/* SVG Chart */}
diff --git a/reactpotfolio/src/components/charts/PortfolioChart.jsx b/reactpotfolio/src/components/charts/PortfolioChart.jsx index 22c1140..db67044 100644 --- a/reactpotfolio/src/components/charts/PortfolioChart.jsx +++ b/reactpotfolio/src/components/charts/PortfolioChart.jsx @@ -6,15 +6,23 @@ const PortfolioChart = ({ data }) => { // Assuming portfolio API returns { date: 'YYYY-MM-DD', value: number } // PriceChart normalization handles 'date' key. + console.log("PortfolioChart received data:", data); + console.log("Data type:", typeof data, "Is Array:", Array.isArray(data), "Length:", data?.length); + return ( -
-

Portfolio Performance

-
- {data && data.length > 0 ? ( +
+
+ {data && Array.isArray(data) && data.length > 0 ? ( ) : ( -
- Loading Performance Data... +
+
Loading Performance Data...
+
+ {data === null ? 'Waiting for data...' : + data === undefined ? 'Data undefined' : + !Array.isArray(data) ? `Invalid data type: ${typeof data}` : + 'No data available'} +
)}
diff --git a/reactpotfolio/src/components/charts/PriceChart.jsx b/reactpotfolio/src/components/charts/PriceChart.jsx index 46ab9fd..751817e 100644 --- a/reactpotfolio/src/components/charts/PriceChart.jsx +++ b/reactpotfolio/src/components/charts/PriceChart.jsx @@ -109,7 +109,7 @@ const normalizeData = (inputData) => { }; -const PriceChart = ({ symbol, height = 400, color = '#DB292D', initialPeriod = '1M' }) => { +const PriceChart = ({ symbol, height = 400, color = '#DB292D', initialPeriod = '1M', data = null }) => { const chartContainerRef = useRef(null); const chartRef = useRef(null); const seriesRef = useRef(null); @@ -118,8 +118,22 @@ const PriceChart = ({ symbol, height = 400, color = '#DB292D', initialPeriod = ' const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + // If external data is provided, use it directly + useEffect(() => { + if (data && Array.isArray(data) && data.length > 0) { + console.log('PriceChart: Using external data', data.length, 'points'); + setChartData(data); + } + }, [data]); + // Fetch historical data from API const fetchHistoryData = async (periodLabel) => { + // Skip fetch if external data is provided + if (data) { + console.log('PriceChart: Skipping fetch, using external data'); + return; + } + if (!symbol) { console.log('PriceChart: No symbol provided'); return; @@ -355,42 +369,44 @@ const PriceChart = ({ symbol, height = 400, color = '#DB292D', initialPeriod = ' return (
- {/* Period Tabs */} -
- {PERIOD_CONFIGS.map(config => ( - - ))} -
+ {/* Period Tabs - Only show if no external data is provided */} + {!data && ( +
+ {PERIOD_CONFIGS.map(config => ( + + ))} +
+ )} {/* Loading/Error State */} {loading && ( diff --git a/reactpotfolio/src/layouts/Header.jsx b/reactpotfolio/src/layouts/Header.jsx index f851d9a..6d7e4a9 100644 --- a/reactpotfolio/src/layouts/Header.jsx +++ b/reactpotfolio/src/layouts/Header.jsx @@ -1,5 +1,17 @@ import React, { useState, useEffect } from 'react'; import { NavLink, useNavigate } from 'react-router-dom'; +import { + AppBar, + Toolbar, + Typography, + Box, + TextField, + IconButton, + Chip, + InputAdornment, + Container +} from '@mui/material'; +import { Search, AccountBalanceWallet, TrendingUp } from '@mui/icons-material'; import { getCreditBalance } from '../api/accountApi'; import { formatCurrency } from '../utils/formatCurrency'; @@ -36,92 +48,143 @@ const Header = () => { e.preventDefault(); if (searchQuery.trim()) { navigate(`/search?q=${encodeURIComponent(searchQuery)}`); - setSearchQuery(''); // Optional: clear after search + setSearchQuery(''); } }; + const navLinkStyle = (isActive) => ({ + textDecoration: 'none', + color: isActive ? '#00c853' : 'rgba(255,255,255,0.7)', + fontWeight: isActive ? '700' : '500', + fontSize: '0.9rem', + padding: '8px 16px', + borderRadius: '6px', + backgroundColor: isActive ? 'rgba(0, 200, 83, 0.08)' : 'transparent', + transition: 'all 0.2s', + '&:hover': { + color: '#00c853', + backgroundColor: 'rgba(0, 200, 83, 0.04)' + } + }); + return ( -
-
-

Portfolio Manager

-
-
- + + + {/* Logo/Brand */} + + + + Portfolio Manager + + + + {/* Search Bar */} + + setSearchQuery(e.target.value)} - className="input-dark" - style={{ - flex: 1, - backgroundColor: '#121212', - border: '1px solid #333', - color: 'white', - borderRadius: '8px', - padding: '10px 15px' + InputProps={{ + startAdornment: ( + + + + ), + sx: { + bgcolor: 'rgba(255,255,255,0.05)', + borderRadius: '8px', + color: '#fff', + '& .MuiOutlinedInput-notchedOutline': { + borderColor: 'rgba(255,255,255,0.1)' + }, + '&:hover .MuiOutlinedInput-notchedOutline': { + borderColor: 'rgba(255,255,255,0.2)' + }, + '&.Mui-focused .MuiOutlinedInput-notchedOutline': { + borderColor: '#00c853', + borderWidth: '1px' + }, + '& input::placeholder': { + color: 'rgba(255,255,255,0.4)', + opacity: 1 + } + } }} /> - - -
-
+ -
- + {/* Spacer */} + + + {/* Navigation */} + + + {({ isActive }) => ( + + Dashboard + + )} + + + {({ isActive }) => ( + + Holdings + + )} + + + {({ isActive }) => ( + + Transactions + + )} + + -
-
Credit Balance
-
{formatCurrency(credit)}
-
-
-
+ {/* Credit Balance */} + } + label={formatCurrency(credit)} + sx={{ + bgcolor: 'rgba(76, 175, 80, 0.1)', + color: '#4caf50', + border: '1px solid rgba(76, 175, 80, 0.3)', + fontWeight: '700', + fontSize: '0.9rem', + px: 1, + '& .MuiChip-icon': { + color: '#4caf50' + }, + '& .MuiChip-label': { + px: 1 + } + }} + /> + + + ); }; diff --git a/reactpotfolio/src/pages/AssetDetails/AssetDetails.jsx b/reactpotfolio/src/pages/AssetDetails/AssetDetails.jsx index 7dbf20b..2ea02ff 100644 --- a/reactpotfolio/src/pages/AssetDetails/AssetDetails.jsx +++ b/reactpotfolio/src/pages/AssetDetails/AssetDetails.jsx @@ -172,30 +172,61 @@ const AssetDetails = () => { const price = livePrice || asset.currentPrice || asset.nav; const totalCost = price * quantity; - const response = await fetch(`${JAVA_API_URL}/pms/buy`, { + // First, add the asset to PMS (this will automatically deduct from wallet) + const pmsResponse = await fetch(`http://localhost:8080/api/pms/add`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ symbol: asset.tickerSymbol || asset.symbol, - name: asset.name, + companyName: asset.name, quantity: quantity, - price: price, - totalCost: totalCost, - currency: asset.currency, - type: type, + buyPrice: price, + currentPrice: price, + assetType: type, + currency: asset.currency || 'USD', + exchange: asset.exchange || asset.exchangeName || 'N/A', + industry: asset.industry || asset.sector || 'N/A', }), }); - if (!response.ok) { - const errorData = await response.json(); + if (!pmsResponse.ok) { + const errorData = await pmsResponse.json().catch(() => ({})); + // Check for insufficient balance error + if (pmsResponse.status === 400 && errorData.message && errorData.message.includes('Insufficient balance')) { + throw new Error('Insufficient wallet balance. Please add funds to your wallet.'); + } throw new Error(errorData.message || 'Failed to complete purchase'); } - const result = await response.json(); + const pmsResult = await pmsResponse.json(); + + // Then, record the transaction with exact timestamp + const now = new Date(); + const transactionResponse = await fetch(`http://localhost:8080/transactions/add`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + symbol: asset.tickerSymbol || asset.symbol, + quantity: quantity, + buyPrice: price, + transactionDate: now.toISOString(), // Send full ISO timestamp + transactionType: 'BUY', + }), + }); + + if (!transactionResponse.ok) { + console.warn('Asset added but failed to record transaction'); + } + alert(`Successfully purchased ${quantity} shares of ${asset.name}`); handleBuyClose(); + + // Dispatch event to update credit balance + window.dispatchEvent(new Event('transactionUpdated')); } catch (err) { console.error('Buy error:', err); setBuyError(err.message || 'Failed to complete purchase'); @@ -753,6 +784,53 @@ const AssetDetails = () => { {analysis ? ( + {/* Analysis Components Explanation */} + + + 📊 Analysis Components + + + This AI-powered recommendation is based on: + + + + + 30% Weight: + + + News Sentiment Analysis using FinBERT AI model - Analyzes latest 20 news articles about {asset.companyName || asset.name} + + + + + 30% Weight: + + + Analyst Recommendations - Aggregated professional analyst ratings (Strong Buy, Buy, Hold, Sell, Strong Sell) + + + + + 20% Weight: + + + RSI (Relative Strength Index) - 14-period momentum indicator identifying overbought/oversold conditions + + + + + 20% Weight: + + + Volatility Analysis - Annualized historical volatility (90-day standard deviation) measuring price risk + + + + + Note: For portfolio holdings, a fifth component (20% performance vs buy price) is added to the analysis. + + + @@ -817,6 +895,67 @@ const AssetDetails = () => { + + RSI (14-period) + + 70 ? '#ef4444' : + analysis.rsi < 30 ? '#10b981' : '#888', + fontWeight: '700' + }}> + {analysis.rsi?.toFixed(2) || 'N/A'} + + + + + {analysis.rsi > 70 ? 'Overbought (>70) - Consider selling' : + analysis.rsi < 30 ? 'Oversold (<30) - Buy opportunity' : + 'Neutral (30-70)'} + + + + + Volatility (Annualized) + + + {analysis.volatility?.toFixed(1) || 'N/A'}% + + + + + 90-day historical volatility + + + Current Price diff --git a/reactpotfolio/src/pages/Holdings/Holdings.jsx b/reactpotfolio/src/pages/Holdings/Holdings.jsx index 4513365..f36a658 100644 --- a/reactpotfolio/src/pages/Holdings/Holdings.jsx +++ b/reactpotfolio/src/pages/Holdings/Holdings.jsx @@ -1,8 +1,12 @@ import React, { useState, useEffect } from 'react'; import { Link, useNavigate } from 'react-router-dom'; +import { io } from 'socket.io-client'; import AssetCard from '../../components/cards/AssetCard'; import BuyAssetModal from '../../components/modals/BuyAssetModal'; -import { getAssets, addAsset, sellAsset } from '../../api/assetsApi'; +import { getAssets, addAsset, sellAsset, updateCurrentPrice } from '../../api/assetsApi'; +import { createChart } from 'lightweight-charts'; +import { formatCurrency } from '../../utils/formatCurrency'; +import { formatPercentage } from '../../utils/formatPercentage'; import './Holdings.css'; const Holdings = () => { @@ -10,10 +14,18 @@ const Holdings = () => { const [assets, setAssets] = useState([]); const [showBuyModal, setShowBuyModal] = useState(false); const [loading, setLoading] = useState(true); + const [selectedAsset, setSelectedAsset] = useState(null); + const [showDetailsModal, setShowDetailsModal] = useState(false); + const [livePrices, setLivePrices] = useState({}); + const [buyQuantity, setBuyQuantity] = useState(1); + const [sellQuantity, setSellQuantity] = useState(1); const fetchAssets = async () => { try { + console.log("Holdings: Fetching assets..."); const data = await getAssets(); + console.log("Holdings: Received assets:", data); + console.log("Holdings: Asset count:", data?.length); setAssets(data); } catch (error) { console.error("Error fetching assets:", error); @@ -26,10 +38,46 @@ const Holdings = () => { fetchAssets(); }, []); + // Socket.IO for live price updates every 2 minutes + useEffect(() => { + if (!selectedAsset) return; + + const socket = io('http://localhost:5000'); + + socket.on('connect', () => { + console.log('Holdings Socket connected for:', selectedAsset.symbol); + socket.emit('subscribe_ticker', { ticker: selectedAsset.symbol }); + }); + + socket.on('price_update', (data) => { + console.log('Holdings Price update:', data); + setLivePrices(prev => ({ + ...prev, + [data.ticker]: data.price + })); + + // Update database + updateCurrentPrice(data.ticker, data.price) + .catch((error) => { + console.error(`Failed to update database for ${data.ticker}:`, error); + }); + }); + + socket.on('error', (error) => { + console.error('Holdings Socket error:', error); + }); + + return () => { + socket.emit('unsubscribe_ticker', { ticker: selectedAsset.symbol }); + socket.disconnect(); + }; + }, [selectedAsset]); + const handleAssetClick = (asset) => { - // Navigate to the asset details page - const assetType = asset.assetType?.toLowerCase().replace(' ', '') || 'stock'; - navigate(`/asset/${assetType}/${asset.symbol}`); + setSelectedAsset(asset); + setShowDetailsModal(true); + setBuyQuantity(1); + setSellQuantity(1); }; const handleBuySubmit = async (formData) => { @@ -42,29 +90,95 @@ const Holdings = () => { } }; - const handleSellAsset = async (asset) => { + const handleBuyAsset = async () => { + if (!selectedAsset || buyQuantity <= 0) return; + try { - await sellAsset(asset.id); - await fetchAssets(); // Refresh list + // currentPrice is already the unit price + const currentUnitPrice = livePrices[selectedAsset.symbol] || selectedAsset.currentPrice; + + // Add to existing quantity + await addAsset({ + symbol: selectedAsset.symbol, + companyName: selectedAsset.companyName, + quantity: selectedAsset.quantity + buyQuantity, + buyPrice: currentUnitPrice, + assetType: selectedAsset.assetType + }); + + await fetchAssets(); + setShowDetailsModal(false); + setBuyQuantity(1); + } catch (error) { + console.error("Error buying asset:", error); + alert(error.message || "Failed to buy asset"); + } + }; + + const handleSellAsset = async () => { + if (!selectedAsset || sellQuantity <= 0 || sellQuantity > selectedAsset.quantity) { + alert("Invalid sell quantity"); + return; + } + + try { + if (sellQuantity === selectedAsset.quantity) { + // Sell entire position + await sellAsset(selectedAsset.id); + } else { + // Reduce quantity - currentPrice is already the unit price + const currentUnitPrice = livePrices[selectedAsset.symbol] || selectedAsset.currentPrice; + + await addAsset({ + symbol: selectedAsset.symbol, + companyName: selectedAsset.companyName, + quantity: selectedAsset.quantity - sellQuantity, + buyPrice: currentUnitPrice, + assetType: selectedAsset.assetType + }); + } + + await fetchAssets(); + setShowDetailsModal(false); + setSellQuantity(1); } catch (error) { console.error("Error selling asset:", error); + alert(error.message || "Failed to sell asset"); } }; + const navigateToDetails = () => { + if (!selectedAsset) return; + const assetType = selectedAsset.assetType?.toLowerCase().replace(' ', '') || 'stock'; + navigate(`/asset/${assetType}/${selectedAsset.symbol}`); + }; + const getAssetsByType = (type) => assets.filter(asset => asset.assetType === type); - const renderSection = (title, type) => { + const renderSection = (title, type, icon, color) => { const items = getAssetsByType(type); if (items.length === 0) return null; return (
-

{title}

+
+ {icon} +

{title}

+ + {items.length} asset{items.length !== 1 ? 's' : ''} + +
{items.map(asset => ( handleAssetClick(asset)} /> ))}
@@ -76,9 +190,16 @@ const Holdings = () => { return
Loading Holdings...
; } + // currentPrice from database is already the unit price + const unitPrice = selectedAsset && (livePrices[selectedAsset.symbol] || selectedAsset.currentPrice); + const totalValue = selectedAsset && unitPrice ? unitPrice * selectedAsset.quantity : 0; + const buyPrice = selectedAsset?.buyPrice || 0; + const gainLoss = unitPrice - buyPrice; + const gainLossPercent = buyPrice > 0 ? ((gainLoss / buyPrice) * 100) : 0; + return (
-
+
{ display: 'flex', alignItems: 'center', justifyContent: 'center', - borderRadius: '15px', // squircle + borderRadius: '15px', fontSize: '1.2rem', cursor: 'pointer', - lineHeight: '5', // helps center vertically - paddingTop: '5px' // nudges arrow upward + lineHeight: '5', + paddingTop: '5px' }} onMouseOver={(e) => e.target.style.backgroundColor = '#b71c1c'} onMouseOut={(e) => e.target.style.backgroundColor = '#DB292D'} >← - -

Your Holdings

- -
- {renderSection('Stocks', 'Stocks')} - {renderSection('Mutual Funds', 'Mutual Funds')} - {renderSection('Commodities', 'Commodities')} - {renderSection('Crypto', 'Crypto')} + {renderSection('Stocks', 'stock', '📈', '#10b981')} + {renderSection('Mutual Funds', 'fund', '📊', '#3b82f6')} + {renderSection('Crypto', 'crypto', '₿', '#f59e0b')} + {renderSection('Commodities', 'commodity', '🥇', '#8b5cf6')} + + {assets.length === 0 && ( +
+
📊
+

No Holdings Yet

+

Start building your portfolio by searching and buying assets

+
+ )} {showBuyModal && ( { onSubmit={handleBuySubmit} /> )} + + {/* Asset Details Modal */} + {showDetailsModal && selectedAsset && ( +
setShowDetailsModal(false)} + > +
e.stopPropagation()} + > + {/* Header */} +
+
+
+ {selectedAsset.assetType} +
+

+ {selectedAsset.companyName} +

+
+ {selectedAsset.symbol} +
+
+ +
+ + {/* Price Information */} +
+
+
+ Current Price +
+
+ {formatCurrency(unitPrice)} +
+
+ Live Update +
+
+ +
+
+ Buy Price +
+
+ {formatCurrency(buyPrice)} +
+
+ Purchase Price +
+
+ +
= 0 ? 'rgba(16, 185, 129, 0.1)' : 'rgba(239, 68, 68, 0.1)', + border: `1px solid ${gainLoss >= 0 ? 'rgba(16, 185, 129, 0.3)' : 'rgba(239, 68, 68, 0.3)'}`, + borderRadius: '12px', + padding: '20px' + }}> +
+ Gain/Loss +
+
= 0 ? '#10b981' : '#ef4444' + }}> + {gainLoss >= 0 ? '+' : ''}{formatCurrency(gainLoss)} +
+
= 0 ? '#10b981' : '#ef4444', + marginTop: '4px', + fontWeight: '600' + }}> + {gainLoss >= 0 ? '+' : ''}{formatPercentage(gainLossPercent)} +
+
+ +
+
+ Holdings +
+
+ {selectedAsset.quantity} +
+
+ Total Value: {formatCurrency(totalValue)} +
+
+
+ + {/* Chart Placeholder */} +
+
+
📊
+
Price Chart (7 Days)
+
+ Click "Details" button for full chart +
+
+
+ + {/* Buy/Sell Actions */} +
+ {/* Buy Section */} +
+

+ Buy More +

+
+ + setBuyQuantity(parseInt(e.target.value) || 1)} + style={{ + width: '100%', + padding: '12px', + backgroundColor: '#111', + border: '1px solid rgba(255,255,255,0.2)', + borderRadius: '8px', + color: '#fff', + fontSize: '1rem' + }} + /> +
+
+ Total: {formatCurrency(unitPrice * buyQuantity)} +
+ +
+ + {/* Sell Section */} +
+

+ Sell Position +

+
+ + setSellQuantity(Math.min(parseInt(e.target.value) || 1, selectedAsset.quantity))} + style={{ + width: '100%', + padding: '12px', + backgroundColor: '#111', + border: '1px solid rgba(255,255,255,0.2)', + borderRadius: '8px', + color: '#fff', + fontSize: '1rem' + }} + /> +
+
+ Total: {formatCurrency(unitPrice * sellQuantity)} +
+ +
+
+ + {/* Details Button */} + +
+
+ )}
); }; diff --git a/reactpotfolio/src/pages/Home/Home.jsx b/reactpotfolio/src/pages/Home/Home.jsx index 6378851..88c464c 100644 --- a/reactpotfolio/src/pages/Home/Home.jsx +++ b/reactpotfolio/src/pages/Home/Home.jsx @@ -1,9 +1,22 @@ import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; +import { io } from 'socket.io-client'; +import { + Box, + Container, + Typography, + Card, + CardContent, + Grid, + Stack, + Divider, + Chip +} from '@mui/material'; +import { TrendingUp, TrendingDown, AccountBalance, ShowChart, AccountBalanceWallet } from '@mui/icons-material'; import PortfolioChart from '../../components/charts/PortfolioChart'; import PerformanceCard from '../../components/cards/PerformanceCard'; -import { getPortfolioSummary, getPortfolioPerformance, getAssetAllocation } from '../../api/portfolioApi'; -import { getAssets } from '../../api/assetsApi'; +import { getPortfolioSummary, getPortfolioPerformance, getAssetAllocation, getInvestmentBreakdown, getPerformers } from '../../api/portfolioApi'; +import { getAssets, updateCurrentPrice } from '../../api/assetsApi'; import AssetAllocationPieChart from '../../components/charts/AssetAllocationPieChart'; import './Home.css'; @@ -16,7 +29,11 @@ const Home = () => { const [chartData, setChartData] = useState(null); const [allocationData, setAllocationData] = useState(null); const [assets, setAssets] = useState([]); + const [breakdown, setBreakdown] = useState([]); + const [performers, setPerformers] = useState({ topPerformers: [], lowestPerformers: [] }); const [loading, setLoading] = useState(true); + const [livePrices, setLivePrices] = useState({}); + const [lastUpdateTime, setLastUpdateTime] = useState(null); @@ -27,12 +44,24 @@ const Home = () => { const performanceData = await getPortfolioPerformance(); const allocation = await getAssetAllocation(); const assetsData = await getAssets(); + const breakdownData = await getInvestmentBreakdown(); + const performersData = await getPerformers(); + + console.log("Portfolio Performance Data:", performanceData); + console.log("Performance Data Length:", performanceData?.length); + console.log("Performers Data:", performersData); + console.log("Top Performers:", performersData?.topPerformers); + console.log("Lowest Performers:", performersData?.lowestPerformers); + setSummary(summaryData); - setChartData(performanceData); + setChartData(performanceData || []); setAllocationData(allocation); setAssets(assetsData); + setBreakdown(breakdownData); + setPerformers(performersData); } catch (error) { console.error("Failed to fetch portfolio data", error); + setChartData([]); // Set empty array on error } finally { setLoading(false); } @@ -41,6 +70,59 @@ const Home = () => { fetchData(); }, []); + // Socket.IO for live portfolio prices (updates every 3 minutes) + useEffect(() => { + if (!assets || assets.length === 0) return; + + const socket = io('http://localhost:5000'); + + socket.on('connect', () => { + console.log('Portfolio Socket connected'); + // Subscribe to assets based on type + assets.forEach(asset => { + if (asset.assetType?.toLowerCase().includes('fund') || asset.assetType?.toLowerCase().includes('mutual')) { + // For mutual funds, use different endpoint or skip live updates + // Mutual funds update once a day, so we can skip socket subscription + console.log(`Skipping socket for mutual fund: ${asset.symbol}`); + } else { + // For stocks, crypto, commodities + socket.emit('subscribe_ticker', { ticker: asset.symbol }); + } + }); + }); + + socket.on('price_update', (data) => { + console.log('Portfolio Price update:', data); + setLivePrices(prev => ({ + ...prev, + [data.ticker]: data.price + })); + setLastUpdateTime(data.timestamp); + + // Update database with new price + updateCurrentPrice(data.ticker, data.price) + .then(() => { + console.log(`Database updated for ${data.ticker}: ${data.price}`); + }) + .catch((error) => { + console.error(`Failed to update database for ${data.ticker}:`, error); + }); + }); + + socket.on('error', (error) => { + console.error('Portfolio Socket error:', error); + }); + + return () => { + assets.forEach(asset => { + if (!asset.assetType?.toLowerCase().includes('fund')) { + socket.emit('unsubscribe_ticker', { ticker: asset.symbol }); + } + }); + socket.disconnect(); + }; + }, [assets]); + const handleCardClick = (asset) => { // Navigate to the asset details page const assetType = asset.assetType?.toLowerCase().replace(' ', '') || 'stock'; @@ -48,123 +130,427 @@ const Home = () => { }; if (loading) { - return
Loading...
; + return ( + + Loading... + + ); } - // Calculate Investment Breakdown - const investmentBreakdown = { - 'Stocks': 0, - 'Mutual Funds': 0, - 'Crypto': 0, - 'Commodities': 0 + // Map breakdown data with icons and colors + const iconMap = { + 'Stocks': { icon: , color: '#10b981' }, + 'Mutual Funds': { icon: , color: '#3b82f6' }, + 'Crypto': { icon: , color: '#f59e0b' }, + 'Commodities': { icon: , color: '#8b5cf6' } }; - assets.forEach(asset => { - if (investmentBreakdown[asset.assetType] !== undefined) { - investmentBreakdown[asset.assetType] += asset.currentValue; - } - }); + const investmentBreakdown = breakdown.map(item => ({ + type: item.type, + value: item.value, + icon: iconMap[item.type]?.icon || , + color: iconMap[item.type]?.color || '#10b981' + })); + + // Calculate live portfolio value and gain/loss + const livePortfolioValue = assets.reduce((total, asset) => { + const currentPrice = livePrices[asset.symbol] || asset.currentPrice; + return total + (currentPrice * asset.quantity); + }, 0); + + const totalInvestedValue = summary?.totalInvested || 0; + const liveGain = livePortfolioValue - totalInvestedValue; + const liveGainPercentage = totalInvestedValue > 0 ? (liveGain / totalInvestedValue) * 100 : 0; + + const isPositive = liveGain >= 0; + + // Format last update time + const formatUpdateTime = (timestamp) => { + if (!timestamp) return 'Updating...'; + const date = new Date(timestamp); + return date.toLocaleTimeString('en-IN', { + timeZone: 'Asia/Kolkata', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }) + ' IST'; + }; return ( -
-
-

Hello, {summary.userName}

- -
-
Total Portfolio Value
-
{formatCurrency(summary.portfolioValue)}
-
- {formatCurrency(summary.totalGain)} ({formatPercentage(summary.gainPercentage)}) -
-
- - - -
- -
- - -
- -
- {/* Top Performers Column */} -
-

Top Performers

-
- {[...assets] - .sort((a, b) => b.percentageChange - a.percentageChange) - .slice(0, 3) - .map(asset => ( -
handleCardClick(asset)} style={{ cursor: 'pointer' }}> - -
- ))} - {assets.length === 0 &&
No assets available.
} -
-
- - {/* Lowest Performers Column */} -
-

Lowest Performers

-
- {[...assets] - .sort((a, b) => a.percentageChange - b.percentageChange) - .slice(0, 3) - .map(asset => ( -
handleCardClick(asset)} style={{ cursor: 'pointer' }}> - -
- ))} - {assets.length === 0 &&
No assets available.
} -
-
-
- -
-

Investment Breakdown

-
- {Object.entries(investmentBreakdown).map(([type, value]) => ( -
navigate('/holdings')} - className="card breakdown-card" - style={{ - padding: '20px', - cursor: 'pointer', - transition: 'transform 0.2s, background-color 0.2s', - backgroundColor: '#1e1e1e' - }} - onMouseOver={(e) => { - e.currentTarget.style.transform = 'translateY(-5px)'; - e.currentTarget.style.backgroundColor = '#252525'; - }} - onMouseOut={(e) => { - e.currentTarget.style.transform = 'translateY(0)'; - e.currentTarget.style.backgroundColor = '#1e1e1e'; - }} - > -
{type}
-
0 ? 'white' : 'var(--text-secondary)' }}> - {formatCurrency(value)} -
-
- ))} -
-
-
+ + + {/* Hero Section */} + + + Portfolio Dashboard + + + Welcome back, {summary?.userName || 'User'} + + + + {/* Portfolio Value Card */} + + + + + } + label="Total Portfolio Value" + sx={{ + bgcolor: 'rgba(76, 175, 80, 0.1)', + color: '#4caf50', + border: '1px solid rgba(76, 175, 80, 0.3)', + fontWeight: '600', + fontSize: '0.75rem', + mb: 1.5, + '& .MuiChip-icon': { + color: '#4caf50' + } + }} + /> + + {formatCurrency(livePortfolioValue || summary?.portfolioValue || 0)} + + + Last updated: {formatUpdateTime(lastUpdateTime)} + + + + + + Total Invested + + + {formatCurrency(summary?.totalInvested || 0)} + + + + + + Total Gain/Loss + + + + {isPositive ? '+' : ''}{formatCurrency(liveGain || summary?.totalGain || 0)} + + + ({isPositive ? '+' : ''}{formatPercentage(liveGainPercentage || summary?.gainPercentage || 0)}) + + + + + + + + {/* Charts Section */} + + + + + + Asset Allocation + + + + + + + + + + Portfolio Performance + + + + + + + + {/* Investment Breakdown */} + + + Investment Breakdown + + + {investmentBreakdown.map((item) => ( + + navigate('/holdings')} + sx={{ + bgcolor: '#111', + border: '1px solid rgba(255,255,255,0.1)', + borderRadius: 2, + cursor: 'pointer', + transition: 'all 0.2s', + boxShadow: '0 2px 6px rgba(0,0,0,0.2)', + '&:hover': { + transform: 'translateY(-2px)', + borderColor: item.color, + boxShadow: `0 4px 12px ${item.color}20` + } + }} + > + + + + {item.type} + + + {item.icon} + + + 0 ? '#fff' : 'rgba(255,255,255,0.2)', + fontWeight: '700', + fontSize: '1.5rem' + }} + > + {formatCurrency(item.value)} + + + + + ))} + + + + {/* Performance Section */} + + {/* Top Performers */} + + + + + + + Top Performers + + + + {performers.topPerformers && performers.topPerformers.length > 0 ? ( + performers.topPerformers.map(asset => ( + handleCardClick(asset)} + sx={{ cursor: 'pointer' }} + > + + + )) + ) : ( + + No assets available + + )} + + + + + + {/* Lowest Performers */} + + + + + + + Lowest Performers + + + + {performers.lowestPerformers && performers.lowestPerformers.length > 0 ? ( + performers.lowestPerformers.map(asset => ( + handleCardClick(asset)} + sx={{ cursor: 'pointer' }} + > + + + )) + ) : ( + + No assets available + + )} + + + + + + + ); }; diff --git a/src/main/java/org/hsbc/config/CorsConfig.java b/src/main/java/org/hsbc/config/CorsConfig.java new file mode 100644 index 0000000..ca85225 --- /dev/null +++ b/src/main/java/org/hsbc/config/CorsConfig.java @@ -0,0 +1,48 @@ +package org.hsbc.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Arrays; +import java.util.List; + +@Configuration +public class CorsConfig { + + @Bean + public CorsFilter corsFilter() { + CorsConfiguration config = new CorsConfiguration(); + + // Allow credentials + config.setAllowCredentials(true); + + // Allow all origins (for development) + config.setAllowedOriginPatterns(Arrays.asList("*")); + + // Allow all headers + config.setAllowedHeaders(Arrays.asList("*")); + + // Allow all methods + config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); + + // Expose headers + config.setExposedHeaders(Arrays.asList( + "Authorization", + "Content-Type", + "Accept", + "X-Requested-With", + "Cache-Control" + )); + + // Max age + config.setMaxAge(3600L); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + + return new CorsFilter(source); + } +} diff --git a/src/main/java/org/hsbc/controller/PmsController.java b/src/main/java/org/hsbc/controller/PmsController.java index 7f357e1..e127d11 100644 --- a/src/main/java/org/hsbc/controller/PmsController.java +++ b/src/main/java/org/hsbc/controller/PmsController.java @@ -12,7 +12,8 @@ import java.util.List; @RestController -@RequestMapping("/pms") +@RequestMapping("/api/pms") +@CrossOrigin(origins = "*") public class PmsController { private static final Logger log = LoggerFactory.getLogger(PmsController.class); @@ -66,6 +67,13 @@ public double getPLPercentage(@PathVariable Long id) throws InvalidPmsIdExceptio public double getTotalValue() { return service.getTotalPortfolioValue(); } + + @PutMapping("/update-price/{symbol}") + public PmsEntity updateCurrentPrice( + @PathVariable String symbol, + @RequestParam double price) { + return service.updateCurrentPrice(symbol, price); + } } diff --git a/src/main/java/org/hsbc/controller/PortfolioController.java b/src/main/java/org/hsbc/controller/PortfolioController.java new file mode 100644 index 0000000..7aad0c7 --- /dev/null +++ b/src/main/java/org/hsbc/controller/PortfolioController.java @@ -0,0 +1,271 @@ +package org.hsbc.controller; + +import org.hsbc.entity.PmsEntity; +import org.hsbc.service.PmsService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.client.RestTemplate; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.*; + +@RestController +@RequestMapping("/api/portfolio") +@CrossOrigin(origins = "*") +public class PortfolioController { + + @Autowired + private PmsService pmsService; + + private final RestTemplate restTemplate = new RestTemplate(); + private final ObjectMapper objectMapper = new ObjectMapper(); + + @GetMapping("/summary") + public ResponseEntity> getPortfolioSummary() { + List assets = pmsService.getAllAssets(); + + double totalPortfolioValue = 0; + double totalInvestedValue = 0; + + for (PmsEntity asset : assets) { + totalPortfolioValue += asset.getCurrentPrice() * asset.getQuantity(); + + // Use buyPrice * quantity if buyingValue is 0 + double buyingValue = asset.getBuyingValue(); + if (buyingValue == 0) { + buyingValue = asset.getBuyPrice() * asset.getQuantity(); + } + totalInvestedValue += buyingValue; + } + + double totalGain = totalPortfolioValue - totalInvestedValue; + double gainPercentage = totalInvestedValue > 0 ? (totalGain / totalInvestedValue) * 100 : 0; + + System.out.println("Portfolio Summary: totalPortfolioValue=" + totalPortfolioValue + + ", totalInvestedValue=" + totalInvestedValue + + ", totalGain=" + totalGain + ", gainPercentage=" + gainPercentage + "%"); + + Map summary = new HashMap<>(); + summary.put("userName", "Alex Johnson"); + summary.put("portfolioValue", totalPortfolioValue); + summary.put("totalInvested", totalInvestedValue); + summary.put("totalGain", totalGain); + summary.put("gainPercentage", gainPercentage); + + return ResponseEntity.ok(summary); + } + + @GetMapping("/performance") + public ResponseEntity>> getPortfolioPerformance() { + List assets = pmsService.getAllAssets(); + + if (assets.isEmpty()) { + return ResponseEntity.ok(new ArrayList<>()); + } + + // Fetch 1-year historical data for each asset and aggregate + Map aggregatedData = new TreeMap<>(); // TreeMap for sorted dates + + for (PmsEntity asset : assets) { + String symbol = asset.getSymbol(); + int quantity = asset.getQuantity(); + String assetType = asset.getAssetType() != null ? asset.getAssetType() : "Stocks"; + + System.out.println("Fetching history for " + symbol + " (type: " + assetType + ")"); + + // Call Flask API to get 1-year historical data + String url = "http://localhost:5000/api/history/" + symbol + "?period=1Y&interval=1d"; + + try { + String response = restTemplate.getForObject(url, String.class); + JsonNode historyData = objectMapper.readTree(response); + + // Flask API returns 'data' field, not 'history' + if (historyData.has("data") && historyData.get("data").isArray()) { + for (JsonNode dataPoint : historyData.get("data")) { + // Flask returns 'time' field (YYYY-MM-DD string for daily data) + String date = dataPoint.get("time").asText(); + + // For mutual funds, use 'nav' field if exists, otherwise use 'close' + double price = 0.0; + if (dataPoint.has("nav")) { + price = dataPoint.get("nav").asDouble(); + } else if (dataPoint.has("close")) { + price = dataPoint.get("close").asDouble(); + } else { + System.err.println("No price data for " + symbol + " on " + date); + continue; + } + + double value = price * quantity; + + // Add to aggregated data + aggregatedData.put(date, aggregatedData.getOrDefault(date, 0.0) + value); + } + System.out.println("Added " + historyData.get("data").size() + " data points for " + symbol); + } else { + System.err.println("No data array in response for " + symbol + ". Response: " + response); + } + } catch (Exception e) { + System.err.println("Error fetching history for " + symbol + ": " + e.getMessage()); + e.printStackTrace(); + } + } + + // Convert to response format + List> performance = new ArrayList<>(); + for (Map.Entry entry : aggregatedData.entrySet()) { + Map dataPoint = new HashMap<>(); + dataPoint.put("date", entry.getKey()); + dataPoint.put("value", Math.round(entry.getValue())); + performance.add(dataPoint); + } + + System.out.println("Returning " + performance.size() + " total data points for portfolio chart"); + return ResponseEntity.ok(performance); + } + + @GetMapping("/allocation") + public ResponseEntity>> getAssetAllocation() { + List assets = pmsService.getAllAssets(); + Map allocation = new HashMap<>(); + + for (PmsEntity asset : assets) { + double value = asset.getCurrentPrice() * asset.getQuantity(); + String assetType = asset.getAssetType() != null ? asset.getAssetType() : "Unknown"; + + allocation.put(assetType, allocation.getOrDefault(assetType, 0.0) + value); + } + + List> result = new ArrayList<>(); + for (Map.Entry entry : allocation.entrySet()) { + Map item = new HashMap<>(); + item.put("assetType", entry.getKey()); + item.put("value", entry.getValue()); + result.add(item); + } + + return ResponseEntity.ok(result); + } + + @GetMapping("/breakdown") + public ResponseEntity>> getInvestmentBreakdown() { + List assets = pmsService.getAllAssets(); + Map breakdown = new HashMap<>(); + + // Initialize all types + breakdown.put("Stocks", 0.0); + breakdown.put("Mutual Funds", 0.0); + breakdown.put("Crypto", 0.0); + breakdown.put("Commodities", 0.0); + + for (PmsEntity asset : assets) { + double investedValue = asset.getBuyingValue(); + String assetType = asset.getAssetType() != null ? asset.getAssetType().trim() : "Stocks"; + + // Normalize asset type names to match frontend (case-insensitive) + String normalizedType = assetType; + if (assetType.equalsIgnoreCase("Stock") || assetType.equalsIgnoreCase("Stocks")) { + normalizedType = "Stocks"; + } else if (assetType.equalsIgnoreCase("Commodity") || assetType.equalsIgnoreCase("Commodities")) { + normalizedType = "Commodities"; + } else if (assetType.equalsIgnoreCase("Fund") || assetType.equalsIgnoreCase("Mutual Fund") + || assetType.equalsIgnoreCase("Mutual Funds")) { + normalizedType = "Mutual Funds"; + } else if (assetType.equalsIgnoreCase("Crypto") || assetType.equalsIgnoreCase("Cryptocurrency")) { + normalizedType = "Crypto"; + } + + breakdown.put(normalizedType, breakdown.getOrDefault(normalizedType, 0.0) + investedValue); + } + + // Only return non-zero values + List> result = new ArrayList<>(); + for (Map.Entry entry : breakdown.entrySet()) { + if (entry.getValue() > 0) { + Map item = new HashMap<>(); + item.put("type", entry.getKey()); + item.put("value", entry.getValue()); + result.add(item); + } + } + + // If result is empty, return all types with 0 values for UI consistency + if (result.isEmpty()) { + for (String type : Arrays.asList("Stocks", "Mutual Funds", "Crypto", "Commodities")) { + Map item = new HashMap<>(); + item.put("type", type); + item.put("value", 0.0); + result.add(item); + } + } + + return ResponseEntity.ok(result); + } + + @GetMapping("/performers") + public ResponseEntity>>> getPerformers() { + List assets = pmsService.getAllAssets(); + + if (assets.isEmpty()) { + Map>> emptyResult = new HashMap<>(); + emptyResult.put("topPerformers", new ArrayList<>()); + emptyResult.put("lowestPerformers", new ArrayList<>()); + return ResponseEntity.ok(emptyResult); + } + + // Calculate percentage change for each asset and create list + List> assetPerformance = new ArrayList<>(); + + for (PmsEntity asset : assets) { + double currentValue = asset.getCurrentPrice() * asset.getQuantity(); + // Use buyPrice * quantity if buyingValue is 0 + double buyingValue = asset.getBuyingValue(); + if (buyingValue == 0) { + buyingValue = asset.getBuyPrice() * asset.getQuantity(); + } + double gain = currentValue - buyingValue; + double percentageChange = buyingValue > 0 ? (gain / buyingValue) * 100 : 0; + + Map assetData = new HashMap<>(); + assetData.put("id", asset.getId()); + assetData.put("companyName", asset.getCompanyName()); + assetData.put("symbol", asset.getSymbol()); + assetData.put("currentValue", currentValue); + assetData.put("percentageChange", percentageChange); + assetData.put("assetType", asset.getAssetType()); + + System.out.println("Asset " + asset.getSymbol() + ": currentValue=" + currentValue + + ", buyingValue=" + buyingValue + ", percentageChange=" + percentageChange + "%"); + + assetPerformance.add(assetData); + } + + // Sort by percentage change (descending - highest first) + assetPerformance.sort((a, b) -> + Double.compare((Double) b.get("percentageChange"), (Double) a.get("percentageChange")) + ); + + // Get top 3 performers (highest percentage change) + List> topPerformers = assetPerformance.stream() + .limit(3) + .collect(java.util.stream.Collectors.toList()); + + // Get lowest 3 performers (lowest percentage change - from the end of sorted list) + int size = assetPerformance.size(); + List> lowestPerformers = new ArrayList<>(); + for (int i = Math.max(0, size - 3); i < size; i++) { + lowestPerformers.add(assetPerformance.get(i)); + } + // Reverse to show worst first + java.util.Collections.reverse(lowestPerformers); + + Map>> result = new HashMap<>(); + result.put("topPerformers", topPerformers); + result.put("lowestPerformers", lowestPerformers); + + return ResponseEntity.ok(result); + } +} diff --git a/src/main/java/org/hsbc/controller/TransactionController.java b/src/main/java/org/hsbc/controller/TransactionController.java index d2e8097..d033c04 100644 --- a/src/main/java/org/hsbc/controller/TransactionController.java +++ b/src/main/java/org/hsbc/controller/TransactionController.java @@ -2,6 +2,7 @@ import org.hsbc.entity.TransactionEntity; import org.hsbc.service.TransactionService; +import org.springframework.web.bind.annotation.CrossOrigin; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; @@ -15,6 +16,7 @@ @RestController @RequestMapping("/transactions") +@CrossOrigin(origins = "*") public class TransactionController { private final TransactionService service; diff --git a/src/main/java/org/hsbc/controller/WalletController.java b/src/main/java/org/hsbc/controller/WalletController.java index 5fb1228..0cbb9a5 100644 --- a/src/main/java/org/hsbc/controller/WalletController.java +++ b/src/main/java/org/hsbc/controller/WalletController.java @@ -1,13 +1,16 @@ package org.hsbc.controller; +import org.hsbc.entity.WalletEntity; import org.hsbc.service.WalletService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.*; +import java.util.Map; @RestController @RequestMapping("/wallet") +@CrossOrigin(origins = "*") public class WalletController { private final WalletService service; @@ -36,4 +39,10 @@ public double addMoney(@RequestParam double amount) { public double deductMoney(@RequestParam double amount) { return service.deductMoney(amount); } + + // 4️⃣ Get wallet summary + @GetMapping("/summary") + public WalletEntity getWalletSummary() { + return service.getWalletSummary(); + } } diff --git a/src/main/java/org/hsbc/entity/PmsEntity.java b/src/main/java/org/hsbc/entity/PmsEntity.java index 8116204..98968de 100644 --- a/src/main/java/org/hsbc/entity/PmsEntity.java +++ b/src/main/java/org/hsbc/entity/PmsEntity.java @@ -1,6 +1,7 @@ package org.hsbc.entity; import jakarta.persistence.*; +import java.time.LocalDate; @Entity @@ -19,6 +20,7 @@ public class PmsEntity String exchange; String industry; String assetType; + LocalDate purchaseDate; public PmsEntity() { } @@ -138,6 +140,14 @@ public void setAssetType(String assetType) { this.assetType = assetType; } + public LocalDate getPurchaseDate() { + return purchaseDate; + } + + public void setPurchaseDate(LocalDate purchaseDate) { + this.purchaseDate = purchaseDate; + } + @Override public String toString() { return "PmsEntity{" + @@ -152,6 +162,7 @@ public String toString() { ", exchange='" + exchange + '\'' + ", industry='" + industry + '\'' + ", assetType='" + assetType + '\'' + + ", purchaseDate=" + purchaseDate + '}'; } } diff --git a/src/main/java/org/hsbc/entity/TransactionEntity.java b/src/main/java/org/hsbc/entity/TransactionEntity.java index cb26c97..bca8b70 100644 --- a/src/main/java/org/hsbc/entity/TransactionEntity.java +++ b/src/main/java/org/hsbc/entity/TransactionEntity.java @@ -1,7 +1,7 @@ package org.hsbc.entity; import jakarta.persistence.*; -import java.time.LocalDate; +import java.time.LocalDateTime; @Entity @Table(name = "transactions") @@ -14,29 +14,28 @@ public class TransactionEntity { String symbol; int quantity; double buyPrice; - LocalDate transactionDate; + LocalDateTime transactionDate; - - String TransactionType ; + String transactionType; public TransactionEntity() { } - public TransactionEntity(Long transactionId, String symbol, int quantity, double buyPrice, LocalDate transactionDate, String transactionType) { + public TransactionEntity(Long transactionId, String symbol, int quantity, double buyPrice, LocalDateTime transactionDate, String transactionType) { this.transactionId = transactionId; this.symbol = symbol; this.quantity = quantity; this.buyPrice = buyPrice; this.transactionDate = transactionDate; - TransactionType = transactionType; + this.transactionType = transactionType; } - public TransactionEntity(String symbol, int quantity, double buyPrice, LocalDate transactionDate, String transactionType) { + public TransactionEntity(String symbol, int quantity, double buyPrice, LocalDateTime transactionDate, String transactionType) { this.symbol = symbol; this.quantity = quantity; this.buyPrice = buyPrice; this.transactionDate = transactionDate; - TransactionType = transactionType; + this.transactionType = transactionType; } public Long getTransactionId() { @@ -71,20 +70,20 @@ public void setBuyPrice(double buyPrice) { this.buyPrice = buyPrice; } - public LocalDate getTransactionDate() { + public LocalDateTime getTransactionDate() { return transactionDate; } - public void setTransactionDate(LocalDate transactionDate) { + public void setTransactionDate(LocalDateTime transactionDate) { this.transactionDate = transactionDate; } public String getTransactionType() { - return TransactionType; + return transactionType; } public void setTransactionType(String transactionType) { - TransactionType = transactionType; + this.transactionType = transactionType; } @Override @@ -95,7 +94,7 @@ public String toString() { ", quantity=" + quantity + ", buyPrice=" + buyPrice + ", transactionDate=" + transactionDate + - ", TransactionType='" + TransactionType + '\'' + + ", transactionType='" + transactionType + '\'' + '}'; } } diff --git a/src/main/java/org/hsbc/service/PmsService.java b/src/main/java/org/hsbc/service/PmsService.java index b4fc673..c17172a 100644 --- a/src/main/java/org/hsbc/service/PmsService.java +++ b/src/main/java/org/hsbc/service/PmsService.java @@ -19,7 +19,7 @@ public interface PmsService { double getTotalPortfolioValue(); List getAllAssets(); - PmsEntity getAssetById(Long id) throws InvalidPmsIdException; + PmsEntity updateCurrentPrice(String symbol, double newPrice); } \ No newline at end of file diff --git a/src/main/java/org/hsbc/service/PmsServiceimp.java b/src/main/java/org/hsbc/service/PmsServiceimp.java index 7a34c10..3128496 100644 --- a/src/main/java/org/hsbc/service/PmsServiceimp.java +++ b/src/main/java/org/hsbc/service/PmsServiceimp.java @@ -1,13 +1,19 @@ package org.hsbc.service; + +//Import statemnts import org.hsbc.entity.PmsEntity; import org.hsbc.exception.InvalidPmsIdException; import org.hsbc.repo.PmsRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +import java.time.LocalDate; import java.util.List; import java.util.Optional; @@ -17,13 +23,25 @@ public class PmsServiceimp implements PmsService { LoggerFactory.getLogger(PmsServiceimp.class); @Autowired private PmsRepository repository; - - // 1️⃣ Add Asset - @Override - public PmsEntity addAsset(PmsEntity asset) { - asset.setBuyingValue(asset.getBuyPrice() * asset.getQuantity()); - return repository.save(asset); - } + + @Autowired + private WalletService walletService; + + // 1️⃣ Add Asset + @Override + public PmsEntity addAsset(PmsEntity asset) { + // Calculate total cost + double totalCost = asset.getBuyPrice() * asset.getQuantity(); + + // Check and deduct from wallet (this will throw exception if insufficient balance) + walletService.deductMoney(totalCost); + + // Set purchase date and buying value + asset.setPurchaseDate(LocalDate.now()); + asset.setBuyingValue(totalCost); + + return repository.save(asset); + } // 2️⃣ Remove Asset @Override @@ -91,6 +109,22 @@ public PmsEntity getAssetById(Long id) throws InvalidPmsIdException { } return optAsset.get(); } + + @Override + public PmsEntity updateCurrentPrice(String symbol, double newPrice) { + List assets = repository.findAll(); + for (PmsEntity asset : assets) { + if (asset.getSymbol().equalsIgnoreCase(symbol)) { + asset.setCurrentPrice(newPrice); + System.out.println("Updated " + symbol + " price to: " + newPrice); + return repository.save(asset); + } + } + throw new ResponseStatusException( + HttpStatus.NOT_FOUND, + "Asset not found with symbol " + symbol + ); + } // // public PmsEntity findAllPms(long id) throws InvalidException { // Optional optProduct = repository.findById(id); diff --git a/src/main/java/org/hsbc/service/WalletService.java b/src/main/java/org/hsbc/service/WalletService.java index e4cd42e..783a852 100644 --- a/src/main/java/org/hsbc/service/WalletService.java +++ b/src/main/java/org/hsbc/service/WalletService.java @@ -1,9 +1,13 @@ package org.hsbc.service; +import org.hsbc.entity.WalletEntity; + public interface WalletService { double getBalance(); double addMoney(double amount); double deductMoney(double amount); + + WalletEntity getWalletSummary(); } diff --git a/src/main/java/org/hsbc/service/WalletServiceImpl.java b/src/main/java/org/hsbc/service/WalletServiceImpl.java index 9824db8..f947f7d 100644 --- a/src/main/java/org/hsbc/service/WalletServiceImpl.java +++ b/src/main/java/org/hsbc/service/WalletServiceImpl.java @@ -52,4 +52,9 @@ public double deductMoney(double amount) { repository.save(wallet); return wallet.getBalance(); } + + @Override + public WalletEntity getWalletSummary() { + return getWallet(); + } } diff --git a/src/test/java/org/hsbc/controller/TransactionControllerTest.java b/src/test/java/org/hsbc/controller/TransactionControllerTest.java index 3bd1db0..ddbda44 100644 --- a/src/test/java/org/hsbc/controller/TransactionControllerTest.java +++ b/src/test/java/org/hsbc/controller/TransactionControllerTest.java @@ -13,7 +13,7 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.Arrays; import java.util.List; @@ -46,7 +46,7 @@ void setUp() { // 1️⃣ Add transaction @Test void testAddTransaction() throws Exception { - TransactionEntity txn = new TransactionEntity("AAPL", 10, 150.0, LocalDate.now(), "BUY"); + TransactionEntity txn = new TransactionEntity("AAPL", 10, 150.0, LocalDateTime.now(), "BUY"); txn.setTransactionId(1L); when(service.addTransaction(any(TransactionEntity.class))).thenReturn(txn); @@ -63,8 +63,8 @@ void testAddTransaction() throws Exception { @Test void testGetAllTransactions() throws Exception { List txnList = Arrays.asList( - new TransactionEntity("AAPL", 10, 150.0, LocalDate.now(), "BUY"), - new TransactionEntity("GOOGL", 5, 2000.0, LocalDate.now(), "SELL") + new TransactionEntity("AAPL", 10, 150.0, LocalDateTime.now(), "BUY"), + new TransactionEntity("GOOGL", 5, 2000.0, LocalDateTime.now(), "SELL") ); when(service.getAllTransactions()).thenReturn(txnList); @@ -79,7 +79,7 @@ void testGetAllTransactions() throws Exception { @Test void testGetBySymbol() throws Exception { List txnList = Arrays.asList( - new TransactionEntity("TSLA", 20, 700.0, LocalDate.now(), "BUY") + new TransactionEntity("TSLA", 20, 700.0, LocalDateTime.now(), "BUY") ); when(service.getTransactionsBySymbol("TSLA")).thenReturn(txnList); diff --git a/src/test/java/org/hsbc/service/TransactionServiceimpTest.java b/src/test/java/org/hsbc/service/TransactionServiceimpTest.java index b60946a..3d43be4 100644 --- a/src/test/java/org/hsbc/service/TransactionServiceimpTest.java +++ b/src/test/java/org/hsbc/service/TransactionServiceimpTest.java @@ -10,7 +10,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -36,7 +36,7 @@ void setUp() { transaction.setSymbol("AAPL"); transaction.setQuantity(10); transaction.setBuyPrice(150.0); - transaction.setTransactionDate(LocalDate.now()); + transaction.setTransactionDate(LocalDateTime.now()); transaction.setTransactionType("BUY"); }