|
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 |
|
|
|
|
|
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 ...") |
|
|
|
|
|
print("Sanity check on the yfinance_utils ...") |
|
|
|
data = yf.download("MSFT", "2020-01-01", "2020-01-03") |
|
print(data.head()) |
|
|
|
|
|
st.set_page_config(page_title="Asset Class Comparison", layout="wide") |
|
|
|
|
|
st.title("Asset Class Performance Comparison") |
|
st.write("Compare the performance of different asset classes over time") |
|
|
|
|
|
|
|
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) |
|
|
|
|
|
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)") |
|
|
|
|
|
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", |
|
"Microsoft", "Google", "Nvidia", |
|
"Bitcoin" |
|
] |
|
) |
|
|
|
|
|
today = datetime.now().date() |
|
|
|
usd_to_sgd = get_usd_sgd_rate() if currency == "SGD" else 1.0 |
|
currency_symbol = "$" if currency == "USD" else "S$" |
|
|
|
|
|
tickers = { |
|
"Gold": "GC=F", |
|
"HDB": "A12.SI", |
|
"SGS Bonds": "A35.SI", |
|
"US Treasury Bonds": "TLT", |
|
"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", |
|
} |
|
|
|
|
|
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 |
|
|
|
|
|
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.") |
|
|
|
|
|
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) |
|
|
|
|
|
if asset_series: |
|
returns_data = pd.DataFrame(asset_series) |
|
else: |
|
returns_data = pd.DataFrame() |
|
|
|
|
|
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() |
|
|
|
|
|
fig = go.Figure() |
|
|
|
|
|
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" |
|
) |
|
|
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
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 |
|
|
|
|
|
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, |
|
}) |
|
|
|
|
|
df = pd.DataFrame(summary_data) |
|
|
|
|
|
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") |
|
|
|
|
|
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" |
|
) |
|
} |
|
) |
|
|
|
|
|
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") |
|
|
|
|
|
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") |
|
|
|
|
|
st.subheader("Yearly Return Statistics") |
|
for asset in selected_assets: |
|
valid_series = returns_data[asset].dropna() |
|
if len(valid_series) > 1: |
|
|
|
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 |
|
|
|
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") |
|
|
|
|
|
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.") |
|
|
|
|
|
if failed_assets: |
|
st.warning(f"Could not load data for the following assets: {', '.join(failed_assets)}") |
|
|