mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-27 12:40:35 +00:00
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:
@@ -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.
|
||||
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -4,3 +4,4 @@ AMC,420,
|
||||
MSFT,3,
|
||||
BAC,2,
|
||||
O,1,
|
||||
AAPL,4
|
||||
|
||||
|
@@ -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",
|
||||
|
||||
@@ -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("")
|
||||
|
||||
@@ -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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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"""
|
||||
|
||||
@@ -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
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user