File size: 11,785 Bytes
2aaf2a2
 
 
 
 
 
 
 
 
 
 
afd8f1d
 
2aaf2a2
04cf5a7
f2ba8bf
c2b76ba
04cf5a7
 
 
 
c2b76ba
2aaf2a2
 
 
 
 
 
b382e22
2aaf2a2
 
 
 
 
a71b954
2aaf2a2
b382e22
2aaf2a2
a71b954
2aaf2a2
b382e22
 
 
 
 
2aaf2a2
 
 
 
 
afd8f1d
2aaf2a2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2b4eab3
15ca7f4
 
 
2aaf2a2
a71b954
 
afd8f1d
a71b954
 
 
 
 
 
2aaf2a2
 
 
 
 
 
 
 
 
 
 
afd8f1d
a71b954
 
2aaf2a2
 
 
 
 
 
 
 
 
 
 
 
 
2b4eab3
15ca7f4
 
 
2aaf2a2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
afd8f1d
 
 
 
 
 
 
 
 
2aaf2a2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4f24509
 
 
 
 
 
2aaf2a2
 
 
 
 
 
 
 
 
 
 
 
 
2b4eab3
 
2aaf2a2
 
 
 
 
e22872a
 
 
 
 
 
 
 
 
 
 
 
b382e22
e22872a
 
 
 
 
 
 
 
 
 
4f24509
e22872a
 
 
 
 
 
 
4f24509
e22872a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4f24509
 
 
e22872a
 
 
 
2aaf2a2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a71b954
 
 
 
 
 
b382e22
a71b954
 
 
 
 
 
 
 
 
 
 
 
 
2aaf2a2
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
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)}")