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).
+
+
+And the pie chart:
+
+
+
+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]
+## 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: