diff --git a/gamestonk_terminal/portfolio_optimization/port_opt_api.py b/gamestonk_terminal/portfolio_optimization/port_opt_api.py index 71e697b73cc..ccf8e3731df 100644 --- a/gamestonk_terminal/portfolio_optimization/port_opt_api.py +++ b/gamestonk_terminal/portfolio_optimization/port_opt_api.py @@ -3,11 +3,31 @@ __docformat__ = "numpy" import argparse from typing import List +import matplotlib.pyplot as plt +import numpy as np import pandas as pd import yfinance as yf -from pypfopt.efficient_frontier import EfficientFrontier +from pypfopt import plotting from pypfopt import risk_models from pypfopt import expected_returns +from gamestonk_terminal.helper_funcs import parse_known_args_and_warn, plot_autoscale +from gamestonk_terminal.portfolio_optimization.port_opt_helper import * +from gamestonk_terminal.config_plot import PLOT_DPI +from gamestonk_terminal import feature_flags as gtff + +period_choices = [ + "1d", + "5d", + "1mo", + "3mo", + "6mo", + "1y", + "2y", + "5y", + "10y", + "ytd", + "max", +] def equal_weight(list_of_stocks: List[str], other_args: List[str]): @@ -65,29 +85,166 @@ def property_weighting( return weights -def max_sharpe(list_of_stocks: List[str], other_args: List[str]): +def show_ef(list_of_stocks: List[str], other_args: List[str]): + parser = argparse.ArgumentParser(add_help=False, prog="ef") + + parser.add_argument( + "-p", + "--period", + default="3mo", + dest="period", + help="period to get yfinance data from", + choices=period_choices, + ) + parser.add_argument( + "-n", default=300, dest="n_port", help="number of portfolios to simulate" + ) + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return {} + stock_prices = process_stocks(list_of_stocks, ns_parser.period) + mu = expected_returns.mean_historical_return(stock_prices) + S = risk_models.sample_cov(stock_prices) + ef = EfficientFrontier(mu, S) + fig, ax = plt.subplots(figsize=plot_autoscale(), dpi=PLOT_DPI) + + # Generate random portfolios + n_samples = ns_parser.n_port + w = np.random.dirichlet(np.ones(len(mu)), n_samples) + rets = w.dot(mu) + stds = np.sqrt(np.diag(w @ S @ w.T)) + sharpes = rets / stds + ax.scatter(stds, rets, marker=".", c=sharpes, cmap="viridis_r") + + plotting.plot_efficient_frontier(ef, ax=ax, show_assets=True) + # Find the tangency portfolio + ef.max_sharpe() + ret_sharpe, std_sharpe, _ = ef.portfolio_performance() + ax.scatter(std_sharpe, ret_sharpe, marker="*", s=100, c="r", label="Max Sharpe") + + ax.set_title("Efficient Frontier") + ax.legend() + plt.tight_layout() + plt.grid(b=True, which="major", color="#666666", linestyle="-") + plt.minorticks_on() + plt.grid(b=True, which="minor", color="#999999", linestyle="-", alpha=0.2) + + if gtff.USE_ION: + plt.ion() + + plt.show() + print("") + + except Exception as e: + print(e) + print("") + + +def ef_portfolio(list_of_stocks: List[str], port_type: str, other_args: List[str]): """ - Return a portfolio that maximizes the sharpe ratio. Currently defaulting to 3m of historical data + Return a portfolio based on condition in port_type Currently defaulting to 3m of historical data Parameters ---------- list_of_stocks: List[str] List of the stocks to be included in the weights - + port_type: str + Method to be used on ef object (example: max_sharpe, min_volatility) Returns ------- weights: dict Dictionary of weights where keys are the tickers. """ - df = yf.download(list_of_stocks, period="3mo", group_by="ticker") - df1 = pd.DataFrame(index=df.index) - # process df - for stock in list_of_stocks: - df1[stock] = df[stock]["Adj Close"] + parser = argparse.ArgumentParser(add_help=False, prog="ef") - mu = expected_returns.mean_historical_return(df1) - S = risk_models.sample_cov(df1) - ef = EfficientFrontier(mu, S) - weights = ef.max_sharpe() - ef.portfolio_performance(verbose=True) - return weights + parser.add_argument( + "-p", + "--period", + default="3mo", + dest="period", + help="period to get yfinance data from", + choices=period_choices, + ) + + if port_type == "max_sharpe": + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return {} + period = ns_parser.period + stock_prices = process_stocks(list_of_stocks, period) + ef = prepare_efficient_frontier(stock_prices) + ef_sharpe = dict(ef.max_sharpe()) + weights = {key: round(value, 5) for key, value in ef_sharpe.items()} + print("") + ef.portfolio_performance(verbose=True) + print("") + return weights + except Exception as e: + print(e) + print("") + elif port_type == "min_volatility": + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return {} + period = ns_parser.period + stock_prices = process_stocks(list_of_stocks, period) + ef = prepare_efficient_frontier(stock_prices) + ef_min_vol = dict(ef.min_volatility()) + weights = {key: round(value, 5) for key, value in ef_min_vol.items()} + print("") + ef.portfolio_performance(verbose=True) + print("") + return weights + except Exception as e: + print(e) + print("") + return {} + + elif port_type == "eff_risk": + + parser.add_argument("-r", "--risk", type=float, dest="risk_level", default=0.1) + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + stock_prices = process_stocks(list_of_stocks, ns_parser.period) + ef = prepare_efficient_frontier(stock_prices) + ef_eff_risk = dict(ef.efficient_risk(ns_parser.risk_level)) + weights = {key: round(value, 5) for key, value in ef_eff_risk.items()} + print("") + ef.portfolio_performance(verbose=True) + print("") + return weights + except Exception as e: + print(e) + print("") + return {} + elif port_type == "eff_ret": + + parser.add_argument( + "-r", "--return", type=float, dest="target_return", default=0.1 + ) + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + stock_prices = process_stocks(list_of_stocks, ns_parser.period) + ef = prepare_efficient_frontier(stock_prices) + ef_eff_risk = dict(ef.efficient_return(ns_parser.target_return)) + weights = {key: round(value, 5) for key, value in ef_eff_risk.items()} + print("") + ef.portfolio_performance(verbose=True) + print("") + return weights + except Exception as e: + print(e) + print("") + return {} + + else: + raise ValueError("EF Method not found") + return {} diff --git a/gamestonk_terminal/portfolio_optimization/port_opt_helper.py b/gamestonk_terminal/portfolio_optimization/port_opt_helper.py new file mode 100644 index 00000000000..c5107aa572c --- /dev/null +++ b/gamestonk_terminal/portfolio_optimization/port_opt_helper.py @@ -0,0 +1,51 @@ +""" Portfolio Optimization Helper Functions""" +__docformat__ = "numpy" + +from typing import List +import pandas as pd +import yfinance as yf +from pypfopt.efficient_frontier import EfficientFrontier +from pypfopt import risk_models +from pypfopt import expected_returns + + +def process_stocks(list_of_stocks: List[str], period: str = "3mo") -> pd.DataFrame: + """ + + Parameters + ---------- + list_of_stocks: List[str] + List of tickers to get historical data for + period: str + Period to get data from yfinance + + Returns + ------- + stock_closes: DataFrame + DataFrame containing daily (adjusted) close prices for each stock in list + """ + stock_prices = yf.download(list_of_stocks, period=period, group_by="ticker") + stock_closes = pd.DataFrame(index=stock_prices.index) + # process df + for stock in list_of_stocks: + stock_closes[stock] = stock_prices[stock]["Adj Close"] + return stock_closes + + +def prepare_efficient_frontier(stock_prices: pd.DataFrame): + """ + Take in a dataframe of prices and return an efficient frontier object + Parameters + ---------- + stock_prices : DataFrame + DataFrame where indices are DateTime and columns are stocks + + Returns + ------- + ef: EfficientFrontier + EfficientFrontier object + """ + mu = expected_returns.mean_historical_return(stock_prices) + S = risk_models.sample_cov(stock_prices) + ef = EfficientFrontier(mu, S) + return ef diff --git a/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py b/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py index 6c7db144f96..1adb748df92 100644 --- a/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py +++ b/gamestonk_terminal/portfolio_optimization/port_optimization_controller.py @@ -4,7 +4,7 @@ __docformat__ = "numpy" import argparse from typing import List, Set -from datetime import datetime +import matplotlib.pyplot as plt import pandas as pd from gamestonk_terminal import feature_flags as gtff from gamestonk_terminal.helper_funcs import get_flair, parse_known_args_and_warn @@ -30,6 +30,11 @@ class PortfolioOptimization: "mkt_cap", "div_yield", "max_sharpe", + "min_vol", + "eff_risk", + "eff_ret", + "show_ef", + "yolo", ] def __init__(self, tickers: Set[str] = None): @@ -65,7 +70,13 @@ class PortfolioOptimization: print(" mkt_cap marketcap weighted portfolio") print(" div_yield dividend weighted portfolio\n") print(" Mean Variance Optimization :") - print(" max_sharpe portfolio with maximum sharpe ratio\n") + print(" max_sharpe portfolio with maximum sharpe ratio") + print(" min_vol portfolio with minimum volatility") + print(" eff_risk portfolio that maximizes returns at given risk") + print(" eff_ret portfolio that minimizes risk at given return") + print(" show_ef show the efficient frontier") + print("") + plt.close("all") def switch(self, an_input: str): """Process and dispatch input @@ -128,11 +139,39 @@ class PortfolioOptimization: print("") def call_max_sharpe(self, other_args: List[str]): - weights = po_api.max_sharpe(self.tickers, other_args) + weights = po_api.ef_portfolio(self.tickers, "max_sharpe", other_args) print("Maximum Sharpe Weights:") print(weights) print("") + def call_min_vol(self, other_args: List[str]): + weights = po_api.ef_portfolio(self.tickers, "min_volatility", other_args) + print("Minimum volatility Weights:") + print(weights) + print("") + + def call_eff_risk(self, other_args: List[str]): + weights = po_api.ef_portfolio(self.tickers, "eff_risk", other_args) + print("Weights for max returns at risk level") + print(weights) + print("") + + def call_eff_ret(self, other_args: List[str]): + weights = po_api.ef_portfolio(self.tickers, "eff_ret", other_args) + print("Weights for min risk at target returns") + print(weights) + print("") + + def call_show_ef(self, other_args): + po_api.show_ef(self.tickers, other_args) + print("") + + def call_yolo(self, _): + # Easter egg :) + print("DFV YOLO") + print({"GME": 200}) + print("") + @staticmethod def add_stocks(self, other_args: List[str]): diff --git a/terminal.py b/terminal.py index 123c73a78da..b47ba357026 100644 --- a/terminal.py +++ b/terminal.py @@ -31,7 +31,9 @@ from gamestonk_terminal.residuals_analysis import ra_controller from gamestonk_terminal.portfolio import port_controller from gamestonk_terminal.cryptocurrency import crypto_controller from gamestonk_terminal.screener import screener_controller -from gamestonk_terminal.portfolio_optimization import port_optimization_controller as po_controller +from gamestonk_terminal.portfolio_optimization import ( + port_optimization_controller as po_controller, +) # import warnings # warnings.simplefilter("always") @@ -89,7 +91,7 @@ def main(): "pa", "crypto", "ra", - "po" + "po", ] menu_parser.add_argument("opt", choices=choices)