prasanth.thangavel
commited on
Commit
·
2aaf2a2
1
Parent(s):
ea5b3dc
First commit of the app
Browse files- .gitignore +87 -0
- README.md +69 -0
- app.py +195 -0
- notebooks/scratchpad.ipynb +237 -0
- requirements.txt +6 -0
- tests/integration/test_yfinance_utils_integration.py +12 -0
- tests/unit/test_currency_utils.py +21 -0
- tests/unit/test_fd_utils.py +11 -0
- tests/unit/test_yfinance_utils.py +23 -0
- utils/currency_utils.py +11 -0
- utils/fd_utils.py +4 -0
- utils/yfinance_utils.py +16 -0
.gitignore
ADDED
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Python
|
2 |
+
__pycache__/
|
3 |
+
*.py[cod]
|
4 |
+
*$py.class
|
5 |
+
*.so
|
6 |
+
.Python
|
7 |
+
build/
|
8 |
+
develop-eggs/
|
9 |
+
dist/
|
10 |
+
downloads/
|
11 |
+
eggs/
|
12 |
+
.eggs/
|
13 |
+
lib/
|
14 |
+
lib64/
|
15 |
+
parts/
|
16 |
+
sdist/
|
17 |
+
var/
|
18 |
+
wheels/
|
19 |
+
*.egg-info/
|
20 |
+
.installed.cfg
|
21 |
+
*.egg
|
22 |
+
|
23 |
+
# Virtual Environment
|
24 |
+
venv/
|
25 |
+
env/
|
26 |
+
ENV/
|
27 |
+
.env
|
28 |
+
.venv
|
29 |
+
env.bak/
|
30 |
+
venv.bak/
|
31 |
+
|
32 |
+
# IDE specific files
|
33 |
+
.idea/
|
34 |
+
.vscode/
|
35 |
+
*.swp
|
36 |
+
*.swo
|
37 |
+
.DS_Store
|
38 |
+
|
39 |
+
# Jupyter Notebook
|
40 |
+
.ipynb_checkpoints
|
41 |
+
|
42 |
+
# Distribution / packaging
|
43 |
+
.Python
|
44 |
+
env/
|
45 |
+
build/
|
46 |
+
develop-eggs/
|
47 |
+
dist/
|
48 |
+
downloads/
|
49 |
+
eggs/
|
50 |
+
.eggs/
|
51 |
+
lib/
|
52 |
+
lib64/
|
53 |
+
parts/
|
54 |
+
sdist/
|
55 |
+
var/
|
56 |
+
wheels/
|
57 |
+
*.egg-info/
|
58 |
+
.installed.cfg
|
59 |
+
*.egg
|
60 |
+
|
61 |
+
# Unit test / coverage reports
|
62 |
+
htmlcov/
|
63 |
+
.tox/
|
64 |
+
.coverage
|
65 |
+
.coverage.*
|
66 |
+
.cache
|
67 |
+
nosetests.xml
|
68 |
+
coverage.xml
|
69 |
+
*.cover
|
70 |
+
.hypothesis/
|
71 |
+
|
72 |
+
# Streamlit
|
73 |
+
.streamlit/
|
74 |
+
|
75 |
+
# Logs
|
76 |
+
*.log
|
77 |
+
logs/
|
78 |
+
|
79 |
+
# Local development settings
|
80 |
+
.env.local
|
81 |
+
.env.development.local
|
82 |
+
.env.test.local
|
83 |
+
.env.production.local
|
84 |
+
|
85 |
+
# Misc
|
86 |
+
.DS_Store
|
87 |
+
Thumbs.db
|
README.md
CHANGED
@@ -12,3 +12,72 @@ short_description: An asset class comparator for my personal usecase
|
|
12 |
---
|
13 |
|
14 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
---
|
13 |
|
14 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
15 |
+
|
16 |
+
# Asset Class Performance Comparison
|
17 |
+
|
18 |
+
This Streamlit app allows you to compare the performance of different asset classes over time, including stocks, bonds, gold, and fixed deposits.
|
19 |
+
|
20 |
+
## Features
|
21 |
+
|
22 |
+
- Compare multiple asset classes simultaneously
|
23 |
+
- Choose between USD and SGD currencies
|
24 |
+
- Customize investment parameters (initial amount, time period)
|
25 |
+
- View performance graphs and annualized returns
|
26 |
+
- Support for major indices and individual stocks
|
27 |
+
|
28 |
+
## How to Use
|
29 |
+
|
30 |
+
1. Select your preferred currency (USD or SGD)
|
31 |
+
2. Enter your initial investment amount
|
32 |
+
3. Choose the time period for comparison
|
33 |
+
4. Select the assets you want to compare
|
34 |
+
5. View the performance graph and returns
|
35 |
+
|
36 |
+
## Assets Available
|
37 |
+
|
38 |
+
- Fixed Deposit
|
39 |
+
- Gold
|
40 |
+
- SGS Bonds
|
41 |
+
- US Treasury Bonds
|
42 |
+
- Major Indices (NASDAQ, S&P 500, Dow Jones)
|
43 |
+
- Individual Stocks (Microsoft, Google, Nvidia, Apple, etc.)
|
44 |
+
|
45 |
+
## Technical Details
|
46 |
+
|
47 |
+
The app uses:
|
48 |
+
- Streamlit for the web interface
|
49 |
+
- yfinance for market data
|
50 |
+
- Plotly for interactive graphs
|
51 |
+
- Pandas for data manipulation
|
52 |
+
|
53 |
+
## Hosting
|
54 |
+
|
55 |
+
This app is hosted on Hugging Face Spaces. You can access it at [link to be added after deployment].
|
56 |
+
|
57 |
+
## Local Development
|
58 |
+
|
59 |
+
To run locally:
|
60 |
+
1. Clone this repository
|
61 |
+
2. Install dependencies: `pip install -r requirements.txt`
|
62 |
+
3. Run the app: `streamlit run app.py`
|
63 |
+
|
64 |
+
## Project Structure
|
65 |
+
|
66 |
+
```
|
67 |
+
asset-class-comparison/
|
68 |
+
├── app.py # Main Streamlit application
|
69 |
+
├── requirements.txt # Python dependencies
|
70 |
+
├── README.md # This file
|
71 |
+
└── utils/ # Utility functions
|
72 |
+
├── yfinance_utils.py # yfinance data fetching utilities
|
73 |
+
├── currency_utils.py # Currency conversion utilities
|
74 |
+
└── fd_utils.py # Fixed deposit calculation utilities
|
75 |
+
```
|
76 |
+
|
77 |
+
## Contributing
|
78 |
+
|
79 |
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
80 |
+
|
81 |
+
## License
|
82 |
+
|
83 |
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
app.py
ADDED
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import pandas as pd
|
3 |
+
import yfinance as yf
|
4 |
+
import plotly.graph_objects as go
|
5 |
+
from datetime import datetime, timedelta
|
6 |
+
import numpy as np
|
7 |
+
|
8 |
+
# Import utility functions
|
9 |
+
from utils.yfinance_utils import fetch_yfinance_daily
|
10 |
+
from utils.currency_utils import get_usd_sgd_rate
|
11 |
+
from utils.fd_utils import calculate_fd_returns
|
12 |
+
|
13 |
+
# Set page config
|
14 |
+
st.set_page_config(page_title="Asset Class Comparison", layout="wide")
|
15 |
+
|
16 |
+
# Title and description
|
17 |
+
st.title("Asset Class Performance Comparison")
|
18 |
+
st.write("Compare the performance of different asset classes over time")
|
19 |
+
|
20 |
+
# Sidebar for user inputs
|
21 |
+
st.sidebar.header("Investment Parameters")
|
22 |
+
currency = st.sidebar.selectbox("Display Currency", ["USD", "SGD"], index=0)
|
23 |
+
initial_investment = st.sidebar.number_input(f"Initial Investment Amount ({currency})", min_value=1000, value=10000, step=1000)
|
24 |
+
start_date = st.sidebar.date_input("Start Date", value=datetime.now() - timedelta(days=365*10))
|
25 |
+
user_end_date = st.sidebar.date_input("End Date", value=datetime.now())
|
26 |
+
fd_rate = st.sidebar.number_input("Fixed Deposit Rate (%)", min_value=0.0, value=2.9, step=0.1) / 100
|
27 |
+
|
28 |
+
# Asset selection
|
29 |
+
selected_assets = st.sidebar.multiselect(
|
30 |
+
"Select Assets to Compare",
|
31 |
+
[
|
32 |
+
"Fixed Deposit",
|
33 |
+
"Gold",
|
34 |
+
"SGS Bonds",
|
35 |
+
"US Treasury Bonds",
|
36 |
+
"NASDAQ Composite",
|
37 |
+
"NASDAQ Large Cap",
|
38 |
+
"NASDAQ 100",
|
39 |
+
"S&P 500",
|
40 |
+
"Dow Jones",
|
41 |
+
"Microsoft",
|
42 |
+
"Google",
|
43 |
+
"Nvidia",
|
44 |
+
"Apple",
|
45 |
+
"Amazon",
|
46 |
+
"Tesla",
|
47 |
+
"Netflix",
|
48 |
+
"Meta",
|
49 |
+
],
|
50 |
+
default=["Fixed Deposit", "Microsoft", "Google", "Nvidia"]
|
51 |
+
)
|
52 |
+
|
53 |
+
# Today's date for reference
|
54 |
+
today = datetime.now().date()
|
55 |
+
|
56 |
+
usd_to_sgd = get_usd_sgd_rate() if currency == "SGD" else 1.0
|
57 |
+
currency_symbol = "$" if currency == "USD" else "S$"
|
58 |
+
|
59 |
+
# Create a dictionary of tickers for yfinance
|
60 |
+
tickers = {
|
61 |
+
"Gold": "GC=F",
|
62 |
+
"SGS Bonds": "^TNX",
|
63 |
+
"US Treasury Bonds": "^TNX",
|
64 |
+
"NASDAQ Composite": "^IXIC",
|
65 |
+
"NASDAQ Large Cap": "^NDX",
|
66 |
+
"NASDAQ 100": "^NDX",
|
67 |
+
"S&P 500": "^GSPC",
|
68 |
+
"Dow Jones": "^DJI",
|
69 |
+
"Microsoft": "MSFT",
|
70 |
+
"Google": "GOOGL",
|
71 |
+
"Nvidia": "NVDA",
|
72 |
+
"Apple": "AAPL",
|
73 |
+
"Amazon": "AMZN",
|
74 |
+
"Tesla": "TSLA",
|
75 |
+
"Netflix": "NFLX",
|
76 |
+
"Meta": "META",
|
77 |
+
}
|
78 |
+
|
79 |
+
# Determine the effective end date for each asset
|
80 |
+
asset_end_dates = {}
|
81 |
+
for asset in selected_assets:
|
82 |
+
if asset == "Fixed Deposit":
|
83 |
+
asset_end_dates[asset] = user_end_date
|
84 |
+
else:
|
85 |
+
if user_end_date > today:
|
86 |
+
asset_end_dates[asset] = today
|
87 |
+
else:
|
88 |
+
asset_end_dates[asset] = user_end_date
|
89 |
+
|
90 |
+
# Warn the user if a future end date is selected for market assets
|
91 |
+
if any(user_end_date > today and asset != "Fixed Deposit" for asset in selected_assets):
|
92 |
+
st.warning(f"Market data is only available up to today ({today}). For market assets, the end date has been set to today.")
|
93 |
+
|
94 |
+
# Calculate returns for each selected asset
|
95 |
+
asset_series = {}
|
96 |
+
failed_assets = []
|
97 |
+
actual_start_dates = {}
|
98 |
+
|
99 |
+
for asset in selected_assets:
|
100 |
+
asset_start = start_date
|
101 |
+
asset_end = asset_end_dates[asset]
|
102 |
+
if asset == "Fixed Deposit":
|
103 |
+
fd_index = pd.date_range(start=asset_start, end=user_end_date)
|
104 |
+
daily_rate = (1 + fd_rate) ** (1/365) - 1
|
105 |
+
fd_values = initial_investment * (1 + daily_rate) ** np.arange(len(fd_index))
|
106 |
+
if currency == "SGD":
|
107 |
+
fd_values = fd_values * usd_to_sgd
|
108 |
+
asset_series[asset] = pd.Series(fd_values, index=fd_index)
|
109 |
+
actual_start_dates[asset] = asset_start
|
110 |
+
else:
|
111 |
+
price_data = fetch_yfinance_daily(tickers[asset], asset_start, asset_end)
|
112 |
+
if price_data is not None and not price_data.empty:
|
113 |
+
price_data = price_data.sort_index()
|
114 |
+
actual_start = price_data.index[0]
|
115 |
+
actual_start_dates[asset] = actual_start
|
116 |
+
aligned_index = pd.date_range(start=actual_start, end=asset_end)
|
117 |
+
price_data = price_data.reindex(aligned_index)
|
118 |
+
price_data = price_data.ffill()
|
119 |
+
asset_values = initial_investment * (price_data / price_data.iloc[0])
|
120 |
+
if currency == "SGD":
|
121 |
+
asset_values = asset_values * usd_to_sgd
|
122 |
+
asset_series[asset] = asset_values
|
123 |
+
else:
|
124 |
+
failed_assets.append(asset)
|
125 |
+
|
126 |
+
# Combine all asset series into a single DataFrame
|
127 |
+
if asset_series:
|
128 |
+
returns_data = pd.DataFrame(asset_series)
|
129 |
+
else:
|
130 |
+
returns_data = pd.DataFrame()
|
131 |
+
|
132 |
+
# Remove failed assets from selected_assets (except FD)
|
133 |
+
selected_assets = [asset for asset in selected_assets if asset not in failed_assets or asset == "Fixed Deposit"]
|
134 |
+
|
135 |
+
if not selected_assets:
|
136 |
+
st.error("No assets could be loaded. Please try different assets.")
|
137 |
+
st.stop()
|
138 |
+
|
139 |
+
# Create the plot
|
140 |
+
fig = go.Figure()
|
141 |
+
|
142 |
+
for asset in selected_assets:
|
143 |
+
fig.add_trace(go.Scatter(
|
144 |
+
x=returns_data.index,
|
145 |
+
y=returns_data[asset],
|
146 |
+
name=asset,
|
147 |
+
mode='lines'
|
148 |
+
))
|
149 |
+
|
150 |
+
fig.update_layout(
|
151 |
+
title="Asset Performance Comparison",
|
152 |
+
xaxis_title="Date",
|
153 |
+
yaxis_title=f"Investment Value ({currency_symbol})",
|
154 |
+
hovermode="x unified",
|
155 |
+
height=600
|
156 |
+
)
|
157 |
+
|
158 |
+
# Display the plot
|
159 |
+
st.plotly_chart(fig, use_container_width=True)
|
160 |
+
|
161 |
+
# Calculate and display final returns
|
162 |
+
st.subheader("Final Investment Values")
|
163 |
+
for asset in selected_assets:
|
164 |
+
valid_series = returns_data[asset].dropna()
|
165 |
+
if not valid_series.empty:
|
166 |
+
final_value = valid_series.iloc[-1]
|
167 |
+
st.write(f"{asset}: {currency_symbol}{final_value:,.2f}")
|
168 |
+
else:
|
169 |
+
st.write(f"{asset}: Data unavailable")
|
170 |
+
|
171 |
+
# Calculate and display annualized returns
|
172 |
+
st.subheader("Annualized Returns")
|
173 |
+
for asset in selected_assets:
|
174 |
+
valid_series = returns_data[asset].dropna()
|
175 |
+
if len(valid_series) > 1:
|
176 |
+
actual_start = actual_start_dates[asset]
|
177 |
+
days = (valid_series.index[-1] - valid_series.index[0]).days
|
178 |
+
years = days / 365
|
179 |
+
final_value = valid_series.iloc[-1]
|
180 |
+
annualized_return = ((final_value / initial_investment) ** (1/years) - 1) * 100
|
181 |
+
if pd.Timestamp(actual_start).date() > start_date:
|
182 |
+
st.write(f"{asset}: {annualized_return:.2f}% (Data available from {actual_start.strftime('%Y-%m-%d')})")
|
183 |
+
else:
|
184 |
+
st.write(f"{asset}: {annualized_return:.2f}%")
|
185 |
+
else:
|
186 |
+
st.write(f"{asset}: N/A")
|
187 |
+
|
188 |
+
# Show warnings for data availability
|
189 |
+
for asset in selected_assets:
|
190 |
+
if asset in actual_start_dates and pd.Timestamp(actual_start_dates[asset]).date() > start_date:
|
191 |
+
st.warning(f"Data for {asset} is only available from {actual_start_dates[asset].strftime('%Y-%m-%d')}. The analysis starts from this date.")
|
192 |
+
|
193 |
+
# Show warning for failed assets
|
194 |
+
if failed_assets:
|
195 |
+
st.warning(f"Could not load data for the following assets: {', '.join(failed_assets)}")
|
notebooks/scratchpad.ipynb
ADDED
@@ -0,0 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"cells": [
|
3 |
+
{
|
4 |
+
"cell_type": "code",
|
5 |
+
"execution_count": 2,
|
6 |
+
"metadata": {},
|
7 |
+
"outputs": [],
|
8 |
+
"source": [
|
9 |
+
"import yfinance as yf"
|
10 |
+
]
|
11 |
+
},
|
12 |
+
{
|
13 |
+
"cell_type": "code",
|
14 |
+
"execution_count": 14,
|
15 |
+
"metadata": {},
|
16 |
+
"outputs": [
|
17 |
+
{
|
18 |
+
"name": "stderr",
|
19 |
+
"output_type": "stream",
|
20 |
+
"text": [
|
21 |
+
"[*********************100%***********************] 1 of 1 completed\n"
|
22 |
+
]
|
23 |
+
}
|
24 |
+
],
|
25 |
+
"source": [
|
26 |
+
"ticker = \"META\"\n",
|
27 |
+
"start_date = \"2010-01-01\"\n",
|
28 |
+
"end_date = \"2020-01-10\"\n",
|
29 |
+
"\n",
|
30 |
+
"data = yf.download(ticker, start=start_date, end=end_date) # [\"Close\"][ticker]"
|
31 |
+
]
|
32 |
+
},
|
33 |
+
{
|
34 |
+
"cell_type": "code",
|
35 |
+
"execution_count": 15,
|
36 |
+
"metadata": {},
|
37 |
+
"outputs": [
|
38 |
+
{
|
39 |
+
"data": {
|
40 |
+
"text/html": [
|
41 |
+
"<div>\n",
|
42 |
+
"<style scoped>\n",
|
43 |
+
" .dataframe tbody tr th:only-of-type {\n",
|
44 |
+
" vertical-align: middle;\n",
|
45 |
+
" }\n",
|
46 |
+
"\n",
|
47 |
+
" .dataframe tbody tr th {\n",
|
48 |
+
" vertical-align: top;\n",
|
49 |
+
" }\n",
|
50 |
+
"\n",
|
51 |
+
" .dataframe thead tr th {\n",
|
52 |
+
" text-align: left;\n",
|
53 |
+
" }\n",
|
54 |
+
"\n",
|
55 |
+
" .dataframe thead tr:last-of-type th {\n",
|
56 |
+
" text-align: right;\n",
|
57 |
+
" }\n",
|
58 |
+
"</style>\n",
|
59 |
+
"<table border=\"1\" class=\"dataframe\">\n",
|
60 |
+
" <thead>\n",
|
61 |
+
" <tr>\n",
|
62 |
+
" <th>Price</th>\n",
|
63 |
+
" <th>Close</th>\n",
|
64 |
+
" <th>High</th>\n",
|
65 |
+
" <th>Low</th>\n",
|
66 |
+
" <th>Open</th>\n",
|
67 |
+
" <th>Volume</th>\n",
|
68 |
+
" </tr>\n",
|
69 |
+
" <tr>\n",
|
70 |
+
" <th>Ticker</th>\n",
|
71 |
+
" <th>META</th>\n",
|
72 |
+
" <th>META</th>\n",
|
73 |
+
" <th>META</th>\n",
|
74 |
+
" <th>META</th>\n",
|
75 |
+
" <th>META</th>\n",
|
76 |
+
" </tr>\n",
|
77 |
+
" <tr>\n",
|
78 |
+
" <th>Date</th>\n",
|
79 |
+
" <th></th>\n",
|
80 |
+
" <th></th>\n",
|
81 |
+
" <th></th>\n",
|
82 |
+
" <th></th>\n",
|
83 |
+
" <th></th>\n",
|
84 |
+
" </tr>\n",
|
85 |
+
" </thead>\n",
|
86 |
+
" <tbody>\n",
|
87 |
+
" <tr>\n",
|
88 |
+
" <th>2012-05-18</th>\n",
|
89 |
+
" <td>38.050671</td>\n",
|
90 |
+
" <td>44.788914</td>\n",
|
91 |
+
" <td>37.821750</td>\n",
|
92 |
+
" <td>41.852752</td>\n",
|
93 |
+
" <td>573576400</td>\n",
|
94 |
+
" </tr>\n",
|
95 |
+
" <tr>\n",
|
96 |
+
" <th>2012-05-21</th>\n",
|
97 |
+
" <td>33.870365</td>\n",
|
98 |
+
" <td>36.488029</td>\n",
|
99 |
+
" <td>32.845198</td>\n",
|
100 |
+
" <td>36.358638</td>\n",
|
101 |
+
" <td>168192700</td>\n",
|
102 |
+
" </tr>\n",
|
103 |
+
" <tr>\n",
|
104 |
+
" <th>2012-05-22</th>\n",
|
105 |
+
" <td>30.854584</td>\n",
|
106 |
+
" <td>33.432435</td>\n",
|
107 |
+
" <td>30.794866</td>\n",
|
108 |
+
" <td>32.457032</td>\n",
|
109 |
+
" <td>101786600</td>\n",
|
110 |
+
" </tr>\n",
|
111 |
+
" <tr>\n",
|
112 |
+
" <th>2012-05-23</th>\n",
|
113 |
+
" <td>31.849892</td>\n",
|
114 |
+
" <td>32.347546</td>\n",
|
115 |
+
" <td>31.212894</td>\n",
|
116 |
+
" <td>31.222848</td>\n",
|
117 |
+
" <td>73600000</td>\n",
|
118 |
+
" </tr>\n",
|
119 |
+
" <tr>\n",
|
120 |
+
" <th>2012-05-24</th>\n",
|
121 |
+
" <td>32.875057</td>\n",
|
122 |
+
" <td>33.054213</td>\n",
|
123 |
+
" <td>31.620969</td>\n",
|
124 |
+
" <td>32.795434</td>\n",
|
125 |
+
" <td>50237200</td>\n",
|
126 |
+
" </tr>\n",
|
127 |
+
" <tr>\n",
|
128 |
+
" <th>...</th>\n",
|
129 |
+
" <td>...</td>\n",
|
130 |
+
" <td>...</td>\n",
|
131 |
+
" <td>...</td>\n",
|
132 |
+
" <td>...</td>\n",
|
133 |
+
" <td>...</td>\n",
|
134 |
+
" </tr>\n",
|
135 |
+
" <tr>\n",
|
136 |
+
" <th>2020-01-03</th>\n",
|
137 |
+
" <td>207.691162</td>\n",
|
138 |
+
" <td>209.413043</td>\n",
|
139 |
+
" <td>205.979229</td>\n",
|
140 |
+
" <td>206.238019</td>\n",
|
141 |
+
" <td>11188400</td>\n",
|
142 |
+
" </tr>\n",
|
143 |
+
" <tr>\n",
|
144 |
+
" <th>2020-01-06</th>\n",
|
145 |
+
" <td>211.602707</td>\n",
|
146 |
+
" <td>211.781855</td>\n",
|
147 |
+
" <td>205.551226</td>\n",
|
148 |
+
" <td>205.730374</td>\n",
|
149 |
+
" <td>17058900</td>\n",
|
150 |
+
" </tr>\n",
|
151 |
+
" <tr>\n",
|
152 |
+
" <th>2020-01-07</th>\n",
|
153 |
+
" <td>212.060547</td>\n",
|
154 |
+
" <td>213.573421</td>\n",
|
155 |
+
" <td>210.756694</td>\n",
|
156 |
+
" <td>211.821682</td>\n",
|
157 |
+
" <td>14912400</td>\n",
|
158 |
+
" </tr>\n",
|
159 |
+
" <tr>\n",
|
160 |
+
" <th>2020-01-08</th>\n",
|
161 |
+
" <td>214.210419</td>\n",
|
162 |
+
" <td>215.225638</td>\n",
|
163 |
+
" <td>211.612661</td>\n",
|
164 |
+
" <td>212.000831</td>\n",
|
165 |
+
" <td>13475000</td>\n",
|
166 |
+
" </tr>\n",
|
167 |
+
" <tr>\n",
|
168 |
+
" <th>2020-01-09</th>\n",
|
169 |
+
" <td>217.275970</td>\n",
|
170 |
+
" <td>217.355597</td>\n",
|
171 |
+
" <td>215.265442</td>\n",
|
172 |
+
" <td>216.519526</td>\n",
|
173 |
+
" <td>12642800</td>\n",
|
174 |
+
" </tr>\n",
|
175 |
+
" </tbody>\n",
|
176 |
+
"</table>\n",
|
177 |
+
"<p>1923 rows × 5 columns</p>\n",
|
178 |
+
"</div>"
|
179 |
+
],
|
180 |
+
"text/plain": [
|
181 |
+
"Price Close High Low Open Volume\n",
|
182 |
+
"Ticker META META META META META\n",
|
183 |
+
"Date \n",
|
184 |
+
"2012-05-18 38.050671 44.788914 37.821750 41.852752 573576400\n",
|
185 |
+
"2012-05-21 33.870365 36.488029 32.845198 36.358638 168192700\n",
|
186 |
+
"2012-05-22 30.854584 33.432435 30.794866 32.457032 101786600\n",
|
187 |
+
"2012-05-23 31.849892 32.347546 31.212894 31.222848 73600000\n",
|
188 |
+
"2012-05-24 32.875057 33.054213 31.620969 32.795434 50237200\n",
|
189 |
+
"... ... ... ... ... ...\n",
|
190 |
+
"2020-01-03 207.691162 209.413043 205.979229 206.238019 11188400\n",
|
191 |
+
"2020-01-06 211.602707 211.781855 205.551226 205.730374 17058900\n",
|
192 |
+
"2020-01-07 212.060547 213.573421 210.756694 211.821682 14912400\n",
|
193 |
+
"2020-01-08 214.210419 215.225638 211.612661 212.000831 13475000\n",
|
194 |
+
"2020-01-09 217.275970 217.355597 215.265442 216.519526 12642800\n",
|
195 |
+
"\n",
|
196 |
+
"[1923 rows x 5 columns]"
|
197 |
+
]
|
198 |
+
},
|
199 |
+
"execution_count": 15,
|
200 |
+
"metadata": {},
|
201 |
+
"output_type": "execute_result"
|
202 |
+
}
|
203 |
+
],
|
204 |
+
"source": [
|
205 |
+
"data"
|
206 |
+
]
|
207 |
+
},
|
208 |
+
{
|
209 |
+
"cell_type": "code",
|
210 |
+
"execution_count": null,
|
211 |
+
"metadata": {},
|
212 |
+
"outputs": [],
|
213 |
+
"source": []
|
214 |
+
}
|
215 |
+
],
|
216 |
+
"metadata": {
|
217 |
+
"kernelspec": {
|
218 |
+
"display_name": "return-on-investment",
|
219 |
+
"language": "python",
|
220 |
+
"name": "python3"
|
221 |
+
},
|
222 |
+
"language_info": {
|
223 |
+
"codemirror_mode": {
|
224 |
+
"name": "ipython",
|
225 |
+
"version": 3
|
226 |
+
},
|
227 |
+
"file_extension": ".py",
|
228 |
+
"mimetype": "text/x-python",
|
229 |
+
"name": "python",
|
230 |
+
"nbconvert_exporter": "python",
|
231 |
+
"pygments_lexer": "ipython3",
|
232 |
+
"version": "3.12.9"
|
233 |
+
}
|
234 |
+
},
|
235 |
+
"nbformat": 4,
|
236 |
+
"nbformat_minor": 2
|
237 |
+
}
|
requirements.txt
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
streamlit==1.32.0
|
2 |
+
pandas==2.2.1
|
3 |
+
yfinance==0.2.36
|
4 |
+
plotly==5.19.0
|
5 |
+
python-dotenv==1.0.1
|
6 |
+
numpy==1.26.4
|
tests/integration/test_yfinance_utils_integration.py
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import sys
|
2 |
+
import os
|
3 |
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
|
4 |
+
|
5 |
+
from utils import yfinance_utils
|
6 |
+
import pandas as pd
|
7 |
+
|
8 |
+
def test_fetch_yfinance_daily_real():
|
9 |
+
result = yfinance_utils.fetch_yfinance_daily('MSFT', '2023-01-01', '2023-01-10')
|
10 |
+
assert result is not None
|
11 |
+
assert not result.empty
|
12 |
+
assert isinstance(result, pd.Series)
|
tests/unit/test_currency_utils.py
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import sys
|
2 |
+
import os
|
3 |
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
|
4 |
+
|
5 |
+
import pytest
|
6 |
+
from unittest.mock import patch
|
7 |
+
from utils import currency_utils
|
8 |
+
|
9 |
+
def test_get_usd_sgd_rate_success():
|
10 |
+
with patch('yfinance.download') as mock_download:
|
11 |
+
import pandas as pd
|
12 |
+
df = pd.DataFrame({'Close': [1.3, 1.35]}, index=pd.date_range('2020-01-01', periods=2))
|
13 |
+
mock_download.return_value = df
|
14 |
+
rate = currency_utils.get_usd_sgd_rate()
|
15 |
+
assert rate == 1.35
|
16 |
+
|
17 |
+
def test_get_usd_sgd_rate_fail():
|
18 |
+
with patch('yfinance.download') as mock_download:
|
19 |
+
mock_download.side_effect = Exception('fail')
|
20 |
+
rate = currency_utils.get_usd_sgd_rate()
|
21 |
+
assert rate == 1.0
|
tests/unit/test_fd_utils.py
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import sys
|
2 |
+
import os
|
3 |
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
|
4 |
+
|
5 |
+
from utils import fd_utils
|
6 |
+
from datetime import date
|
7 |
+
|
8 |
+
def test_calculate_fd_returns():
|
9 |
+
result = fd_utils.calculate_fd_returns(1000, 0.05, date(2020, 1, 1), date(2021, 1, 1))
|
10 |
+
# 1 year at 5% interest
|
11 |
+
assert abs(result - 1050) < 1
|
tests/unit/test_yfinance_utils.py
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import sys
|
2 |
+
import os
|
3 |
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
|
4 |
+
|
5 |
+
import pytest
|
6 |
+
from unittest.mock import patch, MagicMock
|
7 |
+
from utils import yfinance_utils
|
8 |
+
import pandas as pd
|
9 |
+
|
10 |
+
def test_fetch_yfinance_daily_success():
|
11 |
+
with patch('yfinance.download') as mock_download:
|
12 |
+
df = pd.DataFrame({'Adj Close': [1, 2, 3]}, index=pd.date_range('2020-01-01', periods=3))
|
13 |
+
mock_download.return_value = df
|
14 |
+
result = yfinance_utils.fetch_yfinance_daily('MSFT', '2020-01-01', '2020-01-03')
|
15 |
+
assert isinstance(result, pd.Series)
|
16 |
+
assert not result.empty
|
17 |
+
|
18 |
+
def test_fetch_yfinance_daily_empty():
|
19 |
+
with patch('yfinance.download') as mock_download:
|
20 |
+
df = pd.DataFrame()
|
21 |
+
mock_download.return_value = df
|
22 |
+
result = yfinance_utils.fetch_yfinance_daily('MSFT', '2020-01-01', '2020-01-03')
|
23 |
+
assert result is None
|
utils/currency_utils.py
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import yfinance as yf
|
2 |
+
|
3 |
+
|
4 |
+
def get_usd_sgd_rate():
|
5 |
+
try:
|
6 |
+
fx = yf.download('USDSGD=X', period='5d', interval='1d')
|
7 |
+
if not fx.empty:
|
8 |
+
return float(fx['Close'][-1])
|
9 |
+
except Exception:
|
10 |
+
pass
|
11 |
+
return 1.0 # fallback to 1 if failed
|
utils/fd_utils.py
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
def calculate_fd_returns(initial_amount, rate, start_date, end_date):
|
2 |
+
days = (end_date - start_date).days
|
3 |
+
years = days / 365
|
4 |
+
return initial_amount * (1 + rate) ** years
|
utils/yfinance_utils.py
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import yfinance as yf
|
2 |
+
import pandas as pd
|
3 |
+
|
4 |
+
def fetch_yfinance_daily(ticker, start_date, end_date):
|
5 |
+
try:
|
6 |
+
data = yf.download(ticker, start=start_date, end=end_date)
|
7 |
+
if data.empty:
|
8 |
+
print(f"No data found for {ticker} between {start_date} and {end_date}")
|
9 |
+
return None
|
10 |
+
print("data type returned:", type(data['Close']))
|
11 |
+
return data['Close'][ticker]
|
12 |
+
except Exception:
|
13 |
+
return None
|
14 |
+
|
15 |
+
if __name__ == "__main__":
|
16 |
+
print(fetch_yfinance_daily("MSFT", "2020-01-01", "2020-01-10"))
|