Add csv option for portfolio (#502)

* Add custom portfolio that can be updated from csv file

* Add custom portfolio that can be updated from csv file
This commit is contained in:
jmaslek
2021-06-16 16:45:44 -04:00
committed by GitHub
parent bfc728a5c5
commit dab2489d87
7 changed files with 276 additions and 14 deletions

View File

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

View File

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

View File

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

View File

@@ -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 │
╘════════════════════╧═════════╛

View File

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

View File

@@ -0,0 +1,6 @@
Ticker,Shares,Other
AAPL,5,
GME,5,lol
MSFT,3,
O,2,s
BAC,3,
1 Ticker Shares Other
2 AAPL 5
3 GME 5 lol
4 MSFT 3
5 O 2 s
6 BAC 3

View File

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