diff --git a/ROADMAP.md b/ROADMAP.md index 8e8d8567d48..876b38836f5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -14,6 +14,7 @@ * [Exploratory Data Analysis](#Exploratory-Data-Analysis) * [Residual Analysis](#Residual-Analysis) * [Portfolio Analysis](#Portfolio-Analysis) +* [Brokers](#Brokers) * [Portfolio Optimization](#Portfolio-Optimization) * [Economy](#Economy) * [Options](#Options) @@ -150,20 +151,30 @@ **NEXT** --- +## Brokers +*[ ] Port from current portfolio menu + +**NEXT** +* [ ] Merge data from different brokers +* [ ] Add more options around merged portfolios +* [ ] Add Brokers (td, webull, etc) + +--- ## Portfolio Analysis * [x] Add alpaca (@jmaslek) - [PR #259](https://github.com/DidierRLopes/GamestonkTerminal/pull/229) * [x] Add robinhood (@jmaslek) - [PR #229](https://github.com/DidierRLopes/GamestonkTerminal/pull/229) * [x] Add Ally Invest (@jmaslek) - [PR #267](https://github.com/DidierRLopes/GamestonkTerminal/pull/267) * [x] Degiro support (@deel) - [PR #381](https://github.com/DidierRLopes/GamestonkTerminal/pull/381) * [x] Degiro improvement (@Chavithra) - [PR #445](https://github.com/DidierRLopes/GamestonkTerminal/pull/381) +* [x] Add custom csv integration (jmaslek) - [PR #502](https://github.com/GamestonkTerminal/GamestonkTerminal/pull/502) **NEXT** -* [ ] Merge data from different brokers (@jmaslek) -* [ ] Add more options around merged portfolios -* [ ] Add Brokers (td, webull, etc) (@jmaslek) +* [ ] Port to new Broker Menu * [ ] Refactoring -* [ ] Summaries / tear sheets (@jmaslek) +* [ ] Summaries / tear sheets (potentially QuantStats) +* [ ] Add plots for `group` functions +* [ ] Add more functionality for custom portfolio viewing (suggested XRay) ___ diff --git a/gamestonk_terminal/discovery/stockgrid_view.py b/gamestonk_terminal/discovery/stockgrid_view.py index 87c78dacac6..b5f1accc8e0 100644 --- a/gamestonk_terminal/discovery/stockgrid_view.py +++ b/gamestonk_terminal/discovery/stockgrid_view.py @@ -95,6 +95,7 @@ def darkshort(other_args: List[str]): df["Net Short Volume $"] = df["Net Short Volume $"] / 100_000_000 df["Short Volume"] = df["Short Volume"] / 1_000_000 df["Net Short Volume"] = df["Net Short Volume"] / 1_000_000 + df["Short Volume %"] = df["Short Volume %"] * 100 df.columns = [ "Ticker", "Date", diff --git a/gamestonk_terminal/helper_funcs.py b/gamestonk_terminal/helper_funcs.py index af90bfc634f..e44f27e4d11 100644 --- a/gamestonk_terminal/helper_funcs.py +++ b/gamestonk_terminal/helper_funcs.py @@ -25,6 +25,12 @@ if cfgPlot.BACKEND is not None: matplotlib.use(cfgPlot.BACKEND) +def check_valid_path(path: str) -> str: + if not os.path.exists(path): + raise argparse.ArgumentTypeError("Path does not exist") + return path + + def check_int_range(mini, maxi): """ Checks if argparse argument is an int between 2 values. diff --git a/gamestonk_terminal/portfolio/CUSTOM_PORT.md b/gamestonk_terminal/portfolio/CUSTOM_PORT.md new file mode 100644 index 00000000000..3f5b0a033ba --- /dev/null +++ b/gamestonk_terminal/portfolio/CUSTOM_PORT.md @@ -0,0 +1,60 @@ +# PORTFOLIO + +This document will walk through the custom portfolio loading. + +## csv +``` +usage: csv [-p PATH] [--no_sector] [--no_last_price] [--nan] +``` +* -p/--path : Path to csv. A template is supplied and the default path points to it. +* --no_sector : Flag to avoid getting the sector of each stock. +* --no_last_price : Flag to avoid getting the latest price for each supplied stock. +* --nan : Flag to display NaN columns. Useful if importing partial information. + +This function allows you to load from a predefined csv file. The csv file can contain any information you want, but the stocks +should be under a column `Ticker`. The number owned should be defined in `Shares`. By default, this will loop through +all Tickers and pull sector and latest price. An example column that could be added would be if the ticker is considered +a Value or Growth stock. + +```python +(✨) (pa)> csv +╒════╤══════════╤══════════╤════════════════════╤══════════════╤═════════╕ +│ │ Ticker │ Shares │ sector │ last_price │ value │ +╞════╪══════════╪══════════╪════════════════════╪══════════════╪═════════╡ +│ 0 │ AAPL │ 5 │ Technology │ 129.64 │ 648.2 │ +├────┼──────────┼──────────┼────────────────────┼──────────────┼─────────┤ +│ 1 │ GME │ 5 │ Consumer Cyclical │ 222.5 │ 1112.5 │ +├────┼──────────┼──────────┼────────────────────┼──────────────┼─────────┤ +│ 2 │ MSFT │ 3 │ Technology │ 258.36 │ 775.08 │ +├────┼──────────┼──────────┼────────────────────┼──────────────┼─────────┤ +│ 3 │ O │ 2 │ Real Estate │ 69.61 │ 139.22 │ +├────┼──────────┼──────────┼────────────────────┼──────────────┼─────────┤ +│ 4 │ BAC │ 3 │ Financial Services │ 41.39 │ 124.17 │ +╘════╧══════════╧══════════╧════════════════════╧══════════════╧═════════╛ +``` +## group +``` +usage: groupby [-g GROUP] +``` +* -g/--group : Column to group data by. Ex: "sector" + +Displays data in groups based on columns. Currently this only looks at the `value` column. + +Example commands to load csv file and then group by sector. +``` +csv +group -g sector +``` +```python +(✨) (pa)> group -g sector +╒════════════════════╤═════════╕ +│ sector │ value │ +╞════════════════════╪═════════╡ +│ Consumer Cyclical │ 1112.5 │ +├────────────────────┼─────────┤ +│ Financial Services │ 124.17 │ +├────────────────────┼─────────┤ +│ Real Estate │ 139.22 │ +├────────────────────┼─────────┤ +│ Technology │ 1423.28 │ +╘════════════════════╧═════════╛ diff --git a/gamestonk_terminal/portfolio/custom_model.py b/gamestonk_terminal/portfolio/custom_model.py new file mode 100644 index 00000000000..1c1b1880916 --- /dev/null +++ b/gamestonk_terminal/portfolio/custom_model.py @@ -0,0 +1,155 @@ +"""Custom portfolio model""" +__docformat__ = "numpy" +import argparse +from typing import List +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 + + +def load_csv_portfolio(other_args: List[str]): + + parser = argparse.ArgumentParser( + prog="csv", + add_help=False, + description="Function to get portfolio holdings from predefined csv file", + ) + 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", + ) + + parser.add_argument( + "-p", + "--path", + default="./gamestonk_terminal/portfolio/custom_portfolio/portfolio.csv", + type=check_valid_path, + help="Path to csv file", + dest="path", + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return pd.DataFrame() + df = pd.read_csv(ns_parser.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 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, + 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/custom_portfolio/portfolio.csv b/gamestonk_terminal/portfolio/custom_portfolio/portfolio.csv new file mode 100644 index 00000000000..59fb9f950aa --- /dev/null +++ b/gamestonk_terminal/portfolio/custom_portfolio/portfolio.csv @@ -0,0 +1,6 @@ +Ticker,Shares,Other +AAPL,5, +GME,5,lol +MSFT,3, +O,2,s +BAC,3, diff --git a/gamestonk_terminal/portfolio/port_controller.py b/gamestonk_terminal/portfolio/port_controller.py index f341d17122b..2199930e89d 100644 --- a/gamestonk_terminal/portfolio/port_controller.py +++ b/gamestonk_terminal/portfolio/port_controller.py @@ -18,6 +18,7 @@ from gamestonk_terminal.portfolio.portfolio_helpers import ( merge_portfolios, print_portfolio, ) +from gamestonk_terminal.portfolio import custom_model class PortfolioController: @@ -35,6 +36,8 @@ class PortfolioController: "quit", "rhhold", "rhhist", + "csv", + "group", ] BROKERS = [ @@ -48,9 +51,9 @@ class PortfolioController: self.port_parser.add_argument("cmd", choices=self.CHOICES) self.broker_list = set() self.merged_holdings = None + self.custom_portfolio = pd.DataFrame() - @classmethod - def print_help(cls, broker_list): + def print_help(self): """Print help""" print( @@ -63,12 +66,11 @@ class PortfolioController: " q quit this menu, and shows back to main menu, logs out of brokers\n" " quit quit to abandon program, logs out of brokers\n" ) - cls.print_portfolio_menu(broker_list) + self.print_portfolio_menu() - @staticmethod - def print_portfolio_menu(broker_list): + def print_portfolio_menu(self): print( - f"Current Broker: {('None', ', '.join(broker_list))[bool(broker_list)]}\n\n" + f"Current Broker: {('None', ', '.join(self.broker_list))[bool(self.broker_list)]}\n\n" "Ally:\n" " allyhold view ally holdings\n" "Alpaca:\n" @@ -82,7 +84,17 @@ class PortfolioController: "\nMerge:\n" " login login to your brokers\n" " hold view net holdings across all logins\n" + "\nCustom:\n" + " csv load holdings from CSV\n" ) + if not self.custom_portfolio.empty: + self.print_portfolio_view() + + def print_portfolio_view(self): + if not self.custom_portfolio.empty: + print("Custom Portfolio Options:") + print(" group view holdings by a user input group") + print("") def switch(self, an_input: str): """Process and dispatch input @@ -104,7 +116,7 @@ class PortfolioController: def call_help(self, _): """Process Help command""" - self.print_help(self.broker_list) + self.print_help() def call_q(self, _): """Process Q command - quit the menu""" @@ -117,7 +129,6 @@ class PortfolioController: return True def call_login(self, other_args): - broker_list = self.broker_list logged_in = False if not other_args: print("Please enter brokers you wish to login to") @@ -141,7 +152,7 @@ class PortfolioController: print(f"{broker} not supported") if logged_in: - self.print_portfolio_menu(broker_list) + self.print_portfolio_menu() def call_rhhist(self, other_args: List[str]): rh_api.plot_historical(other_args) @@ -189,12 +200,24 @@ class PortfolioController: self.merged_holdings = merge_portfolios(holdings) print_portfolio(self.merged_holdings) + def call_csv(self, other_args): + """Process csv command""" + self.custom_portfolio = custom_model.load_csv_portfolio(other_args) + self.print_portfolio_view() + + def call_group(self, other_args): + """Process group command""" + if not self.custom_portfolio.empty: + custom_model.breakdown_by_group(self.custom_portfolio, other_args) + else: + print("Please load a portfolio") + def menu(): """Portfolio Analysis Menu""" port_controller = PortfolioController() - port_controller.print_help(port_controller.broker_list) + port_controller.print_help() while True: # Get input command from user