From 0b01f8e8b8453160a8f78059f8e2bf7c8367b5b1 Mon Sep 17 00:00:00 2001 From: northern-64bit <75195383+northern-64bit@users.noreply.github.com> Date: Sat, 22 Jan 2022 05:20:54 +0100 Subject: [PATCH] Allocation Command (#1204) * Adding the allocation (al) command to the portfolio menu * Fix linting * Fixing errors * More fixes * Requested changes * More fixes Co-authored-by: jmaslek --- .../portfolio/portfolio_controller.py | 52 +++++++ .../portfolio/portfolio_model.py | 134 +++++++++++++++++- .../portfolio/portfolio_view.py | 33 +++++ website/content/portfolio/_index.md | 2 + website/content/portfolio/al/_index.md | 13 ++ 5 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 website/content/portfolio/al/_index.md diff --git a/gamestonk_terminal/portfolio/portfolio_controller.py b/gamestonk_terminal/portfolio/portfolio_controller.py index bdddaffcb4b..42324a27b5b 100644 --- a/gamestonk_terminal/portfolio/portfolio_controller.py +++ b/gamestonk_terminal/portfolio/portfolio_controller.py @@ -16,6 +16,7 @@ from gamestonk_terminal import feature_flags as gtff from gamestonk_terminal.helper_funcs import ( valid_date, check_positive_float, + check_positive, ) from gamestonk_terminal.menu import session @@ -42,6 +43,7 @@ class PortfolioController(BaseController): "rmv", "ar", "rmr", + "al", ] CHOICES_MENUS = [ "bro", @@ -90,6 +92,7 @@ class PortfolioController(BaseController): [info]Graphs:[/info][cmds] rmr graph your returns versus the market's returns + al displays the allocation of the portfolio """ console.print(text=help_text, menu="Portfolio") @@ -350,3 +353,52 @@ class PortfolioController(BaseController): console.print("Cannot generate a graph from an empty dataframe\n") else: console.print("Please add items to the portfolio\n") + + def call_al(self, other_args: List[str]): + """Process al command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="al", + description="Gives allocation", + ) + parser.add_argument( + "-l", + "--limit", + help="Number to show", + type=check_positive, + default=20, + dest="limit", + ) + parser.add_argument( + "-n", + "--noetf", + action="store_true", + default=False, + dest="no_etf_positions", + help="If etf positions should not be added to the portfolio", + ) + parser.add_argument( + "-g", + "--graph", + action="store_true", + default=False, + dest="graph", + help="Display table or pi chart of holdings", + ) + ns_parser = parse_known_args_and_warn(parser, other_args) + if not self.portfolio.empty: + val, hist = portfolio_model.convert_df(self.portfolio) + if not val.empty: + df = portfolio_model.get_allocation( + val, + hist, + self.portfolio, + ns_parser.limit, + ns_parser.no_etf_positions, + ) + portfolio_view.display_allocation(df, ns_parser.graph) + else: + console.print("Cannot generate a graph from an empty dataframe\n") + else: + console.print("Please add items to the portfolio\n") diff --git a/gamestonk_terminal/portfolio/portfolio_model.py b/gamestonk_terminal/portfolio/portfolio_model.py index 86aa277a856..8a2de2e396a 100644 --- a/gamestonk_terminal/portfolio/portfolio_model.py +++ b/gamestonk_terminal/portfolio/portfolio_model.py @@ -16,6 +16,7 @@ from gamestonk_terminal.portfolio import ( yfinance_model, portfolio_helper, ) +from gamestonk_terminal.etf import stockanalysis_model from gamestonk_terminal.rich_config import console # pylint: disable=E1136 @@ -278,7 +279,7 @@ def convert_df(portfolio: pd.DataFrame) -> pd.DataFrame: cashes = changes[changes["Type"] == "cash"] if cashes.empty: raise ValueError("Brokers require cash, input cash deposits") - changes = changes[changes["Type"] == ("stock" or "etf")] + changes = changes[(changes["Type"] == "stock") | (changes["Type"] == "etf")] uniques = list(set(changes["Name"].tolist())) if uniques: hist = yfinance_model.get_stocks(uniques, min(changes["Date"])) @@ -387,6 +388,137 @@ def get_rolling_beta( return comb +def fix_etf_allocation( + data: pd.DataFrame, + df_new: pd.DataFrame, + etf: str, + no_etf_positions: bool, + number: int, +): + """Adjusting the dataframe to etf positions, by adding etf holdings to identical portfolio positions or + if 'add_etf_positions' is true add a new position + + Parameters + ---------- + data : pd.DataFrame + The dataframe of positions + df_new: pd.DataFrame + A dataframe of etf holdings + etf: str + String of the etf + no_etf_positions: bool + If new positions should not be added to the 'data' dataframe for etf holdings + number: int + Number of etf positions to be checked + + Returns + ---------- + data: pd.DataFrame + etf adjusted dataframe + """ + i = 0 + length = int(df_new.index.size) - 1 + while i <= ( + length ^ ((number ^ length) & -(number < length)) + ): # gets minimum of number and length + if df_new.iloc[[i]].index[0].lower() in data.index: + change = ( + float(df_new.iloc[i]["% Of Etf"].replace("%", "")) + * data["value"].loc[data.index == etf][0] + / 100 + ) + data.loc[data.index == df_new.iloc[[i]].index[0].lower(), "value"] += change + elif not no_etf_positions: + change = ( + float(df_new.iloc[i]["% Of Etf"].replace("%", "")) + * data["value"].loc[data.index == etf][0] + / 100 + ) + data.loc[len(data.index)] = [f"{etf}_etf", change] + data.rename( + index={len(data.index) - 1: df_new.iloc[[i]].index[0].lower()}, + inplace=True, + ) + data.loc[data.index == etf, "value"] -= change + data.loc[data.index == etf, "value"] -= change + i += 1 + data.rename( + index={data.loc[data.index == etf].index[0]: f"{etf}_rest"}, inplace=True + ) + return data + + +def get_allocation( + data: pd.DataFrame, + hist: pd.DataFrame, + portfolio: pd.DataFrame, + number: int, + no_etf_positions: bool, +) -> pd.DataFrame: + """Formatting the dataframe to an allocation dataframe of the positions + + Parameters + ---------- + data : pd.DataFrame + The dataframe of positions + hist: pd.DataFrame + A dataframe of the positions historical performances + portfolio: pd.DataFrame + Portfolio dataframe + number: int + Number of etf positions to be checked + no_etf_positions: bool + If new positions should not be added to the 'data' dataframe for etf holdings + + Returns + ---------- + df: pd.DataFrame + Dataframe of allocation + """ + # Fixing dataframe formatting + df = data["Quantity"].tail(1).transpose() + df.columns = ["amount"] + df = df.sort_index(axis=0, ascending=True) + hist = hist.tail(1).transpose() + hist = hist.reset_index(["first"]) + hist.columns = ["position", "value"] + hist = pd.DataFrame(hist["value"]) + + i = 0 + value = [] + while i < df.index.size: + value.append(df["amount"].iloc[i] * hist["value"].iloc[i]) + i += 1 + df["value"] = value + + i = 0 + length = df.index.size + while i < length: + row_index = df.iloc[[i]].index[0] + if ( + portfolio["Type"].loc[portfolio["Name"] == row_index].to_list()[0] == "etf" + ): # check if position is an etf + df = fix_etf_allocation( + df, + stockanalysis_model.get_etf_holdings(row_index), + row_index, + no_etf_positions, + number, + ) + if df.iloc[i].loc["value"] < 0: + # Converting negative positions to "shorts" in order to correctly calculate allocation + df.loc[row_index, "value"] = abs(df.loc[row_index, "value"]) + df.loc[row_index, "amount"] = abs(df.loc[row_index, "amount"]) + df.rename(index={row_index: f"short_{row_index}"}, inplace=True) + i += 1 + + total_value = df["value"].sum() + df["pct_allocation"] = df["value"] / total_value * 100 # calculate allocation + df = df.sort_values(by="pct_allocation", ascending=False) + + return df + + def get_main_text(df: pd.DataFrame) -> str: """Get main performance summary from a dataframe with returns diff --git a/gamestonk_terminal/portfolio/portfolio_view.py b/gamestonk_terminal/portfolio/portfolio_view.py index dd6229dfe89..fe4269318f6 100644 --- a/gamestonk_terminal/portfolio/portfolio_view.py +++ b/gamestonk_terminal/portfolio/portfolio_view.py @@ -251,6 +251,39 @@ def plot_ef( return ImageReader(imgdata) +def display_allocation(data: pd.DataFrame, graph: bool): + """Displays allocation + Parameters + ---------- + data: pd.DataFrame + The portfolio allocation dataframe + graph: bool + If pie chart shall be displayed instead of table""" + if graph: + labels = data.index.values + sizes = data["value"].to_list() + fig, ax = plt.subplots() + ax.pie(sizes, labels=labels, autopct="%1.1f%%", startangle=90) + ax.axis("equal") + ax.set_title("Portfolio Allocation") + fig.set_tight_layout(True) + + plt.show() + else: + if gtff.USE_TABULATE_DF: + print( + tabulate( + data, + headers=data.columns, + tablefmt="fancy_grid", + floatfmt=".2f", + ), + ) + else: + console.print(data.to_string()) + console.print("") + + class Report: def __init__(self, df: pd.DataFrame, hist: pd.DataFrame, m_tick: str, n: int): """Generate financial reports. diff --git a/website/content/portfolio/_index.md b/website/content/portfolio/_index.md index ebea3a585ca..9dc72465fbe 100644 --- a/website/content/portfolio/_index.md +++ b/website/content/portfolio/_index.md @@ -11,6 +11,7 @@ What do you want to do? > bro brokers holdings, supports: robinhood, ally, degiro, coinbase > po portfolio optimization, optimal portfolio weights from pyportfolioopt +> pa portfolio analysis, analyse portfolios Portfolio: load load data into the portfolio @@ -24,4 +25,5 @@ Reports: Graphs: rmr graph your returns versus the market's returns + al displays the allocation of the portfolio ``` diff --git a/website/content/portfolio/al/_index.md b/website/content/portfolio/al/_index.md new file mode 100644 index 00000000000..3a28bf4570c --- /dev/null +++ b/website/content/portfolio/al/_index.md @@ -0,0 +1,13 @@ +``` +usage: al [-l] [-n] [-g] [-h] +``` + +Displays the allocation of the portfolio + +``` +optional arguments: + -l, --limit number of etf positions to go through (default: 20) + -n, --noetf if etf positions should not be added to the portfolio (default: False) + -g, --graph display pi chart of holdings (default: False) + -h, --help show this help message (default: False) +```