import streamlit as st import pandas as pd import yfinance as yf import plotly.graph_objects as go from datetime import datetime, timedelta import numpy as np # Import utility functions from utils.yfinance_utils import fetch_yfinance_daily from utils.currency_utils import get_usd_sgd_rate from utils.fd_utils import calculate_fd_returns from utils.hdb_utils import calculate_hdb_returns print("Starting the app ...") # Sanity check on the yfinance_utils print("Sanity check on the yfinance_utils ...") # print(fetch_yfinance_daily("MSFT", "2020-01-01", "2020-01-03")) data = yf.download("MSFT", "2020-01-01", "2020-01-03") print(data.head()) # Set page config st.set_page_config(page_title="Asset Class Comparison", layout="wide") # Title and description st.title("Asset Class Performance Comparison") st.write("Compare the performance of different asset classes over time") # st.write("Note: Cryptocurrencies (BTC, ETH, SOL, DOGE) are highly volatile and should be considered high-risk investments") # Sidebar for user inputs st.sidebar.header("Investment Parameters") currency = st.sidebar.selectbox("Display Currency", ["USD", "SGD"], index=0) initial_investment = st.sidebar.number_input(f"Initial Investment Amount ({currency})", min_value=1000, value=10000, step=1000) start_date = st.sidebar.date_input("Start Date", value=datetime.now() - timedelta(days=365*25)) user_end_date = st.sidebar.date_input("End Date", value=datetime.now()) fd_rate = st.sidebar.number_input("Fixed Deposit Rate (%)", min_value=0.0, value=2.9, step=0.1) / 100 use_log_scale = st.sidebar.checkbox("Use Log Scale", value=True) # Calculate and display investment period investment_days = (user_end_date - start_date).days investment_years = investment_days / 365 st.write(f"Investment Period: {investment_days} days ({investment_years:.1f} years)") # Asset selection selected_assets = st.sidebar.multiselect( "Select Assets to Compare", [ "Fixed Deposit", "HDB", "Gold", "SGS Bonds", "US Treasury Bonds", "NASDAQ Composite", "NASDAQ Large Cap", "NASDAQ 100", "S&P 500", "Dow Jones", "Microsoft", "Google", "Nvidia", "Apple", "Amazon", "Tesla", "Netflix", "Meta", "Bitcoin", "Ethereum", "Solana", "Dogecoin", ], default=[ "Fixed Deposit", "HDB", "Gold", "US Treasury Bonds", "SGS Bonds", "S&P 500", "Dow Jones", "NASDAQ Composite", #"NASDAQ Large Cap", "NASDAQ 100", "Microsoft", "Google", "Nvidia", "Bitcoin" ] ) # Today's date for reference today = datetime.now().date() usd_to_sgd = get_usd_sgd_rate() if currency == "SGD" else 1.0 currency_symbol = "$" if currency == "USD" else "S$" # Create a dictionary of tickers for yfinance tickers = { "Gold": "GC=F", "HDB": "A12.SI", "SGS Bonds": "A35.SI", # Nikko AM SGD Investment Grade Corporate Bond ETF "US Treasury Bonds": "TLT", # iShares 20+ Year Treasury Bond ETF "NASDAQ Composite": "^IXIC", "NASDAQ Large Cap": "^NDX", "NASDAQ 100": "^NDX", "S&P 500": "^GSPC", "Dow Jones": "^DJI", "Microsoft": "MSFT", "Google": "GOOGL", "Nvidia": "NVDA", "Apple": "AAPL", "Amazon": "AMZN", "Tesla": "TSLA", "Netflix": "NFLX", "Meta": "META", "Bitcoin": "BTC-USD", "Ethereum": "ETH-USD", "Solana": "SOL-USD", "Dogecoin": "DOGE-USD", } # Determine the effective end date for each asset asset_end_dates = {} for asset in selected_assets: if asset == "Fixed Deposit": asset_end_dates[asset] = user_end_date else: if user_end_date > today: asset_end_dates[asset] = today else: asset_end_dates[asset] = user_end_date # Warn the user if a future end date is selected for market assets if any(user_end_date > today and asset != "Fixed Deposit" for asset in selected_assets): st.warning(f"Market data is only available up to today ({today}). For market assets, the end date has been set to today.") # Calculate returns for each selected asset asset_series = {} failed_assets = [] actual_start_dates = {} for asset in selected_assets: asset_start = start_date asset_end = asset_end_dates[asset] if asset == "Fixed Deposit": fd_index = pd.date_range(start=asset_start, end=user_end_date) daily_rate = (1 + fd_rate) ** (1/365) - 1 fd_values = initial_investment * (1 + daily_rate) ** np.arange(len(fd_index)) if currency == "SGD": fd_values = fd_values * usd_to_sgd asset_series[asset] = pd.Series(fd_values, index=fd_index) actual_start_dates[asset] = asset_start elif asset == "HDB": hdb_values = calculate_hdb_returns(asset_start, asset_end, initial_investment) if hdb_values is not None: if currency == "SGD": hdb_values = hdb_values * usd_to_sgd asset_series[asset] = hdb_values actual_start_dates[asset] = asset_start else: failed_assets.append(asset) else: price_data = fetch_yfinance_daily(tickers[asset], asset_start, asset_end) if price_data is not None and not price_data.empty: price_data = price_data.sort_index() actual_start = price_data.index[0] actual_start_dates[asset] = actual_start aligned_index = pd.date_range(start=actual_start, end=asset_end) price_data = price_data.reindex(aligned_index) price_data = price_data.ffill() asset_values = initial_investment * (price_data / price_data.iloc[0]) if currency == "SGD": asset_values = asset_values * usd_to_sgd asset_series[asset] = asset_values else: failed_assets.append(asset) # Combine all asset series into a single DataFrame if asset_series: returns_data = pd.DataFrame(asset_series) else: returns_data = pd.DataFrame() # Remove failed assets from selected_assets (except FD) selected_assets = [asset for asset in selected_assets if asset not in failed_assets or asset == "Fixed Deposit"] if not selected_assets: st.error("No assets could be loaded. Please try different assets.") st.stop() # Create the plot fig = go.Figure() # Add vertical lines for every 5 years start_year = returns_data.index[0].year end_year = returns_data.index[-1].year for year in range(start_year, end_year + 1, 5): fig.add_vline(x=datetime(year, 1, 1), line_dash="dash", line_color="gray", opacity=0.3) for asset in selected_assets: fig.add_trace(go.Scatter( x=returns_data.index, y=returns_data[asset], name=asset, mode='lines' )) fig.update_layout( title="Asset Performance Comparison", xaxis_title="Date", yaxis_title=f"Investment Value ({currency_symbol})", hovermode="x unified", height=600, yaxis_type="log" if use_log_scale else "linear" ) # Display the plot st.plotly_chart(fig, use_container_width=True) # Create a summary table st.subheader("Investment Summary") summary_data = [] for asset in selected_assets: valid_series = returns_data[asset].dropna() if not valid_series.empty: final_value = valid_series.iloc[-1] days = (valid_series.index[-1] - valid_series.index[0]).days years = days / 365 annualized_return = ((final_value / initial_investment) ** (1/years) - 1) * 100 # Calculate yearly return statistics yearly_data = valid_series.resample('YE').first() yearly_returns = yearly_data.pct_change().dropna() positive_years = (yearly_returns > 0).sum() total_years = len(yearly_returns) positive_percentage = (positive_years / total_years) * 100 summary_data.append({ "Asset": asset, f"Final Value ({currency_symbol})": final_value, "Annualized Return (%)": annualized_return, "Positive Years": f"{positive_years}/{total_years}", "Positive Years %": positive_percentage, }) else: summary_data.append({ "Asset": asset, f"Final Value ({currency_symbol})": None, "Annualized Return (%)": None, "Positive Years": "N/A", "Positive Years %": None, }) # Convert to DataFrame df = pd.DataFrame(summary_data) # Format the display values df[f"Final Value ({currency_symbol})"] = df[f"Final Value ({currency_symbol})"].apply(lambda x: f"{x:,.2f}" if x is not None else "N/A") df["Annualized Return (%)"] = df["Annualized Return (%)"].apply(lambda x: f"{x:.2f}" if x is not None else "N/A") df["Positive Years %"] = df["Positive Years %"].apply(lambda x: f"{x:.1f}" if x is not None else "N/A") # Display the summary table with sorting enabled st.dataframe( df, hide_index=True, column_config={ f"Final Value ({currency_symbol})": st.column_config.NumberColumn( format="%.2f" ), "Annualized Return (%)": st.column_config.NumberColumn( format="%.2f" ), "Positive Years %": st.column_config.NumberColumn( format="%.1f" ), "Performance": st.column_config.ImageColumn( "Performance" ) } ) # Calculate and display final returns st.subheader("Final Investment Values") for asset in selected_assets: valid_series = returns_data[asset].dropna() if not valid_series.empty: final_value = valid_series.iloc[-1] st.write(f"{asset}: {currency_symbol}{final_value:,.2f}") else: st.write(f"{asset}: Data unavailable") # Calculate and display annualized returns st.subheader("Annualized Returns") for asset in selected_assets: valid_series = returns_data[asset].dropna() if len(valid_series) > 1: actual_start = actual_start_dates[asset] days = (valid_series.index[-1] - valid_series.index[0]).days years = days / 365 final_value = valid_series.iloc[-1] annualized_return = ((final_value / initial_investment) ** (1/years) - 1) * 100 if pd.Timestamp(actual_start).date() > start_date: st.write(f"{asset}: {annualized_return:.2f}% (Data available from {actual_start.strftime('%Y-%m-%d')})") else: st.write(f"{asset}: {annualized_return:.2f}%") else: st.write(f"{asset}: N/A") # Calculate and display yearly return statistics st.subheader("Yearly Return Statistics") for asset in selected_assets: valid_series = returns_data[asset].dropna() if len(valid_series) > 1: # Resample to yearly data yearly_data = valid_series.resample('YE').first() # Calculate yearly returns yearly_returns = yearly_data.pct_change().dropna() # Count positive and negative years positive_years = (yearly_returns > 0).sum() total_years = len(yearly_returns) positive_percentage = (positive_years / total_years) * 100 st.write(f"{asset}: {positive_years} out of {total_years} years ({positive_percentage:.1f}%) had positive returns") else: st.write(f"{asset}: Insufficient data for yearly analysis") # Show warnings for data availability for asset in selected_assets: if asset in actual_start_dates and pd.Timestamp(actual_start_dates[asset]).date() > start_date: st.warning(f"Data for {asset} is only available from {actual_start_dates[asset].strftime('%Y-%m-%d')}. The analysis starts from this date.") # Show warning for failed assets if failed_assets: st.warning(f"Could not load data for the following assets: {', '.join(failed_assets)}")