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
-
-
-
-
+
-
-
+ {/* 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