Spaces:
Sleeping
Sleeping
Merge pull request #2 from Carsten134/refactoring
Browse files- .gitignore +6 -0
- main.py +7 -244
- utilities/py/__pycache__/plots.cpython-311.pyc +0 -0
- utilities/py/__pycache__/styling.cpython-311.pyc +0 -0
- utilities/py/__pycache__/summary_tables.cpython-311.pyc +0 -0
- utilities/py/composer.py +140 -0
- utilities/py/data_management.py +151 -0
- utilities/py/ui_elements.py +75 -0
.gitignore
CHANGED
@@ -6,3 +6,9 @@
|
|
6 |
*.idea
|
7 |
*app.py
|
8 |
*.env
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
*.idea
|
7 |
*app.py
|
8 |
*.env
|
9 |
+
|
10 |
+
# VSCode settings
|
11 |
+
.vscode
|
12 |
+
|
13 |
+
# Pycache
|
14 |
+
*/__pycache__
|
main.py
CHANGED
@@ -1,255 +1,18 @@
|
|
1 |
-
import yfinance as yf
|
2 |
-
import numpy as np
|
3 |
import pandas as pd
|
4 |
|
5 |
-
import streamlit as st
|
6 |
-
|
7 |
from utilities.py.styling import streamlit_style
|
8 |
-
from utilities.py import
|
9 |
-
from utilities.py import summary_tables
|
10 |
-
# from utilities.py import mongodb
|
11 |
-
|
12 |
-
from pypfopt import EfficientFrontier
|
13 |
-
from pypfopt import risk_models
|
14 |
-
from pypfopt import expected_returns
|
15 |
-
from pypfopt import HRPOpt, hierarchical_portfolio
|
16 |
|
17 |
-
import plotly.express as px
|
18 |
-
import plotly.graph_objects as go
|
19 |
|
20 |
streamlit_style()
|
21 |
-
|
22 |
company_list_df = pd.read_csv("utilities/data/Company List.csv")
|
23 |
|
24 |
-
|
25 |
-
company_symbol = (company_list_df["Ticker"] + ".NS").to_list()
|
26 |
-
|
27 |
-
name_to_symbol_dict = dict()
|
28 |
-
symbol_to_name_dict = dict()
|
29 |
-
|
30 |
-
for CSymbol, CName in zip(company_symbol, company_name):
|
31 |
-
name_to_symbol_dict[CName] = CSymbol
|
32 |
-
|
33 |
-
for CSymbol, CName in zip(company_symbol, company_name):
|
34 |
-
symbol_to_name_dict[CSymbol] = CName
|
35 |
-
|
36 |
-
streamlit_company_list_input = st.multiselect(
|
37 |
-
"Select Multiple Companies", company_name, default=None
|
38 |
-
)
|
39 |
-
|
40 |
-
optimisation_method = st.selectbox(
|
41 |
-
"Choose an optimization method accordingly",
|
42 |
-
(
|
43 |
-
"Efficient Frontier",
|
44 |
-
"Hierarchical Risk Parity",
|
45 |
-
),
|
46 |
-
)
|
47 |
-
|
48 |
-
parameter_for_optimisation = 0
|
49 |
-
if optimisation_method == "Efficient Frontier":
|
50 |
-
parameter_for_optimisation = st.selectbox(
|
51 |
-
"Choose an optimization parameter accordingly",
|
52 |
-
(
|
53 |
-
"Maximum Sharpe Ratio",
|
54 |
-
"Efficient Risk",
|
55 |
-
"Minimum Volatility",
|
56 |
-
"Efficient Return",
|
57 |
-
),
|
58 |
-
)
|
59 |
-
|
60 |
-
company_name_to_symbol = [name_to_symbol_dict[i] for i in streamlit_company_list_input]
|
61 |
-
|
62 |
-
number_of_symbols = len(company_name_to_symbol)
|
63 |
-
|
64 |
-
start_date = st.date_input(
|
65 |
-
"Start Date",
|
66 |
-
format="YYYY-MM-DD",
|
67 |
-
value=pd.Timestamp("1947-08-15"),
|
68 |
-
max_value=pd.Timestamp.now(),
|
69 |
-
)
|
70 |
-
|
71 |
-
initial_investment = st.number_input("How much would you want to invest?", value=45000)
|
72 |
-
|
73 |
-
if number_of_symbols > 1:
|
74 |
-
company_data = pd.DataFrame()
|
75 |
-
|
76 |
-
for cname in company_name_to_symbol:
|
77 |
-
stock_data_temp = yf.download(
|
78 |
-
cname, start=start_date, end=pd.Timestamp.now().strftime("%Y-%m-%d")
|
79 |
-
)["Adj Close"]
|
80 |
-
stock_data_temp.name = cname
|
81 |
-
company_data = pd.merge(
|
82 |
-
company_data,
|
83 |
-
stock_data_temp,
|
84 |
-
how="outer",
|
85 |
-
right_index=True,
|
86 |
-
left_index=True,
|
87 |
-
)
|
88 |
-
|
89 |
-
company_data.dropna(axis=1, how="all", inplace=True)
|
90 |
-
|
91 |
-
company_data.dropna(inplace=True)
|
92 |
-
|
93 |
-
for i in company_data.columns:
|
94 |
-
company_data[i] = company_data[i].abs()
|
95 |
-
|
96 |
-
st.write(
|
97 |
-
f"Note: Due to unavailability of full data, this Analysis uses data from the date: {company_data.index[0]}"
|
98 |
-
)
|
99 |
-
|
100 |
-
number_of_symbols = len(company_data.columns)
|
101 |
-
|
102 |
-
st.dataframe(company_data, use_container_width=True)
|
103 |
-
|
104 |
-
if number_of_symbols > 1:
|
105 |
-
company_stock_returns_data = company_data.pct_change().dropna()
|
106 |
-
|
107 |
-
mu = 0
|
108 |
-
S = 0
|
109 |
-
ef = 0
|
110 |
-
company_asset_weights = 0
|
111 |
-
|
112 |
-
if optimisation_method == "Efficient Frontier":
|
113 |
-
mu = expected_returns.mean_historical_return(company_data)
|
114 |
-
S = risk_models.sample_cov(company_data)
|
115 |
-
|
116 |
-
ef = EfficientFrontier(mu, S)
|
117 |
-
|
118 |
-
if parameter_for_optimisation == "Maximum Sharpe Raio":
|
119 |
-
ef.max_sharpe()
|
120 |
-
elif parameter_for_optimisation == "Minimum Volatility":
|
121 |
-
ef.min_volatility()
|
122 |
-
elif parameter_for_optimisation == "Efficient Risk":
|
123 |
-
ef.efficient_risk(0.5)
|
124 |
-
else:
|
125 |
-
ef.efficient_return(0.05)
|
126 |
-
|
127 |
-
company_asset_weights = pd.DataFrame.from_dict(
|
128 |
-
ef.clean_weights(), orient="index"
|
129 |
-
).reset_index()
|
130 |
-
elif optimisation_method == "Hierarchical Risk Parity":
|
131 |
-
mu = expected_returns.returns_from_prices(company_data)
|
132 |
-
S = risk_models.sample_cov(company_data)
|
133 |
-
|
134 |
-
ef = HRPOpt(mu, S)
|
135 |
-
|
136 |
-
company_asset_weights = ef.optimize()
|
137 |
-
company_asset_weights = pd.DataFrame.from_dict(
|
138 |
-
company_asset_weights, orient="index", columns=["Weight"]
|
139 |
-
).reset_index()
|
140 |
-
|
141 |
-
company_asset_weights.columns = ["Ticker", "Allocation"]
|
142 |
-
|
143 |
-
company_asset_weights_copy = company_asset_weights
|
144 |
-
|
145 |
-
company_asset_weights["Name"] = [
|
146 |
-
symbol_to_name_dict[i] for i in company_asset_weights["Ticker"]
|
147 |
-
]
|
148 |
-
|
149 |
-
company_asset_weights = company_asset_weights[["Name", "Ticker", "Allocation"]]
|
150 |
-
|
151 |
-
st.dataframe(company_asset_weights, use_container_width=True)
|
152 |
-
|
153 |
-
ef.portfolio_performance()
|
154 |
-
|
155 |
-
(
|
156 |
-
expected_annual_return,
|
157 |
-
annual_volatility,
|
158 |
-
sharpe_ratio,
|
159 |
-
) = ef.portfolio_performance()
|
160 |
-
|
161 |
-
st_portfolio_performance = pd.DataFrame.from_dict(
|
162 |
-
{
|
163 |
-
"Expected annual return": (expected_annual_return * 100).round(2),
|
164 |
-
"Annual volatility": (annual_volatility * 100).round(2),
|
165 |
-
"Sharpe ratio": sharpe_ratio.round(2),
|
166 |
-
},
|
167 |
-
orient="index",
|
168 |
-
).reset_index()
|
169 |
-
|
170 |
-
st_portfolio_performance.columns = ["Metrics", "Summary"]
|
171 |
-
|
172 |
-
if optimisation_method == "Efficient Frontier":
|
173 |
-
st.write(
|
174 |
-
"Optimization Method - ",
|
175 |
-
optimisation_method,
|
176 |
-
"---- Parameter - ",
|
177 |
-
parameter_for_optimisation,
|
178 |
-
)
|
179 |
-
else:
|
180 |
-
st.write("Optimization Method - ", optimisation_method)
|
181 |
-
|
182 |
-
st.dataframe(st_portfolio_performance, use_container_width=True)
|
183 |
-
|
184 |
-
plots.pie_chart_company_asset_weights(company_asset_weights)
|
185 |
-
|
186 |
-
portfolio_returns = (
|
187 |
-
company_stock_returns_data * list(ef.clean_weights().values())
|
188 |
-
).sum(axis=1)
|
189 |
-
|
190 |
-
annual_portfolio_returns = portfolio_returns.resample("Y").apply(
|
191 |
-
lambda x: (x + 1).prod() - 1
|
192 |
-
)
|
193 |
-
|
194 |
-
cumulative_returns = (portfolio_returns + 1).cumprod() * initial_investment
|
195 |
-
|
196 |
-
tab1, tab2, tab3 = st.tabs(["Plots", "Annual Returns", "Montly Returns"])
|
197 |
-
|
198 |
-
with tab1:
|
199 |
-
plots.plot_annual_returns(annual_portfolio_returns)
|
200 |
-
plots.plot_cummulative_returns(cumulative_returns)
|
201 |
-
|
202 |
-
with tab2:
|
203 |
-
annual_portfolio_returns = summary_tables.annual_returns_dataframe(
|
204 |
-
annual_portfolio_returns
|
205 |
-
)
|
206 |
-
annual_cumulative_returns = (
|
207 |
-
summary_tables.annual_cumulative_returns_dataframe(cumulative_returns)
|
208 |
-
)
|
209 |
-
annual_stock_returns = summary_tables.company_wise_annual_return(
|
210 |
-
company_stock_returns_data, company_asset_weights
|
211 |
-
)
|
212 |
-
|
213 |
-
merged_annual_returns_data = pd.merge(
|
214 |
-
annual_portfolio_returns,
|
215 |
-
annual_cumulative_returns,
|
216 |
-
on="Year",
|
217 |
-
suffixes=("_portfolio", "_cumulative"),
|
218 |
-
)
|
219 |
-
|
220 |
-
merged_annual_returns_data = pd.merge(
|
221 |
-
merged_annual_returns_data, annual_stock_returns, on="Year"
|
222 |
-
)
|
223 |
-
|
224 |
-
st.write("Annual Returns")
|
225 |
-
st.dataframe(merged_annual_returns_data, use_container_width=True)
|
226 |
-
|
227 |
-
with tab3:
|
228 |
-
monthly_portfolio_return = summary_tables.monthly_returns_dataframe(
|
229 |
-
portfolio_returns
|
230 |
-
)
|
231 |
-
monthly_stock_return = summary_tables.company_wise_monthly_return(
|
232 |
-
company_stock_returns_data, company_asset_weights
|
233 |
-
)
|
234 |
-
monthly_cumulative_returns = (
|
235 |
-
summary_tables.monthly_cumulative_returns_dataframe(cumulative_returns)
|
236 |
-
)
|
237 |
-
|
238 |
-
merged_monthly_returns_data = pd.merge(
|
239 |
-
monthly_portfolio_return,
|
240 |
-
monthly_cumulative_returns,
|
241 |
-
on=["Year", "Month"],
|
242 |
-
how="inner",
|
243 |
-
)
|
244 |
|
245 |
-
|
246 |
-
merged_monthly_returns_data,
|
247 |
-
monthly_stock_return,
|
248 |
-
on=["Year", "Month"],
|
249 |
-
how="inner",
|
250 |
-
)
|
251 |
|
252 |
-
|
253 |
-
st.dataframe(merged_monthly_returns_data, use_container_width=True)
|
254 |
|
255 |
-
|
|
|
|
|
|
|
|
1 |
import pandas as pd
|
2 |
|
|
|
|
|
3 |
from utilities.py.styling import streamlit_style
|
4 |
+
from utilities.py.composer import CapiPortApp
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
|
|
|
|
|
6 |
|
7 |
streamlit_style()
|
8 |
+
# data import
|
9 |
company_list_df = pd.read_csv("utilities/data/Company List.csv")
|
10 |
|
11 |
+
capi_port = CapiPortApp(company_list_df)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
|
13 |
+
capi_port.render_user_input()
|
|
|
|
|
|
|
|
|
|
|
14 |
|
15 |
+
ready_to_render_results = len(capi_port.user_input.get_selected_comp_ids()) > 1
|
|
|
16 |
|
17 |
+
if ready_to_render_results:
|
18 |
+
capi_port.render_results()
|
utilities/py/__pycache__/plots.cpython-311.pyc
ADDED
Binary file (3.54 kB). View file
|
|
utilities/py/__pycache__/styling.cpython-311.pyc
ADDED
Binary file (1.69 kB). View file
|
|
utilities/py/__pycache__/summary_tables.cpython-311.pyc
ADDED
Binary file (6.03 kB). View file
|
|
utilities/py/composer.py
ADDED
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pandas as pd
|
2 |
+
import streamlit as st
|
3 |
+
|
4 |
+
from utilities.py.data_management import CompData, PortfolioOptimizer
|
5 |
+
from utilities.py.ui_elements import UserInput
|
6 |
+
from utilities.py import plots, summary_tables
|
7 |
+
|
8 |
+
|
9 |
+
class CapiPortApp:
|
10 |
+
def __init__(self, company_df):
|
11 |
+
"""
|
12 |
+
Class for composing the apps UI on a high level. Is meant to provide a readable overview, of what happens inside the app, without being concerned with unnecessary details.
|
13 |
+
|
14 |
+
Uppon initializing the composer fetches the data within the apps source code (for now) and let's a dedicated class handle the raw data.
|
15 |
+
"""
|
16 |
+
self.comp_data = CompData(company_df)
|
17 |
+
|
18 |
+
# get all the necessary ui elements
|
19 |
+
self.user_input = UserInput(self.comp_data)
|
20 |
+
|
21 |
+
def render_user_input(self):
|
22 |
+
self.user_input.company_selection()
|
23 |
+
self.user_input.opt_method_selection()
|
24 |
+
self.user_input.start_date()
|
25 |
+
self.user_input.initial_investment()
|
26 |
+
|
27 |
+
|
28 |
+
def render_results(self):
|
29 |
+
"""
|
30 |
+
CAUTION: the composer assumes, that the user has given all the necessary data.
|
31 |
+
|
32 |
+
NECESSARY DATA:
|
33 |
+
- at least two companies
|
34 |
+
- an optimization method
|
35 |
+
- a start date
|
36 |
+
- initial investment
|
37 |
+
|
38 |
+
Check the conditions the necessary conditions beforehand :)
|
39 |
+
"""
|
40 |
+
# fetch user input
|
41 |
+
user_input_data = self.user_input.get_user_input_data()
|
42 |
+
|
43 |
+
# optimize the chosen portfolio according to the specifications
|
44 |
+
portfolio_opt = PortfolioOptimizer(self.comp_data,
|
45 |
+
self.user_input.get_selected_comp_ids(),
|
46 |
+
user_input_data.start_date)
|
47 |
+
|
48 |
+
company_asset_weights = portfolio_opt.optimize(user_input_data.opt_method,
|
49 |
+
user_input_data.ef_parameter)
|
50 |
+
|
51 |
+
# show first the stock data...
|
52 |
+
st.dataframe(portfolio_opt.stock_data, use_container_width=True)
|
53 |
+
|
54 |
+
# print disclaimer
|
55 |
+
first_date_available = portfolio_opt.stock_data.index[0]
|
56 |
+
opt_header = f"Used {user_input_data.opt_method}" if user_input_data.ef_parameter is None else f"Used {user_input_data.opt_method} with {user_input_data.ef_parameter}"
|
57 |
+
|
58 |
+
|
59 |
+
st.write(
|
60 |
+
f"Note: Due to unavailability of full data, this Analysis uses data from the date: {first_date_available}")
|
61 |
+
|
62 |
+
st.write(opt_header)
|
63 |
+
|
64 |
+
# show asset weights, portfolio performance and the pie chart
|
65 |
+
st.dataframe(company_asset_weights, use_container_width=True)
|
66 |
+
|
67 |
+
st.dataframe(portfolio_opt.get_portfolio_performance(),
|
68 |
+
use_container_width=True)
|
69 |
+
|
70 |
+
plots.pie_chart_company_asset_weights(company_asset_weights)
|
71 |
+
|
72 |
+
# summarize the resulting data
|
73 |
+
portfolio_returns = portfolio_opt.get_portfolio_returns()
|
74 |
+
annual_portfolio_returns = portfolio_opt.get_annual_portfolio_returns()
|
75 |
+
|
76 |
+
cumulative_returns = (portfolio_returns +
|
77 |
+
1).cumprod() * user_input_data.init_invest
|
78 |
+
|
79 |
+
# render the tabs
|
80 |
+
tab1, tab2, tab3 = st.tabs(
|
81 |
+
["Plots", "Annual Returns", "Montly Returns"])
|
82 |
+
|
83 |
+
with tab1:
|
84 |
+
plots.plot_annual_returns(annual_portfolio_returns)
|
85 |
+
plots.plot_cummulative_returns(cumulative_returns)
|
86 |
+
|
87 |
+
with tab2:
|
88 |
+
annual_portfolio_returns = summary_tables.annual_returns_dataframe(
|
89 |
+
annual_portfolio_returns
|
90 |
+
)
|
91 |
+
annual_cumulative_returns = (
|
92 |
+
summary_tables.annual_cumulative_returns_dataframe(
|
93 |
+
cumulative_returns)
|
94 |
+
)
|
95 |
+
annual_stock_returns = summary_tables.company_wise_annual_return(
|
96 |
+
portfolio_opt.stock_data_returns, company_asset_weights
|
97 |
+
)
|
98 |
+
|
99 |
+
merged_annual_returns_data = pd.merge(
|
100 |
+
annual_portfolio_returns,
|
101 |
+
annual_cumulative_returns,
|
102 |
+
on="Year",
|
103 |
+
suffixes=("_portfolio", "_cumulative"),
|
104 |
+
)
|
105 |
+
|
106 |
+
merged_annual_returns_data = pd.merge(
|
107 |
+
merged_annual_returns_data, annual_stock_returns, on="Year"
|
108 |
+
)
|
109 |
+
|
110 |
+
st.write("Annual Returns")
|
111 |
+
st.dataframe(merged_annual_returns_data, use_container_width=True)
|
112 |
+
|
113 |
+
with tab3:
|
114 |
+
monthly_portfolio_return = summary_tables.monthly_returns_dataframe(
|
115 |
+
portfolio_returns
|
116 |
+
)
|
117 |
+
monthly_stock_return = summary_tables.company_wise_monthly_return(
|
118 |
+
portfolio_opt.stock_data_returns, company_asset_weights
|
119 |
+
)
|
120 |
+
monthly_cumulative_returns = (
|
121 |
+
summary_tables.monthly_cumulative_returns_dataframe(
|
122 |
+
cumulative_returns)
|
123 |
+
)
|
124 |
+
|
125 |
+
merged_monthly_returns_data = pd.merge(
|
126 |
+
monthly_portfolio_return,
|
127 |
+
monthly_cumulative_returns,
|
128 |
+
on=["Year", "Month"],
|
129 |
+
how="inner",
|
130 |
+
)
|
131 |
+
|
132 |
+
merged_monthly_returns_data = pd.merge(
|
133 |
+
merged_monthly_returns_data,
|
134 |
+
monthly_stock_return,
|
135 |
+
on=["Year", "Month"],
|
136 |
+
how="inner",
|
137 |
+
)
|
138 |
+
|
139 |
+
st.write("Montly Return")
|
140 |
+
st.dataframe(merged_monthly_returns_data, use_container_width=True)
|
utilities/py/data_management.py
ADDED
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pandas as pd
|
2 |
+
import yfinance as yf
|
3 |
+
|
4 |
+
from pypfopt import EfficientFrontier
|
5 |
+
from pypfopt import risk_models
|
6 |
+
from pypfopt import expected_returns
|
7 |
+
from pypfopt import HRPOpt, hierarchical_portfolio
|
8 |
+
|
9 |
+
|
10 |
+
class CompData:
|
11 |
+
def __init__(self, company_data):
|
12 |
+
"""
|
13 |
+
Class that manages company and stock data
|
14 |
+
"""
|
15 |
+
self.df = company_data
|
16 |
+
self.company_names = self.df["Name"].to_list()
|
17 |
+
self.company_symbols = (self.df["Ticker"] + ".NS").to_list()
|
18 |
+
|
19 |
+
# utilities for tranlation
|
20 |
+
name_to_id_dict = dict()
|
21 |
+
id_to_name_dict = dict()
|
22 |
+
|
23 |
+
for CSymbol, CName in zip(self.company_symbols, self.company_names):
|
24 |
+
name_to_id_dict[CName] = CSymbol
|
25 |
+
|
26 |
+
for CSymbol, CName in zip(self.company_symbols, self.company_names):
|
27 |
+
id_to_name_dict[CSymbol] = CName
|
28 |
+
|
29 |
+
self.name_to_id = name_to_id_dict
|
30 |
+
self.id_to_name = id_to_name_dict
|
31 |
+
|
32 |
+
def fetch_stock_data(self, company_ids: list, start_date: str) -> pd.DataFrame:
|
33 |
+
"""
|
34 |
+
Use yfinance client sdk to fetch stock data from the yahoo finance api
|
35 |
+
"""
|
36 |
+
company_data = pd.DataFrame()
|
37 |
+
|
38 |
+
# get the stock data for the companies
|
39 |
+
for cname in company_ids:
|
40 |
+
stock_data_temp = yf.download(
|
41 |
+
cname, start=start_date, end=pd.Timestamp.now().strftime("%Y-%m-%d")
|
42 |
+
)["Adj Close"]
|
43 |
+
stock_data_temp.name = cname
|
44 |
+
company_data = pd.merge(
|
45 |
+
company_data,
|
46 |
+
stock_data_temp,
|
47 |
+
how="outer",
|
48 |
+
right_index=True,
|
49 |
+
left_index=True,
|
50 |
+
)
|
51 |
+
|
52 |
+
# cleaning the data
|
53 |
+
company_data.dropna(axis=1, how="all", inplace=True)
|
54 |
+
|
55 |
+
company_data.dropna(inplace=True)
|
56 |
+
|
57 |
+
for i in company_data.columns:
|
58 |
+
company_data[i] = company_data[i].abs()
|
59 |
+
|
60 |
+
return company_data
|
61 |
+
|
62 |
+
def comp_id_to_name(self, list_of_ids: list):
|
63 |
+
return [self.id_to_name[i] for i in list_of_ids]
|
64 |
+
|
65 |
+
def comp_name_to_id(self, list_of_names: list):
|
66 |
+
return [self.name_to_id[i] for i in list_of_names]
|
67 |
+
|
68 |
+
|
69 |
+
class PortfolioOptimizer:
|
70 |
+
|
71 |
+
def __init__(self, comp_data: CompData, company_ids: list, start_date: str):
|
72 |
+
self.comp_data = comp_data
|
73 |
+
self.stock_data = self.comp_data.fetch_stock_data(
|
74 |
+
company_ids, start_date)
|
75 |
+
self.stock_data_returns = self.stock_data.pct_change().dropna()
|
76 |
+
|
77 |
+
def optimize(self, method: str, ef_parameter=None):
|
78 |
+
company_asset_weights = 0
|
79 |
+
|
80 |
+
# Do the portfolio optimization
|
81 |
+
if method == "Efficient Frontier":
|
82 |
+
mu = expected_returns.mean_historical_return(self.stock_data)
|
83 |
+
S = risk_models.sample_cov(self.stock_data)
|
84 |
+
|
85 |
+
self.ef = EfficientFrontier(mu, S)
|
86 |
+
|
87 |
+
if ef_parameter == "Maximum Sharpe Raio":
|
88 |
+
self.ef.max_sharpe()
|
89 |
+
elif ef_parameter == "Minimum Volatility":
|
90 |
+
self.ef.min_volatility()
|
91 |
+
elif ef_parameter == "Efficient Risk":
|
92 |
+
self.ef.efficient_risk(0.5)
|
93 |
+
else:
|
94 |
+
self.ef.efficient_return(0.05)
|
95 |
+
|
96 |
+
company_asset_weights = pd.DataFrame.from_dict(
|
97 |
+
self.ef.clean_weights(), orient="index"
|
98 |
+
).reset_index()
|
99 |
+
|
100 |
+
elif method == "Hierarchical Risk Parity":
|
101 |
+
mu = expected_returns.returns_from_prices(self.stock_data)
|
102 |
+
S = risk_models.sample_cov(self.stock_data)
|
103 |
+
|
104 |
+
self.ef = HRPOpt(mu, S)
|
105 |
+
|
106 |
+
company_asset_weights = self.ef.optimize()
|
107 |
+
company_asset_weights = pd.DataFrame.from_dict(
|
108 |
+
company_asset_weights, orient="index", columns=["Weight"]
|
109 |
+
).reset_index()
|
110 |
+
|
111 |
+
# cleaning the returned data from the optimization
|
112 |
+
company_asset_weights.columns = ["Ticker", "Allocation"]
|
113 |
+
|
114 |
+
company_asset_weights["Name"] = self.comp_data.comp_id_to_name(
|
115 |
+
company_asset_weights["Ticker"])
|
116 |
+
|
117 |
+
company_asset_weights = company_asset_weights[[
|
118 |
+
"Name", "Ticker", "Allocation"]]
|
119 |
+
|
120 |
+
return company_asset_weights
|
121 |
+
|
122 |
+
def get_portfolio_performance(self):
|
123 |
+
if self.ef is not None:
|
124 |
+
(
|
125 |
+
expected_annual_return,
|
126 |
+
annual_volatility,
|
127 |
+
sharpe_ratio,
|
128 |
+
) = self.ef.portfolio_performance()
|
129 |
+
|
130 |
+
st_portfolio_performance = pd.DataFrame.from_dict(
|
131 |
+
{
|
132 |
+
"Expected annual return": (expected_annual_return * 100).round(2),
|
133 |
+
"Annual volatility": (annual_volatility * 100).round(2),
|
134 |
+
"Sharpe ratio": sharpe_ratio.round(2),
|
135 |
+
},
|
136 |
+
orient="index",
|
137 |
+
).reset_index()
|
138 |
+
|
139 |
+
st_portfolio_performance.columns = ["Metrics", "Summary"]
|
140 |
+
|
141 |
+
return st_portfolio_performance
|
142 |
+
else:
|
143 |
+
return None
|
144 |
+
|
145 |
+
def get_portfolio_returns(self):
|
146 |
+
return (
|
147 |
+
self.stock_data_returns * list(self.ef.clean_weights().values())
|
148 |
+
).sum(axis=1)
|
149 |
+
|
150 |
+
def get_annual_portfolio_returns(self):
|
151 |
+
return self.get_portfolio_returns().resample("Y").apply(lambda x: (x + 1).prod() - 1)
|
utilities/py/ui_elements.py
ADDED
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pandas as pd
|
2 |
+
|
3 |
+
import streamlit as st
|
4 |
+
from utilities.py.data_management import CompData
|
5 |
+
|
6 |
+
class UserInputData:
|
7 |
+
def __init__(self, company_list, opt_method, start_date, init_invest, ef_parameter = None):
|
8 |
+
self.company_list = company_list
|
9 |
+
self.opt_method = opt_method
|
10 |
+
self.ef_parameter = ef_parameter
|
11 |
+
self.start_date = start_date
|
12 |
+
self.init_invest = init_invest
|
13 |
+
|
14 |
+
|
15 |
+
class UserInput:
|
16 |
+
|
17 |
+
def __init__(self, company_data: CompData):
|
18 |
+
"""
|
19 |
+
Class that renders the user selection (company, optimization technique, etc.)
|
20 |
+
"""
|
21 |
+
self.comp_data = company_data
|
22 |
+
self.ef_parameter_input = None
|
23 |
+
self.company_list_input = None
|
24 |
+
self.opt_method_input = None
|
25 |
+
self.start_date_input = None
|
26 |
+
self.initial_investment_input = None
|
27 |
+
|
28 |
+
def company_selection(self):
|
29 |
+
self.company_list_input = st.multiselect(
|
30 |
+
"Select Multiple Companies", self.comp_data.company_names, default=None
|
31 |
+
)
|
32 |
+
|
33 |
+
def opt_method_selection(self):
|
34 |
+
self.opt_method_input = st.selectbox(
|
35 |
+
"Choose an optimization method accordingly",
|
36 |
+
(
|
37 |
+
"Efficient Frontier",
|
38 |
+
"Hierarchical Risk Parity",
|
39 |
+
),
|
40 |
+
)
|
41 |
+
|
42 |
+
if self.opt_method_input == "Efficient Frontier":
|
43 |
+
self.ef_parameter_input = st.selectbox(
|
44 |
+
"Choose an optimization parameter accordingly",
|
45 |
+
(
|
46 |
+
"Maximum Sharpe Ratio",
|
47 |
+
"Efficient Risk",
|
48 |
+
"Minimum Volatility",
|
49 |
+
"Efficient Return",
|
50 |
+
),
|
51 |
+
)
|
52 |
+
|
53 |
+
def start_date(self):
|
54 |
+
self.start_date_input = st.date_input(
|
55 |
+
"Start Date",
|
56 |
+
format="YYYY-MM-DD",
|
57 |
+
value=pd.Timestamp("1947-08-15"),
|
58 |
+
max_value=pd.Timestamp.now(),
|
59 |
+
)
|
60 |
+
|
61 |
+
def initial_investment(self):
|
62 |
+
self.innit_invest_input = st.number_input("How much would you want to invest?", value=45000)
|
63 |
+
|
64 |
+
def get_selected_comp_ids(self):
|
65 |
+
if self.company_list_input is not None:
|
66 |
+
return self.comp_data.comp_name_to_id(self.company_list_input)
|
67 |
+
print("WARINING: Selected company ids accessed, eventhough company not yet rendered in UI")
|
68 |
+
return None
|
69 |
+
|
70 |
+
def get_user_input_data(self) -> UserInputData:
|
71 |
+
return UserInputData(self.company_list_input,
|
72 |
+
self.opt_method_input,
|
73 |
+
self.start_date_input,
|
74 |
+
self.innit_invest_input,
|
75 |
+
self.ef_parameter_input)
|