diff --git a/ROADMAP.md b/ROADMAP.md index 1642af3fe10..f5c466f3a9a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -142,8 +142,13 @@ * [ ] Add Brokers (td, webull, etc) (@jmaslek) * [ ] Refactoring * [ ] Summaries / tear sheets (@jmaslek) -* [ ] Portfolio Optimization (@jmaslek) +___ +##Portfolio Optimization +* [x] Basic Optimization through PyPortFolioOpt(@jmaslek) - [PR #329](https://github.com/DidierRLopes/GamestonkTerminal/pull/329) +**NEXT** +* [ ] Decide what features should be present +* [ ] Allow for more custom optimization constrains --- ## FRED diff --git a/gamestonk_terminal/README.md b/gamestonk_terminal/README.md index 4ab6d9c5790..2b6d79c7b2e 100644 --- a/gamestonk_terminal/README.md +++ b/gamestonk_terminal/README.md @@ -241,6 +241,21 @@ Command|Description|Brokers `hold` | view net holdings across all logins   +## Portfolio Optimization [»](portfolio_optimization/README.md) +command|desciription +------|------ +`equal_weight`|Returns equal weights for all selected stocks +`mkt_cap`| Returns weights that are weighted by market cap +`div_yield`|Returns weights that are weighted by dividend yield +`max_sharpe`| Returns portfolio of Efficient Frontier with maximum sharpe ratio +`min_vol`| Returns portfolio on Efficient Frontier with minimum volatility +`eff_risk`| Returns portfolio on Efficient Frontier that maximizes returns at a given risk +`eff_ret`| Returns portfolio on Efficient Frontier that minimizes risk at a given return level +`show_eff`|Plots random portfolios and shows the Efficient Frontier + +  + + ## Cryptocurrency [»](cryptocurrency/README.md) Command|Description diff --git a/gamestonk_terminal/comparison_analysis/ca_controller.py b/gamestonk_terminal/comparison_analysis/ca_controller.py index 3214c4c6b76..f999ed092af 100644 --- a/gamestonk_terminal/comparison_analysis/ca_controller.py +++ b/gamestonk_terminal/comparison_analysis/ca_controller.py @@ -16,6 +16,7 @@ from gamestonk_terminal.comparison_analysis import yahoo_finance_api as yf_api from gamestonk_terminal.comparison_analysis import market_watch_api as mw_api from gamestonk_terminal.comparison_analysis import finbrain_api as f_api from gamestonk_terminal.comparison_analysis import finviz_compare_view +from gamestonk_terminal.portfolio_optimization import po_controller from gamestonk_terminal.menu import session from prompt_toolkit.completion import NestedCompleter @@ -43,6 +44,7 @@ class ComparisonAnalysisController: "ownership", "performance", "technical", + "po", ] def __init__( @@ -108,6 +110,8 @@ class ComparisonAnalysisController: print(" performance brief performance comparison") print(" technical brief technical comparison") print("") + print(" > po portfolio optimization for selected tickers") + print("") return @staticmethod @@ -315,6 +319,10 @@ class ComparisonAnalysisController: """Process technical command""" finviz_compare_view.screener(other_args, "technical", self.ticker, self.similar) + def call_po(self, _): + """Open Portfolio Optimization menu with ticker and similar""" + return po_controller.menu([self.ticker] + self.similar) + def menu(stock: pd.DataFrame, ticker: str, start: datetime, interval: str): """Comparison Analysis Menu""" diff --git a/gamestonk_terminal/main_helper.py b/gamestonk_terminal/main_helper.py index 81238c99223..40c8f6714ba 100644 --- a/gamestonk_terminal/main_helper.py +++ b/gamestonk_terminal/main_helper.py @@ -60,6 +60,9 @@ def print_help(s_ticker, s_start, s_interval, b_is_market_open): ) print(" pa portfolio analysis, \t\t supports: robinhood, alpaca, ally ") print(" crypto cryptocurrencies, \t\t uses coingecko api") + print( + " po portfolio optimization, \t\t optimal portfolio weights from pyportfolioopt" + ) if s_ticker: print( diff --git a/gamestonk_terminal/portfolio_optimization/README.md b/gamestonk_terminal/portfolio_optimization/README.md new file mode 100644 index 00000000000..bf96cf77629 --- /dev/null +++ b/gamestonk_terminal/portfolio_optimization/README.md @@ -0,0 +1,169 @@ +# Portfolio Optimization + +This menu aims to discover optimal portfolios for selected stocks. + +Currently we support two methods: + +* [Property Weighting](#weighting) + * Equal Weights + * Market Cap Weighting + * Divident Yield Weighting +* [Mean-Variance-Optimization](#eff_front) + * Max sharpe ratio + * Minimum volatility + * Maximum returns at given risk level + * Minimum risk level at a given return + +## Procedure +There are three ways to load stocks to be analyzed. +* add +* select +* from ca menu +###add +Adds selected tickers to the menu to be considered + +```` +usage: add ticker1,ticker2,ticker3,... +```` +###select +Clears current list and loads selected ticker +```` +usage: select ticker1,ticker2,ticker3,... +```` + +### ca menu +From the ca menu, the loaded ticker and the selected similar tickers can be loaded by entering the `> po` menu. + +From this `po` menu, you can return to `ca` using `ca`, which will keep your loaded stock, but reset the selected stocks from that menu. + +Once your stocks are listed, you can select one of the options. +## Property weighted +* equal weights +* market cap weighted +* dividend yield weighted +### equal weights +Returns an equally weighted portfolio where the weights are 1/number of stocks. +```` +equal_weight [-v --value VALUE] [--pie] +```` +* -v/--value If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. +* --pie Flag that displays a pie chart of the allocations. + +### market cap weighted +Returns portfolio values that are weighted by their relative Market Cap. +```` +mkt_cap [-v --value VALUE] [--pie] +```` +* -v/--value If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. +* --pie Flag that displays a pie chart of the allocations. + +###dividend yield weighted +Returns portfolio values that are weighted by relative Dividend Yield. +```` +div_yield [-v --value VALUE] [--pie] +```` +* -v/--value If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. +* --pie Flag that displays a pie chart of the allocations. + +## Mean Variance Optimization + +These approaches are based off of the efficient frontier approach, which is meant to solve the following optimization problem. + + + +With constraints: + + + +\ + + +Where S is the covariance matrix between stocks and R is the expected returns. The condition that all weights add up to 1 +just implies that you want to have a net long portfolio (with no margin). +A long-short portfolio can have negative weights and usually wants to have everything add up to 0 for a market-neutral strategy. + +Currently, we do not allow for changing risk models or adding constraints. If there is something specific, please submit a feature request, or if you can +write it, feel free to add a PR! + +All of our current implementations use the [PyPortFolioOpt](#https://pyportfolioopt.readthedocs.io/en/latest/index.html) package. + +### max_sharpe +The sharpe ratio is defined as + +(Mean Returns - Risk Free Rate)/(Standard Deviation of Returns) + +The usage is: +```` +max_sharpe [-p PERIOD] [-v --value VALUE] [--pie] +```` +* -p/--period Amount of time to retrieve data from yfinance. Options are: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max and it defaults to 3mo. +* -v/--value If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. +* --pie Flag that displays a pie chart of the allocations. + +### min_vol +This portfolio minimizes the total volatility, which also means it has the smallest returns among the efficient frontier. +The usage is: +```` +min_vol [-p PERIOD] [-v --value VALUE] [--pie] +```` +* -p/--period Amount of time to retrieve data from yfinance. Options are: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max and it defaults to 3mo. +* -v/--value If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. +* --pie Flag that displays a pie chart of the allocations. + +### eff_risk +This portfolio maximizes the returns at a given risk tolerance +The usage is: +```` +eff_risk [-p PERIOD] [-r --risk RISK_LEVEL] [-v --value VALUE] [--pie] +```` +* -p/--period Amount of time to retrieve data from yfinance. Options are: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max and it defaults to 3mo. +* -r/--risk Risk tolerance. Default is 0.1 (10%) +* -v/--value If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. +* --pie Flag that displays a pie chart of the allocations. + +### eff_ret +This portfolio minimizes the risk at a given return level +The usage is: +```` +eff_ret [-p PERIOD] [-r --return] [-v --value VALUE] [--pie] +```` +* -p/--period Amount of time to retrieve data from yfinance. Options are: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max and it defaults to 3mo. +* -r/--return. Desired return. Default is 0.1 (10%) +* -v/--value If provided, this represents an actual allocation amount for the portfolio. Defaults to 1, which just returns the weights. +* --pie Flag that displays a pie chart of the allocations. + +### show_eff +This function plots random portfolios based on their risk and returns and shows the efficient frontier. +The usage is: +```` +show_eff [-p PERIOD] [-n N_PORTFOLIOS] +```` +* -p/--period Amount of time to retrieve data from yfinance. Options are: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,ytd,max and it defaults to 3mo. +* -n Number of portfolios to simulate. + +##Sample Usage +In this example, we generate weights for a list of 6 stocks using the eff_ret command. This optimization looks to maximize returns +at a given risk level. We start by adding the stocks we want to analyze: +```` +select aapl,amzn,msft,f,gm,ge +```` +Which shows: +```` +Current Tickers: GE, GM, AMZN, AAPL, F, MSFT +```` +To perform the optimization, we will set a target return of 25% (.25). Given how stocks performed during COVID, +there is a lot of volatility, so many optimizations may not get low volatility. The module also returns annualized volatility, +so the number is your portfolio volatility * `sqrt(252`. This optimization will be (including a pie chart!). Note we could also supply a different +time period, which changes the expected returns and historical volatility, which changes the optimization. We could also specify a dollar +amount that you wish to allocate using the `-v` flag. +```` +eff_ret -r .25 --pie +```` +The console will show (numbers will vary based on when this is done). +![console](https://user-images.githubusercontent.com/18151143/114740311-bd429c80-9d17-11eb-90e2-97430781431a.png) + +And the pie chart: + +![yummypie](https://user-images.githubusercontent.com/18151143/114740289-b9167f00-9d17-11eb-9c29-470785b21d09.png) + +Note that since `AAPL` had zero allocation, it was omitted from the chart. \ No newline at end of file diff --git a/gamestonk_terminal/portfolio_optimization/__init__.py b/gamestonk_terminal/portfolio_optimization/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_helper.py b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py new file mode 100644 index 00000000000..3ca81f396a2 --- /dev/null +++ b/gamestonk_terminal/portfolio_optimization/optimizer_helper.py @@ -0,0 +1,226 @@ +""" Portfolio Optimization Helper Functions""" +__docformat__ = "numpy" + +import argparse +from typing import List +import math +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +import yfinance as yf +from pypfopt.efficient_frontier import EfficientFrontier +from pypfopt import risk_models +from pypfopt import expected_returns +from gamestonk_terminal.config_plot import PLOT_DPI +from gamestonk_terminal import feature_flags as gtff +from gamestonk_terminal.helper_funcs import plot_autoscale, parse_known_args_and_warn + +title_maps = { + "max_sharpe": "Maximum Sharpe Portfolio", + "min_volatility": "Minimum Volatility Portfolio", + "eff_risk": "Maximum Return Portfolio at Risk = {:1f} %", + "eff_ret": "Minimum Volatility Portfolio at Target Return = {:.1f} %", + "equal": "Equally Weighted Portfolio", + "marketCap": "MarketCap Weighted Portfolio", + "dividendYield": "Dividend Yield Weighted Portfolio", +} + + +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, progress=False, group_by="ticker" + ) + stock_closes = pd.DataFrame(index=stock_prices.index) + 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 + + +def display_weights(weights: dict): + """ + Print weights in a nice format + Parameters + ---------- + weights: dict + weights to display. Keys are stocks. Values are either weights or values if -v specified + """ + if not weights: + return + weight_df = pd.DataFrame.from_dict(data=weights, orient="index", columns=["value"]) + if math.isclose(weight_df.sum()["value"], 1, rel_tol=0.1): + weight_df["weight"] = (weight_df["value"] * 100).astype(str).apply( + lambda s: s[:6] + ) + " %" + print(pd.DataFrame(weight_df["weight"])) + else: + print(weight_df) + + +def my_autopct(x): + """Function for autopct of plt.pie. This results in values not being printed in the pie if they are 'too small'""" + if x > 4: + return f"{x:.2f} %" + else: + return "" + + +def pie_chart_weights(weights: dict, optimizer: str, value: float): + """ + Show a pie chart of holdings + Parameters + ---------- + weights: dict + weights to display. Keys are stocks. Values are either weights or values if -v specified + optimzer: str + Optimization technique used for title + """ + plt.close("all") + if not weights: + return + + init_stocks = list(weights.keys()) + init_sizes = list(weights.values()) + stocks = [] + sizes = [] + for stock, size in zip(init_stocks, init_sizes): + if size > 0: + stocks.append(stock) + sizes.append(size) + + total_size = np.sum(sizes) + + leg_labels = [ + f"{str(a)}: {str(round(100*b/total_size,3))[:4]}%" + for a, b in zip(stocks, sizes) + ] + plt.figure(figsize=plot_autoscale(), dpi=PLOT_DPI) + if math.isclose(sum(sizes), 1, rel_tol=0.1): + wedges, _, autotexts = plt.pie( + sizes, + labels=stocks, + autopct=my_autopct, + textprops=dict(color="k"), + explode=[s / (5 * total_size) for s in sizes], + normalize=True, + shadow=True, + ) + else: + wedges, _, autotexts = plt.pie( + sizes, + labels=stocks, + autopct="", + textprops=dict(color="k"), + explode=[s / (5 * total_size) for s in sizes], + normalize=True, + shadow=True, + ) + for i, a in enumerate(autotexts): + if sizes[i] / total_size > 0.05: + a.set_text(f"{sizes[i]:.2f}") + else: + a.set_text("") + + plt.axis("equal") + + plt.legend( + wedges, + leg_labels, + title="Stocks", + loc="center left", + bbox_to_anchor=(0.85, 0, 0.5, 1), + ) + + plt.setp(autotexts, size=8, weight="bold") + + if optimizer in ["eff_ret", "eff_risk"]: + plt.title(title_maps[optimizer].format(100 * value)) + else: + plt.title(title_maps[optimizer]) + + if gtff.USE_ION: + plt.ion() + + plt.show() + print("") + + +def parse_from_port_type( + parser_in: argparse.ArgumentParser, port_type: str, other_args: List[str] +): + """ + + Parameters + ---------- + parser_in: ArgumentParser + parser to get data from + port_type: str + Type of optimization that will be done. One of ["max_sharpe","min_vol", "eff_risk", "eff_ret"] + other_args: List[str] + Arguments passed to function + Returns + ------- + + ns_parser: + Parsed arguments + """ + + if port_type in ["max_sharpe", "min_volatility"]: + ns_parser = parse_known_args_and_warn(parser_in, other_args) + if not ns_parser: + return None + return ns_parser + + elif port_type == "eff_risk": + parser_in.add_argument( + "-r", "--risk", type=float, dest="risk_level", default=0.1 + ) + ns_parser = parse_known_args_and_warn(parser_in, other_args) + if not ns_parser: + return None + return ns_parser + + elif port_type == "eff_ret": + parser_in.add_argument( + "-r", "--return", type=float, dest="target_return", default=0.1 + ) + + ns_parser = parse_known_args_and_warn(parser_in, other_args) + if not ns_parser: + return None + return ns_parser + else: + print("Incorrect portfolio optimizer type\n") + return None diff --git a/gamestonk_terminal/portfolio_optimization/optimizer_view.py b/gamestonk_terminal/portfolio_optimization/optimizer_view.py new file mode 100644 index 00000000000..e9d74a76138 --- /dev/null +++ b/gamestonk_terminal/portfolio_optimization/optimizer_view.py @@ -0,0 +1,337 @@ +""" Portfolio Optimization Functions""" +__docformat__ = "numpy" + +import argparse +from typing import List +import matplotlib.pyplot as plt +import numpy as np +import yfinance as yf +from pypfopt import plotting +from pypfopt import risk_models +from pypfopt import expected_returns +from pypfopt import EfficientFrontier +from gamestonk_terminal.helper_funcs import parse_known_args_and_warn, plot_autoscale +from gamestonk_terminal.portfolio_optimization.optimizer_helper import ( + process_stocks, + prepare_efficient_frontier, + pie_chart_weights, + display_weights, + parse_from_port_type, +) +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(stocks: List[str], other_args: List[str]): + """ + Equally weighted portfolio, where weight = 1/# of stocks + + Parameters + ---------- + stocks: List[str] + List of tickers to be included in optimization + + Returns + ------- + weights : dict + Dictionary of weights where keys are the tickers + + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="equal_weight", + description="Return equally weighted portfolio holdings", + ) + + parser.add_argument( + "-v", + "--value", + default=1, + type=float, + dest="value", + help="Amount to allocate to portfolio", + ) + parser.add_argument( + "--pie", + action="store_true", + dest="pie", + default=False, + help="Display a pie chart for weights", + ) + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + values = {} + n_stocks = len(stocks) + for stock in stocks: + values[stock] = ns_parser.value * round(1 / n_stocks, 5) + if ns_parser.pie: + pie_chart_weights(values, "equal", 0) + if n_stocks >= 1: + print("Equal Weight Portfolio: ") + display_weights(values) + print("") + + except Exception as e: + print(e) + print("") + + +def property_weighting(stocks: List[str], property_type: str, other_args: List[str]): + """ + Property weighted portfolio where each weight is the relative fraction. + Parameters + ---------- + stocks: List[str] + List of tickers to be included in optimization + property_type: str + Property to weight by. Can be anything in yfinance.Ticker().info. Examples: + "marketCap", "dividendYield", etc + + Returns + ------- + weights: dict + Dictionary of weights where keys are the tickers + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="market_cap_weighted", + description="Return portfolio weights/values that are weighted by marketcap", + ) + + parser.add_argument( + "-v", + "--value", + default=1, + type=float, + dest="value", + help="Amount to allocate to portfolio", + ) + parser.add_argument( + "--pie", + action="store_true", + dest="pie", + default=False, + help="Display a pie chart for weights", + ) + weights = {} + prop = {} + prop_sum = 0 + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + for stock in stocks: + stock_prop = yf.Ticker(stock).info[property_type] + if stock_prop is None: + stock_prop = 0 + prop[stock] = stock_prop + prop_sum += stock_prop + for k, v in prop.items(): + weights[k] = round(v / prop_sum, 5) * ns_parser.value + + if ns_parser.pie: + pie_chart_weights(weights, property_type, 0) + + if property_type == "marketCap": + print("Market Cap Weighted Portfolio: ") + + elif property_type == "dividendYield": + print("Dividend Yield Weighted Portfolio: ") + + if len(stocks) >= 1: + display_weights(weights) + print("") + + except Exception as e: + print(e) + print("") + return + + +def ef_portfolio(stocks: List[str], port_type: str, other_args: List[str]): + """ + Return a portfolio based on condition in port_type Defaults to 3m of historical data + Parameters + ---------- + 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. + """ + + parser = argparse.ArgumentParser(add_help=False, prog=port_type) + + parser.add_argument( + "-p", + "--period", + default="3mo", + dest="period", + help="period to get yfinance data from", + choices=period_choices, + ) + + parser.add_argument( + "-v", + "--value", + dest="value", + help="Amount to allocate to portfolio", + type=float, + default=1.0, + ) + parser.add_argument( + "--pie", + action="store_true", + dest="pie", + default=False, + help="Display a pie chart for weights", + ) + + try: + ns_parser = parse_from_port_type(parser, port_type, other_args) + if not ns_parser: + return + if len(stocks) < 2: + print("Please have at least 2 loaded tickers to calculate weights.\n") + return + period = ns_parser.period + stock_prices = process_stocks(stocks, period) + ef = prepare_efficient_frontier(stock_prices) + + if port_type == "max_sharpe": + + ef_sharpe = dict(ef.max_sharpe()) + weights = { + key: ns_parser.value * round(value, 5) + for key, value in ef_sharpe.items() + } + val = 0 + print("Weights that maximize Sharpe Ratio:") + + elif port_type == "min_volatility": + + ef_min_vol = dict(ef.min_volatility()) + weights = { + key: ns_parser.value * round(value, 5) + for key, value in ef_min_vol.items() + } + val = 0 + print("Weights that minimize volatility") + + elif port_type == "eff_risk": + + ef_eff_risk = dict(ef.efficient_risk(ns_parser.risk_level)) + weights = { + key: ns_parser.value * round(value, 5) + for key, value in ef_eff_risk.items() + } + val = ns_parser.risk_level + print(f"Weights for maximizing returns at risk = {100*val:.1f} %") + + elif port_type == "eff_ret": + + ef_eff_risk = dict(ef.efficient_return(ns_parser.target_return)) + weights = { + key: ns_parser.value * round(value, 5) + for key, value in ef_eff_risk.items() + } + val = ns_parser.target_return + print(f"Weights for minimizing risk at target return = {100*val:.1f} %") + + else: + raise ValueError("EF Method not found") + + if ns_parser.pie: + pie_chart_weights(weights, port_type, val) + + print("") + ef.portfolio_performance(verbose=True) + print("") + display_weights(weights) + print("") + + except Exception as e: + print(e) + print("") + return + + +def show_ef(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 + if len(stocks) < 2: + print("Please have at least 2 loaded tickers to calculate weights.\n") + return + + stock_prices = process_stocks(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("") diff --git a/gamestonk_terminal/portfolio_optimization/po_controller.py b/gamestonk_terminal/portfolio_optimization/po_controller.py new file mode 100644 index 00000000000..df2f8a55fa6 --- /dev/null +++ b/gamestonk_terminal/portfolio_optimization/po_controller.py @@ -0,0 +1,206 @@ +"""Portfolio Optimization Controller Module""" +__docformat__ = "numpy" + + +import argparse +from typing import List +import matplotlib.pyplot as plt +from prompt_toolkit.completion import NestedCompleter +from gamestonk_terminal import feature_flags as gtff +from gamestonk_terminal.helper_funcs import get_flair, parse_known_args_and_warn +from gamestonk_terminal.menu import session +from gamestonk_terminal.portfolio_optimization import optimizer_view as po_api + + +class PortfolioOptimization: + + CHOICES = [ + "help", + "q", + "quit", + "select", + "add", + "equal_weight", + "mkt_cap", + "div_yield", + "max_sharpe", + "min_vol", + "eff_risk", + "eff_ret", + "show_ef", + "yolo", + ] + + def __init__(self, tickers: List[str] = []): + """ + Construct Portfolio Optimization + """ + + self.po_parser = argparse.ArgumentParser(add_help=False, prog="po") + self.po_parser.add_argument("cmd", choices=self.CHOICES) + self.tickers = list(set(tickers)) + # These will allow the ca menu to be re-access + self.ca_ticker = None + self.ca_similar = None + + @staticmethod + def print_help(tickers: List[str]): + """Print help""" + print("\nPortfolio Optimization:") + print(" help show this menu again") + print(" q quit this menu, and shows back to main menu") + print(" quit quit to abandon program") + print(f"\nCurrent Tickers: {('None', ', '.join(tickers))[bool(tickers)]}") + print("") + print(" add add ticker to optimize") + print(" select overwrite current tickers with new tickers") + print("") + print("Optimization:") + print("") + print(" equal_weight equally weighted portfolio") + print(" mkt_cap marketcap weighted portfolio") + print(" div_yield dividend weighted portfolio") + 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 + + Returns + ------- + True, False or None + False - quit the menu + True - quit the program + None - continue in the menu + """ + (known_args, other_args) = self.po_parser.parse_known_args(an_input.split()) + + return getattr( + self, "call_" + known_args.cmd, lambda: "Command not recognized!" + )(other_args) + + def call_help(self, _): + """Process Help command""" + self.print_help(self.tickers) + + def call_q(self, _): + """Process Q command - quit the menu""" + return False + + def call_quit(self, _): + """Process Quit command - quit the program""" + return True + + def call_add(self, other_args: List[str]): + self.add_stocks(self, other_args) + + def call_select(self, other_args: List[str]): + self.tickers = [] + self.add_stocks(self, other_args) + + def call_equal_weight(self, other_args: List[str]): + po_api.equal_weight(self.tickers, other_args) + + def call_mkt_cap(self, other_args: List[str]): + po_api.property_weighting(self.tickers, "marketCap", other_args) + + def call_div_yield(self, other_args: List[str]): + po_api.property_weighting(self.tickers, "dividendYield", other_args) + + def call_max_sharpe(self, other_args: List[str]): + po_api.ef_portfolio(self.tickers, "max_sharpe", other_args) + + def call_min_vol(self, other_args: List[str]): + po_api.ef_portfolio(self.tickers, "min_volatility", other_args) + + def call_eff_risk(self, other_args: List[str]): + po_api.ef_portfolio(self.tickers, "eff_risk", other_args) + + def call_eff_ret(self, other_args: List[str]): + po_api.ef_portfolio(self.tickers, "eff_ret", other_args) + + 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]): + """ Add ticker to current list for optimization""" + parser = argparse.ArgumentParser( + add_help=False, + prog="add", + description="""Add tickers for optimizing.""", + ) + parser.add_argument( + "-t", + "--tickers", + dest="add_tickers", + type=lambda s: [str(item).upper() for item in s.split(",")], + default=[], + help="add tickers to optimzation.", + ) + try: + + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-t") + + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + tickers = set(self.tickers) + for ticker in ns_parser.add_tickers: + tickers.add(ticker) + self.tickers = list(tickers) + + print( + f"\nCurrent Tickers: {('None', ', '.join(self.tickers))[bool(self.tickers)]}" + ) + print("") + except Exception as e: + print(e) + + print("") + + +def menu(tickers: List[str]): + """Portfolio Optimization Menu""" + if tickers == [""]: + tickers = [] + plt.close("all") + po_controller = PortfolioOptimization(tickers) + po_controller.call_help(tickers) + + while True: + # Get input command from user + if session and gtff.USE_PROMPT_TOOLKIT: + completer = NestedCompleter.from_nested_dict( + {c: None for c in po_controller.CHOICES} + ) + an_input = session.prompt( + f"{get_flair()} (po)> ", + completer=completer, + ) + else: + an_input = input(f"{get_flair()} (po)> ") + + try: + process_input = po_controller.switch(an_input) + + if process_input is not None: + return process_input + + except SystemExit: + print("The command selected doesn't exist\n") + continue diff --git a/gamestonk_terminal/screener/README.md b/gamestonk_terminal/screener/README.md index b4f46eb644d..6271a7684db 100644 --- a/gamestonk_terminal/screener/README.md +++ b/gamestonk_terminal/screener/README.md @@ -23,7 +23,8 @@ This menu aims to filter stocks based on pre-specified preset filters, and the u * contains [Oversold (-s) signal example](#signal-oversold) * [signals](#signals) * view filter signals (e.g. -s top_gainers) [Finviz] - +* [po](#port_opt) + * go to the portfolio optimization menu ## view @@ -180,3 +181,13 @@ Prints list of available signals. [Source: Finviz] Captura de ecrã 2021-04-05, às 20 25 13 +## po +Goes to the portfolio menu with list of passed stocks. In order to pass the stocks, just type the tickers of interest, no commas. + +``` +usage: po ticker1 ticker2 ... +``` + +```` +example: po aapl msft tsla gme +```` \ No newline at end of file diff --git a/gamestonk_terminal/screener/screener_controller.py b/gamestonk_terminal/screener/screener_controller.py index cc34b3e80ab..0fe364750b0 100644 --- a/gamestonk_terminal/screener/screener_controller.py +++ b/gamestonk_terminal/screener/screener_controller.py @@ -14,6 +14,7 @@ from gamestonk_terminal.helper_funcs import ( ) from gamestonk_terminal.screener import finviz_view from gamestonk_terminal.screener import yahoo_finance_view +from gamestonk_terminal.portfolio_optimization import po_controller class ScreenerController: @@ -34,6 +35,7 @@ class ScreenerController: "performance", "technical", "signals", + "po", ] def __init__(self): @@ -69,6 +71,8 @@ class ScreenerController: print("") print(" signals view filter signals (e.g. -s top_gainers)") print("") + print(" > po go to the portfolio optimization menu") + print("") @staticmethod def view_available_presets(other_args: List[str]): @@ -217,6 +221,10 @@ class ScreenerController: """Process signals command""" finviz_view.view_signals(other_args) + def call_po(self, other_args: List[str]): + """Call the portfolio optimization menu""" + return po_controller.menu(other_args) + def menu(): """Screener Menu""" diff --git a/poetry.lock b/poetry.lock index b20746d369b..5cbefaf1f5d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -379,6 +379,21 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "cvxpy" +version = "1.1.12" +description = "A domain-specific language for modeling convex optimization problems in Python." +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +ecos = ">=2" +numpy = ">=1.15" +osqp = ">=0.4.1" +scipy = ">=1.1.0" +scs = ">=1.1.6" + [[package]] name = "cycler" version = "0.10.0" @@ -456,6 +471,18 @@ category = "main" optional = false python-versions = ">=3.6" +[[package]] +name = "ecos" +version = "2.0.7.post1" +description = "This is the Python package for ECOS: Embedded Cone Solver. See Github page for more information." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +numpy = ">=1.6" +scipy = ">=0.9" + [[package]] name = "entrypoints" version = "0.3" @@ -1491,6 +1518,19 @@ numpy = ">=1.7" docs = ["sphinx (==1.2.3)", "sphinxcontrib-napoleon", "sphinx-rtd-theme", "numpydoc"] tests = ["pytest", "pytest-cov", "pytest-pep8"] +[[package]] +name = "osqp" +version = "0.6.2.post0" +description = "OSQP: The Operator Splitting QP Solver" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +numpy = ">=1.7" +qdldl = "*" +scipy = ">=0.13.2" + [[package]] name = "overrides" version = "3.1.0" @@ -1930,6 +1970,23 @@ category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +[[package]] +name = "pyportfolioopt" +version = "1.4.1" +description = "Financial portfolio optimization in python" +category = "main" +optional = false +python-versions = ">=3.6.1,<4.0.0" + +[package.dependencies] +cvxpy = ">=1.1.10,<2.0.0" +numpy = ">=1.12,<2.0" +pandas = ">=0.19" +scipy = ">=1.3,<2.0" + +[package.extras] +optionals = ["cvxopt (>=1.2,!=1.2.5.post1,<2.0)", "scikit-learn (>=0.24.1,<0.25.0)", "matplotlib (>=3.2.0,<4.0.0)"] + [[package]] name = "pyrsistent" version = "0.14.11" @@ -2073,6 +2130,18 @@ python-versions = ">=3.6" cffi = {version = "*", markers = "implementation_name == \"pypy\""} py = {version = "*", markers = "implementation_name == \"pypy\""} +[[package]] +name = "qdldl" +version = "0.1.5.post0" +description = "QDLDL, a free LDL factorization routine." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +numpy = ">=1.7" +scipy = ">=0.13.2" + [[package]] name = "qtconsole" version = "5.0.3" @@ -2265,6 +2334,18 @@ Cython = {version = "*", markers = "sys_platform == \"darwin\""} dataclasses = {version = "*", markers = "python_version < \"3.7\""} pyobjc-framework-Cocoa = {version = "*", markers = "sys_platform == \"darwin\""} +[[package]] +name = "scs" +version = "2.1.2" +description = "scs: splitting conic solver" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +numpy = ">=1.7" +scipy = ">=0.13.2" + [[package]] name = "seaborn" version = "0.11.1" @@ -2837,7 +2918,7 @@ prediction = ["fbprophet", "tensorflow", "pmdarima", "transformers", "flair", "t [metadata] lock-version = "1.1" python-versions = "^3.6.8" -content-hash = "144771641ef753b1e937c0db16496c409905f6f692ef81a03d9ba716d9207bb0" +content-hash = "5b3abe79834fc75092d1d9e8e3a809252f56eddc170fb61a93c36ffef9328b73" [metadata.files] absl-py = [ @@ -3063,6 +3144,20 @@ cssselect = [ {file = "cssselect-1.1.0-py2.py3-none-any.whl", hash = "sha256:f612ee47b749c877ebae5bb77035d8f4202c6ad0f0fc1271b3c18ad6c4468ecf"}, {file = "cssselect-1.1.0.tar.gz", hash = "sha256:f95f8dedd925fd8f54edb3d2dfb44c190d9d18512377d3c1e2388d16126879bc"}, ] +cvxpy = [ + {file = "cvxpy-1.1.12-cp36-cp36m-win_amd64.whl", hash = "sha256:86f8e4aa1a79482431b89aa526249d9fda04c74e04a3ec224d1175d281643d71"}, + {file = "cvxpy-1.1.12-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:90b09a7361b9c614166fa673e1fe9321317df6939135330e1621b9a0bf10e3a8"}, + {file = "cvxpy-1.1.12-cp37-cp37m-win_amd64.whl", hash = "sha256:084e5292f2a13133a1cd28d68f5af1df33a5119b0f1aec7cecf834eea3e2d11d"}, + {file = "cvxpy-1.1.12-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:575f6e1c70274008961edff97a7494701faed20b40fbd49327010c0e7d186256"}, + {file = "cvxpy-1.1.12-cp38-cp38-win_amd64.whl", hash = "sha256:76d04444cb50ad2840bbc72a625ac246b5eeeeb580ee20c71bec057822667738"}, + {file = "cvxpy-1.1.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2de5e0e75316839bc71b5df2c2db0131cee7ef06dac566c6c44dc852c091502b"}, + {file = "cvxpy-1.1.12-cp39-cp39-win_amd64.whl", hash = "sha256:9fa4d033ab31855aea090e4b358017b36880390a7d0e6e97ecc5226c77eab489"}, + {file = "cvxpy-1.1.12-py3.6-win-amd64.egg", hash = "sha256:d2c28e0eea1b6dc70262c9ed8b63c5c1747d85c28b3ec24cbaab07583b3ad729"}, + {file = "cvxpy-1.1.12-py3.7-win-amd64.egg", hash = "sha256:44d0e3d60693831ac70bdc9b2e23faf84401347bfcdef4eb5da5ae22ac57f5ff"}, + {file = "cvxpy-1.1.12-py3.8-win-amd64.egg", hash = "sha256:c86f8de9a63ccbd45844a4482df8099824688b5329060f089aad1c3beb939190"}, + {file = "cvxpy-1.1.12-py3.9-win-amd64.egg", hash = "sha256:08c720706abd94c25903a4db5dd2171232e72d0969b874a5de9a8181f96357d5"}, + {file = "cvxpy-1.1.12.tar.gz", hash = "sha256:b499ebfae4fc645e9523621573ffcb1c5b6854a1b5c0ce1d66a8a9a458605807"}, +] cycler = [ {file = "cycler-0.10.0-py2.py3-none-any.whl", hash = "sha256:1d8a5ae1ff6c5cf9b93e8811e581232ad8920aeec647c37316ceac982b08cb2d"}, {file = "cycler-0.10.0.tar.gz", hash = "sha256:cd7b2d1018258d7247a71425e9f26463dfb444d411c39569972f4ce586b0c9d8"}, @@ -3126,6 +3221,30 @@ detecta = [ {file = "detecta-0.0.5-py3-none-any.whl", hash = "sha256:dbaf1938e5d785386c904034e2553824e2aa31793e967984ca65983aca06d23e"}, {file = "detecta-0.0.5.tar.gz", hash = "sha256:d2ea7d13dfbbc994d6ce385a7f8dc0a85fe675a8a8e712a64ec56e54c40603ed"}, ] +ecos = [ + {file = "ecos-2.0.7.post1-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:dd9f01e28fe58894fb394931804884122606fb4e2a59d4514b803e9cd11b7d2b"}, + {file = "ecos-2.0.7.post1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:db7433051f6072d4821ebc582e9ff853d7d631ed98770550d248eae70b29dd26"}, + {file = "ecos-2.0.7.post1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:96ddc1c4e440820bb343c44785da480a64a9c468ec467b997e40f0c7d3236226"}, + {file = "ecos-2.0.7.post1-cp27-cp27m-win32.whl", hash = "sha256:574fa26661d192e48551a30e217296875020fdb7eebf9a072a14d68a0b9de03a"}, + {file = "ecos-2.0.7.post1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7c871b7a49a5855e7df0d60a06d0e2148a4b183d612f7db5e155988629c3ec21"}, + {file = "ecos-2.0.7.post1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:845455f99cd579ee0cdfbcea675b4e4f7674b563e6a54a225977fc0819cda7e0"}, + {file = "ecos-2.0.7.post1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:e002df0f4b6777be68c73756e60f3cf76cb5f2f7d36c4f1a482c5538aac1a287"}, + {file = "ecos-2.0.7.post1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:feda86ddd191b1ae34d5ea615743894d1baa800f2dbb552cb7c7095a87037831"}, + {file = "ecos-2.0.7.post1-cp34-cp34m-win32.whl", hash = "sha256:6b0829b76ba49f6ebf8b9512673a0b702b756704927cbb106043e1b2273dadea"}, + {file = "ecos-2.0.7.post1-cp34-cp34m-win_amd64.whl", hash = "sha256:54f7c480029fbfa738ddb8e538388868b2a17c9189a426d924ca0c36e4cbbbd5"}, + {file = "ecos-2.0.7.post1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1f811e7244a58a7037474b3dead85c7ffd524cf631c90584cc544e438f114cf9"}, + {file = "ecos-2.0.7.post1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:72657189f71dbde01d1df841d2139e04da17071c8d3270919c8501f239d8c8e4"}, + {file = "ecos-2.0.7.post1-cp35-cp35m-win32.whl", hash = "sha256:30e7e9c5ad8a012ba1f69aa6827beb99f208323f678a82fcccc30e4ab8090aad"}, + {file = "ecos-2.0.7.post1-cp35-cp35m-win_amd64.whl", hash = "sha256:94dd0f82a18550232e4924e6c42730c46d7cdc03c4e2dc889e98ec97c0f24061"}, + {file = "ecos-2.0.7.post1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:361758a3568eb5a3a9b37c00a22b36f9548bbd0cd21f1da904a3627285bb4274"}, + {file = "ecos-2.0.7.post1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:831acb6bac205025ffe87002d8f425d2764a70db3d9d053a1f7e0e50bc2a18b9"}, + {file = "ecos-2.0.7.post1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:912b17f72476aff33b32e92decd7c02ddb929db28227563b3750948783cff6f4"}, + {file = "ecos-2.0.7.post1-cp36-cp36m-win32.whl", hash = "sha256:4fdfee011853cd07d494ef58a9299b1f0ccf0268c375109cb2b60727f746ea74"}, + {file = "ecos-2.0.7.post1-cp36-cp36m-win_amd64.whl", hash = "sha256:fb64fb29aef26474f807df4f0c198a6d192291edc9faa3bb05e3bc9b1e2b960a"}, + {file = "ecos-2.0.7.post1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4b3068ef023c4f39f6c0d6fd73d27d8a8008f8b14e1f4ddd8d0f4876e841b986"}, + {file = "ecos-2.0.7.post1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:4f775caf828597d094cddae54f7ed12b4a5aa760f535a92ccabec47741e4d61c"}, + {file = "ecos-2.0.7.post1.tar.gz", hash = "sha256:83e90f42b3f32e2a93f255c3cfad2da78dbd859119e93844c45d2fca20bdc758"}, +] entrypoints = [ {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"}, {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"}, @@ -3810,6 +3929,29 @@ opt-einsum = [ {file = "opt_einsum-3.3.0-py3-none-any.whl", hash = "sha256:2455e59e3947d3c275477df7f5205b30635e266fe6dc300e3d9f9646bfcea147"}, {file = "opt_einsum-3.3.0.tar.gz", hash = "sha256:59f6475f77bbc37dcf7cd748519c0ec60722e91e63ca114e68821c0c54a46549"}, ] +osqp = [ + {file = "osqp-0.6.2.post0-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:21047e9ceb4c2224ba15dd95f5125e2dcabd178aa93d8efa407e67216ea4fe62"}, + {file = "osqp-0.6.2.post0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:598a7bb1534a0ab69bcaf22d6e6c4ec042a41184f007c01b7fa863b528c6b61d"}, + {file = "osqp-0.6.2.post0-cp35-cp35m-manylinux2014_x86_64.whl", hash = "sha256:6b745ba55add8be63eaee059858b7c0cf59925074fcc8355c17c3863fad01a95"}, + {file = "osqp-0.6.2.post0-cp35-cp35m-win_amd64.whl", hash = "sha256:1104dd56d6bfe5ac5e5e076e1cf9e2be76932cadae4e0cfbdf495f3f43e4f8fa"}, + {file = "osqp-0.6.2.post0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6f89feaf7c30dfb5031b370e8c45aed23a60d6471606bad9b7d23506ddf807e6"}, + {file = "osqp-0.6.2.post0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:198b66cbe9326ef181f7598856aae20dcc5b7824b119cc119b412206e858bc28"}, + {file = "osqp-0.6.2.post0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:9aaecd3ed00a99d063e5bd45ebce965d1ebc8f4754d6c05ab5d31c3577a68513"}, + {file = "osqp-0.6.2.post0-cp36-cp36m-win_amd64.whl", hash = "sha256:2d150d880dc5bee015aadac6cff32ab0c2721cd4bb8a3f5b1f0cec819fe70b39"}, + {file = "osqp-0.6.2.post0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:20cd6a352694cc182bb79a8e19e4e03b7970ecb4b95d6abf86e3c578a45783a9"}, + {file = "osqp-0.6.2.post0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:96e42524dd156f62ac8e35d71831588acb2919b5ba27e0e695c757539708dfa5"}, + {file = "osqp-0.6.2.post0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:d6dd2b70ffcee6089758518620087a5909a10621a59493be8ce0ef2a74502a27"}, + {file = "osqp-0.6.2.post0-cp37-cp37m-win_amd64.whl", hash = "sha256:7c9f7e661fe2da984eab6533a56b5366f10a7360a28b8dd18c222fcf6655e247"}, + {file = "osqp-0.6.2.post0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b102c26b2017b039032797a388548dcfe11dbd3d06b3294721fa65976fe1065e"}, + {file = "osqp-0.6.2.post0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a835746179c81944e51a6a2e0efb0aca66402be351af912bfd682527f15a35a7"}, + {file = "osqp-0.6.2.post0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:57f5026b91bb8f39059576c5ee50c0a1c2705d775a776e2e24be2aa47e63d8b0"}, + {file = "osqp-0.6.2.post0-cp38-cp38-win_amd64.whl", hash = "sha256:fb0d77e634362fe59ab2bedba3dde40d5357922e9719bcb4d16a99766cefd5a2"}, + {file = "osqp-0.6.2.post0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e21463d97bcab58350a41a975e9e5e4a685087b6357899a58813d2238795315"}, + {file = "osqp-0.6.2.post0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:02fdbabfffd72a8118328d117a83998800ca015182053fced9d5e8be6ffd702a"}, + {file = "osqp-0.6.2.post0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:b238b9049a29f6955f08567b285b0734dd578b529a3780d617854ffd91e5e4ed"}, + {file = "osqp-0.6.2.post0-cp39-cp39-win_amd64.whl", hash = "sha256:2c6cb14fd1365004d0e7a01f6101e16414a0348db736ee45aac8865fac165e57"}, + {file = "osqp-0.6.2.post0.tar.gz", hash = "sha256:5f0695f26a3bef0fae91254bc283fab790dcca0064bfe0f425167f9c9e8b4cbc"}, +] overrides = [ {file = "overrides-3.1.0.tar.gz", hash = "sha256:30f761124579e59884b018758c4d7794914ef02a6c038621123fec49ea7599c6"}, ] @@ -4069,6 +4211,10 @@ pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] +pyportfolioopt = [ + {file = "PyPortfolioOpt-1.4.1-py3-none-any.whl", hash = "sha256:73f0b4ac2a5d879879a7a91af038b4d7ff82e9998fb2cad7c249302294e8c09e"}, + {file = "PyPortfolioOpt-1.4.1.tar.gz", hash = "sha256:e6c8e983db92cd13caf41d87fb4dcd945d37053f28b75dd19b5be699b6b1d19e"}, +] pyrsistent = [ {file = "pyrsistent-0.14.11.tar.gz", hash = "sha256:3ca82748918eb65e2d89f222b702277099aca77e34843c5eb9d52451173970e2"}, ] @@ -4212,6 +4358,24 @@ pyzmq = [ {file = "pyzmq-22.0.3-pp37-pypy37_pp73-win32.whl", hash = "sha256:279cc9b51db48bec2db146f38e336049ac5a59e5f12fb3a8ad864e238c1c62e3"}, {file = "pyzmq-22.0.3.tar.gz", hash = "sha256:f7f63ce127980d40f3e6a5fdb87abf17ce1a7c2bd8bf2c7560e1bbce8ab1f92d"}, ] +qdldl = [ + {file = "qdldl-0.1.5.post0-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:787d59b4301608e96bdf32ab3a572d9f41b3ea08581774826720986e18da261e"}, + {file = "qdldl-0.1.5.post0-cp35-cp35m-manylinux2014_x86_64.whl", hash = "sha256:223dd49f4aa625c727c3053e9f62814fab2f929e0575b34dd1f38e66b536a849"}, + {file = "qdldl-0.1.5.post0-cp35-cp35m-win_amd64.whl", hash = "sha256:cd0090d5512a8e386534a755f9ebd8be82087d401746cebbfb9f9faa21073c39"}, + {file = "qdldl-0.1.5.post0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3e5e2ee6b45dd655120ca8d6645331c800c494ce765bc0674714ba86e1ebec7d"}, + {file = "qdldl-0.1.5.post0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:a2ab0d4618d5516101fc0ce99c873df61a085c3d7cb4d8347d5eca818795e48f"}, + {file = "qdldl-0.1.5.post0-cp36-cp36m-win_amd64.whl", hash = "sha256:ac7a2891628554de6260132fa090c5b00cf32ae083ece4991a43b887ec6f2114"}, + {file = "qdldl-0.1.5.post0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:817a226b2450e56c50f61a92b727c69f50a4d043876e5f1bcdff096c5a82bc37"}, + {file = "qdldl-0.1.5.post0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:c10f91dda6a422a6d879668fd293132c8939431fffe64abd9700dce170a3aec4"}, + {file = "qdldl-0.1.5.post0-cp37-cp37m-win_amd64.whl", hash = "sha256:e70c57ea65d0bb09708bc9ecafd4fb4b797e64af4da475404e6bcf5a2cc92596"}, + {file = "qdldl-0.1.5.post0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:64f531ef2a12cc77be85045cdffdb2fe339244c73fc07aebe49977541bfc7e79"}, + {file = "qdldl-0.1.5.post0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:cff34b89258a3c9b1b398f1c8eeb480a9c4ae2ba14c4ffc8c78cdb123dab11c6"}, + {file = "qdldl-0.1.5.post0-cp38-cp38-win_amd64.whl", hash = "sha256:3a7ec584aaddff7036d22013c911132572f3d59c65ec6bf7e32beb01765accc4"}, + {file = "qdldl-0.1.5.post0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a7c086ae21a2b2e43b31ac6a5024c79dadf16e7c3152eda9e3263cefe1675e26"}, + {file = "qdldl-0.1.5.post0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:2c09f4b1a1c6f3a0579af004443417e084491e7c844ff9fb73170bb5d43f70b5"}, + {file = "qdldl-0.1.5.post0-cp39-cp39-win_amd64.whl", hash = "sha256:640cab781f87c2f1c04ada65615b3d1d136d319c4595176f37e2e3c99149108f"}, + {file = "qdldl-0.1.5.post0.tar.gz", hash = "sha256:c392c7427651d8b226423c7aba4a0f2338a1f38a4bbdabac6bc4afd8bc934f06"}, +] qtconsole = [ {file = "qtconsole-5.0.3-py3-none-any.whl", hash = "sha256:4a38053993ca2da058f76f8d75b3d8906efbf9183de516f92f222ac8e37d9614"}, {file = "qtconsole-5.0.3.tar.gz", hash = "sha256:c091a35607d2a2432e004c4a112d241ce908086570cf68594176dd52ccaa212d"}, @@ -4359,7 +4523,6 @@ scikit-learn = [ {file = "scikit_learn-0.24.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c658432d8a20e95398f6bb95ff9731ce9dfa343fdf21eea7ec6a7edfacd4b4d9"}, {file = "scikit_learn-0.24.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9dfa564ef27e8e674aa1cc74378416d580ac4ede1136c13dd555a87996e13422"}, {file = "scikit_learn-0.24.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:9c6097b6a9b2bafc5e0f31f659e6ab5e131383209c30c9e978c5b8abdac5ed2a"}, - {file = "scikit_learn-0.24.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:5580eba7345a4d3b097be2f067cc71a306c44bab19e8717a30361f279c929bea"}, {file = "scikit_learn-0.24.1-cp36-cp36m-win32.whl", hash = "sha256:7b04691eb2f41d2c68dbda8d1bd3cb4ef421bdc43aaa56aeb6c762224552dfb6"}, {file = "scikit_learn-0.24.1-cp36-cp36m-win_amd64.whl", hash = "sha256:1adf483e91007a87171d7ce58c34b058eb5dab01b5fee6052f15841778a8ecd8"}, {file = "scikit_learn-0.24.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:ddb52d088889f5596bc4d1de981f2eca106b58243b6679e4782f3ba5096fd645"}, @@ -4367,7 +4530,6 @@ scikit-learn = [ {file = "scikit_learn-0.24.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0567a2d29ad08af98653300c623bd8477b448fe66ced7198bef4ed195925f082"}, {file = "scikit_learn-0.24.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:99349d77f54e11f962d608d94dfda08f0c9e5720d97132233ebdf35be2858b2d"}, {file = "scikit_learn-0.24.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:83b21ff053b1ff1c018a2d24db6dd3ea339b1acfbaa4d9c881731f43748d8b3b"}, - {file = "scikit_learn-0.24.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:abe835a851610f87201819cb315f8d554e1a3e8128912783a31e87264ba5ffb7"}, {file = "scikit_learn-0.24.1-cp37-cp37m-win32.whl", hash = "sha256:c3deb3b19dd9806acf00cf0d400e84562c227723013c33abefbbc3cf906596e9"}, {file = "scikit_learn-0.24.1-cp37-cp37m-win_amd64.whl", hash = "sha256:d54dbaadeb1425b7d6a66bf44bee2bb2b899fe3e8850b8e94cfb9c904dcb46d0"}, {file = "scikit_learn-0.24.1-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:3c4f07f47c04e81b134424d53c3f5e16dfd7f494e44fd7584ba9ce9de2c5e6c1"}, @@ -4375,7 +4537,6 @@ scikit-learn = [ {file = "scikit_learn-0.24.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:4ddd2b6f7449a5d539ff754fa92d75da22de261fd8fdcfb3596799fadf255101"}, {file = "scikit_learn-0.24.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:826b92bf45b8ad80444814e5f4ac032156dd481e48d7da33d611f8fe96d5f08b"}, {file = "scikit_learn-0.24.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:259ec35201e82e2db1ae2496f229e63f46d7f1695ae68eef9350b00dc74ba52f"}, - {file = "scikit_learn-0.24.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:9599a3f3bf33f73fed0fe06d1dfa4e6081365a58c1c807acb07271be0dce9733"}, {file = "scikit_learn-0.24.1-cp38-cp38-win32.whl", hash = "sha256:8772b99d683be8f67fcc04789032f1b949022a0e6880ee7b75a7ec97dbbb5d0b"}, {file = "scikit_learn-0.24.1-cp38-cp38-win_amd64.whl", hash = "sha256:ed9d65594948678827f4ff0e7ae23344e2f2b4cabbca057ccaed3118fdc392ca"}, {file = "scikit_learn-0.24.1-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:8aa1b3ac46b80eaa552b637eeadbbce3be5931e4b5002b964698e33a1b589e1e"}, @@ -4383,7 +4544,6 @@ scikit-learn = [ {file = "scikit_learn-0.24.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:087dfede39efb06ab30618f9ab55a0397f29c38d63cd0ab88d12b500b7d65fd7"}, {file = "scikit_learn-0.24.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:895dbf2030aa7337649e36a83a007df3c9811396b4e2fa672a851160f36ce90c"}, {file = "scikit_learn-0.24.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:9a24d1ccec2a34d4cd3f2a1f86409f3f5954cc23d4d2270ba0d03cf018aa4780"}, - {file = "scikit_learn-0.24.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:54be0a60a5a35005ad69c75902e0f5c9f699db4547ead427e97ef881c3242e6f"}, {file = "scikit_learn-0.24.1-cp39-cp39-win32.whl", hash = "sha256:fab31f48282ebf54dd69f6663cd2d9800096bad1bb67bbc9c9ac84eb77b41972"}, {file = "scikit_learn-0.24.1-cp39-cp39-win_amd64.whl", hash = "sha256:4562dcf4793e61c5d0f89836d07bc37521c3a1889da8f651e2c326463c4bd697"}, ] @@ -4417,6 +4577,9 @@ scipy = [ screeninfo = [ {file = "screeninfo-0.6.7.tar.gz", hash = "sha256:1c4bac1ca329da3f68cbc4d2fbc92256aa9bb8ff8583ee3e14f91f0a7baa69cb"}, ] +scs = [ + {file = "scs-2.1.2.tar.gz", hash = "sha256:667ed6019bb4e2f925bd9291161d2c61cc0077443094437b703ea905333fd585"}, +] seaborn = [ {file = "seaborn-0.11.1-py3-none-any.whl", hash = "sha256:4e1cce9489449a1c6ff3c567f2113cdb41122f727e27a984950d004a88ef3c5c"}, {file = "seaborn-0.11.1.tar.gz", hash = "sha256:44e78eaed937c5a87fc7a892c329a7cc091060b67ebd1d0d306b446a74ba01ad"}, diff --git a/pyproject.toml b/pyproject.toml index 5895d2c9dd0..1082086197e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ detecta = "^0.0.5" tradingview-ta = "^3.2.3" finvizfinance = "^0.9.3" Pillow = "^8.2.0" +pyportfolioopt = "^1.4.1" [tool.poetry.dev-dependencies] pytest = "^6.2.2" diff --git a/requirements-full.txt b/requirements-full.txt index 2bdd5e071dc..1c51e43294a 100644 --- a/requirements-full.txt +++ b/requirements-full.txt @@ -133,6 +133,7 @@ pycparser==2.20; python_version >= "3.5" and python_full_version < "3.0.0" or py pygments==2.8.1; python_version >= "3.6" pymeeus==0.5.9; python_version >= "3.5" pyparsing==2.4.7; python_full_version >= "3.6.0" and python_version >= "3.6" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6") +pyportfolioopt==1.4.1 pyrsistent==0.17.3; python_version >= "3.6" pysocks==1.7.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" pystan==2.19.1.1 diff --git a/requirements-old.txt b/requirements-old.txt index f06595a2ee6..3a4b6bb05c4 100644 --- a/requirements-old.txt +++ b/requirements-old.txt @@ -90,6 +90,7 @@ pyasn1==0.4.8; python_version >= "3.5" and python_full_version < "3.0.0" and pyt pycoingecko==1.4.0 pymeeus==0.4.3; python_version >= "3.5" pyparsing==2.4.7; python_full_version >= "3.6.0" and python_version >= "3.6" +pyportfolioopt==1.4.1 pysocks==1.7.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" pystan==2.18 python-dateutil==2.8.1; python_full_version >= "3.6.1" and python_version >= "3.5" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6") and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0") and (python_version >= "2.7" and python_full_version < "3.0.0" and python_version < "4" or python_version >= "2.7" and python_version < "4" and python_full_version >= "3.3.0") diff --git a/requirements-win.txt b/requirements-win.txt index a5198652fef..560a2508261 100644 --- a/requirements-win.txt +++ b/requirements-win.txt @@ -92,6 +92,7 @@ pyasn1==0.4.8; python_version >= "3.5" and python_full_version < "3.0.0" and pyt pycoingecko==1.4.0 pymeeus==0.4.3; python_version >= "3.5" pyparsing==2.4.7; python_full_version >= "3.6.0" and python_version >= "3.6" +pyportfolioopt==1.4.1 pysocks==1.7.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" pystan==2.19.1.1 python-dateutil==2.8.1; python_full_version >= "3.6.1" and python_version >= "3.5" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6") and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0") and (python_version >= "2.7" and python_full_version < "3.0.0" and python_version < "4" or python_version >= "2.7" and python_version < "4" and python_full_version >= "3.3.0") diff --git a/requirements.txt b/requirements.txt index b89711f769c..4f4ba01310a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -92,6 +92,7 @@ pycparser==2.20; python_version >= "3.5" and python_full_version < "3.0.0" or py pygments==2.8.1; python_version >= "3.6" pymeeus==0.5.9; python_version >= "3.5" pyparsing==2.4.7; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" +pyportfolioopt==1.4.1 pyrsistent==0.17.3; python_version >= "3.6" pysocks==1.7.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" python-dateutil==2.8.1; python_full_version >= "3.6.1" and python_version >= "3.6" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6") and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0") diff --git a/terminal.py b/terminal.py index f2441ccbdab..627cc715f08 100644 --- a/terminal.py +++ b/terminal.py @@ -31,6 +31,7 @@ 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 po_controller # import warnings # warnings.simplefilter("always") @@ -88,7 +89,9 @@ def main(): "pa", "crypto", "ra", + "po", ] + menu_parser.add_argument("opt", choices=choices) completer = NestedCompleter.from_nested_dict({c: None for c in choices}) @@ -242,6 +245,9 @@ def main(): elif ns_known_args.opt == "crypto": b_quit = crypto_controller.menu() + elif ns_known_args.opt == "po": + b_quit = po_controller.menu([s_ticker]) + elif ns_known_args.opt == "pred": if not gtff.ENABLE_PREDICT: