diff options
author | jmaslek <jmaslek11@gmail.com> | 2021-09-16 12:20:53 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-09-16 12:20:53 -0400 |
commit | 5a809d262d043663340d7a64d2aa5fd5dcc38f05 (patch) | |
tree | 1fd7f4f75ae58bb0ff0e18f195d04aeb8094d6ba | |
parent | 20eaaab5ac89ffe060db8934db4ae005cc5834f0 (diff) |
Refactor port-> po and pa menus (#748)
* Refactor property weightings
* Refactor pypfopt commands
* refactor pa menu
* Address review comments
* Typo
13 files changed, 1557 insertions, 1188 deletions
diff --git a/gamestonk_terminal/helper_funcs.py b/gamestonk_terminal/helper_funcs.py index dd8196acdab..5e800a04fdf 100644 --- a/gamestonk_terminal/helper_funcs.py +++ b/gamestonk_terminal/helper_funcs.py @@ -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. diff --git a/gamestonk_terminal/portfolio/portfolio_analysis/pa_controller.py b/gamestonk_terminal/portfolio/portfolio_analysis/pa_controller.py index 53f2027fb3a..bba2a3e222f 100644 --- a/gamestonk_terminal/portfolio/portfolio_analysis/pa_controller.py +++ b/gamestonk_terminal/portfolio/portfolio_analysis/pa_controller.py @@ -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(): diff --git a/gamestonk_terminal/portfolio/portfolio_analysis/portfolio_model.py b/gamestonk_terminal/portfolio/portfolio_analysis/portfolio_model.py new file mode 100644 index 00000000000..3301ac697e2 --- /dev/null +++ b/gamestonk_terminal/portfolio/portfolio_analysis/portfolio_model.py @@ -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 diff --git a/gamestonk_terminal/portfolio/portfolio_analysis/portfolio_parser.py b/gamestonk_terminal/portfolio/portfolio_analysis/portfolio_parser.py deleted file mode 100644 index 051bb2163b5..00000000000 --- a/gamestonk_terminal/portfolio/portfolio_analysis/portfolio_parser.py +++ /dev/null @@ -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") diff --git a/gamestonk_terminal/portfolio/portfolio_analysis/portfolio_view.py b/gamestonk_terminal/portfolio/portfolio_analysis/portfolio_view.py new file mode 100644 index 00000000000..b03131ea12c --- /dev/null +++ b/gamestonk_terminal/portfolio/portfolio_analysis/portfolio_view.py @@ -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") diff --git a/gamestonk_terminal/portfolio/portfolio_analysis/portfolios/my_portfolio.csv b/gamestonk_terminal/portfolio/portfolio_analysis/portfolios/my_portfolio.csv index 9123480e064..99482ebce9d 100644 --- a/gamestonk_terminal/portfolio/portfolio_analysis/portfolios/my_portfolio.csv +++ b/gamestonk_terminal/portfolio/portfolio_analysis/portfolios/my_portfolio.csv @@ -4,3 +4,4 @@ AMC,420, MSFT,3, BAC,2, O,1, +AAPL,4 diff --git a/gamestonk_terminal/portfolio/portfolio_controller.py b/gamestonk_terminal/portfolio/portfolio_controller.py index 2e4116c36d2..8870e39f110 100644 --- a/gamestonk_terminal/portfolio/portfolio_controller.py +++ b/gamestonk_terminal/portfolio/portfolio_controller.py @@ -1,3 +1,6 @@ +"""Portfolio Controller""" +__docformat__ = "numpy" + import argparse import os @@ -25,13 +28,6 @@ class PortfolioController: ] CHOICES_MENUS = [ - "load", - "quote", - "candle", - "view", - ] - - CHOICES_MENUS = [ "bro", "pa", "po", diff --git a/gamestonk_terminal/portfolio/portfolio_optimization/optimizer_helper.py b/gamestonk_terminal/portfolio/portfolio_optimization/optimizer_helper.py index cbd20d60732..a9866b09afd 100644 --- a/gamestonk_terminal/portfolio/portfolio_optimization/optimizer_helper.py +++ b/gamestonk_terminal/portfolio/portfolio_optimization/optimizer_helper.py @@ -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 |