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 <jmaslek11@gmail.com>
This commit is contained in:
northern-64bit
2022-01-22 05:20:54 +01:00
committed by GitHub
parent fca711ac0c
commit 0b01f8e8b8
5 changed files with 233 additions and 1 deletions

View File

@@ -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")

View File

@@ -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

View File

@@ -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.

View File

@@ -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
```

13
website/content/portfolio/al/_index.md vendored Normal file
View File

@@ -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)
```