diff --git a/MIRMT.code-workspace b/MIRMT.code-workspace new file mode 100644 index 0000000..8c5a8c3 --- /dev/null +++ b/MIRMT.code-workspace @@ -0,0 +1,9 @@ +{ + "folders": [ + { + "uri": "vscode-remote://codespaces+cuddly-space-doodle-rxrj99x7p6vf5v5w/workspaces/FinRisk" + } + ], + "remoteAuthority": "codespaces+cuddly-space-doodle-rxrj99x7p6vf5v5w", + "settings": {} +} \ No newline at end of file diff --git a/MindInventory-Financial-Risk-Analysis/LICENSE b/MindInventory-Financial-Risk-Analysis/LICENSE new file mode 100644 index 0000000..5ccef58 --- /dev/null +++ b/MindInventory-Financial-Risk-Analysis/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Samar Patel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2db81d2 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +![cover_photo](./readmefile/Cover.png) + +# MindInventory - Portfolio & Risk Management System +*FinRisk allows you to build a custom portfolio and assess its risk within +few clicks!* + +*Portfolio Risk management is like wearing a helmet while riding a bike—it +shields your money during investments. For average investors, it's hard to +understand the risk of their portfolio, FinRisk is here to help.* + +## Core Features +### Real Time Market Condition Previewer +Quick Glimpse of the Financial Frenzy! The market preview includes current +market status of Dow Jones, S&P 500, Nasdaq, and Russell 2000 index. Hold onto +your seat as we zoom into the exciting world of the top 8 tech and meme stocks! +🚀🎢 Get the scoop on current prices, price changes, and the percentage shake-ups. +![market_preview](./readmefile/market_preview.png) +### Portfolio Builder +Dream big and curate a portfolio that reflects your financial aspirations. You +have the power to choose up to 10 stocks!For each stock in your portfolio, some +fundamental information required: stock tickers, the specific quantity of shares, +and the record of purchase dates. +![portfolio_builder](./readmefile/portfolio_builder.png) + +### Portfolio Risk Simulator +The portfolio risk simulator is a powerful tool that allows you to assess your portfolio's risk using +Monte Carlo Simulation. The simulation unravels crucial metrics: Value at Risk (VaR) and Conditional Value +at Risk (CVaR). VaR adn CVaR are two of the most popular risk metrics used by financial institutions to +measure the risk of their portfolios. Simulation can be tailored to your needs by adjusting the number +of simulations, historical data of your selected stocks, the confidence levels (alpha). The simulation +will also generate a line chart for your portfolio's daily returns. All data can be downloaded as a CSV file for +further analysis. +![portfolio_risk_simulator](./readmefile/risk_model.png) +## How to Use +👉 click [MRMST](https://mrmst.streamlit.app/) to launch the app! \ No newline at end of file diff --git a/assets/Collector.py b/assets/Collector.py new file mode 100644 index 0000000..a2509f1 --- /dev/null +++ b/assets/Collector.py @@ -0,0 +1,53 @@ +import yfinance +import pandas as pd + + +class InfoCollector: + + @staticmethod + def get_ticker(stock_name: str) -> yfinance.Ticker: + return yfinance.Ticker(stock_name) + + @staticmethod + def get_history(ticker: yfinance.Ticker, + period="1mo", interval="1d", + start=None, end=None): + """ + period : str + Valid periods: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max + Either Use period parameter or use start and end + interval : str + Valid intervals: 1m,2m,5m,15m,30m,60m,90m,1h,1d,5d,1wk,1mo,3mo + Intraday data cannot extend last 60 days + start: str + Download start date string (YYYY-MM-DD) or _datetime, inclusive. + Default is 99 years ago + E.g. for start="2020-01-01", the first data point will be on "2020-01-01" + end: str + Download end date string (YYYY-MM-DD) or _datetime, exclusive. + Default is now + E.g. for end="2023-01-01", the last data point will be on "2022-12-31" + """ + return ticker.history(period=period, interval=interval, + start=start, end=end) + + @staticmethod + def get_demo_daily_history(interval: str): + return InfoCollector.get_history( + ticker=yfinance.Ticker("AAPL"), + period="1d", + interval=interval, + start="2023-11-15", + end="2023-11-16") + + @staticmethod + def get_prev_date(stock_info: pd.DataFrame): + return stock_info.index[0] + + @staticmethod + def get_daily_info(stock_info: pd.DataFrame, key_info: str): + return stock_info.loc[stock_info.index[0], key_info] + + @staticmethod + def download_batch_history(stocks: list, start_time, end_time): + return yfinance.download(stocks, start=start_time, end=end_time) \ No newline at end of file diff --git a/assets/Portfolio.py b/assets/Portfolio.py new file mode 100644 index 0000000..aa18c23 --- /dev/null +++ b/assets/Portfolio.py @@ -0,0 +1,27 @@ +from assets.Stock import Stock + + +class Portfolio: + + def __init__(self) -> None: + self.stocks = {} + self.book_amount = 0 + self.market_value = 0 + + def add_stock(self, stock: Stock) -> None: + if stock.stock_name in self.stocks.keys(): + raise Exception("Stock included in portfolio. Please remove stock to add again") + + self.stocks[stock.stock_name] = stock + self.book_amount += stock.get_book_cost() + + def remove_stock(self, stock_name: str) -> None: + if stock_name not in self.stocks.keys(): + raise Exception("Stock not in portfolio") + self.book_amount -= self.stocks[stock_name].get_book_cost() + self.stocks.pop(stock_name) + + def update_market_value(self) -> None: + for stock in self.stocks.values(): + stock_market_value = stock.get_market_value() + self.market_value += stock_market_value \ No newline at end of file diff --git a/assets/Stock.py b/assets/Stock.py new file mode 100644 index 0000000..6d960f2 --- /dev/null +++ b/assets/Stock.py @@ -0,0 +1,105 @@ +import datetime +from assets.Collector import InfoCollector + + +class Stock: + + def __init__(self, stock_name: str): + self.stock_name = stock_name + self.ticker = InfoCollector.get_ticker(stock_name) + self.owned_quantity = 0 + self.average_price = 0 + self.previous_close = None + self.previous_open = None + self.previous_volume = None + self.previous_date = None + + self._update_stock() + + def __eq__(self, other): + if self.stock_name == other.stock_name: + return True + return False + + def _update_stock(self) -> None: + """ + Updates the stock information, used as a check function to check if + stock exist + """ + stock_info = InfoCollector.get_history(self.ticker, period="1d") + if len(stock_info) == 0: + raise Exception("Invalid stock, enter a valid stock") + else: + self.previous_date = InfoCollector.get_prev_date(stock_info) + self.previous_open = InfoCollector.get_daily_info(stock_info, "Open") + self.previous_close = InfoCollector.get_daily_info(stock_info, "Close") + self.previous_volume = InfoCollector.get_daily_info(stock_info, "Volume") + + def _get_purchase_price(self, purchase_date: datetime.datetime) -> float: + """ + Gets the purchase price (assumed be closed price) of the stock based + on given date if price at given date not found, track back for 5 days, + thought: smart implementation might be required for caching + """ + time_delta = datetime.timedelta(days=1) + start_date = purchase_date + end_date = purchase_date + time_delta + + for _ in range(5): + info = InfoCollector.get_history(self.ticker, + start=start_date, + end=end_date) + if len(info) > 0: + purchased_price = InfoCollector.get_daily_info(info, "Close") + return purchased_price + start_date = start_date - time_delta + end_date = end_date - time_delta + + raise Exception("Purchase price not found, please check the date or stock sticker") + + def add_buy_action(self, quantity: int, + purchase_date: datetime.datetime) -> None: + """ + Add a purchase to the stock. Currently, do not support to add another + purchase to the stock + """ + + # update own quantity + + self.owned_quantity += quantity + + if self.average_price == 0: + + self.average_price = self._get_purchase_price(purchase_date=purchase_date) + else: + cur_purchase_price = self._get_purchase_price(purchase_date=purchase_date) + purchase_cost = quantity * cur_purchase_price + + # update average price and owned_quantity + total_cost = purchase_cost + cur_purchase_price + self.average_price = total_cost / self.owned_quantity + + def get_book_cost(self) -> float: + if self.owned_quantity == 0: + raise Exception("Stock not owned, please purchase first") + + if self.average_price is None: + raise Exception("Purchase price not found, please check the date or stock sticker") + + return self.average_price * self.owned_quantity + + def get_market_value(self) -> float: + self._update_stock() + if self.owned_quantity == 0: + raise Exception("Stock not owned, please purchase first") + + if self.previous_close is None: + raise Exception("Stock price not found, please check the date or stock sticker") + + return self.previous_close * self.owned_quantity + + def get_gain_loss(self) -> float: + return self.get_market_value() - self.get_book_cost() + + def get_pct_change(self) -> float: + return (self.get_gain_loss() / self.get_book_cost()) * 100 \ No newline at end of file diff --git a/default_page.py b/default_page.py new file mode 100644 index 0000000..22df6f7 --- /dev/null +++ b/default_page.py @@ -0,0 +1,61 @@ +import streamlit as st +import stTools as tools + + +def load_page(): + st.markdown( + """ + Welcome to :green[MRMST]! Explore this app to assess and simulate + your investment portfolio's risk effortlessly! :green[Risk management] is like + :blue[wearing a helmet while riding a bike]—it shields your money during investments. + It's a strategy set to understand uncertainties in stocks or bonds. + + Imagine your investment journey as a game; knowing rules and setbacks gives you a competitive edge. + :green[Value at Risk (VaR)] and :green[Conditional Value at Risk (CVaR)] aid in smart risk navigation, + keeping your game plan robust. Don't worry, I'll explain these concepts in a bit. + + Build your :green[portfolio] on the sidebar; guidance is provided! Contact me at + [LinkedIn](https://www.linkedin.com/in/samarpatel/) + """ + ) + + st.subheader(f"Market Preview") + + col_stock1, col_stock_2, col_stock_3, col_stock_4 = st.columns(4) + + with col_stock1: + tools.create_candle_stick_plot(stock_ticker_name="^DJI", + stock_name="Dow Jones Industrial") + + with col_stock_2: + tools.create_candle_stick_plot(stock_ticker_name="^IXIC", + stock_name="Nasdaq Composite") + + with col_stock_3: + tools.create_candle_stick_plot(stock_ticker_name="^GSPC", + stock_name="S&P 500") + + with col_stock_4: + tools.create_candle_stick_plot(stock_ticker_name="^RUT", + stock_name="Russell 2000") + + # make 2 columns for sectors + col_sector1, col_sector2 = st.columns(2) + + with col_sector1: + st.subheader(f"Tech Stocks") + stock_list = ["AAPL", "MSFT", "AMZN", "GOOG", "META", "TSLA", "NVDA", "AVGO"] + stock_name = ["Apple", "Microsoft", "Amazon", "Google", "Meta", "Tesla", "Nvidia", "Broadcom"] + + df_stocks = tools.create_stocks_dataframe(stock_list, stock_name) + tools.create_dateframe_view(df_stocks) + + with col_sector2: + st.subheader(f"Meme Stocks") + # give me a list of 8 meme stocks + stock_list = ["GME", "AMC", "BB", "NOK", "RIVN", "SPCE", "F", "T"] + stock_name = ["GameStop", "AMC Entertainment", "BlackBerry", "Nokia", "Rivian", + "Virgin Galactic", "Ford", "AT&T"] + + df_stocks = tools.create_stocks_dataframe(stock_list, stock_name) + tools.create_dateframe_view(df_stocks) diff --git a/main_page.py b/main_page.py new file mode 100644 index 0000000..cdcde9e --- /dev/null +++ b/main_page.py @@ -0,0 +1,33 @@ +import streamlit as st +import side_bar as comp +import stTools as tools +import default_page +import portfolio_page +import model_page + +st.set_page_config( + page_title="MindInventory Financial Risk Tool", + page_icon="🦈", + layout="wide" +) + +tools.remove_white_space() + +st.title("MindInventory Risk Management Simulation Tool") + +comp.load_sidebar() + +if "load_portfolio_check" not in st.session_state: + st.session_state["load_portfolio_check"] = False + +if "run_simulation_check" not in st.session_state: + st.session_state["run_simulation_check"] = False + +if not st.session_state.load_portfolio_check: + default_page.load_page() + +elif not st.session_state.run_simulation_check and st.session_state.load_portfolio_check: + portfolio_page.load_page() + +elif st.session_state.run_simulation_check: + model_page.load_page() diff --git a/model_page.py b/model_page.py new file mode 100644 index 0000000..f4995e5 --- /dev/null +++ b/model_page.py @@ -0,0 +1,51 @@ +import streamlit as st +import stTools as tools +from models.MonteCarloSimulator import Monte_Carlo_Simulator +import model_page_components + + +def load_page() -> None: + my_portfolio = st.session_state.my_portfolio + # create a monte carlo simulation + monte_carlo_model = Monte_Carlo_Simulator(cVaR_alpha=st.session_state.cVaR_alpha, + VaR_alpha=st.session_state.VaR_alpha) + monte_carlo_model.get_portfolio(portfolio=my_portfolio, + start_time=st.session_state.start_date, + end_time=st.session_state.end_date) + monte_carlo_model.apply_monte_carlo(no_simulations=int(st.session_state.no_simulations), + no_days=int(st.session_state.no_days)) + + model_page_components.add_markdown() + + col1, col2, col3 = st.columns(3) + + with col1: + st.subheader("Initial Investment") + # plot initial investment as metric + book_amount_formatted = tools.format_currency(my_portfolio.book_amount) + tools.create_metric_card(label="Day 0", + value=book_amount_formatted, + delta=None) + + with col2: + st.subheader("Simulation Return (VaR)") + VaR_alpha_formatted = tools.format_currency(monte_carlo_model. + get_VaR(st.session_state.VaR_alpha)) + tools.create_metric_card(label=f"Day {st.session_state.no_days} with VaR(alpha-{st.session_state.VaR_alpha})", + value=VaR_alpha_formatted, + delta=None) + + with col3: + st.subheader("Simulation Return (cVaR)") + + cVaR_alpha_formatted = tools.format_currency(monte_carlo_model. + get_conditional_VaR(st.session_state.cVaR_alpha)) + tools.create_metric_card(label=f"Day {st.session_state.no_days} with cVaR(alpha-{st.session_state.cVaR_alpha})", + value=cVaR_alpha_formatted, + delta=None) + + st.subheader(f"Portfolio Returns after {st.session_state.no_simulations} Simulations") + model_page_components.add_portfolio_returns_graphs(monte_carlo_model.portfolio_returns) + + # add download button + model_page_components.add_download_button(monte_carlo_model.portfolio_returns) \ No newline at end of file diff --git a/model_page_components.py b/model_page_components.py new file mode 100644 index 0000000..d1d1e28 --- /dev/null +++ b/model_page_components.py @@ -0,0 +1,33 @@ +import streamlit as st +import pandas as pd +import stTools as tools + + +def add_portfolio_returns_graphs(portfolio_df: pd.DataFrame) -> None: + tools.create_line_chart(portfolio_df) + # st.line_chart(portfolio_df, use_container_width=True, height=500, width=250) + + +def add_download_button(df: pd.DataFrame) -> None: + # convert my_portfolio_returns ndarray to dataframe + df = pd.DataFrame(df) + + col1, col2, col3, col4 = st.columns(4) + + with col4: + st.download_button(label="Download Portfolio Returns", + data=df.to_csv(), + file_name="Portfolio Returns.csv", + mime="text/csv") + + +def add_markdown() -> None: + st.markdown( + """ + Please see below for your portfolio returns after risk simulation! + + Caring about :green[risk management], :green[VaR], :green[CVaR], and :green[alpha] is like + putting on your gaming headset—it helps you play the investment game smarter, + protecting your money and aiming for a high score in the financial world. + """ + ) diff --git a/models/MonteCarloSimulator.py b/models/MonteCarloSimulator.py new file mode 100644 index 0000000..cc1fad0 --- /dev/null +++ b/models/MonteCarloSimulator.py @@ -0,0 +1,86 @@ +import datetime as dt +import numpy as np +from numpy import ndarray +from assets import Portfolio +from assets.Collector import InfoCollector + + +class Monte_Carlo_Simulator: + + def __init__(self, + cVaR_alpha: float, + VaR_alpha: float): + self.stocks = {} + self.init_cash = 0 + self.no_simulations = 0 + self.no_days = 0 + self.cVaR_alpha = cVaR_alpha + self.VaR_alpha = VaR_alpha + self.pct_mean_return = None + self.pct_cov_matrix = None + self.portfolio_returns = None + + def get_portfolio(self, portfolio: Portfolio, + start_time: dt.datetime, + end_time: dt.datetime) -> None: + stocks = list(portfolio.stocks.keys()) + stocks_data = InfoCollector.download_batch_history(stocks, start_time, end_time) + + # Get the closing price of each stock apply dropna() + stocks_data = stocks_data['Close'].dropna() + + pct_return = stocks_data.pct_change().dropna() + self.pct_mean_return = pct_return.mean() + self.pct_cov_matrix = pct_return.cov() + + self.init_cash = portfolio.book_amount + self._get_weights(portfolio) + + def _get_weights(self, portfolio: Portfolio): + total_book_cost = 0 + for stock in portfolio.stocks.keys(): + self.stocks[stock] = portfolio.stocks[stock].get_book_cost() + total_book_cost += self.stocks[stock] + + for stock in portfolio.stocks.keys(): + self.stocks[stock] = self.stocks[stock] / total_book_cost + + def apply_monte_carlo(self, no_simulations: int, no_days: int) -> None: + # Get weight array + weights = list(self.stocks.values()) + weights = np.array(weights, dtype=np.float64) + + # get mean matrix + mean_matrix = np.full(shape=(no_days, len(weights)), fill_value=self.pct_mean_return) + mean_matrix = np.transpose(mean_matrix) + + portfolio_returns = np.zeros(shape=(no_days, no_simulations), dtype=np.float64) + + for sim in range(0, no_simulations): + # Cholesky Decomposition + Z = np.random.normal(size=(no_days, len(weights))) + L = np.linalg.cholesky(self.pct_cov_matrix) + + daily_returns = mean_matrix + np.inner(L, Z) + portfolio_returns[:, sim] = np.cumprod(np.inner(weights, daily_returns.T) + 1) \ + * self.init_cash + self.portfolio_returns = portfolio_returns + + def get_VaR(self, alpha: float) -> int: + if self.VaR_alpha is None: + self.VaR_alpha = float(alpha) + if self.portfolio_returns is None: + raise Exception("No Monte Carlo simulation has been applied") + + VaR = round(np.quantile(self.portfolio_returns[-1, :], float(self.VaR_alpha)), 1) + return VaR + + def get_conditional_VaR(self, alpha: float) -> ndarray: + self.cVaR_alpha = float(alpha) + if self.portfolio_returns is None: + raise Exception("No Monte Carlo simulation has been applied") + + var = self.get_VaR(self.cVaR_alpha) + cVaR = round(np.mean(self.portfolio_returns[-1, :] + [self.portfolio_returns[-1, :] < float(var)]),1) + return cVaR \ No newline at end of file diff --git a/portfolio_page.py b/portfolio_page.py new file mode 100644 index 0000000..cbfb255 --- /dev/null +++ b/portfolio_page.py @@ -0,0 +1,42 @@ +import streamlit as st +import stTools as tools +import portfolio_page_components + + +def load_page(): + no_stocks = st.session_state.no_investment + + # load portfolio performance + + my_portfolio = tools.build_portfolio(no_stocks=no_stocks) + my_portfolio.update_market_value() + + portfolio_book_amount = my_portfolio.book_amount + portfolio_market_value = my_portfolio.market_value + diff_amount = portfolio_market_value - portfolio_book_amount + pct_change = (diff_amount) \ + / portfolio_book_amount * 100 + + # save my_portfolio to session state + st.session_state.my_portfolio = my_portfolio + + # create 3 columns + col1_summary, col2_pie = st.columns(2) + + with col1_summary: + portfolio_page_components.load_portfolio_performance_cards( + portfolio_book_amount=portfolio_book_amount, + portfolio_market_value=portfolio_market_value, + diff_amount=diff_amount, + pct_change=pct_change + ) + + with col2_pie: + portfolio_page_components.load_portfolio_summary_pie() + + # load portfolio summary + portfolio_page_components.load_portfolio_summary_table() + + # load investment preview + st.subheader("Investment Performance Summary - Since Purchase") + portfolio_page_components.load_portfolio_preview(no_stocks=no_stocks) diff --git a/portfolio_page_components.py b/portfolio_page_components.py new file mode 100644 index 0000000..998617c --- /dev/null +++ b/portfolio_page_components.py @@ -0,0 +1,90 @@ +import streamlit as st +import stTools as tools +import pandas as pd + + +def load_portfolio_performance_cards( + portfolio_book_amount: float, + portfolio_market_value: float, + diff_amount: float, + pct_change: float +) -> None: + st.subheader("Portfolio Performance") + tools.create_metric_card(label="Book Cost of Portfolio", + value=tools.format_currency(portfolio_book_amount), + delta=None) + tools.create_metric_card(label="Market Value of Portfolio", + value=tools.format_currency(portfolio_market_value), + delta=None) + tools.create_metric_card(label="Gain/Loss on Investments Unrealized", + value=tools.format_currency(diff_amount), + delta=f"{pct_change:.2f}%") + + +def load_portfolio_summary_pie() -> None: + st.subheader("Portfolio Distribution") + book_cost_list = {} + for stock in st.session_state.my_portfolio.stocks.values(): + book_cost_list[stock.stock_name] = stock.get_book_cost() + + # create pie chart + tools.create_pie_chart(book_cost_list) + + +def load_portfolio_summary_table() -> None: + st.subheader("Portfolio Summary") + + # for each stock, we get book cost, market value, gain/loss, and pct change + stock_info = {} + for stock in st.session_state.my_portfolio.stocks.values(): + # round to 2 decimal places + book_cost = round(stock.get_book_cost(), 2) + market_value = round(stock.get_market_value(), 2) + gain_loss = round(market_value - book_cost, 2) + pct_change = round((gain_loss) / book_cost * 100, 2) + + stock_info[stock.stock_name] = [book_cost, market_value, gain_loss, pct_change] + # print key and values in stock_info + for key, value in stock_info.items(): + print(key, value) + + column_names = ['Book Cost', 'Market Value', 'Gain/Loss', '% Change'] + stock_df = pd.DataFrame.from_dict(stock_info, + orient='index', + columns=column_names) + # name index column as 'Stock' + stock_df.index.name = 'Stock' + + for column in stock_df.columns: + stock_df[column] = stock_df[column].apply(lambda x: f'{x:,.2f}') + + # plot dataframe stock_df + st.dataframe( + stock_df.style.map(tools.win_highlight, + subset=['Gain/Loss', '% Change']), + column_config={ + "Stock": "Ticker", + "Book Cost": "Book Cost($)", + "Market Value": "Market Value($)", + "Gain/Loss": "Gain/Loss($)", # if positive, green, if negative, red + "% Change": "% Change", # if positive, green, if negative, red + }, + hide_index=True, + width=5600, + ) + + +def load_portfolio_preview(no_stocks: int) -> None: + column_limit = 4 + # create 4 columns + col_stock1, col_stock_2, col_stock_3, col_stock_4 = st.columns(column_limit) + columns_list = [col_stock1, col_stock_2, col_stock_3, col_stock_4] + + columns_no = 0 + for i in range(no_stocks): + if columns_no == 4: + columns_no = 0 + with columns_list[columns_no]: + tools.preview_stock(f"stock_{i + 1}_name", + start_date=st.session_state[f"stock_{i + 1}_purchase_date"]) + columns_no += 1 \ No newline at end of file diff --git a/readmefile/Cover.png b/readmefile/Cover.png new file mode 100644 index 0000000..3a4ea3a Binary files /dev/null and b/readmefile/Cover.png differ diff --git a/readmefile/market_preview.png b/readmefile/market_preview.png new file mode 100644 index 0000000..dfd374a Binary files /dev/null and b/readmefile/market_preview.png differ diff --git a/readmefile/portfolio_builder.png b/readmefile/portfolio_builder.png new file mode 100644 index 0000000..2faaebf Binary files /dev/null and b/readmefile/portfolio_builder.png differ diff --git a/readmefile/risk_model.png b/readmefile/risk_model.png new file mode 100644 index 0000000..5fe1578 Binary files /dev/null and b/readmefile/risk_model.png differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..94411d6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,80 @@ +altair==5.1.2 +appdirs==1.4.4 +attrs==23.1.0 +beautifulsoup4==4.12.2 +blinker==1.7.0 +cachetools==5.3.2 +certifi==2023.7.22 +charset-normalizer==3.3.2 +click==8.1.7 +contourpy==1.2.0 +cycler==0.12.1 +entrypoints==0.4 +Faker==20.0.3 +favicon==0.7.0 +fonttools==4.44.3 +frozendict==2.3.8 +gitdb==4.0.11 +GitPython==3.1.40 +htbuilder==0.6.2 +html5lib==1.1 +idna==3.4 +importlib-metadata==6.8.0 +Jinja2==3.1.2 +jsonschema==4.20.0 +jsonschema-specifications==2023.11.1 +kiwisolver==1.4.5 +lxml==4.9.3 +Markdown==3.5.1 +markdown-it-py==3.0.0 +markdownlit==0.0.7 +MarkupSafe==2.1.3 +matplotlib==3.8.2 +mdurl==0.1.2 +more-itertools==10.1.0 +multitasking==0.0.11 +numpy==1.26.2 +packaging==23.2 +pandas==2.1.3 +peewee==3.17.0 +Pillow==10.1.0 +plotly==5.18.0 +protobuf==4.25.1 +pyarrow==14.0.1 +pydeck==0.8.1b0 +Pygments==2.16.1 +pymdown-extensions==10.4 +pyparsing==3.1.1 +python-dateutil==2.8.2 +pytz==2023.3.post1 +PyYAML==6.0.1 +referencing==0.31.0 +requests==2.31.0 +rich==13.7.0 +rpds-py==0.13.0 +six==1.16.0 +smmap==5.0.1 +soupsieve==2.5 +st-annotated-text==4.0.1 +streamlit==1.28.2 +streamlit-camera-input-live==0.2.0 +streamlit-card==0.0.61 +streamlit-embedcode==0.1.2 +streamlit-extras==0.3.5 +streamlit-faker==0.0.3 +streamlit-image-coordinates==0.1.6 +streamlit-keyup==0.2.0 +streamlit-toggle-switch==1.0.2 +streamlit-vertical-slider==1.0.2 +tenacity==8.2.3 +toml==0.10.2 +toolz==0.12.0 +tornado==6.3.3 +typing_extensions==4.8.0 +tzdata==2023.3 +tzlocal==5.2 +urllib3 +validators==0.22.0 +webencodings==0.5.1 +yfinance==0.2.31 +zipp==3.17.0 diff --git a/side_bar.py b/side_bar.py new file mode 100644 index 0000000..6145623 --- /dev/null +++ b/side_bar.py @@ -0,0 +1,62 @@ +import streamlit as st +import stTools as tools +import side_bar_components + + +def load_sidebar() -> None: + # inject custom CSS to set the width of the sidebar + tools.create_side_bar_width() + + st.sidebar.title("Control Panel") + + if "load_portfolio" not in st.session_state: + st.session_state["load_portfolio"] = False + + if "run_simulation" not in st.session_state: + st.session_state["run_simulation"] = False + + portfo_tab, model_tab = st.sidebar.tabs(["📈 Create Portfolio", + "🐂 Build Risk Model"]) + + # add portfolio tab components + portfo_tab.title("Portfolio Building") + side_bar_components.load_sidebar_dropdown_stocks(portfo_tab) + side_bar_components.load_sidebar_stocks(portfo_tab, + st.session_state.no_investment) + st.session_state["load_portfolio"] = portfo_tab.button("Load Portfolio", + key="side_bar_load_portfolio", + on_click=tools.click_button_port) + + portfo_tab.markdown(""" + You can create a portfolio with a maximum of :green[10] investments. + + For each investment, please provide details such as the :green[stock name], :green[number of shares], and + :green[purchase date]. + + Feel free to stick with the default values or customize them according to your preferences. + + To simplify, the purchase price us determined based on the closing price on + the purchase date. + """) + + # add model tab + model_tab.title("Risk Model Building") + side_bar_components.load_sidebar_risk_model(model_tab) + st.session_state["run_simulation"] = model_tab.button("Run Simulation", + key="main_page_run_simulation", + on_click=tools.click_button_sim) + + model_tab.markdown(""" + :green[VaR (Value at Risk)]: Think of VaR as a safety net, indicating the + maximum potential loss within a confidence level, e.g., a 95% chance of not losing + more than $X. It prepares you for worst-case scenarios, with alpha representing the + confidence level (e.g., 5% -> 95% confidence). + + :green[Conditional Value at Risk)]: CVaR goes beyond, revealing expected losses + beyond the worst-case scenario. It's like a backup plan for extreme situations, + with alpha denoting the confidence level (e.g., 5% -> 95% confidence). + + :red[Why Should You Care?]: In a video game analogy, VaR is your character's maximum damage + tolerance, while CVaR is your backup plan with health potions. Understanding these helps you make + smart moves and avoid losses. + """) \ No newline at end of file diff --git a/side_bar_components.py b/side_bar_components.py new file mode 100644 index 0000000..ed1500b --- /dev/null +++ b/side_bar_components.py @@ -0,0 +1,76 @@ +import streamlit as st +import stTools as tools +import datetime as dt +import random + + +def load_sidebar_dropdown_stocks(port_tab: st.sidebar.tabs) -> None: + # add dropdown menu for portfolio + st.session_state["no_investment"] = port_tab.selectbox("Select No. of Investments", + [2, 3, 4, 5, 6, 7, 8, 9, 10], + index=2, + key="side_bar_portfolio_name") + + +def load_sidebar_stocks(port_tab: st.sidebar.tabs, no_investment: int) -> None: + demo_stock_list = tools.get_stock_demo_data(no_investment) + + # split into three columns + stock_col, share_col, date_col = port_tab.columns(3) + + # create text boxes for each stocks in demo_stock_list + for stock in demo_stock_list: + with stock_col: + tools.create_stock_text_input(state_variable=f"stock_{demo_stock_list.index(stock) + 1}_name", + default_value=stock, + present_text=f"Investment {demo_stock_list.index(stock) + 1}", + key=f"side_bar_stock_{demo_stock_list.index(stock) + 1}_name") + + with share_col: + no_shares = random.randrange(10, 100, 10) + tools.create_stock_text_input(state_variable=f"stock_{demo_stock_list.index(stock) + 1}_share", + default_value=str(no_shares), + present_text="No. of Shares", + key=f"side_bar_stock_{demo_stock_list.index(stock) + 1}_share") + + with date_col: + time_delta = dt.timedelta(days=random.randrange(3, 120, 1)) + tools.create_date_input(state_variable=f"stock_{demo_stock_list.index(stock) + 1}_purchase_date", + present_text="Purchase Date", + default_value=dt.datetime.now() - time_delta, + key=f"side_bar_stock_{demo_stock_list.index(stock) + 1}_purchase_date") + + +def load_sidebar_risk_model(risk_tab: st.sidebar.tabs) -> None: + col_monte1, col_monte2 = risk_tab.columns(2) + + with col_monte1: + tools.create_date_input(state_variable="start_date", + present_text="History Start Date", + default_value=dt.datetime.now() - dt.timedelta(days=365), + key="side_bar_start_date") + + tools.create_stock_text_input(state_variable="no_simulations", + default_value=str(100), + present_text="No. of Simulations", + key="main_no_simulations") + + tools.create_stock_text_input(state_variable="VaR_alpha", + default_value=str(0.05), + present_text="VaR Alpha", + key="side_bar_VaR_alpha") + with col_monte2: + tools.create_date_input(state_variable="end_date", + present_text="History End Date", + default_value=dt.datetime.now(), + key="side_bar_end_date") + + tools.create_stock_text_input(state_variable="no_days", + default_value=str(100), + present_text="No. of Days", + key="main_no_days") + + tools.create_stock_text_input(state_variable="cVaR_alpha", + default_value=str(0.05), + present_text="cVaR Alpha", + key="side_bar_cVaR_alpha") \ No newline at end of file diff --git a/stTools.py b/stTools.py new file mode 100644 index 0000000..c2885a0 --- /dev/null +++ b/stTools.py @@ -0,0 +1,275 @@ +import datetime +import streamlit as st +import yfinance +import datetime as dt +from assets.Collector import InfoCollector +import plotly.graph_objects as go +from streamlit_extras.metric_cards import style_metric_cards +import pandas as pd +from assets import Portfolio +from assets import Stock +import plotly.express as px + + +def create_state_variable(key: str, default_value: any) -> None: + if key not in st.session_state: + st.session_state[key] = default_value + + +def create_stock_text_input( + state_variable: str, + default_value: str, + present_text: str, + key: str +) -> None: + create_state_variable(state_variable, default_value) + + st.session_state[state_variable] = st.text_input(present_text, + key=key, + value=st.session_state[state_variable]) + + +def create_date_input( + state_variable: str, + present_text: str, + default_value: str, + key: str +) -> None: + create_state_variable(state_variable, default_value) + + st.session_state[state_variable] = st.date_input(present_text, + value=st.session_state[state_variable], + key=key) + + +def get_stock_demo_data(no_stocks: int) -> list: + stock_name_list = ['AAPL', 'TSLA', 'GOOG', 'MSFT', + 'AMZN', 'META', 'NVDA', 'PYPL', + 'NFLX', 'ADBE', 'INTC', 'CSCO', ] + return stock_name_list[:no_stocks] + + +def click_button_sim() -> None: + st.session_state["run_simulation"] = True + st.session_state["run_simulation_check"] = True + + +def click_button_port() -> None: + st.session_state["load_portfolio"] = True + st.session_state["load_portfolio_check"] = True + st.session_state["run_simulation_check"] = False + + +def preview_stock( + session_state_name: str, + start_date: datetime.datetime +) -> None: + stock_data = yfinance.download(st.session_state[session_state_name], + start=start_date, + end=dt.datetime.now()) + stock_data = stock_data[['Close']] + + color = None + + # get price difference of close + diff_price = stock_data.iloc[-1]['Close'] - stock_data.iloc[0]['Close'] + if diff_price > 0.0: + color = '#00fa119e' + elif diff_price < 0.0: + color = '#fa00009e' + + # change index form 0 to end + stock_data['day(s) since buy'] = range(0, len(stock_data)) + + create_metric_card(label=st.session_state[session_state_name], + value=f"{stock_data.iloc[-1]['Close']: .2f}", + delta=f"{diff_price: .2f}") + + st.area_chart(stock_data, use_container_width=True, + height=250, width=250, color=color, x='day(s) since buy') + + +def format_currency(number: float) -> str: + formatted_number = "${:,.2f}".format(number) + return formatted_number + + +def create_side_bar_width() -> None: + st.markdown( + """ + + """, unsafe_allow_html=True) + + +def get_current_date() -> str: + return datetime.datetime.now().strftime('%Y-%m-%d') + + +def create_candle_stick_plot(stock_ticker_name: str, stock_name: str) -> None: + # present stock name + stock = InfoCollector.get_ticker(stock_ticker_name) + stock_data = InfoCollector.get_history(stock, period="1d", interval='5m') + stock_data_template = InfoCollector.get_demo_daily_history(interval='5m') + + stock_data = stock_data[['Open', 'High', 'Low', 'Close']] + + # get the first row open price + open_price = stock_data.iloc[0]['Open'] + # get the last row close price + close_price = stock_data.iloc[-1]['Close'] + # get the last row high price + diff_price = close_price - open_price + + # metric card + create_metric_card(label=f"{stock_name}", + value=f"{close_price: .2f}", + delta=f"{diff_price: .2f}") + + # candlestick chart + candlestick_chart = go.Figure(data=[ + go.Candlestick(x=stock_data_template.index, + open=stock_data['Open'], + high=stock_data['High'], + low=stock_data['Low'], + close=stock_data['Close'])]) + candlestick_chart.update_layout(xaxis_rangeslider_visible=False, + margin=dict(l=0, r=0, t=0, b=0)) + st.plotly_chart(candlestick_chart, use_container_width=True, height=100) + + +def create_stocks_dataframe(stock_ticker_list: list, stock_name: list) -> pd.DataFrame: + close_price = [] + daily_change = [] + pct_change = [] + all_price = [] + for stock_ticker in stock_ticker_list: + stock = InfoCollector.get_ticker(stock_ticker) + stock_data = InfoCollector.get_history(stock, period="1d", interval='5m') + # round value to 2 digits + + close_price_value = round(stock_data.iloc[-1]['Close'], 2) + close_price.append(close_price_value) + + # round value to 2 digits + daily_change_value = round(stock_data.iloc[-1]['Close'] - stock_data.iloc[0]['Open'], 2) + daily_change.append(daily_change_value) + + # round value to 2 digits + pct_change_value = round((stock_data.iloc[-1]['Close'] - stock_data.iloc[0]['Open']) + / stock_data.iloc[0]['Open'] * 100, 2) + pct_change.append(pct_change_value) + + all_price.append(stock_data['Close'].tolist()) + + df_stocks = pd.DataFrame( + { + "stock_tickers": stock_ticker_list, + "stock_name": stock_name, + "close_price": close_price, + "daily_change": daily_change, + "pct_change": pct_change, + "views_history": all_price + } + ) + return df_stocks + + +def win_highlight(val: str) -> str: + color = None + val = str(val) + val = val.replace(',', '') + + if float(val) >= 0.0: + color = '#00fa119e' + elif float(val) < 0.0: + color = '#fa00009e' + return f'background-color: {color}' + + +def create_dateframe_view(df: pd.DataFrame) -> None: + df['close_price'] = df['close_price'].apply(lambda x: f'{x:,.2f}') + df['daily_change'] = df['daily_change'].apply(lambda x: f'{x:,.2f}') + df['pct_change'] = df['pct_change'].apply(lambda x: f'{x:,.2f}') + + st.dataframe( + df.style.map(win_highlight, + subset=['daily_change', 'pct_change']), + column_config={ + "stock_tickers": "Tickers", + "stock_name": "Stock", + "close_price": "Price ($)", + "daily_change": "Price Change ($)", # if positive, green, if negative, red + "pct_change": "% Change", # if positive, green, if negative, red + "views_history": st.column_config.LineChartColumn( + "daily trend"), + }, + hide_index=True, + width=620, + ) + + +def build_portfolio(no_stocks: int) -> Portfolio.Portfolio: + # build portfolio using portfolio class + my_portfolio = Portfolio.Portfolio() + for i in range(no_stocks): + stock = Stock.Stock(stock_name=st.session_state[f"stock_{i + 1}_name"]) + stock.add_buy_action(quantity=int(st.session_state[f"stock_{i + 1}_share"]), + purchase_date=st.session_state[f"stock_{i + 1}_purchase_date"]) + my_portfolio.add_stock(stock=stock) + return my_portfolio + + +def get_metric_bg_color() -> str: + return "#282C35" + + +def create_metric_card(label: str, value: str, delta: str) -> None: + st.metric(label=label, + value=value, + delta=delta) + + background_color = get_metric_bg_color() + style_metric_cards(background_color=background_color) + + +def create_pie_chart(key_values: dict) -> None: + labels = list(key_values.keys()) + values = list(key_values.values()) + + # Use `hole` to create a donut-like pie chart + fig = go.Figure(data=[go.Pie(labels=labels, values=values, textinfo='label+percent', + insidetextorientation='radial' + )], + ) + # do not show legend + fig.update_layout(xaxis_rangeslider_visible=False, + margin=dict(l=20, r=20, t=20, b=20), + showlegend=False) + + st.plotly_chart(fig, use_container_width=True, use_container_height=True) + + +def create_line_chart(portfolio_df: pd.DataFrame) -> None: + fig = px.line(portfolio_df) + fig.update_layout(xaxis_rangeslider_visible=False, + margin=dict(l=20, r=20, t=20, b=20), + showlegend=False, + xaxis_title="Day(s) since purchase", + yaxis_title="Portfolio Value ($)") + st.plotly_chart(fig, use_container_width=True, use_container_height=True) \ No newline at end of file