Spaces:
Sleeping
Sleeping
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"))
|