mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-27 12:40:35 +00:00
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:
19
ROADMAP.md
19
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)
|
||||
|
||||
___
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
60
gamestonk_terminal/portfolio/CUSTOM_PORT.md
Normal file
60
gamestonk_terminal/portfolio/CUSTOM_PORT.md
Normal 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 │
|
||||
╘════════════════════╧═════════╛
|
||||
155
gamestonk_terminal/portfolio/custom_model.py
Normal file
155
gamestonk_terminal/portfolio/custom_model.py
Normal 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")
|
||||
@@ -0,0 +1,6 @@
|
||||
Ticker,Shares,Other
|
||||
AAPL,5,
|
||||
GME,5,lol
|
||||
MSFT,3,
|
||||
O,2,s
|
||||
BAC,3,
|
||||
|
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user