Refactor port-> po and pa menus (#748)

* Refactor property weightings

* Refactor pypfopt commands

* refactor pa menu

* Address review comments

* Typo
This commit is contained in:
jmaslek
2021-09-16 12:20:53 -04:00
committed by GitHub
parent 20eaaab5ac
commit 5a809d262d
13 changed files with 1585 additions and 1216 deletions

View File

@@ -27,39 +27,6 @@ if cfgPlot.BACKEND is not None:
matplotlib.use(cfgPlot.BACKEND)
def check_valid_path(path: str) -> str:
"""Argparse type function to test is path is valid
Parameters
----------
path: str
Path supplied
Returns
-------
path: str
Valid path
Raises
-------
argparse.ArgumentTypeError
Given path does not exist
"""
if not os.path.exists(
os.path.abspath(
os.path.join(
"gamestonk_terminal",
"portfolio",
"portfolio_analysis",
"portfolios",
f"{path}.csv",
)
)
):
raise argparse.ArgumentTypeError("Path does not exist")
return path
def check_int_range(mini: int, maxi: int):
"""Checks if argparse argument is an int between 2 values.

View File

@@ -1,15 +1,28 @@
"""Portffolio Analysis Controller"""
__docformat__ = "numpy"
import argparse
import os
from pathlib import Path
import pandas as pd
from prompt_toolkit.completion import NestedCompleter
from gamestonk_terminal import feature_flags as gtff
from gamestonk_terminal.helper_funcs import get_flair
from gamestonk_terminal.helper_funcs import (
get_flair,
parse_known_args_and_warn,
)
from gamestonk_terminal.menu import session
from gamestonk_terminal.portfolio.portfolio_analysis import (
portfolio_model,
portfolio_view,
)
from gamestonk_terminal.portfolio.portfolio_analysis import portfolio_parser
portfolios_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "portfolios")
possible_paths = [
Path(port).stem for port in os.listdir(portfolios_path) if port.endswith(".csv")
]
class PortfolioController:
@@ -21,10 +34,15 @@ class PortfolioController:
"help",
"q",
"quit",
]
CHOICES_COMMANDS = [
"view",
"load",
"group",
]
CHOICES += CHOICES_COMMANDS
def __init__(self):
self.pa_parser = argparse.ArgumentParser(add_help=False, prog="pa")
self.pa_parser.add_argument("cmd", choices=self.CHOICES)
@@ -33,22 +51,24 @@ class PortfolioController:
def print_help(self):
"""Print help"""
print(
"https://github.com/GamestonkTerminal/GamestonkTerminal/tree/main/gamestonk_terminal/portfolio_analysis"
)
print("\nPortfolio Analysis:")
print(" cls clear screen")
print(" ?/help show this menu again")
print(" q quit this menu, and shows back to main menu")
print(" quit quit to abandon program")
print("")
print(" load load portfolio from csv file")
print("")
if self.portfolio_name:
print(f"Portfolio: {self.portfolio_name}")
print("")
print(" group view holdings by a user input group")
print("")
help_string = f"""https://github.com/GamestonkTerminal/GamestonkTerminal/tree/main/gamestonk_terminal/portfolio_analysis
>>PORTFOLIO ANALYSIS<<
What would you like to do?
cls clear screen
?/help show this menu again
q quit this menu, and shows back to main menu
quit quit to abandon program
view view available portfolios
load load portfolio from csv file
Portfolio: {self.portfolio_name or None}
group view holdings grouped by parameter
"""
print(help_string)
def switch(self, an_input: str):
"""Process and dispatch input
@@ -94,21 +114,153 @@ class PortfolioController:
"""Process Quit command - quit the program"""
return True
# TODO: allow loading other files than csv
def call_load(self, other_args):
"""Process csv command"""
self.portfolio_name, self.portfolio = portfolio_parser.load_csv_portfolio(
other_args
"""Process load command"""
parser = argparse.ArgumentParser(
prog="load",
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
description="Function to get portfolio from predefined csv file inside portfolios folder",
)
parser.add_argument(
"-s",
"--sector",
action="store_true",
default=False,
help="Add sector to dataframe",
dest="sector",
)
parser.add_argument(
"--no_last_price",
action="store_false",
default=True,
help="Don't add last price from yfinance",
dest="last_price",
)
parser.add_argument(
"--nan",
action="store_true",
default=False,
help="Show nan entries from csv",
dest="show_nan",
)
parser.add_argument(
"-p",
"--path",
default="my_portfolio",
choices=possible_paths,
help="Path to csv file",
dest="path",
)
if self.portfolio_name:
print(f"Successfully loaded: {self.portfolio_name}\n")
try:
ns_parser = parse_known_args_and_warn(parser, other_args)
if not ns_parser:
return
self.portfolio_name = ns_parser.path
self.portfolio = portfolio_model.load_csv_portfolio(
full_path=os.path.join(portfolios_path, ns_parser.path) + ".csv",
sector=ns_parser.sector,
last_price=ns_parser.last_price,
show_nan=ns_parser.show_nan,
)
if not self.portfolio.empty:
print(f"Successfully loaded: {self.portfolio_name}\n")
except Exception as e:
print(e)
def call_group(self, other_args):
"""Process group command"""
if self.portfolio_name:
portfolio_parser.breakdown_by_group(self.portfolio, other_args)
else:
print("Please load a portfolio")
parser = argparse.ArgumentParser(
prog="group",
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
description="Displays portfolio grouped by a given column",
)
if other_args and "-" not in other_args[0]:
other_args.insert(0, "-g")
parser.add_argument(
"-g",
"--group",
type=str,
dest="group",
default="Ticker",
choices=self.portfolio.columns,
help="Column to group by",
)
# The following arguments will be used in a later PR for customizable 'reports'
# The --func flag will need to be tested that it exists for pandas groupby
# parser.add_argument("-f",
# "--func",
# type=str,
# dest="function",
# help="Aggregate function to apply to groups"
# )
# parser.add_argument("-d",
# "--display",
# default = None,
# help = "Columns to display",
# dest="cols")
try:
ns_parser = parse_known_args_and_warn(parser, other_args)
if not ns_parser:
return
if "value" not in self.portfolio.columns:
print(
"'value' column not in portfolio. Either add manually or load without --no_last_price flag\n"
)
return
portfolio_view.display_group_holdings(
portfolio=self.portfolio, group_column=ns_parser.group
)
except Exception as e:
print(e, "\n")
def call_view(self, other_args):
parser = argparse.ArgumentParser(
prog="view",
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
description="Show available portfolios to load.",
)
parser.add_argument(
"-f",
"-format",
choices=["csv", "all"],
help="Format of portfolios to view. 'csv' will show all csv files available, etc.",
default="all",
dest="file_format",
)
try:
ns_parser = parse_known_args_and_warn(parser, other_args)
if not ns_parser:
return
available_ports = os.listdir(portfolios_path)
if ns_parser.file_format != "all":
available_ports = [
port
for port in available_ports
if port.endswith(ns_parser.file_format)
]
print("\nAvailable Portfolios:\n")
for port in available_ports:
print(Path(port).stem)
print("")
except Exception as e:
print(e, "\n")
def menu():

View File

@@ -0,0 +1,62 @@
"""Portfolio Model"""
__docformat__ = "numpy"
from tabulate import tabulate
import pandas as pd
import yfinance as yf
import gamestonk_terminal.feature_flags as gtff
# pylint: disable=no-member,unsupported-assignment-operation,unsubscriptable-object
def load_csv_portfolio(
full_path: str,
sector: bool = False,
last_price: bool = False,
show_nan: bool = True,
) -> pd.DataFrame:
"""Loads a csv portfolio into a dataframe and adds sector and last price
Parameters
----------
full_path : str
Path to csv portfolio.
sector : bool, optional
Boolean to indicate getting sector from yfinance , by default False
last_price : bool, optional
Boolean to indicate getting last price from yfinance, by default False
show_nan : bool, optional
Boolean to indicate dropping nan values, by default True
Returns
-------
pd.DataFrame
Dataframe conataining csv portfolio
"""
df = pd.read_csv(full_path)
if sector:
df["sector"] = df.apply(
lambda row: yf.Ticker(row.Ticker).info["sector"]
if "sector" in yf.Ticker(row.Ticker).info.keys()
else "yf Other",
axis=1,
)
if last_price:
df["last_price"] = df.apply(
lambda row: yf.Ticker(row.Ticker)
.history(period="1d")["Close"][-1]
.round(2),
axis=1,
)
df["value"] = df["Shares"] * df["last_price"]
if not show_nan:
df = df.dropna(axis=1)
if gtff.USE_TABULATE_DF:
print(tabulate(df, tablefmt="fancy_grid", headers=df.columns), "\n")
else:
print(df.to_string(), "\n")
return df

View File

@@ -1,179 +0,0 @@
"""Portfolio parser module"""
__docformat__ = "numpy"
import os
import argparse
from typing import List, Tuple
from tabulate import tabulate
import pandas as pd
import yfinance as yf
from gamestonk_terminal.helper_funcs import check_valid_path, parse_known_args_and_warn
# pylint: disable=no-member,unsupported-assignment-operation,unsubscriptable-object
def load_csv_portfolio(other_args: List[str]) -> Tuple[str, pd.DataFrame]:
"""Load portfolio from csv
Parameters
----------
other_args: List[str]
Argparse arguments
Returns
----------
portfolio_name : str
Portfolio name
portfolio : pd.DataFrame
Portfolio dataframe
"""
parser = argparse.ArgumentParser(
prog="load",
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
description="Function to get portfolio from predefined csv file inside portfolios folder",
)
parser.add_argument(
"-p",
"--path",
default="my_portfolio",
type=check_valid_path,
help="Path to csv file",
dest="path",
)
parser.add_argument(
"--no_sector",
action="store_true",
default=False,
help="Add sector to dataframe",
dest="sector",
)
parser.add_argument(
"--no_last_price",
action="store_true",
default=False,
help="Add last price from yfinance",
dest="last_price",
)
parser.add_argument(
"--nan",
action="store_true",
default=False,
help="Show nan entries from csv",
dest="show_nan",
)
try:
ns_parser = parse_known_args_and_warn(parser, other_args)
if not ns_parser:
return "", pd.DataFrame()
full_path = os.path.abspath(
os.path.join(
"gamestonk_terminal",
"portfolio",
"portfolio_analysis",
"portfolios",
f"{ns_parser.path}.csv",
)
)
df = pd.read_csv(full_path)
if not ns_parser.sector:
df["sector"] = df.apply(
lambda row: yf.Ticker(row.Ticker).info["sector"]
if "sector" in yf.Ticker(row.Ticker).info.keys()
else "yf Other",
axis=1,
)
if not ns_parser.last_price:
df["last_price"] = df.apply(
lambda row: yf.Ticker(row.Ticker)
.history(period="1d")["Close"][-1]
.round(2),
axis=1,
)
df["value"] = df["Shares"] * df["last_price"]
if not ns_parser.show_nan:
df = df.dropna(axis=1)
print(tabulate(df, tablefmt="fancy_grid", headers=df.columns))
print("")
return ns_parser.path, df
except Exception as e:
print(e, "\n")
return "", pd.DataFrame()
def breakdown_by_group(portfolio: pd.DataFrame, other_args: List[str]):
"""Breakdown of portfolio by a specified group
Parameters
----------
portfolio: pd.DataFrame
Dataframe of portfolio generated from menu
other_args: List[str]
Argparse arguments
"""
parser = argparse.ArgumentParser(
prog="groupby",
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
description="Displays portfolio grouped by a given column",
)
parser.add_argument(
"-g",
"--group",
type=str,
dest="group",
default="Ticker",
help="Column to group by",
)
# The following arguments will be used in a later PR for customizable 'reports'
# The --func flag will need to be tested that it exists for pandas groupby
# parser.add_argument("-f",
# "--func",
# type=str,
# dest="function",
# help="Aggregate function to apply to groups"
# )
# parser.add_argument("-d",
# "--display",
# default = None,
# help = "Columns to display",
# dest="cols")
try:
ns_parser = parse_known_args_and_warn(parser, other_args)
if not ns_parser:
return
group_column = ns_parser.group
if group_column not in portfolio.columns:
print(f"The column {group_column} is not found in your portfolio data")
return
grouped_df = pd.DataFrame(portfolio.groupby(group_column).agg(sum)["value"])
print(
tabulate(grouped_df, headers=[group_column, "value"], tablefmt="fancy_grid")
)
print("")
# The following will be used to display certain columns (i.e show Dollars or Percents)
# valid_columns = []
# if ns_parser.cols:
# for col in ns_parser.cols:
# if col in portfolio.columns:
# valid_columns.append(col)
# else:
# print(f"{col} not in portfolio columns")
# if valid_columns:
# valid_columns = ["Shares"]
except Exception as e:
print(e, "\n")

View File

@@ -0,0 +1,32 @@
"""Portfolio View"""
__docformat__ = "numpy"
import pandas as pd
from tabulate import tabulate
import gamestonk_terminal.feature_flags as gtff
def display_group_holdings(portfolio: pd.DataFrame, group_column: str):
"""Display portfolio holdings based on grouping
Parameters
----------
portfolio : pd.DataFrame
Portfolio dataframe
group_column : str
Column to group by
"""
grouped_df = pd.DataFrame(portfolio.groupby(group_column).agg(sum)["value"])
if gtff.USE_TABULATE_DF:
print(
tabulate(
grouped_df,
headers=[group_column, "value"],
tablefmt="fancy_grid",
floatfmt=".2f",
),
"\n",
)
else:
print(portfolio.to_string(), "\n")

View File

@@ -4,3 +4,4 @@ AMC,420,
MSFT,3,
BAC,2,
O,1,
AAPL,4
1 Ticker Ticker,Shares,Other Shares Other
4 MSFT MSFT,3, 3
5 BAC BAC,2, 2
6 O O,1, 1
7 AAPL,4

View File

@@ -1,3 +1,6 @@
"""Portfolio Controller"""
__docformat__ = "numpy"
import argparse
import os
@@ -24,13 +27,6 @@ class PortfolioController:
"quit",
]
CHOICES_MENUS = [
"load",
"quote",
"candle",
"view",
]
CHOICES_MENUS = [
"bro",
"pa",

View File

@@ -1,20 +1,9 @@
""" Portfolio Optimization Helper Functions """
"""Optimization helpers"""
__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
# These are all the possible yfinance properties
l_valid_property_infos = [
"previousClose",
"regularMarketOpen",
@@ -116,167 +105,3 @@ def check_valid_property_type(aproperty: str) -> str:
return aproperty
raise argparse.ArgumentTypeError(f"{aproperty} is not a valid info")
def process_stocks(list_of_stocks: List[str], period: str = "3mo") -> pd.DataFrame:
"""Get adjusted closing price for each stock in the list
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[:4] if s.find(".") == 1 else "" + s[:5]
) + " %"
print(pd.DataFrame(weight_df["weight"]).to_string(header=False))
else:
print(weight_df.to_string(header=False))
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} %"
return ""
def pie_chart_weights(weights: dict, title_opt: str):
"""Show a pie chart of holdings
Parameters
----------
weights: dict
Weights to display, where keys are tickers, and values are either weights or values if -v specified
title: str
Title to be used on the plot title
"""
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)
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"),
wedgeprops={"linewidth": 3.0, "edgecolor": "white"},
normalize=True,
)
plt.setp(autotexts, color="white", fontweight="bold")
else:
wedges, _, autotexts = plt.pie(
sizes,
labels=stocks,
autopct="",
textprops=dict(color="k"),
wedgeprops={"linewidth": 3.0, "edgecolor": "white"},
normalize=True,
)
plt.setp(autotexts, color="white", fontweight="bold")
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")
leg1 = plt.legend(
wedges,
[str(s) for s in stocks],
title=" Ticker",
loc="upper left",
bbox_to_anchor=(0.80, 0, 0.5, 1),
frameon=False,
)
leg2 = plt.legend(
wedges,
[
f"{' ' if ((100*s/total_size) < 10) else ''}{100*s/total_size:.2f}%"
for s in sizes
],
title=" ",
loc="upper left",
handlelength=0,
bbox_to_anchor=(0.91, 0, 0.5, 1),
frameon=False,
)
plt.gca().add_artist(leg1)
plt.gca().add_artist(leg2)
plt.setp(autotexts, size=8, weight="bold")
plt.gca().set_title(title_opt, pad=20)
if gtff.USE_ION:
plt.ion()
plt.tight_layout()
plt.show()
print("")

View File

@@ -0,0 +1,254 @@
"""Optimization Model"""
__docformat__ = "numpy"
from typing import List, Dict, Tuple
import numpy as np
import pandas as pd
import yfinance as yf
from pypfopt.efficient_frontier import EfficientFrontier
from pypfopt import expected_returns, risk_models
from gamestonk_terminal.portfolio.portfolio_optimization import yahoo_finance_model
def get_equal_weights(stocks: List[str], value: float = 1.0) -> Dict:
"""Equally weighted portfolio, where weight = 1/# of stocks
Parameters
----------
stocks: List[str]
List of tickers to be included in optimization
value : float
Amount of money to allocate instead of percentage
Returns
-------
dict
Dictionary of weights where keys are the tickers
"""
return {stock: value * round(1 / len(stocks), 5) for stock in stocks}
def get_property_weights(
stocks: List[str], s_property: str, value: float = 1.0
) -> Dict:
"""Calculate portfolio weights based on selected property
Parameters
----------
stocks : List[str]
List of portfolio stocks
s_property : str
Property to weight portfolio by
value : float, optional
Amount of money to allocate
Returns
-------
Dict
Dictionary of portfolio weights or allocations
"""
prop = {}
prop_sum = 0
for stock in stocks:
stock_prop = yf.Ticker(stock).info[s_property]
if stock_prop is None:
stock_prop = 0
prop[stock] = stock_prop
prop_sum += stock_prop
if prop_sum == 0:
print(f"No {s_property} was found on list of tickers provided", "\n")
return {}
return {k: value * v / prop_sum for k, v in prop.items()}
def get_maxsharpe_portfolio(
stocks: List[str], period: str = "3mo", rfrate: float = 0.02
) -> Tuple[Dict, EfficientFrontier]:
"""Generate weights for max sharpe portfolio
Parameters
----------
stocks : List[str]
List of portfolio tickers
period : str, optional
Period to get stock data, by default "3mo"
rfrate : float, optional
Risk free rate, by default 0.02
Returns
-------
Dict
Dictionary of tickers and weights
EfficientFrontier
EfficientFrontier object
"""
stock_prices = yahoo_finance_model.process_stocks(stocks, period)
ef = prepare_efficient_frontier(stock_prices)
return dict(ef.max_sharpe(rfrate)), ef
def get_minvol_portfolio(
stocks: List[str], period: str = "3mo"
) -> Tuple[Dict, EfficientFrontier]:
"""Generate weights for minimum voltaility portfolio
Parameters
----------
stocks : List[str]
List of portfolio tickers
period : str, optional
Period to get stock data, by default "3mo"
Returns
-------
Dict
Dictionary of tickers and weights
EfficientFrontier
EfficientFrontier object
"""
stock_prices = yahoo_finance_model.process_stocks(stocks, period)
ef = prepare_efficient_frontier(stock_prices)
return dict(ef.min_volatility()), ef
def get_maxquadutil_portfolio(
stocks: List[str],
period: str = "3mo",
risk_aversion: float = 1.0,
market_neutral: bool = False,
) -> Tuple[Dict, EfficientFrontier]:
"""Get portfolio maximizing quadratic ultility at a given risk aversion
Parameters
----------
stocks : List[str]
List of portfolio stocks
period : str, optional
Period to get stock data, by default "3mo"
risk_aversion : float, optional
Risk aversion level, by default 1.0
market_neutral : bool, optional
Whether portfolio is market neutral, by default False
Returns
-------
Dict
Dictionary of portfolio weights
EfficientFrontier
EfficientFrontier object
"""
stock_prices = yahoo_finance_model.process_stocks(stocks, period)
ef = prepare_efficient_frontier(stock_prices)
return ef.max_quadratic_utility(risk_aversion, market_neutral), ef
def get_efficient_risk_portfolio(
stocks: List[str],
period: str = "3mo",
target_vol: float = 0.1,
market_neutral: bool = False,
) -> Tuple[Dict, EfficientFrontier]:
"""Get portfolio that maximizes returns at given volatility
Parameters
----------
stocks : List[str]
List of portfolio stocks
period : str, optional
Period to get stock data, by default "3mo"
target_vol : float, optional
Target volatility level, by default 0.1
market_neutral : bool, optional
Whether portfolio is market neutral, by default False
Returns
-------
Dict
Dictionary of weights
EfficientFrontier
Efficient Frontier object
"""
stock_prices = yahoo_finance_model.process_stocks(stocks, period)
ef = prepare_efficient_frontier(stock_prices)
return ef.efficient_risk(target_vol, market_neutral), ef
def get_efficient_return_portfolio(
stocks: List[str],
period: str = "3mo",
target_return: float = 0.1,
market_neutral: bool = False,
) -> Tuple[Dict, EfficientFrontier]:
"""Get portfolio that minimizes volatility at given return
Parameters
----------
stocks : List[str]
List of portfolio stocks
period : str, optional
Period to get stock data, by default "3mo"
target_return : float, optional
Target return level, by default 0.1
market_neutral : bool, optional
Whether portfolio is market neutral, by default False
Returns
-------
Dict
Dictionary of weights
EfficientFrontier
Efficient Frontier object
"""
stock_prices = yahoo_finance_model.process_stocks(stocks, period)
ef = prepare_efficient_frontier(stock_prices)
return ef.efficient_return(target_return, market_neutral), ef
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
-------
EfficientFrontier
EfficientFrontier object
"""
mu = expected_returns.mean_historical_return(stock_prices)
S = risk_models.sample_cov(stock_prices)
return EfficientFrontier(mu, S)
def generate_random_portfolios(
stocks: List[str], period: str = "3mo", n_portfolios: int = 300
):
"""[summary]
Parameters
----------
stocks : List[str]
[description]
period : str, optional
[description], by default "3mo"
n_portfolios : int, optional
[description], by default 300
"""
stock_prices = yahoo_finance_model.process_stocks(stocks, period)
mu = expected_returns.mean_historical_return(stock_prices)
S = risk_models.sample_cov(stock_prices)
ef = EfficientFrontier(mu, S)
# Generate random portfolios
n_samples = n_portfolios
w = np.random.dirichlet(np.ones(len(mu)), n_samples)
rets = w.dot(mu)
stds = np.sqrt(np.diag(w @ S @ w.T))
return ef, rets, stds

View File

@@ -7,19 +7,36 @@ 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.helper_funcs import (
get_flair,
parse_known_args_and_warn,
check_non_negative,
)
from gamestonk_terminal.menu import session
from gamestonk_terminal.portfolio.portfolio_optimization import optimizer_view
from gamestonk_terminal.portfolio.portfolio_optimization import (
optimizer_view,
optimizer_helper,
)
period_choices = [
"1d",
"5d",
"1mo",
"3mo",
"6mo",
"1y",
"2y",
"5y",
"10y",
"ytd",
"max",
]
class PortfolioOptimization:
CHOICES = [
"cls",
"?",
"help",
"q",
"quit",
CHOICES = ["cls", "?", "help", "q", "quit"]
CHOICES_COMMANDS = [
"select",
"add",
"rmv",
@@ -35,6 +52,7 @@ class PortfolioOptimization:
"ef",
"yolo",
]
CHOICES += CHOICES_COMMANDS
# pylint: disable=dangerous-default-value
def __init__(self, tickers: List[str]):
@@ -47,43 +65,38 @@ class PortfolioOptimization:
@staticmethod
def print_help(tickers: List[str]):
"""Print help"""
print(
"https://github.com/GamestonkTerminal/GamestonkTerminal/tree/main/gamestonk_terminal/portfolio_optimization"
)
print("\nPortfolio Optimization:")
print(" cls clear screen")
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(" select select list of tickers to be optimized")
print(" add add tickers to the list of the tickers to be optimized")
print(
" rmv remove tickers from the list of the tickers to be optimized"
)
print("")
print("Optimization:")
print(" equal equally weighted")
print(" mktcap weighted according to market cap (property marketCap)")
print(
" dividend weighted according to dividend yield (property dividendYield)"
)
print(" property weight according to selected info property")
print("")
print("Mean Variance Optimization:")
print(
" maxsharpe optimizes for maximal Sharpe ratio (a.k.a the tangency portfolio)"
)
print(" minvol optimizes for minimum volatility")
print(
" maxquadutil maximises the quadratic utility, given some risk aversion"
)
print(" effret maximises return for a given target risk")
print(" effrisk minimises risk for a given target return")
print("")
print(" ef show the efficient frontier")
print("")
help_text = f"""https://github.com/GamestonkTerminal/GamestonkTerminal/tree/main/gamestonk_terminal/portfolio_optimization"
>>PORTFOLIO OPTIMIZATION<<
What would you like to do?
cls clear screen
?/help show this menu again
q quit this menu, and shows back to main menu
quit quit to abandon program
Current Tickers: {('None', ', '.join(tickers))[bool(tickers)]}
select select list of tickers to be optimized
add add tickers to the list of the tickers to be optimized
rmv remove tickers from the list of the tickers to be optimized"
Optimization:
equal equally weighted
mktcap weighted according to market cap (property marketCap)
dividend weighted according to dividend yield (property dividendYield)
property weight according to selected info property
Mean Variance Optimization:
maxsharpe optimizes for maximal Sharpe ratio (a.k.a the tangency portfolio
minvol optimizes for minimum volatility
maxquadutil maximises the quadratic utility, given some risk aversion
effret maximises return for a given target risk
effrisk minimises risk for a given target return
ef show the efficient frontier
"""
print(help_text)
def switch(self, an_input: str):
"""Process and dispatch input
@@ -144,51 +157,583 @@ class PortfolioOptimization:
def call_equal(self, other_args: List[str]):
"""Process equal command"""
optimizer_view.equal_weight(self.tickers, other_args)
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="equal",
description="Returns an equally weighted portfolio",
)
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
if len(self.tickers) < 2:
print("Please have at least 2 loaded tickers to calculate weights.\n")
return
optimizer_view.display_equal_weight(
stocks=self.tickers, value=ns_parser.value, pie=ns_parser.pie
)
except Exception as e:
print(e, "\n")
def call_mktcap(self, other_args: List[str]):
"""Process mktcap command"""
other_args.insert(0, "marketCap")
optimizer_view.property_weighting(self.tickers, other_args)
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="mktcap",
description="Returns a portfolio that is weighted based on Market Cap.",
)
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
if len(self.tickers) < 2:
print("Please have at least 2 stocks selected to perform calculations.")
return
optimizer_view.display_property_weighting(
self.tickers,
s_property="marketCap",
value=ns_parser.value,
pie=ns_parser.pie,
)
except Exception as e:
print(e, "\n")
def call_dividend(self, other_args: List[str]):
"""Process dividend command"""
other_args.insert(0, "dividendYield")
optimizer_view.property_weighting(self.tickers, other_args)
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="dividend",
description="Returns a portfolio that is weighted based dividend yield.",
)
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
if len(self.tickers) < 2:
print("Please have at least 2 stocks selected to perform calculations.")
return
optimizer_view.display_property_weighting(
self.tickers,
s_property="dividendYield",
value=ns_parser.value,
pie=ns_parser.pie,
)
except Exception as e:
print(e, "\n")
def call_property(self, other_args: List[str]):
"""Process property command"""
optimizer_view.property_weighting(self.tickers, other_args)
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="property",
description="Returns a portfolio that is weighted based on selected property.",
)
parser.add_argument(
"-p",
"--property",
required=bool("-h" not in other_args),
type=optimizer_helper.check_valid_property_type,
dest="property",
help="""Property info to weigh. Use one of:
previousClose, regularMarketOpen, twoHundredDayAverage, trailingAnnualDividendYield,
payoutRatio, volume24Hr, regularMarketDayHigh, navPrice, averageDailyVolume10Day, totalAssets,
regularMarketPreviousClose, fiftyDayAverage, trailingAnnualDividendRate, open, toCurrency,
averageVolume10days,expireDate, yield, algorithm, dividendRate, exDividendDate, beta, circulatingSupply,
regularMarketDayLow, priceHint, currency, trailingPE, regularMarketVolume, lastMarket, maxSupply,
openInterest,marketCap, volumeAllCurrencies, strikePrice, averageVolume, priceToSalesTrailing12Months,
dayLow, ask, ytdReturn,askSize,volume, fiftyTwoWeekHigh, forwardPE, fromCurrency, fiveYearAvgDividendYield,
fiftyTwoWeekLow, bid,dividendYield,bidSize, dayHigh, annualHoldingsTurnover, enterpriseToRevenue, beta3Year,
profitMargins, enterpriseToEbitda, 52WeekChange, morningStarRiskRating, forwardEps, revenueQuarterlyGrowth,
sharesOutstanding, fundInceptionDate, annualReportExpenseRatio, bookValue, sharesShort, sharesPercentSharesOut
heldPercentInstitutions, netIncomeToCommon, trailingEps, lastDividendValue, SandP52WeekChange, priceToBook,
heldPercentInsiders, shortRatio, sharesShortPreviousMonthDate, floatShares, enterpriseValue,fundFamily,
threeYearAverageReturn, lastSplitFactor, legalType, lastDividendDate, morningStarOverallRating,
earningsQuarterlyGrowth, pegRatio, lastCapGain, shortPercentOfFloat, sharesShortPriorMonth,
impliedSharesOutstanding, fiveYearAverageReturn, and regularMarketPrice.""",
)
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
if len(self.tickers) < 2:
print("Please have at least 2 stocks selected to perform calculations.")
return
optimizer_view.display_property_weighting(
self.tickers,
s_property=ns_parser.property,
value=ns_parser.value,
pie=ns_parser.pie,
)
except Exception as e:
print(e, "\n")
def call_maxsharpe(self, other_args: List[str]):
"""Process maxsharpe command"""
optimizer_view.max_sharpe(self.tickers, other_args)
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="maxsharpe",
description="Maximise the Sharpe Ratio",
)
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",
)
parser.add_argument(
"-r",
"--risk-free-rate",
type=float,
dest="risk_free_rate",
default=0.02,
help="""Risk-free rate of borrowing/lending. The period of the risk-free rate
should correspond to the frequency of expected returns.""",
)
try:
ns_parser = parse_known_args_and_warn(parser, other_args)
if not ns_parser:
return
if len(self.tickers) < 2:
print("Please have at least 2 loaded tickers to calculate weights.\n")
return
optimizer_view.display_max_sharpe(
stocks=self.tickers,
period=ns_parser.period,
value=ns_parser.value,
rfrate=ns_parser.risk_free_rate,
pie=ns_parser.pie,
)
except Exception as e:
print(e, "\n")
def call_minvol(self, other_args: List[str]):
"""Process minvol command"""
optimizer_view.min_volatility(self.tickers, other_args)
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="minvol",
description="Optimizes for minimum volatility",
)
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_known_args_and_warn(parser, other_args)
if not ns_parser:
return
if len(self.tickers) < 2:
print("Please have at least 2 loaded tickers to calculate weights.\n")
return
optimizer_view.display_min_volatility(
stocks=self.tickers,
period=ns_parser.period,
value=ns_parser.value,
pie=ns_parser.pie,
)
except Exception as e:
print(e, "\n")
def call_maxquadutil(self, other_args: List[str]):
"""Process maxquadutil command"""
optimizer_view.max_quadratic_utility(self.tickers, other_args)
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="maxquadutil",
description="Maximises the quadratic utility, given some risk aversion",
)
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(
"-n",
"--market-neutral",
action="store_true",
default=False,
dest="market_neutral",
help="""whether the portfolio should be market neutral (weights sum to zero), defaults to False.
Requires negative lower weight bound.""",
)
parser.add_argument(
"--pie",
action="store_true",
dest="pie",
default=False,
help="Display a pie chart for weights. Only if neutral flag is left False.",
)
parser.add_argument(
"-r",
"--risk-aversion",
type=float,
dest="risk_aversion",
default=1.0,
help="risk aversion parameter",
)
try:
ns_parser = parse_known_args_and_warn(parser, other_args)
if not ns_parser:
return
if len(self.tickers) < 2:
print("Please have at least 2 loaded tickers to calculate weights.\n")
return
if ns_parser.pie and ns_parser.market_neutral:
print(
"Cannot show pie chart for market neutral due to negative weights."
)
return
optimizer_view.display_max_quadratic_utility(
stocks=self.tickers,
period=ns_parser.period,
value=ns_parser.value,
risk_aversion=ns_parser.risk_aversion,
market_neutral=ns_parser.market_neutral,
pie=ns_parser.pie,
)
except Exception as e:
print(e, "\n")
def call_effrisk(self, other_args: List[str]):
"""Process effrisk command"""
optimizer_view.efficient_risk(self.tickers, other_args)
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="effrisk",
description="""Maximise return for a target risk. The resulting portfolio will have
a volatility less than the target (but not guaranteed to be equal)""",
)
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(
"-n",
"--market-neutral",
action="store_true",
default=False,
dest="market_neutral",
help="""whether the portfolio should be market neutral (weights sum to zero), defaults to False.
Requires negative lower weight bound.""",
)
parser.add_argument(
"--pie",
action="store_true",
dest="pie",
default=False,
help="Display a pie chart for weights. Only if neutral flag is left False.",
)
if other_args and "-" not in other_args[0]:
other_args.insert(0, "-t")
parser.add_argument(
"-t",
"--target-volatility",
type=float,
dest="target_volatility",
default=0.1,
help="The desired maximum volatility of the resulting portfolio",
)
try:
ns_parser = parse_known_args_and_warn(parser, other_args)
if not ns_parser:
return
if len(self.tickers) < 2:
print("Please have at least 2 loaded tickers to calculate weights.\n")
return
if ns_parser.pie and ns_parser.market_neutral:
print(
"Cannot show pie chart for market neutral due to negative weights."
)
return
optimizer_view.display_efficient_risk(
stocks=self.tickers,
period=ns_parser.period,
value=ns_parser.value,
target_volatility=ns_parser.target_volatility,
market_neutral=ns_parser.market_neutral,
pie=ns_parser.pie,
)
except Exception as e:
print(e, "\n")
def call_effret(self, other_args: List[str]):
"""Process effret command"""
optimizer_view.efficient_return(self.tickers, other_args)
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="effret",
description="Calculate the 'Markowitz portfolio', minimising volatility for a given target return",
)
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(
"-n",
"--market-neutral",
action="store_true",
default=False,
dest="market_neutral",
help="""whether the portfolio should be market neutral (weights sum to zero), defaults to False.
Requires negative lower weight bound.""",
)
parser.add_argument(
"--pie",
action="store_true",
dest="pie",
default=False,
help="Display a pie chart for weights. Only if neutral flag is left False.",
)
if other_args and "-" not in other_args[0]:
other_args.insert(0, "-t")
parser.add_argument(
"-t",
"--target-return",
type=float,
dest="target_return",
default=0.1,
help="the desired return of the resulting portfolio",
)
try:
ns_parser = parse_known_args_and_warn(parser, other_args)
if not ns_parser:
return
if len(self.tickers) < 2:
print("Please have at least 2 loaded tickers to calculate weights.\n")
return
if ns_parser.pie and ns_parser.market_neutral:
print(
"Cannot show pie chart for market neutral due to negative weights."
)
return
optimizer_view.display_efficient_return(
stocks=self.tickers,
period=ns_parser.period,
value=ns_parser.value,
target_return=ns_parser.target_return,
market_neutral=ns_parser.market_neutral,
pie=ns_parser.pie,
)
except Exception as e:
print(e, "\n")
def call_ef(self, other_args):
"""Process ef command"""
optimizer_view.show_ef(self.tickers, other_args)
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="ef",
description="""This function plots random portfolios based
on their risk and returns and shows the efficient frontier.""",
)
parser.add_argument(
"-p",
"--period",
default="3mo",
dest="period",
help="period to get yfinance data from",
choices=period_choices,
)
if other_args and "-" not in other_args[0]:
other_args.insert(0, "-n")
parser.add_argument(
"-n",
"--number-portfolios",
default=300,
type=check_non_negative,
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(self.tickers) < 2:
print("Please have at least 2 loaded tickers to calculate weights.\n")
return
optimizer_view.display_ef(
stocks=self.tickers,
period=ns_parser.period,
n_portfolios=ns_parser.n_port,
)
except Exception as e:
print(e, "\n")
def call_yolo(self, _):
# Easter egg :)
print("DFV YOLO")
print({"GME": 200})
print("")
print("GME: ALL", "\n")
def add_stocks(self, other_args: List[str]):
"""Add ticker or Select tickes for portfolio to be optimized"""

View File

@@ -0,0 +1,32 @@
"""YFinance Model"""
__docformat__ = "numpy"
from typing import List
import pandas as pd
import yfinance as yf
def process_stocks(list_of_stocks: List[str], period: str = "3mo") -> pd.DataFrame:
"""Get adjusted closing price for each stock in the list
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

View File

@@ -1,59 +1,62 @@
from unittest import TestCase # , mock
import vcr
# import vcr
from gamestonk_terminal.portfolio.portfolio_optimization import optimizer_view
from tests.helpers import check_print
# from gamestonk_terminal.portfolio.portfolio_optimization import optimizer_view
# from tests.helpers import check_print
class TestOptimizerView(TestCase):
@check_print(assert_in="TSLA")
@vcr.use_cassette(
"tests/cassettes/test_port_opt/test_opt_view/test_equal_weights.yaml",
record_mode="new_episodes",
)
def test_equal_weights(self):
optimizer_view.equal_weight(["TSLA", "GME"], [])
pass
# @check_print(assert_in="TSLA")
# @vcr.use_cassette(
# "tests/cassettes/test_port_opt/test_opt_view/test_equal_weights.yaml",
# record_mode="new_episodes",
# )
# def test_equal_weights(self):
# optimizer_view.display_equal_weight(["TSLA", "GME"], [])
@check_print(assert_in="TSLA")
@vcr.use_cassette(
"tests/cassettes/test_port_opt/test_opt_view/test_property_weights.yaml",
record_mode="new_episodes",
)
def test_property_weighting(self):
optimizer_view.property_weighting(["TSLA", "GME"], ["-p", "previousClose"])
# @check_print(assert_in="TSLA")
# @vcr.use_cassette(
# "tests/cassettes/test_port_opt/test_opt_view/test_property_weights.yaml",
# record_mode="new_episodes",
# )
# def test_property_weighting(self):
# optimizer_view.display_property_weighting(
# ["TSLA", "GME"], ["-p", "previousClose"]
# )
@check_print(assert_in="TSLA")
@vcr.use_cassette(
"tests/cassettes/test_port_opt/test_opt_view/general1.yaml",
record_mode="new_episodes",
)
def test_max_sharpe(self):
optimizer_view.max_sharpe(["TSLA", "GME"], [])
# @check_print(assert_in="TSLA")
# @vcr.use_cassette(
# "tests/cassettes/test_port_opt/test_opt_view/general1.yaml",
# record_mode="new_episodes",
# )
# def test_max_sharpe(self):
# optimizer_view.max_sharpe(["TSLA", "GME"], [])
@check_print(assert_in="TSLA")
@vcr.use_cassette(
"tests/cassettes/test_port_opt/test_opt_view/general1.yaml",
record_mode="new_episodes",
)
def test_min_volatility(self):
optimizer_view.min_volatility(["TSLA", "GME"], [])
# @check_print(assert_in="TSLA")
# @vcr.use_cassette(
# "tests/cassettes/test_port_opt/test_opt_view/general1.yaml",
# record_mode="new_episodes",
# )
# def test_min_volatility(self):
# optimizer_view.min_volatility(["TSLA", "GME"], [])
@check_print(assert_in="TSLA")
@vcr.use_cassette(
"tests/cassettes/test_port_opt/test_opt_view/general1.yaml",
record_mode="new_episodes",
)
def test_max_quadratic_utility(self):
optimizer_view.max_quadratic_utility(["TSLA", "GME"], [])
# @check_print(assert_in="TSLA")
# @vcr.use_cassette(
# "tests/cassettes/test_port_opt/test_opt_view/general1.yaml",
# record_mode="new_episodes",
# )
# def test_max_quadratic_utility(self):
# optimizer_view.max_quadratic_utility(["TSLA", "GME"], [])
@check_print(assert_in="The minimum volatility")
@vcr.use_cassette(
"tests/cassettes/test_port_opt/test_opt_view/general1.yaml",
record_mode="new_episodes",
)
def test_efficient_risk(self):
optimizer_view.efficient_risk(["TSLA", "GME"], [])
# @check_print(assert_in="The minimum volatility")
# @vcr.use_cassette(
# "tests/cassettes/test_port_opt/test_opt_view/general1.yaml",
# record_mode="new_episodes",
# )
# def test_efficient_risk(self):
# optimizer_view.efficient_risk(["TSLA", "GME"], [])
# @check_print(assert_in="TSLA")
# @vcr.use_cassette(