Create app.py
Browse files
app.py
ADDED
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import yfinance as yf
|
3 |
+
import pandas as pd
|
4 |
+
import matplotlib.pyplot as plt
|
5 |
+
from datetime import datetime, timedelta
|
6 |
+
|
7 |
+
def fetch_stock_data(symbol, start_date, end_date):
|
8 |
+
return yf.download(symbol, start=start_date, end=end_date)
|
9 |
+
|
10 |
+
def backtest_mixed_investment(data, start_date, end_date, monthly_investment, stock_allocation, savings_rate):
|
11 |
+
investment_dates = []
|
12 |
+
stock_shares = 0
|
13 |
+
savings_balance = 0
|
14 |
+
total_invested = 0
|
15 |
+
stock_value = []
|
16 |
+
savings_value = []
|
17 |
+
|
18 |
+
stock_investment = monthly_investment * stock_allocation
|
19 |
+
savings_investment = monthly_investment * (1 - stock_allocation)
|
20 |
+
daily_rate = (1 + savings_rate) ** (1/365) - 1
|
21 |
+
|
22 |
+
current_date = pd.to_datetime(start_date)
|
23 |
+
prev_date = current_date
|
24 |
+
while current_date <= pd.to_datetime(end_date):
|
25 |
+
if current_date in data.index:
|
26 |
+
price = data.loc[current_date, 'Adj Close']
|
27 |
+
new_shares = stock_investment / price
|
28 |
+
stock_shares += new_shares
|
29 |
+
|
30 |
+
days_passed = (current_date - prev_date).days
|
31 |
+
savings_balance *= (1 + daily_rate) ** days_passed
|
32 |
+
savings_balance += savings_investment
|
33 |
+
|
34 |
+
total_invested += monthly_investment
|
35 |
+
|
36 |
+
investment_dates.append(current_date)
|
37 |
+
stock_value.append(stock_shares * price)
|
38 |
+
savings_value.append(savings_balance)
|
39 |
+
|
40 |
+
prev_date = current_date
|
41 |
+
|
42 |
+
current_date += pd.DateOffset(months=1)
|
43 |
+
|
44 |
+
stock_value_series = pd.Series(stock_value, index=investment_dates).reindex(data.index, method='ffill')
|
45 |
+
savings_value_series = pd.Series(savings_value, index=investment_dates).reindex(data.index, method='ffill')
|
46 |
+
portfolio_value = stock_value_series + savings_value_series
|
47 |
+
|
48 |
+
return portfolio_value, stock_value_series, savings_value_series, total_invested
|
49 |
+
|
50 |
+
def backtest_stock_only(data, start_date, end_date, monthly_investment):
|
51 |
+
investment_dates = []
|
52 |
+
total_shares = 0
|
53 |
+
total_invested = 0
|
54 |
+
|
55 |
+
current_date = pd.to_datetime(start_date)
|
56 |
+
while current_date <= pd.to_datetime(end_date):
|
57 |
+
if current_date in data.index:
|
58 |
+
price = data.loc[current_date, 'Adj Close']
|
59 |
+
shares = monthly_investment / price
|
60 |
+
total_shares += shares
|
61 |
+
total_invested += monthly_investment
|
62 |
+
|
63 |
+
investment_dates.append(current_date)
|
64 |
+
|
65 |
+
current_date += pd.DateOffset(months=1)
|
66 |
+
|
67 |
+
total_shares_series = pd.Series([total_shares] * len(investment_dates), index=investment_dates).reindex(data.index, method='ffill')
|
68 |
+
portfolio_value = data['Adj Close'] * total_shares_series
|
69 |
+
|
70 |
+
return portfolio_value, pd.Series([total_invested] * len(data.index), index=data.index), total_invested
|
71 |
+
|
72 |
+
def plot_results(portfolio_value, stock_value, savings_value, total_invested, symbol, start_date, end_date, stock_only=False):
|
73 |
+
fig, ax = plt.subplots(figsize=(12, 6))
|
74 |
+
ax.plot(portfolio_value.index, portfolio_value, label='Portfolio Value')
|
75 |
+
if not stock_only:
|
76 |
+
ax.plot(stock_value.index, stock_value, label='Stock Investment Value')
|
77 |
+
ax.plot(savings_value.index, savings_value, label='Savings Account Value')
|
78 |
+
ax.plot(portfolio_value.index, total_invested, label='Total Cash Invested', linestyle='--')
|
79 |
+
|
80 |
+
ax.set_title(f'Monthly Investment Analysis: {symbol} ({start_date} to {end_date})')
|
81 |
+
ax.set_xlabel('Date')
|
82 |
+
ax.set_ylabel('Value ($)')
|
83 |
+
ax.legend()
|
84 |
+
ax.grid(True)
|
85 |
+
|
86 |
+
total_return = (portfolio_value[-1] - total_invested[-1]) / total_invested[-1] * 100
|
87 |
+
|
88 |
+
ax.annotate(f'Total Return: {total_return:.2f}%', xy=(0.05, 0.95), xycoords='axes fraction', fontsize=10, ha='left', va='top')
|
89 |
+
ax.annotate(f'Final Portfolio Value: ${portfolio_value[-1]:.2f}', xy=(0.05, 0.90), xycoords='axes fraction', fontsize=10, ha='left', va='top')
|
90 |
+
if not stock_only:
|
91 |
+
ax.annotate(f'Final Stock Value: ${stock_value[-1]:.2f}', xy=(0.05, 0.85), xycoords='axes fraction', fontsize=10, ha='left', va='top')
|
92 |
+
ax.annotate(f'Final Savings Value: ${savings_value[-1]:.2f}', xy=(0.05, 0.80), xycoords='axes fraction', fontsize=10, ha='left', va='top')
|
93 |
+
ax.annotate(f'Total Invested: ${total_invested[-1]:.2f}', xy=(0.05, 0.75), xycoords='axes fraction', fontsize=10, ha='left', va='top')
|
94 |
+
|
95 |
+
return fig
|
96 |
+
|
97 |
+
def main():
|
98 |
+
st.title("InvestSim: Stock & Savings Portfolio Analyzer")
|
99 |
+
st.write("Simulate and analyze your investment strategy with stocks and high-yield savings accounts.")
|
100 |
+
|
101 |
+
# Sidebar inputs
|
102 |
+
st.sidebar.header('Input Parameters')
|
103 |
+
symbol = st.sidebar.text_input('Stock Symbol', 'AAPL')
|
104 |
+
start_date = st.sidebar.date_input('Start Date', datetime(2000, 1, 1))
|
105 |
+
end_date = st.sidebar.date_input('End Date', datetime.now())
|
106 |
+
monthly_investment = st.sidebar.number_input('Monthly Investment ($)', min_value=1, value=100)
|
107 |
+
investment_type = st.sidebar.radio("Investment Type", ("Stock Only", "Mixed (Stock + Savings)"))
|
108 |
+
|
109 |
+
if investment_type == "Mixed (Stock + Savings)":
|
110 |
+
stock_allocation = st.sidebar.slider('Stock Allocation (%)', 0, 100, 60) / 100
|
111 |
+
savings_rate = st.sidebar.number_input('HYSA Annual Interest Rate (%)', min_value=0.0, max_value=20.0, value=4.5) / 100
|
112 |
+
else:
|
113 |
+
stock_allocation = 1.0
|
114 |
+
savings_rate = 0.0
|
115 |
+
|
116 |
+
if st.sidebar.button('Run Analysis'):
|
117 |
+
# Fetch stock data
|
118 |
+
data = fetch_stock_data(symbol, start_date, end_date)
|
119 |
+
|
120 |
+
if data.empty:
|
121 |
+
st.error(f"No data available for {symbol}. Please check the stock symbol and date range.")
|
122 |
+
return
|
123 |
+
|
124 |
+
# Run backtest
|
125 |
+
if investment_type == "Mixed (Stock + Savings)":
|
126 |
+
portfolio_value, stock_value, savings_value, total_invested = backtest_mixed_investment(
|
127 |
+
data, start_date, end_date, monthly_investment, stock_allocation, savings_rate
|
128 |
+
)
|
129 |
+
else:
|
130 |
+
portfolio_value, total_invested_series, total_invested = backtest_stock_only(
|
131 |
+
data, start_date, end_date, monthly_investment
|
132 |
+
)
|
133 |
+
stock_value = portfolio_value
|
134 |
+
savings_value = pd.Series([0] * len(portfolio_value), index=portfolio_value.index)
|
135 |
+
|
136 |
+
# Plot results
|
137 |
+
fig = plot_results(portfolio_value, stock_value, savings_value, total_invested_series if investment_type == "Stock Only" else pd.Series([total_invested] * len(portfolio_value), index=portfolio_value.index), symbol, start_date, end_date, stock_only=(investment_type == "Stock Only"))
|
138 |
+
st.pyplot(fig)
|
139 |
+
|
140 |
+
# Display summary statistics
|
141 |
+
st.subheader('Investment Summary')
|
142 |
+
st.write(f"Total Return: {((portfolio_value[-1] - total_invested) / total_invested * 100):.2f}%")
|
143 |
+
st.write(f"Final Portfolio Value: ${portfolio_value[-1]:.2f}")
|
144 |
+
if investment_type == "Mixed (Stock + Savings)":
|
145 |
+
st.write(f"Final Stock Value: ${stock_value[-1]:.2f}")
|
146 |
+
st.write(f"Final Savings Value: ${savings_value[-1]:.2f}")
|
147 |
+
st.write(f"Total Invested: ${total_invested:.2f}")
|
148 |
+
|
149 |
+
if __name__ == '__main__':
|
150 |
+
main()
|